Skip to main content

spacetimedb_lib/
lib.rs

1use crate::db::raw_def::v9::RawModuleDefV9Builder;
2use crate::db::raw_def::RawTableDefV8;
3use anyhow::Context;
4use sats::typespace::TypespaceBuilder;
5use spacetimedb_sats::WithTypespace;
6use std::any::TypeId;
7use std::collections::{btree_map, BTreeMap};
8
9pub mod connection_id;
10pub mod db;
11mod direct_index_key;
12pub mod error;
13mod filterable_value;
14pub mod http;
15pub mod identity;
16pub mod metrics;
17pub mod operator;
18pub mod query;
19pub mod scheduler;
20pub mod st_var;
21pub mod version;
22
23pub mod type_def {
24    pub use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, SumType};
25}
26pub mod type_value {
27    pub use spacetimedb_sats::{AlgebraicValue, ProductValue};
28}
29
30pub use connection_id::ConnectionId;
31pub use direct_index_key::{assert_column_type_valid_for_direct_index, DirectIndexKey};
32#[doc(hidden)]
33pub use filterable_value::Private;
34pub use filterable_value::{FilterableValue, IndexScanRangeBoundsTerminator, TermBound};
35pub use identity::Identity;
36pub use scheduler::ScheduleAt;
37pub use spacetimedb_sats::hash::{self, hash_bytes, Hash};
38pub use spacetimedb_sats::time_duration::TimeDuration;
39pub use spacetimedb_sats::timestamp::Timestamp;
40pub use spacetimedb_sats::uuid::Uuid;
41pub use spacetimedb_sats::SpacetimeType;
42pub use spacetimedb_sats::__make_register_reftype;
43pub use spacetimedb_sats::{self as sats, bsatn, buffer, de, ser};
44pub use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, SumType};
45pub use spacetimedb_sats::{AlgebraicValue, ProductValue};
46
47pub const MODULE_ABI_MAJOR_VERSION: u16 = 10;
48
49// if it ends up we need more fields in the future, we can split one of them in two
50#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
51pub struct VersionTuple {
52    /// Breaking change; different major versions are not at all compatible with each other.
53    pub major: u16,
54    /// Non-breaking change; a host can run a module that requests an older minor version than the
55    /// host implements, but not the other way around
56    pub minor: u16,
57}
58
59impl VersionTuple {
60    pub const fn new(major: u16, minor: u16) -> Self {
61        Self { major, minor }
62    }
63
64    #[inline]
65    pub const fn eq(self, other: Self) -> bool {
66        self.major == other.major && self.minor == other.minor
67    }
68
69    /// Checks if a host implementing this version can run a module that expects `module_version`
70    #[inline]
71    pub const fn supports(self, module_version: VersionTuple) -> bool {
72        self.major == module_version.major && self.minor >= module_version.minor
73    }
74
75    #[inline]
76    pub const fn from_u32(v: u32) -> Self {
77        let major = (v >> 16) as u16;
78        let minor = (v & 0xFF) as u16;
79        Self { major, minor }
80    }
81
82    #[inline]
83    pub const fn to_u32(self) -> u32 {
84        (self.major as u32) << 16 | self.minor as u32
85    }
86}
87
88impl std::fmt::Display for VersionTuple {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        let Self { major, minor } = *self;
91        write!(f, "{major}.{minor}")
92    }
93}
94
95extern crate self as spacetimedb_lib;
96
97//WARNING: Change this structure(or any of their members) is an ABI change.
98#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
99#[sats(crate = crate)]
100pub struct TableDesc {
101    pub schema: RawTableDefV8,
102    /// data should always point to a ProductType in the typespace
103    pub data: sats::AlgebraicTypeRef,
104}
105
106impl TableDesc {
107    pub fn into_table_def(table: WithTypespace<'_, TableDesc>) -> anyhow::Result<RawTableDefV8> {
108        let schema = table
109            .map(|t| &t.data)
110            .resolve_refs()
111            .context("recursive types not yet supported")?;
112        let schema = schema.into_product().ok().context("table not a product type?")?;
113        let table = table.ty();
114        anyhow::ensure!(
115            table.schema.columns.len() == schema.elements.len(),
116            "mismatched number of columns"
117        );
118
119        Ok(table.schema.clone())
120    }
121}
122
123#[derive(Debug, Clone, SpacetimeType)]
124#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
125#[sats(crate = crate)]
126pub struct ReducerDef {
127    pub name: Box<str>,
128    pub args: Vec<ProductTypeElement>,
129}
130
131//WARNING: Change this structure (or any of their members) is an ABI change.
132#[derive(Debug, Clone, Default, SpacetimeType)]
133#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
134#[sats(crate = crate)]
135pub struct RawModuleDefV8 {
136    pub typespace: sats::Typespace,
137    pub tables: Vec<TableDesc>,
138    pub reducers: Vec<ReducerDef>,
139    pub misc_exports: Vec<MiscModuleExport>,
140}
141
142impl RawModuleDefV8 {
143    pub fn builder() -> ModuleDefBuilder {
144        ModuleDefBuilder::default()
145    }
146
147    pub fn with_builder(f: impl FnOnce(&mut ModuleDefBuilder)) -> Self {
148        let mut builder = Self::builder();
149        f(&mut builder);
150        builder.finish()
151    }
152}
153
154/// A versioned raw module definition.
155///
156/// This is what is actually returned by the module when `__describe_module__` is called, serialized to BSATN.
157#[derive(Debug, Clone, SpacetimeType)]
158#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
159#[sats(crate = crate)]
160#[non_exhaustive]
161pub enum RawModuleDef {
162    V8BackCompat(RawModuleDefV8),
163    V9(db::raw_def::v9::RawModuleDefV9),
164    V10(db::raw_def::v10::RawModuleDefV10),
165    // TODO(jgilles): It would be nice to have a custom error message if this fails with an unknown variant,
166    // but I'm not sure if that can be done via the Deserialize trait.
167}
168
169/// A builder for a [`RawModuleDefV8`].
170/// Deprecated.
171#[derive(Default)]
172pub struct ModuleDefBuilder {
173    /// The module definition.
174    module: RawModuleDefV8,
175    /// The type map from `T: 'static` Rust types to sats types.
176    type_map: BTreeMap<TypeId, sats::AlgebraicTypeRef>,
177}
178
179impl ModuleDefBuilder {
180    pub fn add_type<T: SpacetimeType>(&mut self) -> AlgebraicType {
181        TypespaceBuilder::add_type::<T>(self)
182    }
183
184    /// Add a type that may not correspond to a Rust type.
185    /// Used only in tests.
186    #[cfg(feature = "test")]
187    pub fn add_type_for_tests(&mut self, name: &str, ty: AlgebraicType) -> spacetimedb_sats::AlgebraicTypeRef {
188        let slot_ref = self.module.typespace.add(ty);
189        self.module.misc_exports.push(MiscModuleExport::TypeAlias(TypeAlias {
190            name: name.to_owned(),
191            ty: slot_ref,
192        }));
193        slot_ref
194    }
195
196    /// Add a table that may not correspond to a Rust type.
197    /// Wraps it in a `TableDesc` and generates a corresponding `ProductType` in the typespace.
198    /// Used only in tests.
199    /// Returns the `AlgebraicTypeRef` of the generated `ProductType`.
200    #[cfg(feature = "test")]
201    pub fn add_table_for_tests(&mut self, schema: RawTableDefV8) -> spacetimedb_sats::AlgebraicTypeRef {
202        let ty: ProductType = schema
203            .columns
204            .iter()
205            .map(|c| ProductTypeElement {
206                name: Some(c.col_name.clone()),
207                algebraic_type: c.col_type.clone(),
208            })
209            .collect();
210        let data = self.module.typespace.add(ty.into());
211        self.add_type_alias(TypeAlias {
212            name: schema.table_name.clone().into(),
213            ty: data,
214        });
215        self.add_table(TableDesc { schema, data });
216        data
217    }
218
219    pub fn add_table(&mut self, table: TableDesc) {
220        self.module.tables.push(table)
221    }
222
223    pub fn add_reducer(&mut self, reducer: ReducerDef) {
224        self.module.reducers.push(reducer)
225    }
226
227    #[cfg(feature = "test")]
228    pub fn add_reducer_for_tests(&mut self, name: impl Into<Box<str>>, args: ProductType) {
229        self.add_reducer(ReducerDef {
230            name: name.into(),
231            args: args.elements.to_vec(),
232        });
233    }
234
235    pub fn add_misc_export(&mut self, misc_export: MiscModuleExport) {
236        self.module.misc_exports.push(misc_export)
237    }
238
239    pub fn add_type_alias(&mut self, type_alias: TypeAlias) {
240        self.add_misc_export(MiscModuleExport::TypeAlias(type_alias))
241    }
242
243    pub fn typespace(&self) -> &sats::Typespace {
244        &self.module.typespace
245    }
246
247    pub fn finish(self) -> RawModuleDefV8 {
248        self.module
249    }
250}
251
252impl TypespaceBuilder for ModuleDefBuilder {
253    fn add(
254        &mut self,
255        typeid: TypeId,
256        name: Option<&'static str>,
257        make_ty: impl FnOnce(&mut Self) -> AlgebraicType,
258    ) -> AlgebraicType {
259        let r = match self.type_map.entry(typeid) {
260            btree_map::Entry::Occupied(o) => *o.get(),
261            btree_map::Entry::Vacant(v) => {
262                // Bind a fresh alias to the unit type.
263                let slot_ref = self.module.typespace.add(AlgebraicType::unit());
264                // Relate `typeid -> fresh alias`.
265                v.insert(slot_ref);
266
267                // Alias provided? Relate `name -> slot_ref`.
268                if let Some(name) = name {
269                    self.module.misc_exports.push(MiscModuleExport::TypeAlias(TypeAlias {
270                        name: name.to_owned(),
271                        ty: slot_ref,
272                    }));
273                }
274
275                // Borrow of `v` has ended here, so we can now convince the borrow checker.
276                let ty = make_ty(self);
277                self.module.typespace[slot_ref] = ty;
278                slot_ref
279            }
280        };
281        AlgebraicType::Ref(r)
282    }
283}
284
285// an enum to keep it extensible without breaking abi
286#[derive(Debug, Clone, SpacetimeType)]
287#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
288#[sats(crate = crate)]
289pub enum MiscModuleExport {
290    TypeAlias(TypeAlias),
291}
292
293#[derive(Debug, Clone, SpacetimeType)]
294#[cfg_attr(feature = "test", derive(PartialEq, Eq, PartialOrd, Ord))]
295#[sats(crate = crate)]
296pub struct TypeAlias {
297    pub name: String,
298    pub ty: sats::AlgebraicTypeRef,
299}
300
301/// Converts a hexadecimal string reference to a byte array.
302///
303/// This function takes a reference to a hexadecimal string and attempts to convert it into a byte array.
304///
305/// If the hexadecimal string starts with "0x", these characters are ignored.
306pub fn from_hex_pad<R: hex::FromHex<Error = hex::FromHexError>, T: AsRef<[u8]>>(
307    hex: T,
308) -> Result<R, hex::FromHexError> {
309    let hex = hex.as_ref();
310    let hex = if hex.starts_with(b"0x") {
311        &hex[2..]
312    } else if hex.starts_with(b"X'") {
313        &hex[2..hex.len()]
314    } else {
315        hex
316    };
317    hex::FromHex::from_hex(hex)
318}
319
320/// Returns a resolved `AlgebraicType` (containing no `AlgebraicTypeRefs`) for a given `SpacetimeType`,
321/// using the v9 moduledef infrastructure.
322/// Panics if the type is recursive.
323///
324/// TODO: we could implement something like this in `sats` itself, but would need a lightweight `TypespaceBuilder` implementation there.
325pub fn resolved_type_via_v9<T: SpacetimeType>() -> AlgebraicType {
326    let mut builder = RawModuleDefV9Builder::new();
327    let ty = T::make_type(&mut builder);
328    let module = builder.finish();
329
330    WithTypespace::new(&module.typespace, &ty)
331        .resolve_refs()
332        .expect("recursive types not supported")
333}