Skip to content

[TypeScript SDK 1.12.0] ArrayBuilder.name() and .default() strip Array type wrapper, causing BSATN deserialization errors #4219

@Wildeax

Description

@Wildeax

Bug Description

In the TypeScript SDK v1.12.0, ArrayBuilder.name() and ArrayBuilder.default() pass this.element instead of this to ArrayColumnBuilder, which
strips the Array<> type wrapper. This causes any table with Vec<> fields to have an incorrect schema, leading to BSATN deserialization failures at
runtime.

Affected Code

File: src/lib/type_builders.ts (and all compiled dist files)

// Current (broken):
export class ArrayBuilder<Element extends TypeBuilder<any, any>> extends TypeBuilder<...> {
  default(value) {
    return new ArrayColumnBuilder(
      this.element,  // ❌ passes the inner element, stripping Array<> wrapper
      set(defaultMetadata, { defaultValue: value })
    );
  }
  name(name) {
    return new ArrayColumnBuilder(this.element, set(defaultMetadata, { name }));  // ❌ same bug
  }
}

// Fix:
  default(value) {
    return new ArrayColumnBuilder(
      this,  // ✅ preserves Array<> type
      set(defaultMetadata, { defaultValue: value })
    );
  }
  name(name) {
    return new ArrayColumnBuilder(this, set(defaultMetadata, { name }));  // ✅
  }

Note: OptionBuilder has the correct implementation (this instead of this.value), so this bug is specific to ArrayBuilder.

How It Triggers

The auto-generated TypeScript bindings call .name() on any field whose camelCase JS name differs from the snake_case SpacetimeDB column name. For
example:

// Generated by: spacetime generate --lang typescript
export default __t.row({
  playerUserIds: __t.array(__t.u64()).name("player_user_ids"),  // triggers bug
  playerUsernames: __t.array(__t.string()).name("player_usernames"),  // triggers bug
});

Because .name() passes this.element instead of this, the table schema registers these fields as u64 and string instead of Array<u64> and
Array<string>.

Runtime Error

When the SDK receives subscription data containing rows for these tables, the BSATN deserializer reads bytes using the wrong type schema. A u64 reads 8
bytes where Vec<u64> should read a 4-byte length prefix + N*8 bytes of data. This misaligns the entire byte stream, causing subsequent Option<> fields
to read garbage tag values:

Can't deserialize an option type, couldn't find 108 tag

(Expected tag 0=Some or 1=None, got 108 due to byte stream misalignment)

Reproduction

  1. Create a SpacetimeDB module with a table containing a Vec<> field with a snake_case name
  2. Generate TypeScript bindings (spacetime generate --lang typescript)
  3. Subscribe to that table when it contains at least one row
  4. Observe BSATN deserialization error

Workaround

Apply this sed command after npm install:

sed -i 's/return new ArrayColumnBuilder(this\.element,/return new ArrayColumnBuilder(this,/g' \
    node_modules/spacetimedb/dist/index.browser.mjs \
    node_modules/spacetimedb/dist/index.mjs \
    node_modules/spacetimedb/dist/index.cjs \
    node_modules/spacetimedb/src/lib/type_builders.ts

Environment

  • spacetimedb npm package: 1.12.0
  • spacetime CLI: 1.12.0
  • SpacetimeDB server: v1.12.0

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