clockworklabs / clockworklabs/SpacetimeDB
TS modules: table handles from a second SDK copy silently corrupt the published schema
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 25.2k
- Forks
- 1.1k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 46
Description
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 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
- 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)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the two-copy setup from lib/src/index.ts and app/src/index.ts using the link: dependency, then inspect schema(), registerExport, exportContext, tableSourceNames, and buildSubmoduleDispatch. Check how a foreign-copy table handle is identified and how its row type is registered. Done means either the published schema preserves widgets and gadgets correctly or schema() rejects the foreign handle with a clear TypeError before publish.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend-api-design, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100