spacetimedb 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE.txt +2 -2
  2. package/dist/angular/index.cjs +5 -1
  3. package/dist/angular/index.cjs.map +1 -1
  4. package/dist/angular/index.mjs +5 -1
  5. package/dist/angular/index.mjs.map +1 -1
  6. package/dist/browser/angular/index.mjs +5 -1
  7. package/dist/browser/angular/index.mjs.map +1 -1
  8. package/dist/browser/react/index.mjs +8 -1
  9. package/dist/browser/react/index.mjs.map +1 -1
  10. package/dist/browser/svelte/index.mjs +5 -1
  11. package/dist/browser/svelte/index.mjs.map +1 -1
  12. package/dist/browser/vue/index.mjs +5 -1
  13. package/dist/browser/vue/index.mjs.map +1 -1
  14. package/dist/index.browser.mjs +148 -100
  15. package/dist/index.browser.mjs.map +1 -1
  16. package/dist/index.cjs +148 -100
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +148 -100
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/lib/algebraic_type.d.ts.map +1 -1
  21. package/dist/lib/binary_writer.d.ts +1 -0
  22. package/dist/lib/binary_writer.d.ts.map +1 -1
  23. package/dist/lib/indexes.d.ts +1 -1
  24. package/dist/lib/indexes.d.ts.map +1 -1
  25. package/dist/lib/query.d.ts +14 -7
  26. package/dist/lib/query.d.ts.map +1 -1
  27. package/dist/lib/schema.d.ts +2 -0
  28. package/dist/lib/schema.d.ts.map +1 -1
  29. package/dist/lib/table.d.ts +25 -2
  30. package/dist/lib/table.d.ts.map +1 -1
  31. package/dist/min/index.browser.mjs +1 -1
  32. package/dist/min/index.browser.mjs.map +1 -1
  33. package/dist/min/react/index.mjs +1 -1
  34. package/dist/min/react/index.mjs.map +1 -1
  35. package/dist/min/sdk/index.browser.mjs +1 -1
  36. package/dist/min/sdk/index.browser.mjs.map +1 -1
  37. package/dist/react/index.cjs +8 -1
  38. package/dist/react/index.cjs.map +1 -1
  39. package/dist/react/index.mjs +8 -1
  40. package/dist/react/index.mjs.map +1 -1
  41. package/dist/react/useTable.d.ts.map +1 -1
  42. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  43. package/dist/sdk/index.browser.mjs +144 -98
  44. package/dist/sdk/index.browser.mjs.map +1 -1
  45. package/dist/sdk/index.cjs +144 -98
  46. package/dist/sdk/index.cjs.map +1 -1
  47. package/dist/sdk/index.mjs +144 -98
  48. package/dist/sdk/index.mjs.map +1 -1
  49. package/dist/sdk/table_cache.d.ts.map +1 -1
  50. package/dist/sdk/websocket_decompress_adapter.d.ts +17 -7
  51. package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
  52. package/dist/sdk/websocket_test_adapter.d.ts +3 -2
  53. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  54. package/dist/server/index.d.ts +1 -0
  55. package/dist/server/index.d.ts.map +1 -1
  56. package/dist/server/index.mjs +88 -30
  57. package/dist/server/index.mjs.map +1 -1
  58. package/dist/svelte/index.cjs +5 -1
  59. package/dist/svelte/index.cjs.map +1 -1
  60. package/dist/svelte/index.mjs +5 -1
  61. package/dist/svelte/index.mjs.map +1 -1
  62. package/dist/tanstack/SpacetimeDBQueryClient.d.ts +1 -0
  63. package/dist/tanstack/SpacetimeDBQueryClient.d.ts.map +1 -1
  64. package/dist/tanstack/index.cjs +26 -1
  65. package/dist/tanstack/index.cjs.map +1 -1
  66. package/dist/tanstack/index.mjs +26 -1
  67. package/dist/tanstack/index.mjs.map +1 -1
  68. package/dist/vue/index.cjs +5 -1
  69. package/dist/vue/index.cjs.map +1 -1
  70. package/dist/vue/index.mjs +5 -1
  71. package/dist/vue/index.mjs.map +1 -1
  72. package/package.json +1 -1
  73. package/src/lib/algebraic_type.ts +5 -1
  74. package/src/lib/binary_writer.ts +4 -0
  75. package/src/lib/indexes.ts +1 -1
  76. package/src/lib/query.ts +90 -25
  77. package/src/lib/schema.ts +66 -24
  78. package/src/lib/table.ts +47 -10
  79. package/src/react/useTable.ts +5 -0
  80. package/src/sdk/db_connection_impl.ts +38 -43
  81. package/src/sdk/table_cache.ts +14 -11
  82. package/src/sdk/websocket_decompress_adapter.ts +42 -45
  83. package/src/sdk/websocket_test_adapter.ts +3 -2
  84. package/src/server/index.ts +1 -0
  85. package/src/server/runtime.ts +7 -3
  86. package/src/server/schema.test-d.ts +37 -0
  87. package/src/server/view.test-d.ts +6 -0
  88. package/src/tanstack/SpacetimeDBQueryClient.ts +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spacetimedb",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "API and ABI bindings for the SpacetimeDB TypeScript module library",
5
5
  "homepage": "https://github.com/clockworklabs/SpacetimeDB#readme",
6
6
  "bugs": {
@@ -535,7 +535,11 @@ const view = reader.view;
535
535
  ${ty.elements
536
536
  .map(({ name, algebraicType: { tag } }) =>
537
537
  tag in primitiveJSName
538
- ? `\
538
+ ? tag === 'Bool'
539
+ ? `\
540
+ result.${name} = view.getUint8(reader.offset) !== 0;
541
+ reader.offset += 1;`
542
+ : `\
539
543
  result.${name} = view.get${primitiveJSName[tag as JSPrimitives]}(reader.offset, ${primitiveSizes[tag] > 1 ? 'true' : ''});
540
544
  reader.offset += ${primitiveSizes[tag]};`
541
545
  : `result.${name} = reader.read${tag}();`
@@ -42,6 +42,10 @@ export default class BinaryWriter {
42
42
  this.buffer = typeof init === 'number' ? new ResizableBuffer(init) : init;
43
43
  }
44
44
 
45
+ clear() {
46
+ this.offset = 0;
47
+ }
48
+
45
49
  reset(buffer: ResizableBuffer) {
46
50
  this.buffer = buffer;
47
51
  this.offset = 0;
@@ -9,7 +9,7 @@ import type { ColumnIsUnique } from './constraints';
9
9
  * existing column names are referenced.
10
10
  */
11
11
  export type IndexOpts<AllowedCol extends string> = {
12
- accessor?: string;
12
+ accessor: string;
13
13
  name?: string;
14
14
  } & (
15
15
  | { algorithm: 'btree'; columns: readonly AllowedCol[] }
package/src/lib/query.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  TypeBuilder,
12
12
  } from './type_builders';
13
13
  import type { Values } from './type_util';
14
+ import type { Bool as SatsBool } from './algebraic_type_variants';
14
15
 
15
16
  /**
16
17
  * Helper to get the set of table names.
@@ -65,7 +66,7 @@ type From<TableDef extends TypedTableDef> = RowTypedQuery<
65
66
  Readonly<{
66
67
  toSql(): string;
67
68
  where(
68
- predicate: (row: RowExpr<TableDef>) => BooleanExpr<TableDef>
69
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
69
70
  ): From<TableDef>;
70
71
  rightSemijoin<RightTable extends TypedTableDef>(
71
72
  other: TableRef<RightTable>,
@@ -93,7 +94,7 @@ type SemijoinBuilder<TableDef extends TypedTableDef> = RowTypedQuery<
93
94
  Readonly<{
94
95
  toSql(): string;
95
96
  where(
96
- predicate: (row: RowExpr<TableDef>) => BooleanExpr<TableDef>
97
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
97
98
  ): SemijoinBuilder<TableDef>;
98
99
  /** @deprecated No longer needed — builder is already a valid query. */
99
100
  build(): Query<TableDef>;
@@ -120,7 +121,7 @@ class SemijoinImpl<TableDef extends TypedTableDef>
120
121
  }
121
122
 
122
123
  where(
123
- predicate: (row: RowExpr<TableDef>) => BooleanExpr<TableDef>
124
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
124
125
  ): SemijoinImpl<TableDef> {
125
126
  const nextSourceQuery = this.sourceQuery.where(predicate);
126
127
  return new SemijoinImpl<TableDef>(
@@ -167,9 +168,9 @@ class FromBuilder<TableDef extends TypedTableDef>
167
168
  ) {}
168
169
 
169
170
  where(
170
- predicate: (row: RowExpr<TableDef>) => BooleanExpr<TableDef>
171
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
171
172
  ): FromBuilder<TableDef> {
172
- const newCondition = predicate(this.table.cols);
173
+ const newCondition = normalizePredicateExpr(predicate(this.table.cols));
173
174
  const nextWhere = this.whereClause
174
175
  ? this.whereClause.and(newCondition)
175
176
  : newCondition;
@@ -308,7 +309,7 @@ class TableRefImpl<TableDef extends TypedTableDef>
308
309
  }
309
310
 
310
311
  where(
311
- predicate: (row: RowExpr<TableDef>) => BooleanExpr<TableDef>
312
+ predicate: (row: RowExpr<TableDef>) => PredicateExpr<TableDef>
312
313
  ): FromBuilder<TableDef> {
313
314
  return this.asFrom().where(predicate);
314
315
  }
@@ -347,7 +348,8 @@ function createRowExpr<TableDef extends TypedTableDef>(
347
348
  columnBuilder.typeBuilder.algebraicType as InferSpacetimeTypeOfColumn<
348
349
  TableDef,
349
350
  typeof columnName
350
- >
351
+ >,
352
+ columnBuilder.columnMetadata.name
351
353
  );
352
354
  row[columnName] = Object.freeze(column);
353
355
  }
@@ -437,7 +439,10 @@ export class ColumnExpression<
437
439
  ColumnName extends ColumnNames<TableDef>,
438
440
  > {
439
441
  readonly type = 'column' as const;
442
+ // This is the column accessor
440
443
  readonly column: ColumnName;
444
+ // The name of the column in the database.
445
+ readonly columnName: string;
441
446
  readonly table: TableDef['sourceName'];
442
447
  // phantom: actual runtime value is undefined
443
448
  readonly tsValueType?: RowType<TableDef>[ColumnName];
@@ -446,10 +451,12 @@ export class ColumnExpression<
446
451
  constructor(
447
452
  table: TableDef['sourceName'],
448
453
  column: ColumnName,
449
- spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>
454
+ spacetimeType: InferSpacetimeTypeOfColumn<TableDef, ColumnName>,
455
+ columnName?: string
450
456
  ) {
451
457
  this.table = table;
452
458
  this.column = column;
459
+ this.columnName = columnName || column;
453
460
  this.spacetimeType = spacetimeType;
454
461
  }
455
462
 
@@ -628,6 +635,11 @@ export type ValueExpr<TableDef extends TypedTableDef, Value> =
628
635
  | LiteralExpr<Value & LiteralValue>
629
636
  | ColumnExprForValue<TableDef, Value>;
630
637
 
638
+ type PredicateExpr<TableDef extends TypedTableDef> =
639
+ | BooleanExpr<TableDef>
640
+ | ColumnExprForValue<TableDef, SatsBool>
641
+ | boolean;
642
+
631
643
  type LiteralExpr<Value> = {
632
644
  type: 'literal';
633
645
  value: Value;
@@ -654,6 +666,24 @@ function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
654
666
  return literal(val as LiteralValue);
655
667
  }
656
668
 
669
+ function normalizePredicateExpr<TableDef extends TypedTableDef>(
670
+ value: PredicateExpr<TableDef>
671
+ ): BooleanExpr<TableDef> {
672
+ if (value instanceof BooleanExpr) return value;
673
+ if (typeof value === 'boolean') {
674
+ return new BooleanExpr({
675
+ type: 'eq',
676
+ left: literal(value),
677
+ right: literal(true),
678
+ });
679
+ }
680
+ return new BooleanExpr({
681
+ type: 'eq',
682
+ left: value as ValueExpr<TableDef, any>,
683
+ right: literal(true),
684
+ });
685
+ }
686
+
657
687
  type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
658
688
 
659
689
  type BooleanExprData<Table extends TypedTableDef> = (
@@ -686,15 +716,38 @@ type BooleanExprData<Table extends TypedTableDef> = (
686
716
  _tableType?: Table;
687
717
  };
688
718
 
719
+ type AndOrMixedTableScopeError = {
720
+ readonly 'Cannot combine predicates from different table scopes with and/or. In semijoin on(...), keep only the join equality and move extra predicates to .where(...).': never;
721
+ };
722
+
723
+ type RequireSameAndOrTable<
724
+ Expected extends TypedTableDef,
725
+ Actual extends TypedTableDef,
726
+ > = [Expected] extends [Actual]
727
+ ? [Actual] extends [Expected]
728
+ ? unknown
729
+ : AndOrMixedTableScopeError
730
+ : AndOrMixedTableScopeError;
731
+
689
732
  export class BooleanExpr<Table extends TypedTableDef> {
690
733
  constructor(readonly data: BooleanExprData<Table>) {}
691
734
 
692
- and(other: BooleanExpr<Table>): BooleanExpr<Table> {
693
- return new BooleanExpr({ type: 'and', clauses: [this.data, other.data] });
735
+ and<OtherTable extends TypedTableDef>(
736
+ other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
737
+ ): BooleanExpr<Table> {
738
+ return new BooleanExpr({
739
+ type: 'and',
740
+ clauses: [this.data, other.data as BooleanExprData<Table>],
741
+ });
694
742
  }
695
743
 
696
- or(other: BooleanExpr<Table>): BooleanExpr<Table> {
697
- return new BooleanExpr({ type: 'or', clauses: [this.data, other.data] });
744
+ or<OtherTable extends TypedTableDef>(
745
+ other: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>
746
+ ): BooleanExpr<Table> {
747
+ return new BooleanExpr({
748
+ type: 'or',
749
+ clauses: [this.data, other.data as BooleanExprData<Table>],
750
+ });
698
751
  }
699
752
 
700
753
  not(): BooleanExpr<Table> {
@@ -708,28 +761,40 @@ export function not<T extends TypedTableDef>(
708
761
  return new BooleanExpr({ type: 'not', clause: clause.data });
709
762
  }
710
763
 
711
- export function and<T extends TypedTableDef>(
712
- ...clauses: readonly [BooleanExpr<T>, BooleanExpr<T>, ...BooleanExpr<T>[]]
713
- ): BooleanExpr<T> {
764
+ export function and<
765
+ Table extends TypedTableDef,
766
+ OtherTable extends TypedTableDef,
767
+ >(
768
+ first: BooleanExpr<Table>,
769
+ second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
770
+ ...rest: readonly BooleanExpr<Table>[]
771
+ ): BooleanExpr<Table> {
772
+ const clauses = [first, second, ...rest];
714
773
  return new BooleanExpr({
715
774
  type: 'and',
716
775
  clauses: clauses.map(c => c.data) as [
717
- BooleanExprData<T>,
718
- BooleanExprData<T>,
719
- ...BooleanExprData<T>[],
776
+ BooleanExprData<Table>,
777
+ BooleanExprData<Table>,
778
+ ...BooleanExprData<Table>[],
720
779
  ],
721
780
  });
722
781
  }
723
782
 
724
- export function or<T extends TypedTableDef>(
725
- ...clauses: readonly [BooleanExpr<T>, BooleanExpr<T>, ...BooleanExpr<T>[]]
726
- ): BooleanExpr<T> {
783
+ export function or<
784
+ Table extends TypedTableDef,
785
+ OtherTable extends TypedTableDef,
786
+ >(
787
+ first: BooleanExpr<Table>,
788
+ second: BooleanExpr<OtherTable> & RequireSameAndOrTable<Table, OtherTable>,
789
+ ...rest: readonly BooleanExpr<Table>[]
790
+ ): BooleanExpr<Table> {
791
+ const clauses = [first, second, ...rest];
727
792
  return new BooleanExpr({
728
793
  type: 'or',
729
794
  clauses: clauses.map(c => c.data) as [
730
- BooleanExprData<T>,
731
- BooleanExprData<T>,
732
- ...BooleanExprData<T>[],
795
+ BooleanExprData<Table>,
796
+ BooleanExprData<Table>,
797
+ ...BooleanExprData<Table>[],
733
798
  ],
734
799
  });
735
800
  }
@@ -779,7 +844,7 @@ function valueExprToSql<Table extends TypedTableDef>(
779
844
  return literalValueToSql(expr.value);
780
845
  }
781
846
  const table = tableAlias ?? expr.table;
782
- return `${quoteIdentifier(table)}.${quoteIdentifier(expr.column)}`;
847
+ return `${quoteIdentifier(table)}.${quoteIdentifier(expr.columnName)}`;
783
848
  }
784
849
 
785
850
  function literalValueToSql(value: unknown): string {
package/src/lib/schema.ts CHANGED
@@ -61,20 +61,35 @@ export interface TableToSchema<
61
61
  accessorName: AccName;
62
62
  columns: T['rowType']['row'];
63
63
  rowType: T['rowSpacetimeType'];
64
+ // Declarative user-provided table-level indexes.
64
65
  indexes: T['idxs'];
66
+ // Resolved runtime index metadata used by runtime consumers (e.g. TableCache).
67
+ resolvedIndexes: readonly UntypedIndex<keyof T['rowType']['row'] & string>[];
65
68
  constraints: T['constraints'];
66
69
  }
67
70
 
68
71
  export function tablesToSchema<
69
72
  const T extends Record<string, UntypedTableSchema>,
70
73
  >(ctx: ModuleContext, tables: T): TablesToSchema<T> {
74
+ // `TablesToSchema<T>['tables']` is intentionally readonly in the public type,
75
+ // but we need a mutable builder while materializing it from entries.
76
+ type MutableTableDefs = {
77
+ -readonly [AccName in keyof TablesToSchema<T>['tables']]: TablesToSchema<T>['tables'][AccName];
78
+ };
79
+ const tableDefs = Object.create(null) as MutableTableDefs;
80
+ for (const [accName, schema] of Object.entries(tables) as [
81
+ keyof T & string,
82
+ T[keyof T & string],
83
+ ][]) {
84
+ tableDefs[accName] = tableToSchema(
85
+ accName,
86
+ schema,
87
+ schema.tableDef(ctx, accName)
88
+ ) as TablesToSchema<T>['tables'][typeof accName];
89
+ }
90
+
71
91
  return {
72
- tables: Object.fromEntries(
73
- Object.entries(tables).map(([accName, schema]) => [
74
- accName,
75
- tableToSchema(accName, schema, schema.tableDef(ctx, accName)),
76
- ])
77
- ) as TablesToSchema<T>['tables'],
92
+ tables: tableDefs as TablesToSchema<T>['tables'],
78
93
  };
79
94
  }
80
95
 
@@ -90,6 +105,46 @@ export function tableToSchema<
90
105
  schema.rowType.algebraicType.value.elements[i].name;
91
106
 
92
107
  type AllowedCol = keyof T['rowType']['row'] & string;
108
+ // Build fully-resolved runtime index metadata from the host-facing RawTableDef.
109
+ // This is intentionally separate from `schema.idxs`, which keeps the original
110
+ // user-declared `IndexOpts` shape for type-level inference.
111
+ const resolvedIndexes: UntypedIndex<AllowedCol>[] = tableDef.indexes.map(
112
+ idx => {
113
+ const accessorName = idx.accessorName;
114
+ if (typeof accessorName !== 'string' || accessorName.length === 0) {
115
+ throw new TypeError(
116
+ `Index '${idx.sourceName ?? '<unknown>'}' on table '${tableDef.sourceName}' is missing accessor name`
117
+ );
118
+ }
119
+
120
+ const columnIds =
121
+ idx.algorithm.tag === 'Direct'
122
+ ? [idx.algorithm.value]
123
+ : idx.algorithm.value;
124
+
125
+ const unique = tableDef.constraints.some(
126
+ c =>
127
+ c.data.tag === 'Unique' &&
128
+ c.data.value.columns.every(col => columnIds.includes(col))
129
+ );
130
+
131
+ const algorithm = (
132
+ {
133
+ BTree: 'btree',
134
+ Hash: 'hash',
135
+ Direct: 'direct',
136
+ } as const
137
+ )[idx.algorithm.tag];
138
+
139
+ return {
140
+ name: accessorName,
141
+ unique,
142
+ algorithm,
143
+ columns: columnIds.map(getColName) as AllowedCol[],
144
+ };
145
+ }
146
+ );
147
+
93
148
  return {
94
149
  // For client,`schama.tableName` will always be there as canonical name.
95
150
  // For module, if explicit name is not provided via `name`, accessor name will
@@ -98,29 +153,16 @@ export function tableToSchema<
98
153
  accessorName: accName,
99
154
  columns: schema.rowType.row, // typed as T[i]['rowType']['row'] under TablesToSchema<T>
100
155
  rowType: schema.rowSpacetimeType,
156
+ // Keep declarative indexes in their original shape for type-level consumers.
157
+ indexes: schema.idxs,
101
158
  constraints: tableDef.constraints.map(c => ({
102
159
  name: c.sourceName,
103
160
  constraint: 'unique',
104
161
  columns: c.data.value.columns.map(getColName) as [string],
105
162
  })),
106
- // TODO: horrible horrible horrible. we smuggle this `Array<UntypedIndex>`
107
- // by casting it to an `Array<IndexOpts>` as `TableToSchema` expects.
108
- // This is then used in `TableCacheImpl.constructor` and who knows where else.
109
- // We should stop lying about our types.
110
- indexes: tableDef.indexes.map((idx): UntypedIndex<AllowedCol> => {
111
- const columnIds =
112
- idx.algorithm.tag === 'Direct'
113
- ? [idx.algorithm.value]
114
- : idx.algorithm.value;
115
- return {
116
- name: idx.accessorName!,
117
- unique: tableDef.constraints.some(c =>
118
- c.data.value.columns.every(col => columnIds.includes(col))
119
- ),
120
- algorithm: idx.algorithm.tag.toLowerCase() as 'btree',
121
- columns: columnIds.map(getColName),
122
- };
123
- }) as T['idxs'],
163
+ // Expose resolved runtime indexes separately so runtime users don't have to
164
+ // reinterpret `indexes` with unsafe casts.
165
+ resolvedIndexes,
124
166
  tableDef,
125
167
  ...(tableDef.isEvent ? { isEvent: true } : {}),
126
168
  };
package/src/lib/table.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  Indexes,
18
18
  IndexOpts,
19
19
  ReadonlyIndexes,
20
+ UntypedIndex,
20
21
  } from './indexes';
21
22
  import ScheduleAt from './schedule_at';
22
23
  import type { TableSchema } from './table_schema';
@@ -116,7 +117,25 @@ export type UntypedTableDef = {
116
117
  columns: Record<string, ColumnBuilder<any, any, ColumnMetadata<any>>>;
117
118
  // This is really just a ProductType where all the elements have names.
118
119
  rowType: RowBuilder<RowObj>['algebraicType']['value'];
120
+ /**
121
+ * Declarative multi-column indexes supplied by user code in `table({ indexes: [...] }, ...)`.
122
+ *
123
+ * This is intentionally the *declarative* shape (`IndexOpts`) because a lot of
124
+ * type-level behavior is derived from these entries (for example query-builder
125
+ * inference over composite indexes).
126
+ */
119
127
  indexes: readonly IndexOpts<any>[];
128
+ /**
129
+ * Fully-resolved runtime indexes materialized from `RawTableDefV10`.
130
+ *
131
+ * This contains both:
132
+ * 1) field-level indexes inferred from column metadata, and
133
+ * 2) explicit table-level indexes.
134
+ *
135
+ * Runtime consumers like `TableCacheImpl` should use this field instead of
136
+ * reinterpreting `indexes` as runtime index metadata.
137
+ */
138
+ resolvedIndexes: readonly UntypedIndex<any>[];
120
139
  constraints: readonly ConstraintOpts<any>[];
121
140
  tableDef: RawTableDefV10;
122
141
  isEvent?: boolean;
@@ -220,7 +239,12 @@ export type ReadonlyTable<TableDef extends UntypedTableDef> = Prettify<
220
239
  >;
221
240
 
222
241
  export interface ReadonlyTableMethods<TableDef extends UntypedTableDef> {
223
- /** Returns the number of rows in the TX state. */
242
+ /**
243
+ * Returns the number of rows in this table.
244
+ *
245
+ * This reads datastore metadata, so it runs in constant time.
246
+ * It also takes into account modifications by the current transaction.
247
+ */
224
248
  count(): bigint;
225
249
 
226
250
  /** Iterate over all rows in the TX state. Rust Iterator<Item=Row> → TS IterableIterator<Row>. */
@@ -400,6 +424,14 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
400
424
 
401
425
  // convert explicit multi‑column indexes coming from options.indexes
402
426
  for (const indexOpts of userIndexes ?? []) {
427
+ const accessor = indexOpts.accessor;
428
+ if (typeof accessor !== 'string' || accessor.length === 0) {
429
+ const tableLabel = name ?? '<unnamed>';
430
+ const indexLabel = indexOpts.name ?? '<unnamed>';
431
+ throw new TypeError(
432
+ `Index '${indexLabel}' on table '${tableLabel}' must define a non-empty 'accessor'`
433
+ );
434
+ }
403
435
  let algorithm: RawIndexAlgorithm;
404
436
  switch (indexOpts.algorithm) {
405
437
  case 'btree':
@@ -418,16 +450,19 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
418
450
  algorithm = { tag: 'Direct', value: colIds.get(indexOpts.column)! };
419
451
  break;
420
452
  }
421
- // unnamed indexes will be assigned a globally unique name
422
- // The name users supply is actually the accessor name which will be used
423
- // in TypeScript to access the index. This will be used verbatim.
424
- // This is confusing because it is not the index name and there is
425
- // no actual way for the user to set the actual index name.
426
- // I think we should standardize: name and accessorName as the way to set
427
- // the name and accessor name of an index across all SDKs.
453
+
454
+ // Unnamed indexes are assigned a globally unique source name.
455
+ // `accessor` controls the TypeScript property used to access the index.
456
+ // `name` (if present) is preserved as the canonical schema name.
457
+ //
458
+ // IMPORTANT: we intentionally do not reject duplicate accessor names here.
459
+ // This preserves existing behavior for raw table definitions. Downstream
460
+ // runtime consumers decide how duplicates are resolved:
461
+ // - server runtime merges duplicate accessors onto one accessor object
462
+ // - client cache assignment is last-write-wins for duplicate accessors
428
463
  indexes.push({
429
464
  sourceName: undefined,
430
- accessorName: indexOpts.accessor,
465
+ accessorName: accessor,
431
466
  algorithm,
432
467
  canonicalName: indexOpts.name,
433
468
  });
@@ -496,7 +531,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
496
531
  isEvent,
497
532
  };
498
533
  },
499
- idxs: {} as OptsIndices<Opts>,
534
+ // Preserve the declared index options as runtime data so `tableToSchema`
535
+ // can expose them without type-smuggling.
536
+ idxs: userIndexes as OptsIndices<Opts>,
500
537
  constraints: constraints as OptsConstraints<Opts>,
501
538
  schedule,
502
539
  };
@@ -104,6 +104,8 @@ export function useTable<TableDef extends UntypedTableDef>(
104
104
  ) as Prettify<UseTableRowType>[])
105
105
  : (Array.from(table.iter()) as Prettify<UseTableRowType>[]);
106
106
  return [result, subscribeApplied];
107
+ // TODO: investigating refactoring so that this is no longer necessary, as we have had genuine bugs with missed deps.
108
+ // See https://github.com/clockworklabs/SpacetimeDB/pull/4580.
107
109
  // eslint-disable-next-line react-hooks/exhaustive-deps
108
110
  }, [connectionState, accessorName, querySql, subscribeApplied]);
109
111
 
@@ -205,11 +207,14 @@ export function useTable<TableDef extends UntypedTableDef>(
205
207
  table.removeOnUpdate?.(onUpdate);
206
208
  };
207
209
  },
210
+ // TODO: investigating refactoring so that this is no longer necessary, as we have had genuine bugs with missed deps.
211
+ // See https://github.com/clockworklabs/SpacetimeDB/pull/4580.
208
212
  // eslint-disable-next-line react-hooks/exhaustive-deps
209
213
  [
210
214
  connectionState,
211
215
  accessorName,
212
216
  querySql,
217
+ computeSnapshot,
213
218
  callbacks?.onDelete,
214
219
  callbacks?.onInsert,
215
220
  callbacks?.onUpdate,
@@ -37,8 +37,10 @@ import {
37
37
  type PendingCallback,
38
38
  type TableUpdate as CacheTableUpdate,
39
39
  } from './table_cache.ts';
40
- import { WebsocketDecompressAdapter } from './websocket_decompress_adapter.ts';
41
- import type { WebsocketTestAdapter } from './websocket_test_adapter.ts';
40
+ import {
41
+ WebsocketDecompressAdapter,
42
+ type WebsocketAdapter,
43
+ } from './websocket_decompress_adapter.ts';
42
44
  import {
43
45
  SubscriptionBuilderImpl,
44
46
  SubscriptionHandleImpl,
@@ -146,7 +148,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
146
148
  #eventId = 0;
147
149
  #emitter: EventEmitter<ConnectionEvent>;
148
150
  #messageQueue = Promise.resolve();
149
- #outboundQueue: ClientMessage[] = [];
151
+ #outboundQueue: Uint8Array[] = [];
150
152
  #subscriptionManager = new SubscriptionManager<RemoteModule>();
151
153
  #remoteModule: RemoteModule;
152
154
  #reducerCallbacks = new Map<
@@ -171,10 +173,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
171
173
  // private fields.
172
174
  // We use them in testing.
173
175
  private clientCache: ClientCache<RemoteModule>;
174
- private ws?: WebsocketDecompressAdapter | WebsocketTestAdapter;
175
- private wsPromise: Promise<
176
- WebsocketDecompressAdapter | WebsocketTestAdapter | undefined
177
- >;
176
+ private ws?: WebsocketAdapter;
177
+ private wsPromise: Promise<WebsocketAdapter | undefined>;
178
178
 
179
179
  constructor({
180
180
  uri,
@@ -302,6 +302,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
302
302
  #makeReducers(def: RemoteModule): ReducersView<RemoteModule> {
303
303
  const out: Record<string, unknown> = {};
304
304
 
305
+ const writer = new BinaryWriter(1024);
306
+
305
307
  for (const reducer of def.reducers) {
306
308
  const reducerName = reducer.name;
307
309
  const key = reducer.accessorName;
@@ -310,7 +312,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
310
312
  this.#reducerArgsSerializers[reducerName];
311
313
 
312
314
  (out as any)[key] = (params: InferTypeOfRow<typeof reducer.params>) => {
313
- const writer = new BinaryWriter(1024);
315
+ writer.clear();
314
316
  serializeArgs(writer, params);
315
317
  const argsBuffer = writer.getBuffer();
316
318
  return this.callReducer(reducerName, argsBuffer, params);
@@ -323,6 +325,8 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
323
325
  #makeProcedures(def: RemoteModule): ProceduresView<RemoteModule> {
324
326
  const out: Record<string, unknown> = {};
325
327
 
328
+ const writer = new BinaryWriter(1024);
329
+
326
330
  for (const procedure of def.procedures) {
327
331
  const procedureName = procedure.name;
328
332
  const key = procedure.accessorName;
@@ -333,7 +337,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
333
337
  (out as any)[key] = (
334
338
  params: InferTypeOfRow<typeof procedure.params>
335
339
  ): Promise<any> => {
336
- const writer = new BinaryWriter(1024);
340
+ writer.clear();
337
341
  serializeArgs(writer, params);
338
342
  const argsBuffer = writer.getBuffer();
339
343
  return this.callProcedure(procedureName, argsBuffer).then(returnBuf => {
@@ -537,41 +541,36 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
537
541
  return this.#mergeTableUpdates(updates);
538
542
  }
539
543
 
540
- #sendEncoded(
541
- wsResolved: WebsocketDecompressAdapter | WebsocketTestAdapter,
542
- message: ClientMessage
543
- ): void {
544
- stdbLogger(
545
- 'trace',
546
- () => `Sending message to server: ${stringify(message)}`
547
- );
548
- const writer = new BinaryWriter(1024);
549
- ClientMessage.serialize(writer, message);
550
- const encoded = writer.getBuffer();
551
- wsResolved.send(encoded);
552
- }
553
-
554
- #flushOutboundQueue(
555
- wsResolved: WebsocketDecompressAdapter | WebsocketTestAdapter
556
- ): void {
557
- if (!this.isActive || this.#outboundQueue.length === 0) {
558
- return;
559
- }
544
+ #flushOutboundQueue(wsResolved: WebsocketAdapter): void {
560
545
  const pending = this.#outboundQueue.splice(0);
561
546
  for (const message of pending) {
562
- this.#sendEncoded(wsResolved, message);
547
+ wsResolved.send(message);
563
548
  }
564
549
  }
565
550
 
551
+ #clientMessageEncoder = new BinaryWriter(1024);
566
552
  #sendMessage(message: ClientMessage): void {
567
- this.wsPromise.then(wsResolved => {
568
- if (!wsResolved || !this.isActive) {
569
- this.#outboundQueue.push(message);
570
- return;
571
- }
572
- this.#flushOutboundQueue(wsResolved);
573
- this.#sendEncoded(wsResolved, message);
574
- });
553
+ const writer = this.#clientMessageEncoder;
554
+ writer.clear();
555
+ ClientMessage.serialize(writer, message);
556
+ const encoded = writer.getBuffer();
557
+
558
+ if (this.ws && this.isActive) {
559
+ if (this.#outboundQueue.length) this.#flushOutboundQueue(this.ws);
560
+
561
+ stdbLogger(
562
+ 'trace',
563
+ () => `Sending message to server: ${stringify(message)}`
564
+ );
565
+ this.ws.send(encoded);
566
+ } else {
567
+ stdbLogger(
568
+ 'trace',
569
+ () => `Queuing message to server: ${stringify(message)}`
570
+ );
571
+ // use slice() to copy, in case the clientMessageEncoder's buffer gets used
572
+ this.#outboundQueue.push(encoded.slice());
573
+ }
575
574
  }
576
575
 
577
576
  #nextEventId(): string {
@@ -978,11 +977,7 @@ export class DbConnectionImpl<RemoteModule extends UntypedRemoteModule>
978
977
  * ```
979
978
  */
980
979
  disconnect(): void {
981
- this.wsPromise.then(wsResolved => {
982
- if (wsResolved) {
983
- wsResolved.close();
984
- }
985
- });
980
+ this.wsPromise.then(ws => ws?.close());
986
981
  }
987
982
 
988
983
  private on(