clockworklabs / clockworklabs/SpacetimeDB

TS modules: table handles from a second SDK copy silently corrupt the published schema

Open
#5,740 0 comments 0 reactions 0 assignees View on GitHub

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)

  1. Work: foreign-copy handles are interned structurally so single-copy and two-copy bundles behave identically, or
  2. 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)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.