diff --git a/codex-plugin/plugins/spacetimedb/skills/cpp-server/SKILL.md b/codex-plugin/plugins/spacetimedb/skills/cpp-server/SKILL.md index 8d270e93080..aebb82edf06 100644 --- a/codex-plugin/plugins/spacetimedb/skills/cpp-server/SKILL.md +++ b/codex-plugin/plugins/spacetimedb/skills/cpp-server/SKILL.md @@ -77,7 +77,7 @@ FIELD_Index(entity, name) FIELD_NamedMultiColumnIndex(score, by_player_and_level, player_id, level) ``` -Range queries (requires `#include `): +Range queries (included by `spacetimedb.h`; include `` directly only when not using the umbrella header): ```cpp ctx.db[user_age].filter(range_inclusive(uint8_t(18), uint8_t(65))); ctx.db[user_age].filter(range_from(uint8_t(18))); diff --git a/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md b/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md index c9f2e7343fd..8563a5e9808 100644 --- a/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md +++ b/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md @@ -20,7 +20,7 @@ Tables are built with `table()`, bound with `schema()`, and exported as default. ```typescript import { schema, table, t } from 'spacetimedb/server'; -const score_record = table( +const scoreRecord = table( { name: 'score_record', public: true }, { id: t.u64().primaryKey().autoInc(), @@ -29,13 +29,13 @@ const score_record = table( } ); -const spacetimedb = schema({ score_record }); // ONE object, not spread args +const spacetimedb = schema({ scoreRecord }); // ONE object, not spread args export default spacetimedb; export const addRecord = spacetimedb.reducer( { value: t.u32() }, (ctx, { value }) => { - ctx.db.score_record.insert({ id: 0n, owner: ctx.sender, value }); + ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value }); } ); ``` @@ -72,9 +72,9 @@ const entity = table( ); ``` -Options: `name` (snake_case, recommended), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]` +Options: `name` (snake_case, recommended), `public: true`, `event: true`, `indexes: [...]` -`ctx.db` accessors are the keys passed to `schema({...})`, verbatim: `schema({ score_record })` → `ctx.db.score_record`. Use snake_case keys matching the table `name`. Client codegen converts case; server `ctx.db` does not. +`ctx.db` accessors are the keys passed to `schema({...})`, verbatim: `schema({ scoreRecord })` -> `ctx.db.scoreRecord`. Keep TypeScript identifiers and accessors camelCase. Use explicit `name: 'snake_case'` strings when you need a canonical database name that differs from the TypeScript identifier. ## Column Types @@ -149,16 +149,16 @@ Reducer args accept any column type, including arrays of custom types: `{ splits ## DB Operations ```typescript -ctx.db.score_record.insert({ id: 0n, owner: ctx.sender, value: 1 }); // Insert (0n for autoInc) -ctx.db.score_record.id.find(recordId); // Find by PK → row | null +ctx.db.scoreRecord.insert({ id: 0n, owner: ctx.sender, value: 1 }); // Insert (0n for autoInc) +ctx.db.scoreRecord.id.find(recordId); // Find by PK → row | null ctx.db.entity.identity.find(ctx.sender); // Find by unique column [...ctx.db.post.authorId.filter(authorId)]; // Filter → spread to Array [...ctx.db.entity.iter()]; // All rows → Array -ctx.db.score_record.id.update({ ...existing, value: 2 }); // Update (spread + override) -ctx.db.score_record.id.delete(recordId); // Delete by PK +ctx.db.scoreRecord.id.update({ ...existing, value: 2 }); // Update (spread + override) +ctx.db.scoreRecord.id.delete(recordId); // Delete by PK ``` -Insert through the table accessor (`ctx.db.score_record.insert(...)`). Primary-key, unique, and index accessors support lookup or mutation of existing rows, but do not have `insert(...)`. +Insert through the table accessor (`ctx.db.scoreRecord.insert(...)`). Primary-key, unique, and index accessors support lookup or mutation of existing rows, but do not have `insert(...)`. `insert(...)` returns the inserted row, including database-assigned auto-increment fields. @@ -188,7 +188,7 @@ export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... }); type Ctx = ReducerCtx>; function findRecord(ctx: Ctx, id: bigint) { - return ctx.db.score_record.id.find(id); + return ctx.db.scoreRecord.id.find(id); } ``` @@ -236,19 +236,23 @@ The reducer or procedure referenced by a table's `scheduled` option must be expo ```typescript import { ScheduleAt } from 'spacetimedb'; // ScheduleAt comes from the root package -const tick_timer = table({ +const tickTimer = table({ name: 'tick_timer', - scheduled: (): any => tick, // (): any => breaks circular dep }, { - scheduled_id: t.u64().primaryKey().autoInc(), - scheduled_at: t.scheduleAt(), + scheduledId: t.u64().primaryKey().autoInc(), + scheduledAt: t.scheduleAt(), }); export const tick = spacetimedb.reducer( - { timer: tick_timer.rowType }, + { onSchedule: tickTimer }, + { timer: tickTimer.rowType }, (ctx, { timer }) => { /* timer row auto-deleted after this runs */ } ); +// `onSchedule` also works for scheduled procedures whose return type is `t.unit()`. +// Legacy table-side scheduling, `scheduled: (): any => tick`, still works but is not +// recommended for new code because it forces a forward reference. + // One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros) // Repeating: ScheduleAt.interval(60_000_000n) // Read time back from a scheduleAt value (tagged union): @@ -354,7 +358,7 @@ TypeScript outbound HTTP uses `ctx.http.fetch(url, options)`, including for non- Procedures and handlers open short database transactions with `ctx.withTx(tx => ...)`. Perform network I/O before opening the transaction; only database work belongs inside its callback. -Scheduled procedures use the ordinary scheduled-table shape. Its `scheduled` option references an exported `spacetimedb.procedure(...)` value instead of a reducer, and the procedure accepts the scheduled row as its argument. +Scheduled procedures use the same `onSchedule` binding as scheduled reducers. The procedure must be exported, return `t.unit()`, and accept the scheduled row as its argument. Inbound HTTP uses `httpHandler`, `httpRouter`, `Router`, and `SyncResponse`: diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 8fd148e4315..eb29f6369a0 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -203,7 +203,7 @@ pub fn cli() -> clap::Command { Arg::new("native-aot") .long("native-aot") .action(clap::ArgAction::SetTrue) - .help("Configure C# project for NativeAOT-LLVM compilation (experimental, Windows only)"), + .help("Configure C# project for NativeAOT-LLVM compilation (experimental; supported on Windows, and on Linux with .NET 10)"), ) .arg( common_args::dotnet_version().help("Target .NET SDK major version for C# projects (e.g. 8 or 10). Defaults to 10 except on macOS or when only .NET 8 is installed."), diff --git a/crates/cli/src/subcommands/publish.rs b/crates/cli/src/subcommands/publish.rs index 745664880f0..7e70c1f71ec 100644 --- a/crates/cli/src/subcommands/publish.rs +++ b/crates/cli/src/subcommands/publish.rs @@ -316,7 +316,7 @@ i.e. only lowercase ASCII letters and numbers, separated by dashes."), Arg::new("native_aot") .long("native-aot") .action(SetTrue) - .help("Use NativeAOT-LLVM compilation for C# modules (experimental, Windows only)") + .help("Use NativeAOT-LLVM compilation for C# modules (experimental; supported on Windows, and on Linux with .NET 10)") ) .arg(common_args::dotnet_version()) .after_help("Run `spacetime help publish` for more detailed information.") diff --git a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md index 10ba131faef..76b371a3abf 100644 --- a/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md +++ b/docs/docs/00100-intro/00100-getting-started/00400-key-architecture.md @@ -108,7 +108,7 @@ This is a form of [remote procedure call](https://en.wikipedia.org/wiki/Remote_p A reducer can be written in a TypeScript module like so: ```typescript -export const set_player_name = spacetimedb.reducer({ id: t.u64(), name: t.string() }, (ctx, { id, name }) => { +export const setPlayerName = spacetimedb.reducer({ id: t.u64(), name: t.string() }, (ctx, { id, name }) => { // ... }); ``` @@ -322,7 +322,7 @@ and must manually open and commit a transaction in order to read from or modify A procedure can be defined in a TypeScript module: ```typescript -export const make_request = spacetimedb.procedure(t.string(), ctx => { +export const makeRequest = spacetimedb.procedure(t.string(), ctx => { // ... }) ``` @@ -502,7 +502,7 @@ Views must be declared as `public` and accept only a context parameter. They can A view can be written in a TypeScript module like so: ```typescript -export const my_player = spacetimedb.view( +export const myPlayer = spacetimedb.view( { name: 'my_player', public: true }, t.option(players.rowType), (ctx) => { diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md index 3b6321b140f..23d0df3bec2 100644 --- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md +++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md @@ -387,7 +387,7 @@ function validateName(name: string) { } } -export const set_name = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { +export const setName = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { validateName(name); const user = ctx.db.user.identity.find(ctx.sender); if (!user) { @@ -504,7 +504,7 @@ function validateMessage(text: string) { } } -export const send_message = spacetimedb.reducer({ text: t.string() }, (ctx, { text }) => { +export const sendMessage = spacetimedb.reducer({ text: t.string() }, (ctx, { text }) => { validateMessage(text); console.info(`User ${ctx.sender}: ${text}`); ctx.db.message.insert({ diff --git a/docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md b/docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md index ebb386a1d7c..a5dc822c8eb 100644 --- a/docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md +++ b/docs/docs/00200-core-concepts/00100-databases/00100-transactions-atomicity.md @@ -72,7 +72,7 @@ When a reducer calls another reducer directly (not via scheduling), they execute ```typescript -export const parent_reducer = spacetimedb.reducer((ctx) => { +export const parentReducer = spacetimedb.reducer((ctx) => { TableA.insert({ /* ... */ }); // This runs in the SAME transaction diff --git a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md index 5095da5a06e..3a7d9661e39 100644 --- a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md +++ b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md @@ -82,11 +82,11 @@ const score = table( indexes: [{ accessor: 'idx', algorithm: 'btree', - columns: ['player_id', 'level'], + columns: ['playerId', 'level'], }], }, { - player_id: t.u64(), + playerId: t.u64(), level: t.u32(), } ); @@ -118,7 +118,7 @@ public partial struct Player // Multi-column index (use new[] for attribute params — collection expressions invalid in attributes) [SpacetimeDB.Table(Accessor = "Score")] -[SpacetimeDB.Index.BTree(Accessor = "idx", Columns = new[] { "PlayerId", "Level" })] +[SpacetimeDB.Index.BTree(Accessor = "Idx", Columns = new[] { "PlayerId", "Level" })] public partial struct Score { public ulong PlayerId; @@ -216,12 +216,12 @@ const spacetimedb = schema({ player }); export default spacetimedb; // Basic reducer -export const create_player = spacetimedb.reducer({ username: t.string() }, (ctx, { username }) => { +export const createPlayer = spacetimedb.reducer({ username: t.string() }, (ctx, { username }) => { ctx.db.player.insert({ id: 0n, username, score: 0 }); }); // With error handling -export const update_score = spacetimedb.reducer({ id: t.u64(), points: t.i32() }, (ctx, { id, points }) => { +export const updateScore = spacetimedb.reducer({ id: t.u64(), points: t.i32() }, (ctx, { id, points }) => { const player = ctx.db.player.id.find(id); if (!player) throw new Error('Player not found'); player.score += points; @@ -409,12 +409,12 @@ const reminder = table( { id: t.u64().primaryKey().autoInc(), message: t.string(), - scheduled_at: t.scheduleAt(), + scheduledAt: t.scheduleAt(), } ); // `onSchedule` binds the reducer to the schedule table -export const send_reminder = spacetimedb.reducer( +export const sendReminder = spacetimedb.reducer( { onSchedule: reminder }, { arg: reminder.rowType }, (ctx, { arg }) => { @@ -495,7 +495,7 @@ SPACETIMEDB_REDUCER(send_reminder, ReducerContext ctx, Reminder reminder) { ```typescript -export const fetch_data = spacetimedb.procedure( +export const fetchData = spacetimedb.procedure( { url: t.string() }, t.string(), (ctx, { url }) => { @@ -605,29 +605,29 @@ SPACETIMEDB_PROCEDURE(std::string, fetch_data, ProcedureContext ctx, std::string ```typescript // Return single row -export const my_player = spacetimedb.view({ name: 'my_player', public: true }, t.option(player.rowType), ctx => { +export const myPlayer = spacetimedb.view({ name: 'my_player', public: true }, t.option(player.rowType), ctx => { return ctx.db.player.identity.find(ctx.sender); }); // Return potentially multiple rows -export const top_players = spacetimedb.view({ name: 'top_players', public: true }, t.array(player.rowType), ctx => { +export const topPlayers = spacetimedb.view({ name: 'top_players', public: true }, t.array(player.rowType), ctx => { return ctx.db.player.score.filter(1000); }); // Procedural view with update callbacks. // The returned row type has exactly one `.primaryKey()` column. -export const top_players_with_updates = spacetimedb.view({ name: 'top_players_with_updates', public: true }, t.array(player.rowType), ctx => { +export const topPlayersWithUpdates = spacetimedb.view({ name: 'top_players_with_updates', public: true }, t.array(player.rowType), ctx => { return ctx.db.player.score.filter(1000); }); // Perform a generic filter using the query builder. // Equivalent to `SELECT * FROM player WHERE score < 1000`. -export const bottom_players = spacetimedb.view({ name: 'bottom_players', public: true }, t.array(player.rowType), ctx => { +export const bottomPlayers = spacetimedb.view({ name: 'bottom_players', public: true }, t.array(player.rowType), ctx => { return ctx.from.player.where(p => p.score.lt(1000)) }); // Count rows in a table. -export const player_count = spacetimedb.anonymousView({ name: 'player_count', public: true }, t.array(t.row('PlayerCount', { +export const playerCount = spacetimedb.anonymousView({ name: 'player_count', public: true }, t.array(t.row('PlayerCount', { count: t.u64(), })), ctx => { return [{ count: ctx.db.player.count() }]; diff --git a/docs/docs/00200-core-concepts/00100-databases/00500-migrations/00200-automatic-migrations.md b/docs/docs/00200-core-concepts/00100-databases/00500-migrations/00200-automatic-migrations.md index 4a29ff7465f..e41dcaab11a 100644 --- a/docs/docs/00200-core-concepts/00100-databases/00500-migrations/00200-automatic-migrations.md +++ b/docs/docs/00200-core-concepts/00100-databases/00500-migrations/00200-automatic-migrations.md @@ -32,6 +32,8 @@ These changes are allowed by automatic migration, but may cause runtime errors f - **Adding new columns to the end of a table with a default value.** The new column must be added at the end of the table definition and must have a default value specified. Non-updated clients will not be aware of the new column. - **Changing or removing reducers.** Clients attempting to call the old version of a changed reducer or a removed reducer will receive runtime errors. - **Changing tables from public to private.** Clients subscribed to a newly-private table will receive runtime errors. +- **Changing table or column accessor names while preserving canonical names.** The stored data can be migrated, but source code that refers to the old accessors must be updated. +- **Removing empty tables.** SpacetimeDB can remove a table only if it has no rows. Removing a table disconnects active clients. Clients using bindings or subscription queries generated from the old schema must be updated before reconnecting, because the removed table no longer exists. - **Removing `Primary Key` annotations.** Non-updated clients will still use the old primary key as a unique key in their local cache, which can result in non-deterministic behavior when updates are received. - **Removing indexes.** This is only breaking in specific situations. The main issue occurs with subscription queries involving semijoins, such as: @@ -48,12 +50,13 @@ These changes are allowed by automatic migration, but may cause runtime errors f The following changes cannot be performed with automatic migration and will cause the publish to fail: -- **Removing tables.** -- **Removing or modifying existing columns.** This includes changing the type, renaming, or reordering columns. +- **Removing non-empty tables.** Empty tables can be removed automatically, but table removal fails if the existing table contains rows. +- **Removing or modifying existing columns.** This includes changing the type, canonical name, or order of columns. Changing only the generated accessor alias is allowed, but source code that refers to the old accessor must be updated. - **Adding columns without a default value.** New columns must have a default value so existing rows can be populated. - **Adding columns in the middle of a table.** New columns must be added at the end of the table definition. - **Changing whether a table is used for `scheduling`.** - **Adding `Unique` or `Primary Key` constraints.** This could result in existing tables being in an invalid state. +- **Changing an index accessor name.** Automatic migration matches an existing index by its canonical name, so renaming only the generated accessor for that index is not supported. Add a separate index definition with the desired accessor, then update code to use the new accessor. ## Working with Forbidden Changes @@ -91,6 +94,7 @@ For complex schema changes that aren't supported by automatic migration: During automatic migrations, active client connections are maintained and subscriptions continue to function. However: - Clients may witness brief interruptions in scheduled reducers (such as game loops) +- Some migrations, such as removing a table, disconnect active clients so they reconnect against the new schema - New module versions may remove or change reducers, causing runtime errors for clients calling those reducers - Clients won't automatically know about schema changes - you may need to regenerate and update client bindings diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md index 7e8a28ce52d..0e6aec2bbfe 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00300-reducers.md @@ -34,7 +34,7 @@ const user = table( const spacetimedb = schema({ user }); export default spacetimedb; -export const create_user = spacetimedb.reducer({ name: t.string(), email: t.string() }, (ctx, { name, email }) => { +export const createUser = spacetimedb.reducer({ name: t.string(), email: t.string() }, (ctx, { name, email }) => { // Validate input if (name === '') { throw new Error('Name cannot be empty'); @@ -600,7 +600,7 @@ export const queueFetch = spacetimedb.reducer({ url: t.string() }, (ctx, { url } #pragma warning disable STDB_UNSTABLE using SpacetimeDB; -public partial class Module +public static partial class Module { [SpacetimeDB.Table(Accessor = "FetchSchedule", Scheduled = "FetchExternalData", ScheduledAt = "ScheduledAt")] public partial struct FetchSchedule diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md index f05e0db2d0e..9bcb7f3b17c 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00400-reducer-context.md @@ -33,7 +33,7 @@ const user = table( const spacetimedb = schema({ user }); export default spacetimedb; -export const create_user = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { +export const createUser = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { ctx.db.user.insert({ id: 0n, name }); }); ``` @@ -145,7 +145,7 @@ const player = table( const spacetimedb = schema({ player }); export default spacetimedb; -export const update_score = spacetimedb.reducer({ newScore: t.u32() }, (ctx, { newScore }) => { +export const updateScore = spacetimedb.reducer({ newScore: t.u32() }, (ctx, { newScore }) => { // Get the caller's identity const caller = ctx.sender; @@ -257,7 +257,7 @@ SPACETIMEDB_REDUCER(update_score, ReducerContext ctx, uint32_t new_score) { The connection ID identifies the specific client connection that invoked the reducer. This is useful for tracking sessions or implementing per-connection state. :::note -The connection ID may be absent for reducers invoked by the system (such as scheduled reducers or lifecycle reducers) or when called via the CLI without specifying a connection. In TypeScript modules, `ctx.connectionId` is `ConnectionId | null`. +The connection ID is present only when the reducer invocation is associated with a client connection. Reducers invoked by `init`, scheduled reducers, and some CLI or internal calls may not have one. Client-connected and client-disconnected reducers receive the connection ID for the connection being opened or closed. ::: ### Timestamp diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md index a7a13cc3e5d..9758bf53663 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00500-lifecycle.md @@ -183,14 +183,17 @@ Runs when a client establishes a connection. export const onConnect = spacetimedb.clientConnected((ctx) => { console.log(`Client connected: ${ctx.sender}`); - // ctx.connectionId is guaranteed to be defined - const connId = ctx.connectionId!; + // TypeScript exposes this as ConnectionId | null, so guard it before use. + const connId = ctx.connectionId; + if (connId === null) { + throw new Error('client connection ID missing'); + } // Initialize client session ctx.db.sessions.insert({ - connection_id: connId, + connectionId: connId, identity: ctx.sender, - connected_at: ctx.timestamp + connectedAt: ctx.timestamp }); }); ``` @@ -204,8 +207,11 @@ public static void OnConnect(ReducerContext ctx) { Log.Info($"Client connected: {ctx.Sender}"); - // ctx.ConnectionId is guaranteed to be non-null - var connId = ctx.ConnectionId!.Value; + // ctx.ConnectionId is nullable in the API; unwrap it before use. + if (ctx.ConnectionId is not { } connId) + { + throw new Exception("client connection ID missing"); + } // Initialize client session ctx.Db.Session.Insert(new Session @@ -225,8 +231,8 @@ public static void OnConnect(ReducerContext ctx) pub fn on_connect(ctx: &ReducerContext) -> Result<(), String> { log::info!("Client connected: {}", ctx.sender()); - // ctx.connection_id() is guaranteed to be Some(...) - let conn_id = ctx.connection_id().unwrap(); + // ctx.connection_id() returns Option; unwrap it before use. + let conn_id = ctx.connection_id().ok_or("client connection ID missing")?; // Initialize client session ctx.db.sessions().try_insert(Session { @@ -258,7 +264,10 @@ FIELD_PrimaryKey(sessions, connection_id); SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) { LOG_INFO("Client connected: " + ctx.sender().to_string()); - // ctx.connection_id is guaranteed to be present + // ctx.connection_id is optional; unwrap it before use. + if (!ctx.connection_id.has_value()) { + return Err("client connection ID missing"); + } auto conn_id = ctx.connection_id.value(); // Initialize client session @@ -277,7 +286,8 @@ SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) { The `client_connected` reducer: - Cannot take arguments beyond `ReducerContext` -- `ctx.connection_id()` is guaranteed to be present +- Receives the connection ID for the connection being opened. The API exposes + it as nullable or optional, so guard or unwrap it before use. - Failure disconnects the client - Runs for each distinct connection (WebSocket, HTTP call) @@ -292,11 +302,14 @@ Runs when a client connection terminates. export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { console.log(`Client disconnected: ${ctx.sender}`); - // ctx.connectionId is guaranteed to be defined - const connId = ctx.connectionId!; + // TypeScript exposes this as ConnectionId | null, so guard it before use. + const connId = ctx.connectionId; + if (connId === null) { + throw new Error('client connection ID missing'); + } // Clean up client session - ctx.db.sessions.connection_id.delete(connId); + ctx.db.sessions.connectionId.delete(connId); }); ``` @@ -309,8 +322,11 @@ public static void OnDisconnect(ReducerContext ctx) { Log.Info($"Client disconnected: {ctx.Sender}"); - // ctx.ConnectionId is guaranteed to be non-null - var connId = ctx.ConnectionId!.Value; + // ctx.ConnectionId is nullable in the API; unwrap it before use. + if (ctx.ConnectionId is not { } connId) + { + throw new Exception("client connection ID missing"); + } // Clean up client session ctx.Db.Session.ConnectionId.Delete(connId); @@ -325,8 +341,8 @@ public static void OnDisconnect(ReducerContext ctx) pub fn on_disconnect(ctx: &ReducerContext) -> Result<(), String> { log::info!("Client disconnected: {}", ctx.sender()); - // ctx.connection_id() is guaranteed to be Some(...) - let conn_id = ctx.connection_id().unwrap(); + // ctx.connection_id() returns Option; unwrap it before use. + let conn_id = ctx.connection_id().ok_or("client connection ID missing")?; // Clean up client session ctx.db.sessions().connection_id().delete(&conn_id); @@ -354,7 +370,10 @@ FIELD_PrimaryKey(sessions, connection_id); SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) { LOG_INFO("Client disconnected: " + ctx.sender().to_string()); - // ctx.connection_id is guaranteed to be present + // ctx.connection_id is optional; unwrap it before use. + if (!ctx.connection_id.has_value()) { + return Err("client connection ID missing"); + } auto conn_id = ctx.connection_id.value(); // Clean up client session @@ -369,7 +388,8 @@ SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) { The `client_disconnected` reducer: - Cannot take arguments beyond `ReducerContext` -- `ctx.connection_id()` is guaranteed to be present +- Receives the connection ID for the connection being closed. The API exposes + it as nullable or optional, so guard or unwrap it before use. - Failure is logged but doesn't prevent disconnection - Runs when connection ends (close, timeout, error) diff --git a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md index 9d7a4cae7af..b8f58043ca8 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md +++ b/docs/docs/00200-core-concepts/00200-functions/00300-reducers/00600-error-handling.md @@ -24,9 +24,9 @@ Throw a `SenderError`: ```typescript import { SenderError } from 'spacetimedb/server'; -export const transfer_credits = spacetimedb.reducer( - { to_user: t.identity(), amount: t.u32() }, - (ctx, { to_user, amount }) => { +export const transferCredits = spacetimedb.reducer( + { toUser: t.identity(), amount: t.u32() }, + (ctx, { toUser, amount }) => { const fromUser = ctx.db.users.identity.find(ctx.sender); if (!fromUser) { throw new SenderError('User not found'); @@ -41,9 +41,9 @@ export const transfer_credits = spacetimedb.reducer( ); // Alternative: return error object -export const transfer_credits = spacetimedb.reducer( - { to_user: t.u64(), amount: t.u32() }, - (ctx, { to_user, amount }) => { +export const transferCreditsResult = spacetimedb.reducer( + { toUser: t.u64(), amount: t.u32() }, + (ctx, { toUser, amount }) => { // ...validation... if (error) { return { tag: 'err', value: 'Insufficient credits' }; @@ -146,7 +146,7 @@ Unexpected errors caused by bugs in module code. These should be fixed by the de Regular errors (not `SenderError`): ```typescript -export const process_data = spacetimedb.reducer( +export const processData = spacetimedb.reducer( { data: t.array(t.u8()) }, (ctx, { data }) => { // Regular Error indicates a bug diff --git a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md index 615db33c756..30be60eceae 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md +++ b/docs/docs/00200-core-concepts/00200-functions/00400-procedures.md @@ -23,7 +23,7 @@ For this reason, prefer defining reducers rather than procedures unless you need Define a procedure with `spacetimedb.procedure`: ```typescript -export const add_two_numbers = spacetimedb.procedure( +export const addTwoNumbers = spacetimedb.procedure( { lhs: t.u32(), rhs: t.u32() }, t.u64(), (ctx, { lhs, rhs }) => BigInt(lhs) + BigInt(rhs), @@ -31,14 +31,15 @@ export const add_two_numbers = spacetimedb.procedure( ``` The `spacetimedb.procedure` function takes: -* the procedure name, -* (optional) an object representing its parameter types, +* optional procedure options, such as `onSchedule`, +* an optional object representing its parameter types, * its return type, * and the procedure function itself. -The function will receive a `ProcedureContext` and an object of its arguments, and it must return -a value corresponding to its return type. This return value will be sent to the caller, but will -not be broadcast to any other clients. +The exported value's name becomes the procedure name. The callback receives a `ProcedureContext` +and, when the procedure has parameters, an object of its arguments. It must return a value +corresponding to its return type. This return value will be sent to the caller, but will not be +broadcast to any other clients. @@ -235,8 +236,8 @@ struct MyTable { #[spacetimedb::procedure] fn insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) { - ctx.with_tx(|ctx| { - ctx.my_table().insert(MyTable { a, b }); + ctx.with_tx(|tx| { + tx.db.my_table().insert(MyTable { a, b }); }); } ``` @@ -368,11 +369,11 @@ For fallible database operations, instead use `ProcedureContext::try_with_tx`: ```rust #[spacetimedb::procedure] fn maybe_insert_a_value(ctx: &mut ProcedureContext, a: u32, b: String) { - ctx.try_with_tx(|ctx| { + ctx.try_with_tx(|tx| { if a < 10 { return Err("a is less than 10!"); } - ctx.my_table().insert(MyTable { a, b }); + tx.db.my_table().insert(MyTable { a, b }); Ok(()) }); } @@ -439,7 +440,7 @@ const player = table( const spacetimedb = schema({ player }); export default spacetimedb; -export const find_highest_level_player = spacetimedb.procedure(t.unit(), ctx => { +export const findHighestLevelPlayer = spacetimedb.procedure(t.unit(), ctx => { let highestLevelPlayer = ctx.withTx(ctx => Iterator.from(ctx.db.player).reduce( (a, b) => a == null || b.level > a.level ? b : a, @@ -589,7 +590,7 @@ Procedures can make HTTP requests to external services using methods contained i It can perform simple `GET` requests: ```typescript -export const get_request = spacetimedb.procedure(t.unit(), ctx => { +export const getRequest = spacetimedb.procedure(t.unit(), ctx => { try { const response = ctx.http.fetch("https://example.invalid"); const body = response.text(); @@ -604,7 +605,7 @@ export const get_request = spacetimedb.procedure(t.unit(), ctx => { It can also accept an options object to specify a body, headers, HTTP method, and timeout: ```typescript -export const post_request = spacetimedb.procedure(t.unit(), ctx => { +export const postRequest = spacetimedb.procedure(t.unit(), ctx => { try { const response = ctx.http.fetch("https://example.invalid/upload", { method: "POST", @@ -619,7 +620,7 @@ export const post_request = spacetimedb.procedure(t.unit(), ctx => { return {}; }); -export const get_request_with_short_timeout = spacetimedb.procedure(t.unit(), ctx => { +export const getRequestWithShortTimeout = spacetimedb.procedure(t.unit(), ctx => { try { const response = ctx.http.fetch("https://example.invalid", { method: "GET", @@ -892,7 +893,7 @@ export const processItem = spacetimedb.reducer({ itemId: t.u64() }, (ctx, { item }); // Call it from a procedure using the saved reference -export const fetch_and_process = spacetimedb.procedure({ url: t.string() }, t.unit(), (ctx, { url }) => { +export const fetchAndProcess = spacetimedb.procedure({ url: t.string() }, t.unit(), (ctx, { url }) => { // Fetch external data const response = ctx.http.fetch(url); const data = response.json(); diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md index 7e97e0078e3..7aaf54170d4 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md +++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md @@ -536,7 +536,7 @@ export const entitiesInMyChunk = spacetimedb.view( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { [SpacetimeDB.Table(Accessor = "Entity", Public = true)] public partial struct Entity @@ -1097,7 +1097,7 @@ export const allPlayerLevels = spacetimedb.anonymousView( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { [SpacetimeDB.Table(Accessor = "Player", Public = true)] public partial struct Player diff --git a/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md b/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md index 9696462542d..66dbe4425c3 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md +++ b/docs/docs/00200-core-concepts/00200-functions/00600-HTTP-handlers.md @@ -33,7 +33,7 @@ import { schema, SyncResponse } from "spacetimedb/server"; const spacetimedb = schema({}); export default spacetimedb; -export const say_hello = spacetimedb.httpHandler((_ctx, _req) => { +export const sayHello = spacetimedb.httpHandler((_ctx, _req) => { return new SyncResponse("Hello!"); }); ``` @@ -146,7 +146,7 @@ import { Router } from "spacetimedb/server"; export const router = spacetimedb.httpRouter( new Router() - .get("/say-hello", say_hello) + .get("/say-hello", sayHello) ); ``` diff --git a/docs/docs/00200-core-concepts/00300-tables.md b/docs/docs/00200-core-concepts/00300-tables.md index 5a1ece9bcdb..8d7ea865b12 100644 --- a/docs/docs/00200-core-concepts/00300-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables.md @@ -224,15 +224,15 @@ The accessor is the key passed to `schema({...})`, verbatim. By convention the k ```typescript // Table definition -const player_scores = table( +const playerScores = table( { name: 'player_scores', public: true }, { /* columns */ } ); -const spacetimedb = schema({ player_scores }); +const spacetimedb = schema({ playerScores }); // Accessor is the schema key, verbatim -ctx.db.player_scores.insert({ /* ... */ }); +ctx.db.playerScores.insert({ /* ... */ }); ``` | Schema Key | Accessor | diff --git a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md index fa7161d280c..32a5e87e7d7 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md +++ b/docs/docs/00200-core-concepts/00300-tables/00200-column-types.md @@ -195,19 +195,19 @@ const player = table( experience: t.u32(), health: t.f32(), score: t.i64(), - is_online: t.bool(), + isOnline: t.bool(), // Composite types position: Coordinates, status: Status, inventory: t.array(t.u32()), - guild_id: t.option(t.u64()), + guildId: t.option(t.u64()), // Special types owner: t.identity(), connection: t.option(t.connectionId()), - created_at: t.timestamp(), - play_time: t.timeDuration(), + createdAt: t.timestamp(), + playTime: t.timeDuration(), } ); ``` diff --git a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md index 0e29291d1fe..af12f0aa18f 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md +++ b/docs/docs/00200-core-concepts/00300-tables/00210-file-storage.md @@ -204,7 +204,7 @@ const spacetimedb = schema({ document }); export default spacetimedb; // Called after uploading file to external storage -export const register_document = spacetimedb.reducer({ +export const registerDocument = spacetimedb.reducer({ filename: t.string(), mimeType: t.string(), sizeBytes: t.u64(), @@ -394,7 +394,7 @@ const spacetimedb = schema({ document }); export default spacetimedb; // Upload file to S3 and register in database -export const upload_to_s3 = spacetimedb.procedure( +export const uploadToS3 = spacetimedb.procedure( { filename: t.string(), contentType: t.string(), @@ -606,7 +606,7 @@ For larger files, generate a pre-signed URL and let the client upload directly: ```typescript // Procedure returns a pre-signed URL for client-side upload -export const get_upload_url = spacetimedb.procedure( +export const getUploadUrl = spacetimedb.procedure( { filename: t.string(), contentType: t.string() }, t.object('UploadInfo', { uploadUrl: t.string(), s3Key: t.string() }), (ctx, { filename, contentType }) => { @@ -620,7 +620,7 @@ export const get_upload_url = spacetimedb.procedure( ); // Client uploads directly to S3 using the pre-signed URL, then calls: -export const confirm_upload = spacetimedb.reducer({ filename: t.string(), s3Key: t.string() }, (ctx, { filename, s3Key }) => { +export const confirmUpload = spacetimedb.reducer({ filename: t.string(), s3Key: t.string() }, (ctx, { filename, s3Key }) => { ctx.db.document.insert({ id: 0n, ownerId: ctx.sender, @@ -765,7 +765,7 @@ const image = table( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { [SpacetimeDB.Table(Accessor = "Image", Public = true)] public partial struct Image diff --git a/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md b/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md index 2dd4a2082be..a0e00560bbd 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md +++ b/docs/docs/00200-core-concepts/00300-tables/00230-auto-increment.md @@ -27,7 +27,7 @@ const post = table( const spacetimedb = schema({ post }); export default spacetimedb; -export const add_post = spacetimedb.reducer({ title: t.string() }, (ctx, { title }) => { +export const addPost = spacetimedb.reducer({ title: t.string() }, (ctx, { title }) => { // Pass 0 for the auto-increment field const inserted = ctx.db.post.insert({ id: 0n, title }); // inserted.id now contains the assigned value @@ -182,13 +182,13 @@ If the database crashes or restarts, it resumes from the next allocation boundar const user = table( { name: 'user', public: true }, { - user_id: t.u64().autoInc(), + userId: t.u64().autoInc(), name: t.string(), } ); -export const insert_user = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { - ctx.db.user.insert({ user_id: 0n, name }); +export const insertUser = spacetimedb.reducer({ name: t.string() }, (ctx, { name }) => { + ctx.db.user.insert({ userId: 0n, name }); }); ``` @@ -196,7 +196,7 @@ export const insert_user = spacetimedb.reducer({ name: t.string() }, (ctx, { nam ```csharp -public partial class Module +public static partial class Module { [SpacetimeDB.Table(Accessor = "User", Public = true)] public partial struct User @@ -211,6 +211,7 @@ public partial class Module { ctx.Db.User.Insert(new User { UserId = 0, Name = name }); } +} ``` diff --git a/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md b/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md index 24b8c74acc8..9c0977f02a4 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md +++ b/docs/docs/00200-core-concepts/00300-tables/00240-constraints.md @@ -105,7 +105,7 @@ const inventory = table( name: 'inventory', public: true, indexes: [ - { accessor: 'by_user_item', algorithm: 'btree', columns: ['userId', 'itemId'] }, + { accessor: 'byUserItem', algorithm: 'btree', columns: ['userId', 'itemId'] }, ], }, { @@ -122,7 +122,7 @@ const inventory = table( ```csharp [SpacetimeDB.Table(Accessor = "Inventory", Public = true)] -[SpacetimeDB.Index.BTree(Accessor = "by_user_item", Columns = new[] { nameof(UserId), nameof(ItemId) })] +[SpacetimeDB.Index.BTree(Accessor = "ByUserItem", Columns = new[] { nameof(UserId), nameof(ItemId) })] public partial struct Inventory { [SpacetimeDB.PrimaryKey] @@ -183,7 +183,7 @@ When you update a row, SpacetimeDB uses the primary key to determine whether it' ```typescript -export const update_user_name = spacetimedb.reducer({ id: t.u64(), newName: t.string() }, (ctx, { id, newName }) => { +export const updateUserName = spacetimedb.reducer({ id: t.u64(), newName: t.string() }, (ctx, { id, newName }) => { const user = ctx.db.user.id.find(id); if (user) { // This is an update — primary key (id) stays the same diff --git a/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md b/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md index 430fde8c3ca..d192a722cad 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md +++ b/docs/docs/00200-core-concepts/00300-tables/00300-indexes.md @@ -217,7 +217,7 @@ const user = table( name: 'user', public: true, indexes: [ - { accessor: 'idx_age', algorithm: 'btree', columns: ['age'] }, + { accessor: 'idxAge', algorithm: 'btree', columns: ['age'] }, ], }, { @@ -233,7 +233,7 @@ const user = table( ```csharp [SpacetimeDB.Table(Accessor = "User", Public = true)] -[SpacetimeDB.Index.BTree(Accessor = "idx_age", Columns = new[] { "Age" })] +[SpacetimeDB.Index.BTree(Accessor = "IdxAge", Columns = new[] { "Age" })] public partial struct User { [SpacetimeDB.PrimaryKey] @@ -270,6 +270,10 @@ Multi-column indexes support: - **Prefix match**: Queries that specify the leftmost columns in order - **Range on trailing column**: A prefix of equality conditions followed by a range on the next column +For indexes with three or more columns, the same rule applies: provide any leftmost prefix of exact +values, optionally followed by a range on the next column. You cannot put a range in the middle and +then continue with more exact values. + A multi-column index on `(player_id, level)` accelerates these queries: - `player_id = 123` (prefix match on first column) - `player_id = 123 AND level = 5` (full match) @@ -286,11 +290,11 @@ const score = table( name: 'score', public: true, indexes: [ - { accessor: 'by_player_and_level', algorithm: 'btree', columns: ['player_id', 'level'] }, + { accessor: 'byPlayerAndLevel', algorithm: 'btree', columns: ['playerId', 'level'] }, ], }, { - player_id: t.u32(), + playerId: t.u32(), level: t.u32(), points: t.i64(), } @@ -302,7 +306,7 @@ const score = table( ```csharp [SpacetimeDB.Table(Accessor = "Score", Public = true)] -[SpacetimeDB.Index.BTree(Accessor = "by_player_and_level", Columns = new[] { "PlayerId", "Level" })] +[SpacetimeDB.Index.BTree(Accessor = "ByPlayerAndLevel", Columns = new[] { "PlayerId", "Level" })] public partial struct Score { public uint PlayerId; @@ -493,7 +497,7 @@ for (auto user : ctx.db[user_age].filter(range_to(uint8_t(18)))) { } ``` -Use range query functions: `range_inclusive()`, `range_from()`, `range_to()`, and `range_to_inclusive()`. Include `` for full range query support. +Use range query functions: `range_inclusive()`, `range_from()`, `range_to()`, `range_to_inclusive()`, and `range_full()`. These helpers are included by `spacetimedb.h`; include `` directly only when you are not using the umbrella header. @@ -509,12 +513,12 @@ For multi-column indexes, pass a tuple of values. You can specify exact values f import { Range } from 'spacetimedb/server'; // Find all scores for player 123 (prefix match on first column) -for (const score of ctx.db.score.by_player_and_level.filter(123)) { +for (const score of ctx.db.score.byPlayerAndLevel.filter(123)) { console.log(`Level ${score.level}: ${score.points} points`); } // Find scores for player 123 at levels 1-10 (inclusive) -for (const score of ctx.db.score.by_player_and_level.filter([ +for (const score of ctx.db.score.byPlayerAndLevel.filter([ 123, new Range({ tag: 'included', value: 1 }, { tag: 'included', value: 10 }) ])) { @@ -522,7 +526,7 @@ for (const score of ctx.db.score.by_player_and_level.filter([ } // Find the exact score for player 123 at level 5 -for (const score of ctx.db.score.by_player_and_level.filter([123, 5])) { +for (const score of ctx.db.score.byPlayerAndLevel.filter([123, 5])) { console.log(`Points: ${score.points}`); } ``` @@ -532,7 +536,7 @@ for (const score of ctx.db.score.by_player_and_level.filter([123, 5])) { ```csharp // Find all scores for player 123 -foreach (var score in ctx.Db.Score.by_player_and_level.Filter(123u)) +foreach (var score in ctx.Db.Score.ByPlayerAndLevel.Filter(123u)) { Log.Info($"Level {score.Level}: {score.Points} points"); } diff --git a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md index 50bcc4abc22..21d307f5cda 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md +++ b/docs/docs/00200-core-concepts/00300-tables/00400-access-permissions.md @@ -348,7 +348,7 @@ See the [Procedures documentation](../00200-functions/00400-procedures.md) for m ## Views - Read-Only Access -[Views](../00200-functions/00500-views.md) receive a `ViewContext` or `AnonymousViewContext` which provides read-only access to all tables (both public and private). They can query and iterate tables, but cannot insert, update, or delete rows. +[Views](../00200-functions/00500-views.md) receive a `ViewContext` or `AnonymousViewContext` which provides read-only access to all tables (both public and private). They can query tables through indexed lookups, but cannot scan full tables or insert, update, or delete rows. @@ -445,7 +445,7 @@ const spacetimedb = schema({ message }); export default spacetimedb; // Public view that only returns messages the caller can see -export const my_messages = spacetimedb.view( +export const myMessages = spacetimedb.view( { name: 'my_messages', public: true }, t.array(message.rowType), (ctx) => { @@ -463,7 +463,7 @@ export const my_messages = spacetimedb.view( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { // Private table containing all messages [SpacetimeDB.Table(Accessor = "Message")] // Private by default @@ -490,8 +490,7 @@ public partial class Module sent.AddRange(received); return sent; } - - +} ``` @@ -615,7 +614,7 @@ export const myProfile = spacetimedb.view( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { // Private table with sensitive data [SpacetimeDB.Table(Accessor = "UserAccount")] // Private by default @@ -787,7 +786,7 @@ const colleague = t.row('Colleague', { }); // View that returns colleagues in the caller's department, without salary info -export const my_colleagues = spacetimedb.view( +export const myColleagues = spacetimedb.view( { name: 'my_colleagues', public: true }, t.array(colleague), (ctx) => { @@ -812,7 +811,7 @@ export const my_colleagues = spacetimedb.view( ```csharp using SpacetimeDB; -public partial class Module +public static partial class Module { // Private table with all employee data [SpacetimeDB.Table(Accessor = "Employee")] diff --git a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md index 0c3f5df4518..7c2178cfaf1 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md +++ b/docs/docs/00200-core-concepts/00300-tables/00500-schedule-tables.md @@ -176,6 +176,8 @@ To schedule an action, insert a row into the schedule table with a `scheduled_at - **At intervals** - Execute repeatedly at fixed time intervals (e.g., every 5 seconds) - **At specific times** - Execute once at an absolute timestamp +Interval schedules are anchored to their intended execution times. In the current implementation, if the database is busy or offline long enough to miss one or more interval ticks, SpacetimeDB schedules the next future tick rather than running missed ticks back-to-back or drifting the schedule from the delayed execution time. + ### Scheduling at Intervals Use intervals for periodic tasks like game ticks, heartbeats, or recurring maintenance: @@ -405,6 +407,8 @@ ctx.db[reminder].insert(Reminder{ 3. **When the time arrives**, the specified reducer/procedure is automatically called with the row as a parameter 4. **The row is typically deleted** or updated by the reducer after processing +For interval schedules, the current implementation calculates the next run from the previous intended run time. Missed interval ticks are skipped, so a delayed scheduled reducer or procedure resumes on the next future interval boundary. + ### Row Lifecycle SpacetimeDB passes the schedule row to the scheduled reducer or procedure as an argument. One-shot schedule rows are removed at different times depending on the kind of function being called: diff --git a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md index 4bc49a08546..4f6b53829ea 100644 --- a/docs/docs/00200-core-concepts/00300-tables/00600-performance.md +++ b/docs/docs/00200-core-concepts/00300-tables/00600-performance.md @@ -117,16 +117,16 @@ const player = table( id: t.u32(), name: t.string(), // Game state - position_x: t.f32(), - position_y: t.f32(), + positionX: t.f32(), + positionY: t.f32(), health: t.u32(), // Statistics (rarely accessed) - total_kills: t.u32(), - total_deaths: t.u32(), - play_time_seconds: t.u64(), + totalKills: t.u32(), + totalDeaths: t.u32(), + playTimeSeconds: t.u64(), // Settings (rarely changed) - audio_volume: t.f32(), - graphics_quality: t.u8(), + audioVolume: t.f32(), + graphicsQuality: t.u8(), } ); ``` @@ -218,9 +218,9 @@ const player = table( const playerState = table( { name: 'player_state' }, { - player_id: t.u32().unique(), - position_x: t.f32(), - position_y: t.f32(), + playerId: t.u32().unique(), + positionX: t.f32(), + positionY: t.f32(), health: t.u32(), } ); @@ -228,19 +228,19 @@ const playerState = table( const playerStats = table( { name: 'player_stats' }, { - player_id: t.u32().unique(), - total_kills: t.u32(), - total_deaths: t.u32(), - play_time_seconds: t.u64(), + playerId: t.u32().unique(), + totalKills: t.u32(), + totalDeaths: t.u32(), + playTimeSeconds: t.u64(), } ); const playerSettings = table( { name: 'player_settings' }, { - player_id: t.u32().unique(), - audio_volume: t.f32(), - graphics_quality: t.u8(), + playerId: t.u32().unique(), + audioVolume: t.f32(), + graphicsQuality: t.u8(), } ); ``` @@ -388,8 +388,8 @@ Use the smallest integer type that fits your data range: ```typescript // If you only need 0-255, use u8 instead of u64 level: t.u8(), // Not t.u64() -player_count: t.u16(), // Not t.u64() -entity_id: t.u32(), // Not t.u64() +playerCount: t.u16(), // Not t.u64() +entityId: t.u32(), // Not t.u64() ``` @@ -515,7 +515,7 @@ When inserting or updating multiple rows, batch them in a single reducer call ra ```typescript -export const spawn_enemies = spacetimedb.reducer({ count: t.u32() }, (ctx, { count }) => { +export const spawnEnemies = spacetimedb.reducer({ count: t.u32() }, (ctx, { count }) => { for (let i = 0; i < count; i++) { ctx.db.enemy.insert({ id: 0n, // auto_inc diff --git a/docs/docs/00200-core-concepts/00500-authentication/00500-usage.md b/docs/docs/00200-core-concepts/00500-authentication/00500-usage.md index f107ea77513..a022cc1dc65 100644 --- a/docs/docs/00200-core-concepts/00500-authentication/00500-usage.md +++ b/docs/docs/00200-core-concepts/00500-authentication/00500-usage.md @@ -217,8 +217,12 @@ As an example, let's say that your tokens have a "roles" claim, which is a list ```typescript +import { SenderError, type InferSchema, type ReducerCtx } from 'spacetimedb/server'; + +type Ctx = ReducerCtx>; + // Return an error to the client if they don't have admin rights. -function ensureAdminAccess(ctx: ReducerCtx) { +function ensureAdminAccess(ctx: Ctx) { const auth = ctx.senderAuth; if (auth.isInternal) { return; diff --git a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md index 4980dc1b9f5..68d84fe951d 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md @@ -145,7 +145,7 @@ Configure the URI of the SpacetimeDB instance or cluster which hosts the remote ```typescript class DbConnectionBuilder { - public withDatabaseName(name_or_identity: string): DbConnectionBuilder; + public withDatabaseName(nameOrIdentity: string): DbConnectionBuilder; } ``` @@ -545,7 +545,7 @@ Throws an error if the subscription has already ended, either due to a previous ```typescript class SubscriptionHandle { - public unsubscribeThen(on_end: (ctx: SubscriptionEventContext) => void): void; + public unsubscribeThen(onEnd: (ctx: SubscriptionEventContext) => void): void; } ``` diff --git a/docs/docs/00300-resources/00100-how-to/00300-logging.md b/docs/docs/00300-resources/00100-how-to/00300-logging.md index 280c57c06a8..84ec1b5e365 100644 --- a/docs/docs/00300-resources/00100-how-to/00300-logging.md +++ b/docs/docs/00300-resources/00100-how-to/00300-logging.md @@ -23,7 +23,7 @@ import { schema, t } from 'spacetimedb/server'; const spacetimedb = schema({ /* tables */ }); export default spacetimedb; -export const process_data = spacetimedb.reducer({ value: t.u32() }, (ctx, { value }) => { +export const processData = spacetimedb.reducer({ value: t.u32() }, (ctx, { value }) => { console.log(`Processing data with value: ${value}`); if (value > 100) { @@ -222,10 +222,10 @@ Use appropriate log levels for different types of messages: Include relevant context in your log messages: ```typescript -export const transfer_credits = spacetimedb.reducer( - { to_user: t.u64(), amount: t.u32() }, - (ctx, { to_user, amount }) => { - console.log(`Credit transfer: from=${ctx.sender}, to=${to_user}, amount=${amount}`); +export const transferCredits = spacetimedb.reducer( + { toUser: t.u64(), amount: t.u32() }, + (ctx, { toUser, amount }) => { + console.log(`Credit transfer: from=${ctx.sender}, to=${toUser}, amount=${amount}`); // ... transfer logic } diff --git a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md index 7327d99e97c..77bffc4b944 100644 --- a/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md +++ b/docs/docs/00300-resources/00100-how-to/00400-row-level-security.md @@ -73,7 +73,7 @@ using SpacetimeDB; #pragma warning disable STDB_UNSTABLE -public partial class Module +public static partial class Module { /// /// A client can only see their account. @@ -149,7 +149,7 @@ using SpacetimeDB; #pragma warning disable STDB_UNSTABLE -public partial class Module +public static partial class Module { /// /// A client can only see their account. @@ -228,7 +228,7 @@ export const playerFilter = spacetimedb.clientVisibilityFilter.sql( ```cs using SpacetimeDB; -public partial class Module +public static partial class Module { /// /// A client can only see their account. @@ -313,7 +313,7 @@ export const playerFilter = spacetimedb.clientVisibilityFilter.sql(` ```cs using SpacetimeDB; -public partial class Module +public static partial class Module { /// /// A client can only see players on their same level. @@ -374,7 +374,7 @@ export const playerFilter = spacetimedb.clientVisibilityFilter.sql( ```cs using SpacetimeDB; -public partial class Module +public static partial class Module { /// /// An account must have a corresponding player. @@ -388,7 +388,7 @@ public partial class Module /// A player must have a corresponding account. /// [SpacetimeDB.ClientVisibilityFilter] - public static readonly Filter ACCOUNT_FILTER = new Filter.Sql( + public static readonly Filter PLAYER_FILTER = new Filter.Sql( "SELECT p.* FROM account a JOIN player p ON a.id = p.id WHERE a.identity = :sender" ); } diff --git a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md index 6a065748f69..1a8e9295db0 100644 --- a/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md +++ b/docs/docs/00300-resources/00200-reference/00100-cli-reference/00100-cli-reference.md @@ -132,7 +132,7 @@ Run `spacetime help publish` for more detailed information. * `--no-config` — Ignore spacetime.json configuration * `--env ` — Environment name for config file layering (e.g., dev, staging) -* `--native-aot` — Use NativeAOT-LLVM compilation for C# modules (experimental, Windows only) +* `--native-aot` — Use NativeAOT-LLVM compilation for C# modules (experimental; supported on Windows, and on Linux with .NET 10) * `--dotnet-version ` — Target .NET SDK major version for C# projects (e.g. 8 or 10). Auto-detected when omitted. @@ -466,7 +466,7 @@ Initializes a new spacetime project. * `-t`, `--template