Skip to content

TS modules: multiple explicit indexes without accessor crash with 'Cannot assign to read only property filter' #4445

@andiradulescu

Description

@andiradulescu

Description

When a TypeScript module defines a table with two or more explicit btree indexes and the user does not provide the optional accessor field, all reducers PANIC at runtime with:

TypeError: Cannot assign to read only property 'filter' of object '#<Object>'
    in makeTableView

A single explicit index works fine. The crash only occurs with 2+ explicit indexes on the same table.

Reproduction

Minimal module that crashes:

import { schema, table, t } from 'spacetimedb/server';

const task = table(
  {
    public: true,
    indexes: [
      { name: 'task_assigned_to', algorithm: 'btree' as const, columns: ['assignedTo'] },
      { name: 'task_status', algorithm: 'btree' as const, columns: ['status'] },
    ],
  },
  {
    id: t.u64().primaryKey().autoInc(),
    title: t.string(),
    status: t.string(),
    assignedTo: t.string(),
  }
);

const spacetimedb = schema({ task });
export default spacetimedb;

export const addTask = spacetimedb.reducer(
  { title: t.string() },
  (ctx, { title }) => {
    ctx.db.task.insert({ id: 0n, title, status: 'pending', assignedTo: 'nobody' });
  }
);
spacetime publish my-test --module-path . -s local -y
spacetime call my-test add_task '"hello"' -s local
# → PANIC: Cannot assign to read only property 'filter'

Remove either index and the reducer works.

Root Cause

In crates/bindings-typescript/src/lib/table.ts line 428-433, explicit indexes are pushed with:

indexes.push({
  sourceName: undefined,
  accessorName: indexOpts.accessor,  // undefined when user omits `accessor`
  algorithm,
  canonicalName: indexOpts.name,
});

Since accessor is optional in IndexOpts, omitting it sets accessorName: undefined for every explicit index.

Then in crates/bindings-typescript/src/server/runtime.ts lines 798-802:

if (Object.hasOwn(tableView, indexDef.accessorName!)) {
  freeze(Object.assign(tableView[indexDef.accessorName!], index));
} else {
  tableView[indexDef.accessorName!] = freeze(index) as any;
}
  1. First index: Object.hasOwn(tableView, undefined) → false → tableView["undefined"] = freeze(index) — the index object (which has a filter property) is frozen
  2. Second index: Object.hasOwn(tableView, undefined) → true → Object.assign(tableView["undefined"], index) — tries to write filter onto the frozen object → crash

Workaround

Add accessor to every explicit index definition:

indexes: [
  { accessor: 'task_assigned_to', name: 'task_assigned_to', algorithm: 'btree', columns: ['assignedTo'] },
  { accessor: 'task_status', name: 'task_status', algorithm: 'btree', columns: ['status'] },
]

Suggested Fix

Either:

  • Derive accessorName from canonicalName (the name field) when accessor is not provided, so each index gets a unique accessor
  • Guard runtime.ts against undefined accessor names (skip or throw a clear error)

Environment

  • SpacetimeDB CLI: 2.0.1
  • npm spacetimedb: 2.0.1
  • macOS 15.5 (Apple Silicon)
  • Node.js v24.13

Metadata

Metadata

Assignees

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