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