From c8b3c952bd383c0c0a66b826617787bddbaed95f Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Tue, 16 Jun 2026 12:07:22 -0400 Subject: [PATCH 1/7] Fix TypeScript optional row keys (#4940) # Description of Changes Fix TypeScript inference for generated object shapes containing `Option` fields. Previously, generated TypeScript represented an optional SATS field as a required object key whose value could be `undefined`: ```ts { foo: string | undefined; } ``` This PR changes those generated shapes to make the key itself optional: ```ts { foo?: string | undefined; } ``` The original issue was reported in #4516: generated TypeScript reducer/procedure calls required users to pass explicit `undefined` values for omitted optional arguments. This PR fixes that for reducer and procedure params, and also applies the same optional-key inference to generated row shapes. This is a fresh bot-owned replacement for stalled PR #4518. ## Examples A reducer with an optional argument now generates a TypeScript call shape where the optional key may be omitted: ```rust #[spacetimedb::reducer] pub fn update_category( _ctx: &spacetimedb::ReducerContext, id: u64, name: String, button_text: Option, ) { // ... } ``` Before this PR, generated TypeScript callers had to pass the optional argument explicitly: ```ts await conn.reducers.updateCategory({ id: 1n, name: 'updated category name', buttonText: undefined, }); ``` After this PR, generated TypeScript callers can omit the optional key: ```ts await conn.reducers.updateCategory({ id: 1n, name: 'updated category name', }); ``` A table row with an optional column also changes its generated TypeScript row shape: ```rust #[spacetimedb::table(name = player, public)] pub struct Player { #[primary_key] pub id: u64, pub display_name: String, pub alias: Option, } ``` Before this PR, generated TypeScript treated `alias` as a required key: ```ts type Player = { id: bigint; displayName: string; alias: string | undefined; }; ``` After this PR, generated TypeScript treats `alias` as an optional key: ```ts type Player = { id: bigint; displayName: string; alias?: string | undefined; }; ``` # API and ABI breaking changes This is a TypeScript source/API breaking change for generated type shapes that contain `Option` fields. It is not an ABI or wire-format break: SATS encoding and decoding of `Option` are unchanged. The breaking edge is structural TypeScript code that requires optional SATS fields to exist as object properties. For example, code like this may stop compiling: ```ts type RequiresAliasProperty = { id: bigint; displayName: string; alias: string | undefined; }; function renderPlayer(player: RequiresAliasProperty) { return player.alias ?? player.displayName; } conn.db.player.onInsert((_ctx, player) => { renderPlayer(player); }); ``` With this PR, the generated `player` row has `alias?: string | undefined`, so it is not assignable to a type requiring an `alias` property. Code using `Required`, explicit generated-row mirror interfaces, or generic constraints that require optional SATS fields to be present as object keys may need to loosen those keys to optional properties. Reducer and procedure params are primarily loosened by this change. Existing calls that pass `undefined` explicitly should continue to typecheck, while calls can now omit optional keys. # Expected complexity level and risk 2. The change is contained to TypeScript type inference and generated TypeScript test coverage. The main risk is source compatibility for TypeScript consumers that depend on the old required-key shape for `Option` fields. # Testing - [x] `pnpm --filter @clockworklabs/test-app build` - [x] `pnpm test` - [x] targeted `prettier --check` - [x] `git diff --check` Closes #4516. --------- Co-authored-by: clockwork-labs-bot --- .../src/lib/type_builders.test-d.ts | 2 + .../src/lib/type_builders.ts | 42 +++++++++++++++++-- .../src/sdk/db_connection_impl.ts | 15 +++++-- .../src/sdk/event_context.ts | 6 +-- .../bindings-typescript/src/sdk/procedures.ts | 8 +++- .../bindings-typescript/src/sdk/reducers.ts | 7 ++-- .../src/sdk/table_cache.ts | 3 +- .../bindings-typescript/src/sdk/type_utils.ts | 6 +-- .../bindings-typescript/src/server/views.ts | 7 +++- .../src/svelte/useReducer.ts | 4 +- .../test-app/server/src/lib.rs | 8 ++++ .../bindings-typescript/test-app/src/App.tsx | 2 + .../test-app/src/module_bindings/index.ts | 8 ++-- .../set_player_alias_reducer.ts | 16 +++++++ .../src/module_bindings/types/reducers.ts | 2 + 15 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 crates/bindings-typescript/test-app/src/module_bindings/set_player_alias_reducer.ts diff --git a/crates/bindings-typescript/src/lib/type_builders.test-d.ts b/crates/bindings-typescript/src/lib/type_builders.test-d.ts index d0595f25829..bae3089e145 100644 --- a/crates/bindings-typescript/src/lib/type_builders.test-d.ts +++ b/crates/bindings-typescript/src/lib/type_builders.test-d.ts @@ -35,6 +35,8 @@ const rowOptionOptional = { }; type RowOptionOptional = InferTypeOfRow; // eslint-disable-next-line @typescript-eslint/no-unused-vars +const _rowOptionOptionalOmitted: RowOptionOptional = {}; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const _rowOptionOptionalNone: RowOptionOptional = { foo: undefined, }; diff --git a/crates/bindings-typescript/src/lib/type_builders.ts b/crates/bindings-typescript/src/lib/type_builders.ts index 6bf8ae1ec99..136e040a2b6 100644 --- a/crates/bindings-typescript/src/lib/type_builders.ts +++ b/crates/bindings-typescript/src/lib/type_builders.ts @@ -39,9 +39,45 @@ export type Infer = T extends RowObj /** * Helper type to extract the type of a row from an object. */ -export type InferTypeOfRow = { - [K in keyof T & string]: InferTypeOfTypeBuilder>; -}; +type OptionalRowKeys = { + [K in keyof T & string]-?: CollapseColumn extends OptionBuilder + ? K + : never; +}[keyof T & string]; + +type RequiredRowKeys = Exclude< + keyof T & string, + OptionalRowKeys +>; + +export type InferTypeOfRow = Prettify< + { + [K in RequiredRowKeys]: InferTypeOfTypeBuilder>; + } & { + [K in OptionalRowKeys]?: InferTypeOfTypeBuilder>; + } +>; + +type OptionalParamKeys = { + [K in keyof T & string]-?: undefined extends InferTypeOfTypeBuilder< + CollapseColumn + > + ? K + : never; +}[keyof T & string]; + +type RequiredParamKeys = Exclude< + keyof T & string, + OptionalParamKeys +>; + +export type InferTypeOfParams = Prettify< + { + [K in RequiredParamKeys]: InferTypeOfTypeBuilder>; + } & { + [K in OptionalParamKeys]?: InferTypeOfTypeBuilder>; + } +>; /** * Helper type to extract the type of a row from an object. diff --git a/crates/bindings-typescript/src/sdk/db_connection_impl.ts b/crates/bindings-typescript/src/sdk/db_connection_impl.ts index b444794500c..540ecabf247 100644 --- a/crates/bindings-typescript/src/sdk/db_connection_impl.ts +++ b/crates/bindings-typescript/src/sdk/db_connection_impl.ts @@ -23,7 +23,12 @@ import { type SubscriptionEventContextInterface, } from './event_context.ts'; import { EventEmitter } from './event_emitter.ts'; -import type { Deserializer, Identity, InferTypeOfRow, Serializer } from '../'; +import type { + Deserializer, + Identity, + InferTypeOfParams, + Serializer, +} from '../'; import type { ProcedureResultMessage, ReducerResultMessage, @@ -383,7 +388,9 @@ export class DbConnectionImpl const { serialize: serializeArgs } = this.#reducerArgsSerializers[reducerName]; - (out as any)[key] = (params: InferTypeOfRow) => { + (out as any)[key] = ( + params: InferTypeOfParams + ) => { const writer = this.#reducerArgsEncoder; writer.clear(); serializeArgs(writer, params); @@ -414,7 +421,7 @@ export class DbConnectionImpl this.#procedureSerializers[procedureName]; (out as any)[key] = ( - params: InferTypeOfRow + params: InferTypeOfParams ): Promise => { writer.clear(); serializeArgs(writer, params); @@ -436,7 +443,7 @@ export class DbConnectionImpl event: Event< ReducerEventInfo< RemoteModule['reducers'][number]['name'], - InferTypeOfRow + InferTypeOfParams > > ): EventContextInterface { diff --git a/crates/bindings-typescript/src/sdk/event_context.ts b/crates/bindings-typescript/src/sdk/event_context.ts index 705ffeb0147..9efc0100166 100644 --- a/crates/bindings-typescript/src/sdk/event_context.ts +++ b/crates/bindings-typescript/src/sdk/event_context.ts @@ -1,4 +1,4 @@ -import type { InferTypeOfRow } from '../lib/type_builders.ts'; +import type { InferTypeOfParams } from '../lib/type_builders.ts'; import type { DbContext } from './db_context'; import type { Event } from './event.ts'; import type { ReducerEvent } from './reducer_event.ts'; @@ -13,7 +13,7 @@ export interface EventContextInterface event: Event< ReducerEventInfo< RemoteModule['reducers'][number]['name'], - InferTypeOfRow + InferTypeOfParams > >; } @@ -25,7 +25,7 @@ export interface ReducerEventContextInterface< event: ReducerEvent< ReducerEventInfo< RemoteModule['reducers'][number]['name'], - InferTypeOfRow + InferTypeOfParams > >; } diff --git a/crates/bindings-typescript/src/sdk/procedures.ts b/crates/bindings-typescript/src/sdk/procedures.ts index b281662a976..e8ac3a70b88 100644 --- a/crates/bindings-typescript/src/sdk/procedures.ts +++ b/crates/bindings-typescript/src/sdk/procedures.ts @@ -1,5 +1,9 @@ import type { ParamsObj } from '../lib/reducers'; -import type { Infer, InferTypeOfRow, TypeBuilder } from '../lib/type_builders'; +import type { + Infer, + InferTypeOfParams, + TypeBuilder, +} from '../lib/type_builders'; import type { CamelCase } from '../lib/type_util'; import { coerceParams, toCamelCase, type CoerceParams } from '../lib/util'; import type { UntypedRemoteModule } from './spacetime_module'; @@ -20,7 +24,7 @@ export type ProceduresView = IfAny< ? // x: camelCase(name) { [K in RemoteModule['procedures'][number] as K['accessorName']]: ( - params: InferTypeOfRow + params: InferTypeOfParams ) => Promise>; } : never diff --git a/crates/bindings-typescript/src/sdk/reducers.ts b/crates/bindings-typescript/src/sdk/reducers.ts index db4f72b1b50..566df5c3076 100644 --- a/crates/bindings-typescript/src/sdk/reducers.ts +++ b/crates/bindings-typescript/src/sdk/reducers.ts @@ -1,8 +1,7 @@ import type { ProductType } from '../lib/algebraic_type'; import type { ReducerSchema } from '../lib/reducer_schema'; import type { ParamsObj } from '../lib/reducers'; -import type { CoerceRow } from '../lib/table'; -import { RowBuilder, type InferTypeOfRow } from '../lib/type_builders'; +import { RowBuilder, type InferTypeOfParams } from '../lib/type_builders'; import { toCamelCase } from '../lib/util'; import type { SubscriptionEventContextInterface } from './event_context'; import type { UntypedRemoteModule } from './spacetime_module'; @@ -26,7 +25,7 @@ export type ReducersView = IfAny< RemoteModule extends UntypedRemoteModule ? { [K in RemoteModule['reducers'][number] as K['accessorName']]: ( - params: InferTypeOfRow + params: InferTypeOfParams ) => Promise; } : never @@ -43,7 +42,7 @@ export type ReducerEventInfo< export type UntypedReducerDef = { name: string; accessorName: string; - params: CoerceRow; + params: ParamsObj; paramsType: ProductType; }; diff --git a/crates/bindings-typescript/src/sdk/table_cache.ts b/crates/bindings-typescript/src/sdk/table_cache.ts index 42645daaecf..543d3fb78ed 100644 --- a/crates/bindings-typescript/src/sdk/table_cache.ts +++ b/crates/bindings-typescript/src/sdk/table_cache.ts @@ -123,7 +123,8 @@ export class TableCacheImpl< const columns = idx.columns; // Extract the tuple key for this btree index (column order preserved) - const getKey = (row: Row): readonly unknown[] => columns.map(c => row[c]); + const getKey = (row: Row): readonly unknown[] => + columns.map(c => (row as Record)[c]); // The server’s ranged scan fixes all prefix cols to equality and applies // the bound only to the *last* term. We mirror that. diff --git a/crates/bindings-typescript/src/sdk/type_utils.ts b/crates/bindings-typescript/src/sdk/type_utils.ts index dcfc3332be1..99ae49ca3b1 100644 --- a/crates/bindings-typescript/src/sdk/type_utils.ts +++ b/crates/bindings-typescript/src/sdk/type_utils.ts @@ -1,4 +1,4 @@ -import type { Infer, InferTypeOfRow } from '.'; +import type { Infer, InferTypeOfParams } from '.'; import type { Prettify } from '../lib/type_util'; import type { UntypedProcedureDef } from './procedures'; import type { UntypedReducerDef } from './reducers'; @@ -7,11 +7,11 @@ export type IsEmptyObject = [keyof T] extends [never] ? true : false; export type MaybeParams = IsEmptyObject extends true ? [] : [params: T]; export type ParamsType = MaybeParams< - Prettify> + Prettify> >; export type ProcedureParamsType

= MaybeParams< - Prettify> + Prettify> >; export type ProcedureReturnType

= Infer< diff --git a/crates/bindings-typescript/src/server/views.ts b/crates/bindings-typescript/src/server/views.ts index 69fa159e999..58e77062db0 100644 --- a/crates/bindings-typescript/src/server/views.ts +++ b/crates/bindings-typescript/src/server/views.ts @@ -327,8 +327,11 @@ type ViewInfo = { returnTypeBaseSize: number; }; -export type Views = ViewInfo>[]; -export type AnonViews = ViewInfo>[]; +type AnyViewFn = (ctx: ViewCtx, params: any) => any; +type AnyAnonymousViewFn = (ctx: AnonymousViewCtx, params: any) => any; + +export type Views = ViewInfo[]; +export type AnonViews = ViewInfo[]; // A helper to get the product type out of a type builder. // This is only non-never if the type builder is an array. diff --git a/crates/bindings-typescript/src/svelte/useReducer.ts b/crates/bindings-typescript/src/svelte/useReducer.ts index cd554913ee8..236cc8c5faa 100644 --- a/crates/bindings-typescript/src/svelte/useReducer.ts +++ b/crates/bindings-typescript/src/svelte/useReducer.ts @@ -1,6 +1,6 @@ import { onDestroy } from 'svelte'; import { get } from 'svelte/store'; -import type { InferTypeOfRow } from '../lib/type_builders'; +import type { InferTypeOfParams } from '../lib/type_builders'; import type { UntypedReducerDef } from '../sdk/reducers'; import { useSpacetimeDB } from './useSpacetimeDB'; import type { Prettify } from '../lib/type_util'; @@ -9,7 +9,7 @@ type IsEmptyObject = [keyof T] extends [never] ? true : false; type MaybeParams = IsEmptyObject extends true ? [] : [params: T]; type ParamsType = MaybeParams< - Prettify> + Prettify> >; export function useReducer( diff --git a/crates/bindings-typescript/test-app/server/src/lib.rs b/crates/bindings-typescript/test-app/server/src/lib.rs index b2c95d3ea33..e7ce9b0ab57 100644 --- a/crates/bindings-typescript/test-app/server/src/lib.rs +++ b/crates/bindings-typescript/test-app/server/src/lib.rs @@ -47,6 +47,14 @@ pub fn create_player(ctx: &ReducerContext, name: String, location: Point) { }); } +#[reducer] +pub fn set_player_alias(ctx: &ReducerContext, name: String, alias: Option) { + ctx.db.user().insert(User { + identity: ctx.sender(), + username: alias.unwrap_or(name), + }); +} + #[view(accessor = my_user_procedural, public, primary_key = id)] pub fn my_user_procedural(ctx: &ViewContext) -> Vec { ctx.db.player().id().find(1u32).into_iter().collect() diff --git a/crates/bindings-typescript/test-app/src/App.tsx b/crates/bindings-typescript/test-app/src/App.tsx index 5e989140525..2b9d9250c05 100644 --- a/crates/bindings-typescript/test-app/src/App.tsx +++ b/crates/bindings-typescript/test-app/src/App.tsx @@ -18,6 +18,7 @@ function App() { } ); const createPlayer = useReducer(reducers.createPlayer); + const setPlayerAlias = useReducer(reducers.setPlayerAlias); useEffect(() => { setTimeout(() => { @@ -38,6 +39,7 @@ function App() { }; console.log('Creating player:', player); createPlayer(player); + setPlayerAlias({ name: player.name }); }} > Update diff --git a/crates/bindings-typescript/test-app/src/module_bindings/index.ts b/crates/bindings-typescript/test-app/src/module_bindings/index.ts index 49090beab53..78ba2f60fc7 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/index.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.3.0 (commit d03addfc90a0cedebcd6652c4a57f84950c45f3b). +// This was generated using spacetimedb cli version 2.6.0 (commit 35d1b52c8b9981bc3df439e040cdeccf49454e1c). /* eslint-disable */ /* tslint:disable */ @@ -35,6 +35,7 @@ import { // Import all reducer arg schemas import CreatePlayerReducer from './create_player_reducer'; +import SetPlayerAliasReducer from './set_player_alias_reducer'; // Import all procedure arg schemas @@ -119,7 +120,8 @@ const tablesSchema = __schema({ /** The schema information for all reducers in this module. This is defined the same way as the reducers would have been defined in the server, except the body of the reducer is omitted in code generation. */ const reducersSchema = __reducers( - __reducerSchema('create_player', CreatePlayerReducer) + __reducerSchema('create_player', CreatePlayerReducer), + __reducerSchema('set_player_alias', SetPlayerAliasReducer) ); /** The schema information for all procedures in this module. This is defined the same way as the procedures would have been defined in the server. */ @@ -128,7 +130,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '2.3.0' as const, + cliVersion: '2.6.0' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/test-app/src/module_bindings/set_player_alias_reducer.ts b/crates/bindings-typescript/test-app/src/module_bindings/set_player_alias_reducer.ts new file mode 100644 index 00000000000..1c9e8a7e5c4 --- /dev/null +++ b/crates/bindings-typescript/test-app/src/module_bindings/set_player_alias_reducer.ts @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +/* eslint-disable */ +/* tslint:disable */ +import { + TypeBuilder as __TypeBuilder, + t as __t, + type AlgebraicTypeType as __AlgebraicTypeType, + type Infer as __Infer, +} from '../../../src/index'; + +export default { + name: __t.string(), + alias: __t.option(__t.string()), +}; diff --git a/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts b/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts index 1d7e61f99ef..85aeed2836d 100644 --- a/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts +++ b/crates/bindings-typescript/test-app/src/module_bindings/types/reducers.ts @@ -7,5 +7,7 @@ import { type Infer as __Infer } from '../../../../src/index'; // Import all reducer arg schemas import CreatePlayerReducer from '../create_player_reducer'; +import SetPlayerAliasReducer from '../set_player_alias_reducer'; export type CreatePlayerParams = __Infer; +export type SetPlayerAliasParams = __Infer; From b235ce45ff5d540a3c3942c0b6ae97e4a49dbcae Mon Sep 17 00:00:00 2001 From: Kilian Strunz <93079615+kistz@users.noreply.github.com> Date: Wed, 17 Jun 2026 06:00:55 +0200 Subject: [PATCH 2/7] [Procedures] Fix `Identity`and `ConnectionId` Regression (#5323) Closes #5250 the sender and connection_id from the caller while now it is always empty (which is wrong) Correct it. Also fully migrate to `database_identity` which was forgotten about so i deprecated it for procedures (since they are now stable) and just changed it for HttpHandlers (because they are still unstable) @gefjon since you did the lil woopsie (ugh pinging you again lol hope im not annoying haha) Breaking the HttpHandler function which is unstable. Restoring behaviour of 2.3 which got lost with 2.4. 1. Trivial refactoring - [x] The caller identity is there again for Procedures. --------- Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> Co-authored-by: joshua-spacetime Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- crates/bindings/src/http.rs | 10 ++++++++-- crates/bindings/src/lib.rs | 26 +++++++++++++++++++------- modules/module-test/src/lib.rs | 2 +- modules/sdk-test-procedure/src/lib.rs | 12 +++++++----- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/crates/bindings/src/http.rs b/crates/bindings/src/http.rs index ec476bba46e..0fb8a63d53b 100644 --- a/crates/bindings/src/http.rs +++ b/crates/bindings/src/http.rs @@ -127,18 +127,24 @@ impl HandlerContext { } /// Read the current module's [`Identity`]. + #[deprecated(note = "Use `HandlerContext::database_identity` instead.")] pub fn identity(&self) -> Identity { + self.database_identity() + } + + /// Read the current module's [`Identity`]. + pub fn database_identity(&self) -> Identity { Identity::from_byte_array(spacetimedb_bindings_sys::identity()) } /// Acquire a mutable transaction and execute `body` with read-write access. pub fn with_tx(&mut self, body: impl Fn(&TxContext) -> T) -> T { - with_tx(body) + with_tx(body, Identity::ZERO, None) } /// Acquire a mutable transaction and execute `body` with read-write access. pub fn try_with_tx(&mut self, body: impl Fn(&TxContext) -> Result) -> Result { - try_with_tx(body) + try_with_tx(body, Identity::ZERO, None) } /// Create a new random [`Uuid`] `v4` using the built-in RNG. diff --git a/crates/bindings/src/lib.rs b/crates/bindings/src/lib.rs index 62b78e14be4..0d375719829 100644 --- a/crates/bindings/src/lib.rs +++ b/crates/bindings/src/lib.rs @@ -1180,7 +1180,14 @@ impl Deref for TxContext { } } -fn try_with_tx(body: impl Fn(&TxContext) -> Result) -> Result { +/// We need to passthrough identity and connection_id because procedures can be invoked by users. +/// For [HttpContext] this is always anonymous ([Identity::ZERO]). +/// Construct the inner [ReducerContext] with the appropriate caller information. +fn try_with_tx( + body: impl Fn(&TxContext) -> Result, + identity: Identity, + connection_id: Option, +) -> Result { let abort = || { crate::sys::procedure::procedure_abort_mut_tx() .expect("should have a pending mutable anon tx as `procedure_start_mut_tx` preceded") @@ -1191,8 +1198,7 @@ fn try_with_tx(body: impl Fn(&TxContext) -> Result) -> Result .expect("holding `&mut HandlerContext`, so should not be in a tx already; called manually elsewhere?"); let timestamp = Timestamp::from_micros_since_unix_epoch(timestamp); - // Use the internal auth context (no external caller identity). - let tx = ReducerContext::new(crate::Local {}, Identity::ZERO, None, timestamp); + let tx = ReducerContext::new(crate::Local {}, identity, connection_id, timestamp); let tx = TxContext(tx); struct DoOnDrop(F); @@ -1225,9 +1231,9 @@ fn try_with_tx(body: impl Fn(&TxContext) -> Result) -> Result res } -fn with_tx(body: impl Fn(&TxContext) -> T) -> T { +fn with_tx(body: impl Fn(&TxContext) -> T, identity: Identity, connection_id: Option) -> T { use core::convert::Infallible; - match try_with_tx::(|tx| Ok(body(tx))) { + match try_with_tx::(|tx| Ok(body(tx)), identity, connection_id) { Ok(v) => v, Err(e) => match e {}, } @@ -1293,7 +1299,13 @@ impl ProcedureContext { } /// Read the current module's [`Identity`]. + #[deprecated(note = "Use `ProcedureContext::database_identity` instead.")] pub fn identity(&self) -> Identity { + self.database_identity() + } + + /// Read the current module's [`Identity`]. + pub fn database_identity(&self) -> Identity { // Hypothetically, we *could* read the module identity out of the system tables. // However, this would be: // - Onerous, because we have no tooling to inspect the system tables from module code. @@ -1359,7 +1371,7 @@ impl ProcedureContext { /// callers should avoid writing to any captured mutable state within `body`, /// This includes interior mutability through types like [`std::cell::Cell`]. pub fn with_tx(&mut self, body: impl Fn(&TxContext) -> T) -> T { - with_tx(body) + with_tx(body, self.sender(), self.connection_id()) } /// Acquire a mutable transaction @@ -1392,7 +1404,7 @@ impl ProcedureContext { /// callers should avoid writing to any captured mutable state within `body`, /// This includes interior mutability through types like [`std::cell::Cell`]. pub fn try_with_tx(&mut self, body: impl Fn(&TxContext) -> Result) -> Result { - try_with_tx(body) + try_with_tx(body, self.sender(), self.connection_id()) } /// Create a new random [`Uuid`] `v4` using the built-in RNG. diff --git a/modules/module-test/src/lib.rs b/modules/module-test/src/lib.rs index 56e6b288e2d..3cf7b75d099 100644 --- a/modules/module-test/src/lib.rs +++ b/modules/module-test/src/lib.rs @@ -543,7 +543,7 @@ fn with_tx(ctx: &mut ProcedureContext) { /// This is a silly thing to do, but an effective test of the procedure HTTP API. #[spacetimedb::procedure] fn get_my_schema_via_http(ctx: &mut ProcedureContext) -> String { - let module_identity = ctx.identity(); + let module_identity = ctx.database_identity(); match ctx.http.get(format!( "http://localhost:3000/v1/database/{module_identity}/schema?version=9" )) { diff --git a/modules/sdk-test-procedure/src/lib.rs b/modules/sdk-test-procedure/src/lib.rs index 95ae9b523b1..a8befa92e3b 100644 --- a/modules/sdk-test-procedure/src/lib.rs +++ b/modules/sdk-test-procedure/src/lib.rs @@ -41,11 +41,13 @@ fn will_panic(_ctx: &mut ProcedureContext) { } #[procedure] -fn read_my_schema(ctx: &mut ProcedureContext) -> String { - let module_identity = ctx.identity(); - match ctx.http.get(format!( - "http://localhost:3000/v1/database/{module_identity}/schema?version=9" - )) { +fn read_my_schema(ctx: &mut ProcedureContext, server_url: String) -> String { + let module_identity = ctx.database_identity(); + let server_url = server_url.trim_end_matches('/'); + match ctx + .http + .get(format!("{server_url}/v1/database/{module_identity}/schema?version=9")) + { Ok(result) => result.into_body().into_string_lossy(), Err(e) => panic!("{e}"), } From 99fe3fef4cf5ee30041ae3db9880123f7e41447f Mon Sep 17 00:00:00 2001 From: clockwork-tien Date: Wed, 17 Jun 2026 13:21:32 +0300 Subject: [PATCH 3/7] spacetime init --template without arg prints template list and link to website (#5264) # Description of Changes - spacetime init --template without arg prints templates list and link to website # Screenshot screenshot # API and ABI breaking changes # Expected complexity level and risk 1 # Testing - [x] I tested the changes --- crates/cli/src/subcommands/init.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 3f193b3b59a..77eb580943d 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -179,6 +179,8 @@ pub fn cli() -> clap::Command { .short('t') .long("template") .value_name("TEMPLATE") + .num_args(0..=1) + .default_missing_value("") .help("Template ID or GitHub repository (owner/repo or URL)"), ) .arg( @@ -208,6 +210,19 @@ pub async fn fetch_templates_list() -> anyhow::Result> { Ok(templates_list.templates) } +async fn print_templates_list() -> anyhow::Result<()> { + let templates = fetch_templates_list().await?; + + println!("{}", "Available templates:".bold()); + for template in &templates { + println!(" {} - {}", template.id, template.description); + } + println!("\nCreate a project: spacetime init --template "); + println!("Browse all templates: {}", "https://spacetimedb.com/templates".cyan()); + + Ok(()) +} + pub async fn check_and_prompt_login(config: &mut Config) -> anyhow::Result { if config.spacetimedb_token().is_some() { println!("{}", "You are logged in to SpacetimeDB.".green()); @@ -1676,6 +1691,13 @@ fn check_for_git() -> bool { pub async fn exec(mut config: Config, args: &ArgMatches) -> anyhow::Result { let options = InitOptions::from_args(args); + + // --template without arg prints templates list and link to website + if options.template.as_deref() == Some("") { + print_templates_list().await?; + return Ok(PathBuf::new()); + } + let is_interactive = !options.non_interactive; let template = options.template.as_ref(); let server_lang = options.lang.as_ref(); From 052c83fe984a4c4eb7bb4f9afa5c6b1903891d87 Mon Sep 17 00:00:00 2001 From: rekhoff Date: Mon, 29 Jun 2026 14:02:18 -0700 Subject: [PATCH 4/7] Bump version to `2.6.1` --- Cargo.lock | 102 +++++++++--------- Cargo.toml | 70 ++++++------ LICENSE.txt | 4 +- crates/bindings-cpp/CMakeLists.txt | 2 +- .../BSATN.Codegen/BSATN.Codegen.csproj | 2 +- .../BSATN.Runtime/BSATN.Runtime.csproj | 2 +- crates/bindings-csharp/Codegen/Codegen.csproj | 2 +- crates/bindings-csharp/Runtime/Runtime.csproj | 2 +- .../src/module_bindings/index.ts | 4 +- crates/bindings-typescript/package.json | 2 +- .../codegen__codegen_typescript.snap | 2 +- crates/smoketests/modules/Cargo.lock | 14 +-- licenses/BSL.txt | 4 +- .../csharp/SpacetimeDB.ClientSDK.Godot.csproj | 4 +- sdks/csharp/SpacetimeDB.ClientSDK.csproj | 4 +- sdks/csharp/package.json | 2 +- .../src/module_bindings/index.ts | 4 +- 17 files changed, 113 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efc58dc007b..112ef9d1b74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "case-conversion-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "env_logger 0.10.2", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "connect_disconnect_client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2171,7 +2171,7 @@ dependencies = [ [[package]] name = "event-table-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -5767,7 +5767,7 @@ dependencies = [ [[package]] name = "procedural-view-pk-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -5781,7 +5781,7 @@ dependencies = [ [[package]] name = "procedure-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -5798,7 +5798,7 @@ dependencies = [ [[package]] name = "procedure-concurrency-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -7236,7 +7236,7 @@ dependencies = [ [[package]] name = "sdk-unreal-test-harness" -version = "2.6.0" +version = "2.6.1" dependencies = [ "serial_test", "spacetimedb-testing", @@ -7709,7 +7709,7 @@ dependencies = [ [[package]] name = "spacetimedb" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bytemuck", @@ -7732,7 +7732,7 @@ dependencies = [ [[package]] name = "spacetimedb-auth" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "serde", @@ -7745,7 +7745,7 @@ dependencies = [ [[package]] name = "spacetimedb-bench" -version = "2.6.0" +version = "2.6.1" dependencies = [ "ahash 0.8.12", "anyhow", @@ -7796,7 +7796,7 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-macro" -version = "2.6.0" +version = "2.6.1" dependencies = [ "heck 0.4.1", "humantime", @@ -7808,14 +7808,14 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-sys" -version = "2.6.0" +version = "2.6.1" dependencies = [ "spacetimedb-primitives", ] [[package]] name = "spacetimedb-cli" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "base64 0.21.7", @@ -7893,7 +7893,7 @@ dependencies = [ [[package]] name = "spacetimedb-client-api" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-stream", @@ -7952,7 +7952,7 @@ dependencies = [ [[package]] name = "spacetimedb-client-api-messages" -version = "2.6.0" +version = "2.6.1" dependencies = [ "bytes", "bytestring", @@ -7975,7 +7975,7 @@ dependencies = [ [[package]] name = "spacetimedb-codegen" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -7993,7 +7993,7 @@ dependencies = [ [[package]] name = "spacetimedb-commitlog" -version = "2.6.0" +version = "2.6.1" dependencies = [ "async-stream", "bitflags 2.10.0", @@ -8028,7 +8028,7 @@ dependencies = [ [[package]] name = "spacetimedb-core" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "arrayvec", @@ -8155,7 +8155,7 @@ dependencies = [ [[package]] name = "spacetimedb-data-structures" -version = "2.6.0" +version = "2.6.1" dependencies = [ "ahash 0.8.12", "crossbeam-queue", @@ -8171,7 +8171,7 @@ dependencies = [ [[package]] name = "spacetimedb-datastore" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bytes", @@ -8206,7 +8206,7 @@ dependencies = [ [[package]] name = "spacetimedb-durability" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-channel", @@ -8227,7 +8227,7 @@ dependencies = [ [[package]] name = "spacetimedb-execution" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "itertools 0.12.1", @@ -8242,7 +8242,7 @@ dependencies = [ [[package]] name = "spacetimedb-expr" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bigdecimal", @@ -8262,7 +8262,7 @@ dependencies = [ [[package]] name = "spacetimedb-fs-utils" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "chrono", @@ -8278,7 +8278,7 @@ dependencies = [ [[package]] name = "spacetimedb-guard" -version = "2.6.0" +version = "2.6.1" dependencies = [ "portpicker", "reqwest 0.12.24", @@ -8316,7 +8316,7 @@ dependencies = [ [[package]] name = "spacetimedb-lib" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bitflags 2.10.0", @@ -8345,7 +8345,7 @@ dependencies = [ [[package]] name = "spacetimedb-memory-usage" -version = "2.6.0" +version = "2.6.1" dependencies = [ "decorum", "ethnum", @@ -8355,7 +8355,7 @@ dependencies = [ [[package]] name = "spacetimedb-metrics" -version = "2.6.0" +version = "2.6.1" dependencies = [ "arrayvec", "itertools 0.12.1", @@ -8365,7 +8365,7 @@ dependencies = [ [[package]] name = "spacetimedb-paths" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "chrono", @@ -8381,7 +8381,7 @@ dependencies = [ [[package]] name = "spacetimedb-pg" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-trait", @@ -8400,7 +8400,7 @@ dependencies = [ [[package]] name = "spacetimedb-physical-plan" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "derive_more 0.99.20", @@ -8417,7 +8417,7 @@ dependencies = [ [[package]] name = "spacetimedb-primitives" -version = "2.6.0" +version = "2.6.1" dependencies = [ "bitflags 2.10.0", "either", @@ -8430,7 +8430,7 @@ dependencies = [ [[package]] name = "spacetimedb-query" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "itertools 0.12.1", @@ -8448,14 +8448,14 @@ dependencies = [ [[package]] name = "spacetimedb-query-builder" -version = "2.6.0" +version = "2.6.1" dependencies = [ "spacetimedb-lib", ] [[package]] name = "spacetimedb-runtime" -version = "2.6.0" +version = "2.6.1" dependencies = [ "async-task", "futures", @@ -8466,7 +8466,7 @@ dependencies = [ [[package]] name = "spacetimedb-sats" -version = "2.6.0" +version = "2.6.1" dependencies = [ "ahash 0.8.12", "anyhow", @@ -8502,7 +8502,7 @@ dependencies = [ [[package]] name = "spacetimedb-schema" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "convert_case 0.6.0", @@ -8534,7 +8534,7 @@ dependencies = [ [[package]] name = "spacetimedb-sdk" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anymap3", "base64 0.21.7", @@ -8578,7 +8578,7 @@ dependencies = [ [[package]] name = "spacetimedb-smoketests" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "assert_cmd", @@ -8599,7 +8599,7 @@ dependencies = [ [[package]] name = "spacetimedb-snapshot" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "blake3", @@ -8634,7 +8634,7 @@ dependencies = [ [[package]] name = "spacetimedb-sql-parser" -version = "2.6.0" +version = "2.6.1" dependencies = [ "derive_more 0.99.20", "spacetimedb-lib", @@ -8644,7 +8644,7 @@ dependencies = [ [[package]] name = "spacetimedb-standalone" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-trait", @@ -8686,7 +8686,7 @@ dependencies = [ [[package]] name = "spacetimedb-subscription" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "spacetimedb-data-structures", @@ -8701,7 +8701,7 @@ dependencies = [ [[package]] name = "spacetimedb-table" -version = "2.6.0" +version = "2.6.1" dependencies = [ "ahash 0.8.12", "blake3", @@ -8729,7 +8729,7 @@ dependencies = [ [[package]] name = "spacetimedb-testing" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bytes", @@ -8759,7 +8759,7 @@ dependencies = [ [[package]] name = "spacetimedb-update" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bytes", @@ -8849,7 +8849,7 @@ dependencies = [ [[package]] name = "sqltest" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "async-trait", @@ -9266,7 +9266,7 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -9283,7 +9283,7 @@ dependencies = [ [[package]] name = "test-counter" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "futures", @@ -10267,7 +10267,7 @@ dependencies = [ [[package]] name = "view-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", @@ -10283,7 +10283,7 @@ dependencies = [ [[package]] name = "view-pk-client" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "console_error_panic_hook", diff --git a/Cargo.toml b/Cargo.toml index 6f62c05e53a..cd98751dbec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,46 +114,46 @@ inherits = "release" debug = true [workspace.package] -version = "2.6.0" +version = "2.6.1" edition = "2024" # update rust-toolchain.toml too! rust-version = "1.93.0" [workspace.dependencies] -spacetimedb = { path = "crates/bindings", version = "=2.6.0" } -spacetimedb-auth = { path = "crates/auth", version = "=2.6.0" } -spacetimedb-bindings-macro = { path = "crates/bindings-macro", version = "=2.6.0" } -spacetimedb-bindings-sys = { path = "crates/bindings-sys", version = "=2.6.0" } -spacetimedb-cli = { path = "crates/cli", version = "=2.6.0" } -spacetimedb-client-api = { path = "crates/client-api", version = "=2.6.0" } -spacetimedb-client-api-messages = { path = "crates/client-api-messages", version = "=2.6.0" } -spacetimedb-codegen = { path = "crates/codegen", version = "=2.6.0" } -spacetimedb-commitlog = { path = "crates/commitlog", version = "=2.6.0" } -spacetimedb-core = { path = "crates/core", version = "=2.6.0" } -spacetimedb-data-structures = { path = "crates/data-structures", version = "=2.6.0" } -spacetimedb-datastore = { path = "crates/datastore", version = "=2.6.0" } -spacetimedb-durability = { path = "crates/durability", version = "=2.6.0" } -spacetimedb-execution = { path = "crates/execution", version = "=2.6.0" } -spacetimedb-expr = { path = "crates/expr", version = "=2.6.0" } -spacetimedb-guard = { path = "crates/guard", version = "=2.6.0" } -spacetimedb-lib = { path = "crates/lib", default-features = false, version = "=2.6.0" } -spacetimedb-memory-usage = { path = "crates/memory-usage", version = "=2.6.0", default-features = false } -spacetimedb-metrics = { path = "crates/metrics", version = "=2.6.0" } -spacetimedb-paths = { path = "crates/paths", version = "=2.6.0" } -spacetimedb-pg = { path = "crates/pg", version = "=2.6.0" } -spacetimedb-physical-plan = { path = "crates/physical-plan", version = "=2.6.0" } -spacetimedb-primitives = { path = "crates/primitives", version = "=2.6.0" } -spacetimedb-query = { path = "crates/query", version = "=2.6.0" } -spacetimedb-sats = { path = "crates/sats", version = "=2.6.0" } -spacetimedb-schema = { path = "crates/schema", version = "=2.6.0" } -spacetimedb-standalone = { path = "crates/standalone", version = "=2.6.0" } -spacetimedb-sql-parser = { path = "crates/sql-parser", version = "=2.6.0" } -spacetimedb-table = { path = "crates/table", version = "=2.6.0" } -spacetimedb-fs-utils = { path = "crates/fs-utils", version = "=2.6.0" } -spacetimedb-snapshot = { path = "crates/snapshot", version = "=2.6.0" } -spacetimedb-subscription = { path = "crates/subscription", version = "=2.6.0" } -spacetimedb-query-builder = { path = "crates/query-builder", version = "=2.6.0" } -spacetimedb-runtime = { path = "crates/runtime", version = "=2.6.0" } +spacetimedb = { path = "crates/bindings", version = "=2.6.1" } +spacetimedb-auth = { path = "crates/auth", version = "=2.6.1" } +spacetimedb-bindings-macro = { path = "crates/bindings-macro", version = "=2.6.1" } +spacetimedb-bindings-sys = { path = "crates/bindings-sys", version = "=2.6.1" } +spacetimedb-cli = { path = "crates/cli", version = "=2.6.1" } +spacetimedb-client-api = { path = "crates/client-api", version = "=2.6.1" } +spacetimedb-client-api-messages = { path = "crates/client-api-messages", version = "=2.6.1" } +spacetimedb-codegen = { path = "crates/codegen", version = "=2.6.1" } +spacetimedb-commitlog = { path = "crates/commitlog", version = "=2.6.1" } +spacetimedb-core = { path = "crates/core", version = "=2.6.1" } +spacetimedb-data-structures = { path = "crates/data-structures", version = "=2.6.1" } +spacetimedb-datastore = { path = "crates/datastore", version = "=2.6.1" } +spacetimedb-durability = { path = "crates/durability", version = "=2.6.1" } +spacetimedb-execution = { path = "crates/execution", version = "=2.6.1" } +spacetimedb-expr = { path = "crates/expr", version = "=2.6.1" } +spacetimedb-guard = { path = "crates/guard", version = "=2.6.1" } +spacetimedb-lib = { path = "crates/lib", default-features = false, version = "=2.6.1" } +spacetimedb-memory-usage = { path = "crates/memory-usage", version = "=2.6.1", default-features = false } +spacetimedb-metrics = { path = "crates/metrics", version = "=2.6.1" } +spacetimedb-paths = { path = "crates/paths", version = "=2.6.1" } +spacetimedb-pg = { path = "crates/pg", version = "=2.6.1" } +spacetimedb-physical-plan = { path = "crates/physical-plan", version = "=2.6.1" } +spacetimedb-primitives = { path = "crates/primitives", version = "=2.6.1" } +spacetimedb-query = { path = "crates/query", version = "=2.6.1" } +spacetimedb-sats = { path = "crates/sats", version = "=2.6.1" } +spacetimedb-schema = { path = "crates/schema", version = "=2.6.1" } +spacetimedb-standalone = { path = "crates/standalone", version = "=2.6.1" } +spacetimedb-sql-parser = { path = "crates/sql-parser", version = "=2.6.1" } +spacetimedb-table = { path = "crates/table", version = "=2.6.1" } +spacetimedb-fs-utils = { path = "crates/fs-utils", version = "=2.6.1" } +spacetimedb-snapshot = { path = "crates/snapshot", version = "=2.6.1" } +spacetimedb-subscription = { path = "crates/subscription", version = "=2.6.1" } +spacetimedb-query-builder = { path = "crates/query-builder", version = "=2.6.1" } +spacetimedb-runtime = { path = "crates/runtime", version = "=2.6.1" } # Prevent `ahash` from pulling in `getrandom` by disabling default features. # Modules use `getrandom02` and we need to prevent an incompatible version diff --git a/LICENSE.txt b/LICENSE.txt index 38a6a24d4f8..315fbed2e4d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -5,7 +5,7 @@ Business Source License 1.1 Parameters Licensor: Clockwork Laboratories, Inc. -Licensed Work: SpacetimeDB 2.6.0 +Licensed Work: SpacetimeDB 2.6.1 The Licensed Work is (c) 2023 Clockwork Laboratories, Inc. @@ -21,7 +21,7 @@ Additional Use Grant: You may make use of the Licensed Work provided your Licensed Work by creating tables whose schemas are controlled by such third parties. -Change Date: 2031-06-15 +Change Date: 2031-06-29 Change License: GNU Affero General Public License v3.0 with a linking exception diff --git a/crates/bindings-cpp/CMakeLists.txt b/crates/bindings-cpp/CMakeLists.txt index 0da7f780e79..f79dba2d06c 100644 --- a/crates/bindings-cpp/CMakeLists.txt +++ b/crates/bindings-cpp/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15) project(SpacetimeDBCppModuleLibrary - VERSION 2.6.0 + VERSION 2.6.1 LANGUAGES CXX) # Generate version header from template diff --git a/crates/bindings-csharp/BSATN.Codegen/BSATN.Codegen.csproj b/crates/bindings-csharp/BSATN.Codegen/BSATN.Codegen.csproj index 226a50d0d21..682cb10954e 100644 --- a/crates/bindings-csharp/BSATN.Codegen/BSATN.Codegen.csproj +++ b/crates/bindings-csharp/BSATN.Codegen/BSATN.Codegen.csproj @@ -2,7 +2,7 @@ SpacetimeDB.BSATN.Codegen - 2.6.0 + 2.6.1 SpacetimeDB BSATN Codegen The SpacetimeDB BSATN Codegen implements the Roslyn incremental generators for BSATN serialization/deserialization in C#. diff --git a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj index a2bb9f49952..4bac68dc3f6 100644 --- a/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj +++ b/crates/bindings-csharp/BSATN.Runtime/BSATN.Runtime.csproj @@ -1,7 +1,7 @@ SpacetimeDB.BSATN.Runtime - 2.6.0 + 2.6.1 SpacetimeDB BSATN Runtime The SpacetimeDB BSATN Runtime implements APIs for BSATN serialization/deserialization in C#. true diff --git a/crates/bindings-csharp/Codegen/Codegen.csproj b/crates/bindings-csharp/Codegen/Codegen.csproj index 6334103bd7e..cbf6abd24f7 100644 --- a/crates/bindings-csharp/Codegen/Codegen.csproj +++ b/crates/bindings-csharp/Codegen/Codegen.csproj @@ -1,7 +1,7 @@ SpacetimeDB.Codegen - 2.6.0 + 2.6.1 SpacetimeDB Module Codegen The SpacetimeDB Codegen implements the Roslyn incremental generators for writing SpacetimeDB modules in C#. diff --git a/crates/bindings-csharp/Runtime/Runtime.csproj b/crates/bindings-csharp/Runtime/Runtime.csproj index 0d7e22edbf5..a4229e17656 100644 --- a/crates/bindings-csharp/Runtime/Runtime.csproj +++ b/crates/bindings-csharp/Runtime/Runtime.csproj @@ -2,7 +2,7 @@ SpacetimeDB.Runtime - 2.6.0 + 2.6.1 SpacetimeDB Module Runtime The SpacetimeDB Runtime implements the database runtime bindings for writing SpacetimeDB modules in C#. diff --git a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts index 8d6460b7cf0..03c71f243f4 100644 --- a/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts +++ b/crates/bindings-typescript/case-conversion-test-client/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.6.0 (commit 0abf20b9593868ad86af11ba83a7752f8207d0dd). +// This was generated using spacetimedb cli version 2.6.1 (commit 99fe3fef4cf5ee30041ae3db9880123f7e41447f). /* eslint-disable */ /* tslint:disable */ @@ -120,7 +120,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '2.6.0' as const, + cliVersion: '2.6.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/bindings-typescript/package.json b/crates/bindings-typescript/package.json index b429ea3d205..8d83e79e3a1 100644 --- a/crates/bindings-typescript/package.json +++ b/crates/bindings-typescript/package.json @@ -1,6 +1,6 @@ { "name": "spacetimedb", - "version": "2.6.0", + "version": "2.6.1", "description": "API and ABI bindings for the SpacetimeDB TypeScript module library", "homepage": "https://github.com/clockworklabs/SpacetimeDB#readme", "bugs": { diff --git a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap index c1927e0badc..1c43a5d7897 100644 --- a/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap +++ b/crates/codegen/tests/snapshots/codegen__codegen_typescript.snap @@ -289,7 +289,7 @@ const proceduresSchema = __procedures( /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: "2.6.0" as const, + cliVersion: "2.6.1" as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, diff --git a/crates/smoketests/modules/Cargo.lock b/crates/smoketests/modules/Cargo.lock index 196abab70db..5f63514cacc 100644 --- a/crates/smoketests/modules/Cargo.lock +++ b/crates/smoketests/modules/Cargo.lock @@ -1007,7 +1007,7 @@ dependencies = [ [[package]] name = "spacetimedb" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bytemuck", @@ -1028,7 +1028,7 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-macro" -version = "2.6.0" +version = "2.6.1" dependencies = [ "heck 0.4.1", "humantime", @@ -1040,14 +1040,14 @@ dependencies = [ [[package]] name = "spacetimedb-bindings-sys" -version = "2.6.0" +version = "2.6.1" dependencies = [ "spacetimedb-primitives", ] [[package]] name = "spacetimedb-lib" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "bitflags", @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "spacetimedb-primitives" -version = "2.6.0" +version = "2.6.1" dependencies = [ "bitflags", "either", @@ -1078,14 +1078,14 @@ dependencies = [ [[package]] name = "spacetimedb-query-builder" -version = "2.6.0" +version = "2.6.1" dependencies = [ "spacetimedb-lib", ] [[package]] name = "spacetimedb-sats" -version = "2.6.0" +version = "2.6.1" dependencies = [ "anyhow", "arrayvec", diff --git a/licenses/BSL.txt b/licenses/BSL.txt index 2cf1f4da675..5ef65fc26c0 100644 --- a/licenses/BSL.txt +++ b/licenses/BSL.txt @@ -5,7 +5,7 @@ Business Source License 1.1 Parameters Licensor: Clockwork Laboratories, Inc. -Licensed Work: SpacetimeDB 2.6.0 +Licensed Work: SpacetimeDB 2.6.1 The Licensed Work is (c) 2023 Clockwork Laboratories, Inc. @@ -21,7 +21,7 @@ Additional Use Grant: You may make use of the Licensed Work provided your Licensed Work by creating tables whose schemas are controlled by such third parties. -Change Date: 2031-06-15 +Change Date: 2031-06-29 Change License: GNU Affero General Public License v3.0 with a linking exception diff --git a/sdks/csharp/SpacetimeDB.ClientSDK.Godot.csproj b/sdks/csharp/SpacetimeDB.ClientSDK.Godot.csproj index 99280ad2779..a0b681b3a71 100644 --- a/sdks/csharp/SpacetimeDB.ClientSDK.Godot.csproj +++ b/sdks/csharp/SpacetimeDB.ClientSDK.Godot.csproj @@ -16,8 +16,8 @@ logo.png README.md https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk - 2.6.0 - 2.6.0 + 2.6.1 + 2.6.1 $(DefaultItemExcludes);*~/** obj~/godot/packages true diff --git a/sdks/csharp/SpacetimeDB.ClientSDK.csproj b/sdks/csharp/SpacetimeDB.ClientSDK.csproj index 1102f00122f..2902af098f5 100644 --- a/sdks/csharp/SpacetimeDB.ClientSDK.csproj +++ b/sdks/csharp/SpacetimeDB.ClientSDK.csproj @@ -16,8 +16,8 @@ logo.png README.md https://github.com/clockworklabs/com.clockworklabs.spacetimedbsdk - 2.6.0 - 2.6.0 + 2.6.1 + 2.6.1 $(DefaultItemExcludes);*~/** packages diff --git a/sdks/csharp/package.json b/sdks/csharp/package.json index dad295a7254..8e8f01fb1e4 100644 --- a/sdks/csharp/package.json +++ b/sdks/csharp/package.json @@ -1,7 +1,7 @@ { "name": "com.clockworklabs.spacetimedbsdk", "displayName": "SpacetimeDB SDK", - "version": "2.6.0", + "version": "2.6.1", "description": "The SpacetimeDB Client SDK is a software development kit (SDK) designed to interact with and manipulate SpacetimeDB modules..", "keywords": [], "author": { diff --git a/templates/chat-react-ts/src/module_bindings/index.ts b/templates/chat-react-ts/src/module_bindings/index.ts index 0ed178e64ee..278bc7d9efe 100644 --- a/templates/chat-react-ts/src/module_bindings/index.ts +++ b/templates/chat-react-ts/src/module_bindings/index.ts @@ -1,7 +1,7 @@ // THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. -// This was generated using spacetimedb cli version 2.6.0 (commit 0abf20b9593868ad86af11ba83a7752f8207d0dd). +// This was generated using spacetimedb cli version 2.6.1 (commit 99fe3fef4cf5ee30041ae3db9880123f7e41447f). /* eslint-disable */ /* tslint:disable */ @@ -90,7 +90,7 @@ const proceduresSchema = __procedures(); /** The remote SpacetimeDB module schema, both runtime and type information. */ const REMOTE_MODULE = { versionInfo: { - cliVersion: '2.6.0' as const, + cliVersion: '2.6.1' as const, }, tables: tablesSchema.schemaType.tables, reducers: reducersSchema.reducersType.reducers, From f49173da1e170384bef35359f90b0bcf80cf2f3c Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Fri, 26 Jun 2026 07:32:52 -0700 Subject: [PATCH 5/7] [release/candidate/v2.6.1-hotfix1]: Make `HostController` bootstrap aware (#5440) # Description of Changes Module launch now exposes a completion handle so callers can wait until `st_module` is durable before cleaning up controldb bootstrap state. # API and ABI breaking changes None # Expected complexity level and risk 2 # Testing Tests added in the corresponding private PR that deletes bootstrap state from the controldb --- crates/core/src/host/host_controller.rs | 175 ++++++++++++++++-- crates/core/src/host/mod.rs | 9 +- crates/core/src/host/module_host.rs | 44 +++-- crates/core/src/host/v8/mod.rs | 12 +- .../src/host/wasm_common/module_host_actor.rs | 35 +++- 5 files changed, 226 insertions(+), 49 deletions(-) diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index 9cfb6077045..d426d7c7f2e 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -1,4 +1,4 @@ -use super::module_host::{EventStatus, ModuleHost, ModuleInfo, NoSuchModule}; +use super::module_host::{DurableOffset, EventStatus, InitDatabaseResult, ModuleHost, ModuleInfo, NoSuchModule}; use super::scheduler::SchedulerStarter; use super::v8::V8HeapMetrics; use super::wasmtime::{WasmMemoryBytesMetric, WasmtimeRuntime}; @@ -88,6 +88,83 @@ where pub type ProgramStorage = Arc; +/// A launched module host plus any pending controldb program-bootstrap completion work. +pub struct ModuleHostWithBootstrap { + pub module: ModuleHost, + pub bootstrap_completion: Option, +} + +/// A handle describing when program bootstrap can be marked complete. +/// +/// The handle owns any wait state needed to prove that the database has a +/// durable `st_module` row. +pub struct BootstrapCompletion { + program_hash: Hash, + status: ProgramBootstrap, +} + +/// Has the module been bootstrapped from the controldb? +/// +/// Once we have inserted into `st_module`, bootstrapping is no longer necessary, +/// and the initial program bytes can be dropped fromt the controldb. +enum ProgramBootstrap { + /// The module's program bytes have already been written to `st_module` + Durable, + /// The module was bootstrapped from the controldb and the `st_module` write is not yet durable + Pending { + tx_offset: TransactionOffset, + durable_offset: Option, + }, +} + +impl BootstrapCompletion { + fn durable(program_hash: Hash) -> Self { + Self { + program_hash, + status: ProgramBootstrap::Durable, + } + } + + fn pending(program_hash: Hash, tx_offset: TransactionOffset, durable_offset: Option) -> Self { + Self { + program_hash, + status: ProgramBootstrap::Pending { + tx_offset, + durable_offset, + }, + } + } + + pub fn program_hash(&self) -> Hash { + self.program_hash + } + + /// Wait until it is safe to complete program bootstrap in controldb. + /// That is, wait until the initial `st_module` insert becomes durable. + pub async fn wait(self) -> anyhow::Result { + match self.status { + ProgramBootstrap::Durable => Ok(self.program_hash), + ProgramBootstrap::Pending { + tx_offset, + durable_offset, + } => { + let tx_offset = tx_offset + .await + .context("failed waiting for initialized program transaction offset")?; + + if let Some(mut durable_offset) = durable_offset { + durable_offset + .wait_for(tx_offset) + .await + .context("failed waiting for initialized program to become durable")?; + } + + Ok(self.program_hash) + } + } + } +} + /// A host controller manages the lifecycle of spacetime databases and their /// associated modules. /// @@ -152,6 +229,11 @@ pub struct ReducerCallResult { pub execution_duration: Duration, } +pub struct ReducerCallResultWithTxOffset { + pub result: ReducerCallResult, + pub tx_offset: TransactionOffset, +} + impl ReducerCallResult { pub fn is_err(&self) -> bool { self.outcome.is_err() @@ -269,11 +351,30 @@ impl HostController { /// See also: [`Self::get_module_host`] #[tracing::instrument(level = "trace", skip_all)] pub async fn get_or_launch_module_host(&self, database: Database, replica_id: u64) -> anyhow::Result { - let mut rx = self.watch_maybe_launch_module_host(database, replica_id).await?; + let (mut rx, _) = self + .watch_maybe_launch_module_host_with_bootstrap(database, replica_id) + .await?; let module = rx.borrow_and_update(); Ok(module.clone()) } + /// Like [`Self::get_or_launch_module_host`], but returns a bootstrap completion waiter. + #[tracing::instrument(level = "trace", skip_all)] + pub async fn get_or_launch_module_host_with_bootstrap( + &self, + database: Database, + replica_id: u64, + ) -> anyhow::Result { + let (mut rx, bootstrap_completion) = self + .watch_maybe_launch_module_host_with_bootstrap(database, replica_id) + .await?; + let module = rx.borrow_and_update(); + Ok(ModuleHostWithBootstrap { + module: module.clone(), + bootstrap_completion, + }) + } + /// Like [`Self::get_or_launch_module_host`], use a [`ModuleHost`] managed /// by this controller, or launch it if it is not running. /// @@ -287,13 +388,24 @@ impl HostController { database: Database, replica_id: u64, ) -> anyhow::Result> { + self.watch_maybe_launch_module_host_with_bootstrap(database, replica_id) + .await + .map(|(rx, _)| rx) + } + + /// Like [`Self::watch_maybe_launch_module_host`], but returns a bootstrap completion waiter. + async fn watch_maybe_launch_module_host_with_bootstrap( + &self, + database: Database, + replica_id: u64, + ) -> anyhow::Result<(watch::Receiver, Option)> { // Try a read lock first. { if let Ok(guard) = self.acquire_read_lock(replica_id).await && let Some(host) = &*guard { trace!("cached host {}/{}", database.database_identity, replica_id); - return Ok(host.module.subscribe()); + return Ok((host.module.subscribe(), None)); } } @@ -312,7 +424,7 @@ impl HostController { database.database_identity, replica_id ); - return Ok(host.module.subscribe()); + return Ok((host.module.subscribe(), None)); } trace!("launch host {}/{}", database.database_identity, replica_id); @@ -333,12 +445,16 @@ impl HostController { // Note that `tokio::spawn` only cancels its tasks when the runtime shuts down, // at which point we won't be calling `try_init_host` again anyways. let rx = tokio::spawn(async move { - let host = this.try_init_host(database, replica_id).await?; + let initialized = this.try_init_host(database, replica_id).await?; + let HostInit { + host, + bootstrap_completion, + } = initialized; let rx = host.module.subscribe(); *guard = Some(host); - Ok::<_, anyhow::Error>(rx) + Ok::<_, anyhow::Error>((rx, bootstrap_completion)) }) .await??; @@ -371,8 +487,8 @@ impl HostController { ) .await?; - let call_result = launched.module_host.init_database(program).await?; - if let Some(call_result) = call_result { + let InitDatabaseResult { reducer, .. } = launched.module_host.init_database(program).await?; + if let Some(call_result) = reducer { Result::from(call_result)?; } @@ -431,7 +547,7 @@ impl HostController { let mut host = match guard.take() { None => { trace!("host not running, try_init"); - this.try_init_host(database, replica_id).await? + this.try_init_host(database, replica_id).await?.host } Some(host) => { trace!("host found, updating"); @@ -634,7 +750,7 @@ impl HostController { timeout(Duration::from_secs(5), lock.read_owned()).await } - async fn try_init_host(&self, database: Database, replica_id: u64) -> anyhow::Result { + async fn try_init_host(&self, database: Database, replica_id: u64) -> anyhow::Result { let database_identity = database.database_identity; Host::try_init(self, database, replica_id) .await @@ -749,6 +865,11 @@ struct LaunchedModule { scheduler_starter: SchedulerStarter, } +struct HostInit { + host: Host, + bootstrap_completion: Option, +} + struct ModuleLauncher { database: Database, replica_id: u64, @@ -876,7 +997,11 @@ impl Host { /// Note that this does **not** run module initialization routines, but may /// create on-disk artifacts if the host / database did not exist. #[tracing::instrument(level = "debug", skip_all)] - async fn try_init(host_controller: &HostController, database: Database, replica_id: u64) -> anyhow::Result { + async fn try_init( + host_controller: &HostController, + database: Database, + replica_id: u64, + ) -> anyhow::Result { let HostController { data_dir, default_config: config, @@ -962,6 +1087,7 @@ impl Host { (program, true) } }; + let mut bootstrap_completion = Some(BootstrapCompletion::durable(program.hash)); let relational_db = Arc::new(db); let (program, launched) = match HostType::from(program.kind) { @@ -1048,10 +1174,16 @@ impl Host { }; if program_needs_init { - let call_result = launched.module_host.init_database(program).await?; - if let Some(call_result) = call_result { + let program_hash = program.hash; + let InitDatabaseResult { reducer, tx_offset } = launched.module_host.init_database(program).await?; + if let Some(call_result) = reducer { Result::from(call_result)?; } + bootstrap_completion = Some(BootstrapCompletion::pending( + program_hash, + tx_offset, + launched.module_host.durable_tx_offset(), + )); } else { drop(program) } @@ -1089,13 +1221,16 @@ impl Host { let module = watch::Sender::new(module_host); - Ok(Host { - module, - replica_ctx, - scheduler, - disk_metrics_recorder_task, - tx_metrics_recorder_task, - view_cleanup_task, + Ok(HostInit { + host: Host { + module, + replica_ctx, + scheduler, + disk_metrics_recorder_task, + tx_metrics_recorder_task, + view_cleanup_task, + }, + bootstrap_completion, }) } diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index daa506a8cf5..68f6035df80 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -25,10 +25,13 @@ mod wasm_common; pub use disk_storage::DiskStorage; pub use host_controller::{ - extract_schema, CallProcedureReturn, CallResult, ExternalDurability, ExternalStorage, HostController, - HostRuntimeConfig, MigratePlanResult, ProcedureCallResult, ProgramStorage, ReducerCallResult, ReducerOutcome, + extract_schema, BootstrapCompletion, CallProcedureReturn, CallResult, ExternalDurability, ExternalStorage, + HostController, HostRuntimeConfig, MigratePlanResult, ModuleHostWithBootstrap, ProcedureCallResult, ProgramStorage, + ReducerCallResult, ReducerCallResultWithTxOffset, ReducerOutcome, +}; +pub use module_host::{ + InitDatabaseResult, ModuleHost, NoSuchModule, ProcedureCallError, ReducerCallError, UpdateDatabaseResult, }; -pub use module_host::{ModuleHost, NoSuchModule, ProcedureCallError, ReducerCallError, UpdateDatabaseResult}; pub use scheduler::Scheduler; /// Encoded arguments to a database function. diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index 2207470d467..3150a39763d 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -1,6 +1,6 @@ use super::{ - ArgsTuple, FunctionArgs, InvalidProcedureArguments, InvalidReducerArguments, ReducerCallResult, ReducerId, - ReducerOutcome, Scheduler, + ArgsTuple, FunctionArgs, InvalidProcedureArguments, InvalidReducerArguments, ReducerCallResult, + ReducerCallResultWithTxOffset, ReducerId, ReducerOutcome, Scheduler, }; use crate::client::messages::{OneOffQueryResponseMessage, ProcedureResultMessage, SerializableMessage}; use crate::client::{ClientActorId, ClientConnectionSender, WsVersion}; @@ -22,8 +22,8 @@ use crate::sql::ast::SchemaViewer; use crate::sql::execute::SqlResult; use crate::sql::parser::RowLevelExpr; use crate::subscription::module_subscription_actor::ModuleSubscriptions; -use crate::subscription::module_subscription_manager::BroadcastError; pub use crate::subscription::module_subscription_manager::TransactionOffset; +use crate::subscription::module_subscription_manager::{from_tx_offset, BroadcastError}; use crate::subscription::row_list_builder_pool::{BsatnRowListBuilderPool, JsonRowListBuilderFakePool}; use crate::subscription::tx::DeltaTx; use crate::subscription::websocket_building::{BuildableWebsocketFormat, RowListBuilderSource}; @@ -588,8 +588,8 @@ pub(crate) fn init_database( replica_ctx: &ReplicaContext, module_def: &ModuleDef, program: Program, - call_reducer: impl FnOnce(Option, CallReducerParams) -> (ReducerCallResult, bool), -) -> (anyhow::Result>, bool) { + call_reducer: impl FnOnce(Option, CallReducerParams) -> (ReducerCallResultWithTxOffset, bool), +) -> (anyhow::Result, bool) { extract_trapped(init_database_inner(replica_ctx, module_def, program, call_reducer)) } @@ -597,8 +597,8 @@ fn init_database_inner( replica_ctx: &ReplicaContext, module_def: &ModuleDef, program: Program, - call_reducer: impl FnOnce(Option, CallReducerParams) -> (ReducerCallResult, bool), -) -> anyhow::Result<(Option, bool)> { + call_reducer: impl FnOnce(Option, CallReducerParams) -> (ReducerCallResultWithTxOffset, bool), +) -> anyhow::Result<(InitDatabaseResult, bool)> { log::debug!("init database"); let timestamp = Timestamp::now(); let stdb = replica_ctx.relational_db(); @@ -646,17 +646,30 @@ fn init_database_inner( let rcr = match module_def.lifecycle_reducer(Lifecycle::Init) { None => { - if let Some((_tx_offset, tx_data, tx_metrics, reducer)) = stdb.commit_tx(tx)? { - stdb.report_mut_tx_metrics(reducer, tx_metrics, Some(tx_data)); - } - (None, false) + let (tx_offset, tx_data, tx_metrics, reducer) = stdb + .commit_tx(tx)? + .context("database initialization did not commit a transaction")?; + stdb.report_mut_tx_metrics(reducer, tx_metrics, Some(tx_data)); + ( + InitDatabaseResult { + reducer: None, + tx_offset: from_tx_offset(tx_offset), + }, + false, + ) } Some((reducer_id, _)) => { logger.info("Invoking `init` reducer"); let params = CallReducerParams::from_system(timestamp, owner_identity, reducer_id, ArgsTuple::nullary()); let (res, trapped) = call_reducer(Some(tx), params); - (Some(res), trapped) + ( + InitDatabaseResult { + reducer: Some(res.result), + tx_offset: res.tx_offset, + }, + trapped, + ) } }; @@ -664,6 +677,11 @@ fn init_database_inner( Ok(rcr) } +pub struct InitDatabaseResult { + pub reducer: Option, + pub tx_offset: TransactionOffset, +} + pub fn call_identity_connected( caller_auth: ConnectionAuthCtx, caller_connection_id: ConnectionId, @@ -3077,7 +3095,7 @@ impl ModuleHost { instance.common.call_view_with_tx(tx, params, instance.instance) } - pub async fn init_database(&self, program: Program) -> Result, InitDatabaseError> { + pub async fn init_database(&self, program: Program) -> Result { call_instance!( self, "", diff --git a/crates/core/src/host/v8/mod.rs b/crates/core/src/host/v8/mod.rs index 63cab1ba96f..c01c007b123 100644 --- a/crates/core/src/host/v8/mod.rs +++ b/crates/core/src/host/v8/mod.rs @@ -85,7 +85,7 @@ use crate::host::wasm_common::module_host_actor::{ ReducerExecuteResult, ReducerOp, ViewExecuteResult, ViewOp, WasmInstance, }; use crate::host::wasm_common::{RowIters, TimingSpanSet}; -use crate::host::{ModuleHost, ReducerCallError, ReducerCallResult, Scheduler}; +use crate::host::{InitDatabaseResult, ModuleHost, ReducerCallError, ReducerCallResult, Scheduler}; use crate::messages::control_db::HostType; use crate::module_host_context::ModuleCreationContext; use crate::replica_context::ReplicaContext; @@ -534,7 +534,7 @@ impl JsMainInstance { self.request(DisconnectClientRequest { client_id }).await } - pub async fn init_database(&self, program: Program) -> anyhow::Result> { + pub async fn init_database(&self, program: Program) -> anyhow::Result { self.request(InitDatabaseRequest { program }).await } @@ -661,7 +661,7 @@ js_main_request! { js_main_request! { InitDatabaseRequest { program: Program, - } => "init_database", anyhow::Result>, InitDatabase + } => "init_database", anyhow::Result, InitDatabase } js_main_request! { @@ -861,7 +861,7 @@ enum JsMainWorkerRequest { }, /// See [`JsMainInstance::init_database`]. InitDatabase { - reply_tx: JsReplyTx>>, + reply_tx: JsReplyTx>, program: Program, }, } @@ -1481,8 +1481,8 @@ fn handle_main_worker_request( } JsMainWorkerRequest::InitDatabase { reply_tx, program } => { handle_worker_request("init_database", reply_tx, || { - let call_reducer = |tx, params| instance_common.call_reducer_with_tx(tx, params, inst); - let (res, trapped): (Result, anyhow::Error>, bool) = + let call_reducer = |tx, params| instance_common.call_reducer_with_tx_offset(tx, params, inst); + let (res, trapped): (Result, bool) = init_database(replica_ctx, &info.module_def, program, call_reducer); (res, trapped) }) diff --git a/crates/core/src/host/wasm_common/module_host_actor.rs b/crates/core/src/host/wasm_common/module_host_actor.rs index e502e0e1efc..f5528037bea 100644 --- a/crates/core/src/host/wasm_common/module_host_actor.rs +++ b/crates/core/src/host/wasm_common/module_host_actor.rs @@ -15,8 +15,8 @@ use crate::host::module_host::{ }; use crate::host::scheduler::{CallScheduledFunctionResult, ScheduledFunctionParams}; use crate::host::{ - ArgsTuple, ModuleHost, ProcedureCallError, ProcedureCallResult, ReducerCallError, ReducerCallResult, ReducerId, - ReducerOutcome, Scheduler, UpdateDatabaseResult, + ArgsTuple, InitDatabaseResult, ModuleHost, ProcedureCallError, ProcedureCallResult, ReducerCallError, + ReducerCallResult, ReducerCallResultWithTxOffset, ReducerId, ReducerOutcome, Scheduler, UpdateDatabaseResult, }; use crate::identity::Identity; use crate::messages::control_db::HostType; @@ -543,10 +543,10 @@ impl WasmModuleInstance { res } - pub fn init_database(&mut self, program: Program) -> anyhow::Result> { + pub fn init_database(&mut self, program: Program) -> anyhow::Result { let module_def = &self.common.info.clone().module_def; let replica_ctx = &self.instance.replica_ctx().clone(); - let call_reducer = |tx, params| self.call_reducer_with_tx(tx, params); + let call_reducer = |tx, params| self.call_reducer_with_tx_offset(tx, params); let (res, trapped) = init_database(replica_ctx, module_def, program, call_reducer); self.trapped = trapped; res @@ -589,8 +589,18 @@ impl WasmModuleInstance { impl WasmModuleInstance { #[tracing::instrument(level = "trace", skip_all)] fn call_reducer_with_tx(&mut self, tx: Option, params: CallReducerParams) -> (ReducerCallResult, bool) { + let (res, trapped) = self.call_reducer_with_tx_offset(tx, params); + (res.result, trapped) + } + + #[tracing::instrument(level = "trace", skip_all)] + fn call_reducer_with_tx_offset( + &mut self, + tx: Option, + params: CallReducerParams, + ) -> (ReducerCallResultWithTxOffset, bool) { crate::callgrind_flag::invoke_allowing_callgrind(|| { - self.common.call_reducer_with_tx(tx, params, &mut self.instance) + self.common.call_reducer_with_tx_offset(tx, params, &mut self.instance) }) } @@ -946,6 +956,16 @@ impl InstanceCommon { params: CallReducerParams, inst: &mut I, ) -> (ReducerCallResult, bool) { + let (res, trapped) = self.call_reducer_with_tx_offset(tx, params, inst); + (res.result, trapped) + } + + pub(crate) fn call_reducer_with_tx_offset( + &mut self, + tx: Option, + params: CallReducerParams, + inst: &mut I, + ) -> (ReducerCallResultWithTxOffset, bool) { let CallReducerParams { timestamp, caller_identity, @@ -1083,7 +1103,8 @@ impl InstanceCommon { request_id, timer, }; - let event = commit_and_broadcast_event(&info.subscriptions, client, event, out.tx).event; + let CommitAndBroadcastEventSuccess { event, tx_offset, .. } = + commit_and_broadcast_event(&info.subscriptions, client, event, out.tx); let res = ReducerCallResult { outcome: ReducerOutcome::from(&event.status), @@ -1091,7 +1112,7 @@ impl InstanceCommon { execution_duration: total_duration, }; - (res, trapped) + (ReducerCallResultWithTxOffset { result: res, tx_offset }, trapped) } fn handle_outer_error(&mut self, energy: &EnergyStats, reducer_name: &str) -> EventStatus { From 766e5e793c8fd88c3334b1212d51834c3c65b57c Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Thu, 2 Jul 2026 08:22:51 -0700 Subject: [PATCH 6/7] [release/candidate/v2.6.1-hotfix1]: Incorporate a generation value into database program bootstrap (#5471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes HostController now captures the database row’s bootstrap generation when a database is launched and exposes it through `BootstrapCompletion` so cloud can report bootstrap completion per generation after the initial `st_module` write is durable. This generation value is needed to ensure database resets can identify bootstrap completion events from before and after the reset. # API and ABI breaking changes N/A # Expected complexity level and risk 1 # Testing Tests added in private --- crates/core/src/host/host_controller.rs | 29 ++++++++++++----------- crates/core/src/host/instance_env.rs | 1 + crates/core/src/messages/control_db.rs | 2 ++ crates/standalone/src/control_db.rs | 2 ++ crates/standalone/src/control_db/tests.rs | 1 + crates/standalone/src/lib.rs | 4 ++-- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs index d426d7c7f2e..adabb58624a 100644 --- a/crates/core/src/host/host_controller.rs +++ b/crates/core/src/host/host_controller.rs @@ -99,14 +99,14 @@ pub struct ModuleHostWithBootstrap { /// The handle owns any wait state needed to prove that the database has a /// durable `st_module` row. pub struct BootstrapCompletion { - program_hash: Hash, + bootstrap_generation: u64, status: ProgramBootstrap, } /// Has the module been bootstrapped from the controldb? /// /// Once we have inserted into `st_module`, bootstrapping is no longer necessary, -/// and the initial program bytes can be dropped fromt the controldb. +/// and the initial program bytes can be dropped from the controldb. enum ProgramBootstrap { /// The module's program bytes have already been written to `st_module` Durable, @@ -118,16 +118,16 @@ enum ProgramBootstrap { } impl BootstrapCompletion { - fn durable(program_hash: Hash) -> Self { + fn durable(bootstrap_generation: u64) -> Self { Self { - program_hash, + bootstrap_generation, status: ProgramBootstrap::Durable, } } - fn pending(program_hash: Hash, tx_offset: TransactionOffset, durable_offset: Option) -> Self { + fn pending(bootstrap_generation: u64, tx_offset: TransactionOffset, durable_offset: Option) -> Self { Self { - program_hash, + bootstrap_generation, status: ProgramBootstrap::Pending { tx_offset, durable_offset, @@ -135,15 +135,15 @@ impl BootstrapCompletion { } } - pub fn program_hash(&self) -> Hash { - self.program_hash + pub fn bootstrap_generation(&self) -> u64 { + self.bootstrap_generation } /// Wait until it is safe to complete program bootstrap in controldb. /// That is, wait until the initial `st_module` insert becomes durable. - pub async fn wait(self) -> anyhow::Result { + pub async fn wait(self) -> anyhow::Result<()> { match self.status { - ProgramBootstrap::Durable => Ok(self.program_hash), + ProgramBootstrap::Durable => Ok(()), ProgramBootstrap::Pending { tx_offset, durable_offset, @@ -159,7 +159,7 @@ impl BootstrapCompletion { .context("failed waiting for initialized program to become durable")?; } - Ok(self.program_hash) + Ok(()) } } } @@ -1087,7 +1087,8 @@ impl Host { (program, true) } }; - let mut bootstrap_completion = Some(BootstrapCompletion::durable(program.hash)); + let bootstrap_generation = database.bootstrap_generation; + let mut bootstrap_completion = Some(BootstrapCompletion::durable(bootstrap_generation)); let relational_db = Arc::new(db); let (program, launched) = match HostType::from(program.kind) { @@ -1174,13 +1175,12 @@ impl Host { }; if program_needs_init { - let program_hash = program.hash; let InitDatabaseResult { reducer, tx_offset } = launched.module_host.init_database(program).await?; if let Some(call_result) = reducer { Result::from(call_result)?; } bootstrap_completion = Some(BootstrapCompletion::pending( - program_hash, + bootstrap_generation, tx_offset, launched.module_host.durable_tx_offset(), )); @@ -1488,6 +1488,7 @@ pub(crate) async fn extract_schema_with_pools( owner_identity, host_type, initial_program: program.hash, + bootstrap_generation: 0, }; let core = AllocatedJobCore::default(); diff --git a/crates/core/src/host/instance_env.rs b/crates/core/src/host/instance_env.rs index 9c24c5b3a11..e9635e06c59 100644 --- a/crates/core/src/host/instance_env.rs +++ b/crates/core/src/host/instance_env.rs @@ -1376,6 +1376,7 @@ mod test { owner_identity: Identity::ZERO, host_type: HostType::Wasm, initial_program: Hash::ZERO, + bootstrap_generation: 0, }, replica_id: 0, logger, diff --git a/crates/core/src/messages/control_db.rs b/crates/core/src/messages/control_db.rs index 865be57e410..8a28a95d8e3 100644 --- a/crates/core/src/messages/control_db.rs +++ b/crates/core/src/messages/control_db.rs @@ -37,6 +37,8 @@ pub struct Database { /// /// Updating the database's module will **not** change this value. pub initial_program: Hash, + /// Generation of the current bootstrap requirement for `initial_program`. + pub bootstrap_generation: u64, } #[derive(Clone, PartialEq, Serialize, Deserialize)] diff --git a/crates/standalone/src/control_db.rs b/crates/standalone/src/control_db.rs index 887d1aeefba..89a5403ec35 100644 --- a/crates/standalone/src/control_db.rs +++ b/crates/standalone/src/control_db.rs @@ -655,6 +655,7 @@ mod compat { owner_identity, host_type, initial_program, + bootstrap_generation: 0, } } } @@ -667,6 +668,7 @@ mod compat { owner_identity, host_type, initial_program, + bootstrap_generation: _, }: CanonicalDatabase, ) -> Self { Self { diff --git a/crates/standalone/src/control_db/tests.rs b/crates/standalone/src/control_db/tests.rs index e1e09c92a49..171fcadffc7 100644 --- a/crates/standalone/src/control_db/tests.rs +++ b/crates/standalone/src/control_db/tests.rs @@ -126,6 +126,7 @@ fn test_decode() -> ResultTest<()> { owner_identity: id, host_type: HostType::Wasm, initial_program: Hash::ZERO, + bootstrap_generation: 0, }; cdb.insert_database(db.clone())?; diff --git a/crates/standalone/src/lib.rs b/crates/standalone/src/lib.rs index 189bafa11bd..41687a1c00b 100644 --- a/crates/standalone/src/lib.rs +++ b/crates/standalone/src/lib.rs @@ -285,6 +285,7 @@ impl spacetimedb_client_api::ControlStateWriteAccess for StandaloneEnv { owner_identity: *publisher, host_type: spec.host_type, initial_program: program.hash, + bootstrap_generation: 0, }; let _hash_for_assert = program.hash; @@ -425,9 +426,8 @@ impl spacetimedb_client_api::ControlStateWriteAccess for StandaloneEnv { .await?; let _stored_hash_for_assert = self.program_store.put(program_bytes).await?; debug_assert_eq!(_hash_for_assert, _stored_hash_for_assert); - - self.control_db.update_database(database)?; } + self.control_db.update_database(database)?; for instance in self.control_db.get_replicas_by_database(database_id)? { self.delete_replica(instance.id).await?; From e4bd73e1fa160528caa8665d98dea24aeaca7b78 Mon Sep 17 00:00:00 2001 From: Shubham Mishra Date: Fri, 10 Jul 2026 07:34:48 +0530 Subject: [PATCH 7/7] use ST_TABLE_ID in find_st_table_row --- .../src/locking_tx_datastore/replay.rs | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/datastore/src/locking_tx_datastore/replay.rs b/crates/datastore/src/locking_tx_datastore/replay.rs index 8f55d1cf075..95778b6fcff 100644 --- a/crates/datastore/src/locking_tx_datastore/replay.rs +++ b/crates/datastore/src/locking_tx_datastore/replay.rs @@ -1084,7 +1084,7 @@ impl StateView for ReplayCommittedState<'_> { /// then falling back to [`CommittedState::iter_by_col_eq`]. fn find_st_table_row(&self, table_id: TableId) -> Result { if let Some(row_ptr) = self.replay_table_updated.get(&table_id) { - let (table, blob_store, _) = self.state.get_table_and_blob_store(table_id)?; + let (table, blob_store, _) = self.state.get_table_and_blob_store(ST_TABLE_ID)?; // SAFETY: `row_ptr` is stored in `self.replay_table_updated`, // meaning it was inserted into `st_table` by `replay_insert` // and has not yet been deleted by `replay_delete_by_rel`. @@ -1147,7 +1147,11 @@ mod tests { locking_tx_datastore::datastore::tests::{all_rows, begin_mut_tx, commit, setup_event_table, u32_str_u32}, traits::{MutTx as _, MutTxDatastore as _}, }; + use spacetimedb_execution::dml::MutDatastore as _; + use spacetimedb_lib::db::auth::{StAccess, StTableType}; use spacetimedb_lib::error::ResultTest; + use spacetimedb_sats::{product, AlgebraicType}; + use spacetimedb_table::page_pool::PagePool; use super::*; @@ -1180,6 +1184,55 @@ mod tests { Ok(()) } + #[test] + fn test_replay_updated_st_table_lookup_reads_st_table() -> ResultTest<()> { + let datastore = Locking::bootstrap(Identity::ZERO, PagePool::new_for_test())?; + + let mut tx = begin_mut_tx(&datastore); + let schema = TableSchema::new( + TableId::SENTINEL, + TableName::for_test("Foo"), + None, + vec![ + ColumnSchema::for_test(0, "id", AlgebraicType::U32), + ColumnSchema::for_test(1, "name", AlgebraicType::String), + ColumnSchema::for_test(2, "kind", AlgebraicType::String), + ], + vec![], + vec![], + vec![], + StTableType::User, + StAccess::Public, + None, + None, + false, + None, + ); + let table_id = datastore.create_table_mut_tx(&mut tx, schema)?; + + // Fill enough user rows that the `st_table` row pointer below would also + // be addressable in the user table if replay looked in the wrong table. + for id in 0u32..128 { + tx.insert_product_value(table_id, &product![id, "row", ""]).unwrap(); + } + commit(&datastore, tx)?; + + let state = datastore.committed_state.write(); + let mut committed_state = ReplayCommittedState::new(state); + let row_ptr = { + let table_id_value = table_id.into(); + let mut rows = committed_state.iter_by_col_eq(ST_TABLE_ID, StTableFields::TableId, &table_id_value)?; + rows.next().expect("user table should have an `st_table` row").pointer() + }; + committed_state.replay_table_updated.insert(table_id, row_ptr); + + let st_table_row = committed_state.find_st_table_row(table_id)?; + assert_eq!(st_table_row.table_id, table_id); + assert_eq!(st_table_row.table_name, TableName::for_test("Foo")); + + Ok(()) + } + /// Regression test for orphaned `st_event_table` rows. /// /// Prior versions of `MutTxId::drop_table` did not delete a dropped event table's