Skip to content

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

Description

@cloutiertyler

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions