>
>;
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;
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/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();
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/core/src/host/host_controller.rs b/crates/core/src/host/host_controller.rs
index 9cfb6077045..adabb58624a 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 {
+ 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 from 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(bootstrap_generation: u64) -> Self {
+ Self {
+ bootstrap_generation,
+ status: ProgramBootstrap::Durable,
+ }
+ }
+
+ fn pending(bootstrap_generation: u64, tx_offset: TransactionOffset, durable_offset: Option) -> Self {
+ Self {
+ bootstrap_generation,
+ status: ProgramBootstrap::Pending {
+ tx_offset,
+ durable_offset,
+ },
+ }
+ }
+
+ 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<()> {
+ match self.status {
+ ProgramBootstrap::Durable => Ok(()),
+ 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(())
+ }
+ }
+ }
+}
+
/// 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,8 @@ impl Host {
(program, true)
}
};
+ 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) {
@@ -1048,10 +1175,15 @@ impl Host {
};
if program_needs_init {
- let call_result = launched.module_host.init_database(program).await?;
- if let Some(call_result) = call_result {
+ 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(
+ bootstrap_generation,
+ 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,
})
}
@@ -1353,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/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