Summary
If a TypeScript module's bundle contains two physical copies of the spacetimedb SDK (the common case: a library package that itself depends on spacetimedb and exports table() handles, consumed via a link:/file: dependency), passing the library's table handles into the consumer's schema() silently publishes a corrupted schema and the module later dies at runtime with a fatal error. There is no build-time or publish-time diagnostic.
Verified on SpacetimeDB 2.8.1 standalone with spacetimedb@2.8.1 npm.
Minimal repro
lib/src/index.ts (its own spacetimedb dependency):
import { table, t } from 'spacetimedb/server';
export const widgets = table(
{ name: 'widgets', public: true },
{ id: t.u64().primaryKey().autoInc(), label: t.string() }
);
app/src/index.ts (depends on spacetimedb + stdb-two-copy-lib: link:../lib):
import { schema, table, t } from 'spacetimedb/server';
import { widgets } from 'stdb-two-copy-lib';
const gadgets = table(
{ name: 'gadgets', public: true },
{ id: t.u64().primaryKey().autoInc(), note: t.string(), at: t.timestamp() }
);
const spacetimedb = schema({ widgets, gadgets });
export default spacetimedb;
export const touch = spacetimedb.reducer(ctx => {
ctx.db.gadgets.insert({ id: 0n, note: 'hi', at: ctx.timestamp });
});
spacetime build succeeds. spacetime publish succeeds. Then:
> spacetime sql <db> "SELECT * FROM widgets"
id | note | at <-- widgets silently has gadgets' columns (expected: id | label)
> spacetime call <db> touch
Error: The instance encountered a fatal error.
With schedule tables in the mix the corruption is at least loud: publish fails with cross-wired column complaints (e.g. "A scheduled table must have columns scheduled_id/scheduled_at, but table X has columns <some other table's row type>").
Why this matters
Library packages that ship tables are an explicitly supported direction (submodules docs; the components repo). Both spacetime-retry-ts and now spacetime-cron-ts in SpacetimeDBComponents work around this by dependency-injecting the consumer's table/t/ScheduleAt into the library (spacetimeCron({ table, t, ScheduleAt })), which is a real API wart.
Suspected mechanism
The schema walk identifies SDK objects by per-copy identity: e.g. registerExport/exportContext are plain Symbol(...) (not Symbol.for), and handle bookkeeping uses instance identity (tableSourceNames map, instanceof checks). A foreign-copy handle passes the structural duck-check, but its builders' type registration appears to resolve against the wrong typespace, aliasing row types across tables. Note the submodule path (schema({ ns: lib })) already crosses the copy boundary deliberately via the raw-def handoff in buildSubmoduleDispatch, so cross-copy support is clearly intended at some level.
Expected behavior (either would do)
- Work: foreign-copy handles are interned structurally so single-copy and two-copy bundles behave identically, or
- Fail loudly:
schema() detects a handle from a different SDK instance and throws a clear TypeError at module load ("table 'widgets' was created by a different copy of the spacetimedb SDK; ensure a single copy or use ..."), instead of publishing a corrupted def.
Silent corruption + runtime fatal is the worst of the options.
Environment
- spacetimedb-cli / standalone 2.8.1
- spacetimedb npm 2.8.1
- pnpm 10 with
link: dependency (the two-copy layout also occurs with any unhoisted duplicate install)
Summary
If a TypeScript module's bundle contains two physical copies of the
spacetimedbSDK (the common case: a library package that itself depends onspacetimedband exportstable()handles, consumed via alink:/file:dependency), passing the library's table handles into the consumer'sschema()silently publishes a corrupted schema and the module later dies at runtime with a fatal error. There is no build-time or publish-time diagnostic.Verified on SpacetimeDB 2.8.1 standalone with
spacetimedb@2.8.1npm.Minimal repro
lib/src/index.ts(its ownspacetimedbdependency):app/src/index.ts(depends onspacetimedb+stdb-two-copy-lib: link:../lib):spacetime buildsucceeds.spacetime publishsucceeds. Then:With schedule tables in the mix the corruption is at least loud: publish fails with cross-wired column complaints (e.g. "A scheduled table must have columns scheduled_id/scheduled_at, but table X has columns <some other table's row type>").
Why this matters
Library packages that ship tables are an explicitly supported direction (submodules docs; the components repo). Both
spacetime-retry-tsand nowspacetime-cron-tsin SpacetimeDBComponents work around this by dependency-injecting the consumer'stable/t/ScheduleAtinto the library (spacetimeCron({ table, t, ScheduleAt })), which is a real API wart.Suspected mechanism
The schema walk identifies SDK objects by per-copy identity: e.g.
registerExport/exportContextare plainSymbol(...)(notSymbol.for), and handle bookkeeping uses instance identity (tableSourceNamesmap,instanceofchecks). A foreign-copy handle passes the structural duck-check, but its builders' type registration appears to resolve against the wrong typespace, aliasing row types across tables. Note the submodule path (schema({ ns: lib })) already crosses the copy boundary deliberately via the raw-def handoff inbuildSubmoduleDispatch, so cross-copy support is clearly intended at some level.Expected behavior (either would do)
schema()detects a handle from a different SDK instance and throws a clearTypeErrorat module load ("table 'widgets' was created by a different copy of the spacetimedb SDK; ensure a single copy or use ..."), instead of publishing a corrupted def.Silent corruption + runtime fatal is the worst of the options.
Environment
link:dependency (the two-copy layout also occurs with any unhoisted duplicate install)