spacetimedb 2.0.3 → 2.0.4

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 (81) hide show
  1. package/LICENSE.txt +2 -2
  2. package/dist/angular/index.cjs +3 -0
  3. package/dist/angular/index.cjs.map +1 -1
  4. package/dist/angular/index.mjs +3 -0
  5. package/dist/angular/index.mjs.map +1 -1
  6. package/dist/browser/angular/index.mjs +3 -0
  7. package/dist/browser/angular/index.mjs.map +1 -1
  8. package/dist/browser/react/index.mjs +3 -0
  9. package/dist/browser/react/index.mjs.map +1 -1
  10. package/dist/browser/svelte/index.mjs +3 -0
  11. package/dist/browser/svelte/index.mjs.map +1 -1
  12. package/dist/browser/vue/index.mjs +3 -0
  13. package/dist/browser/vue/index.mjs.map +1 -1
  14. package/dist/index.browser.mjs +126 -92
  15. package/dist/index.browser.mjs.map +1 -1
  16. package/dist/index.cjs +126 -92
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +126 -92
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/lib/binary_writer.d.ts +1 -0
  21. package/dist/lib/binary_writer.d.ts.map +1 -1
  22. package/dist/lib/indexes.d.ts +1 -1
  23. package/dist/lib/indexes.d.ts.map +1 -1
  24. package/dist/lib/query.d.ts +4 -2
  25. package/dist/lib/query.d.ts.map +1 -1
  26. package/dist/lib/schema.d.ts +2 -0
  27. package/dist/lib/schema.d.ts.map +1 -1
  28. package/dist/lib/table.d.ts +19 -1
  29. package/dist/lib/table.d.ts.map +1 -1
  30. package/dist/min/index.browser.mjs +1 -1
  31. package/dist/min/index.browser.mjs.map +1 -1
  32. package/dist/min/react/index.mjs +1 -1
  33. package/dist/min/react/index.mjs.map +1 -1
  34. package/dist/min/sdk/index.browser.mjs +1 -1
  35. package/dist/min/sdk/index.browser.mjs.map +1 -1
  36. package/dist/react/index.cjs +3 -0
  37. package/dist/react/index.cjs.map +1 -1
  38. package/dist/react/index.mjs +3 -0
  39. package/dist/react/index.mjs.map +1 -1
  40. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  41. package/dist/sdk/index.browser.mjs +126 -92
  42. package/dist/sdk/index.browser.mjs.map +1 -1
  43. package/dist/sdk/index.cjs +126 -92
  44. package/dist/sdk/index.cjs.map +1 -1
  45. package/dist/sdk/index.mjs +126 -92
  46. package/dist/sdk/index.mjs.map +1 -1
  47. package/dist/sdk/table_cache.d.ts.map +1 -1
  48. package/dist/sdk/websocket_decompress_adapter.d.ts +17 -7
  49. package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
  50. package/dist/sdk/websocket_test_adapter.d.ts +3 -2
  51. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  52. package/dist/server/index.mjs +65 -21
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/svelte/index.cjs +3 -0
  55. package/dist/svelte/index.cjs.map +1 -1
  56. package/dist/svelte/index.mjs +3 -0
  57. package/dist/svelte/index.mjs.map +1 -1
  58. package/dist/tanstack/SpacetimeDBQueryClient.d.ts +1 -0
  59. package/dist/tanstack/SpacetimeDBQueryClient.d.ts.map +1 -1
  60. package/dist/tanstack/index.cjs +24 -0
  61. package/dist/tanstack/index.cjs.map +1 -1
  62. package/dist/tanstack/index.mjs +24 -0
  63. package/dist/tanstack/index.mjs.map +1 -1
  64. package/dist/vue/index.cjs +3 -0
  65. package/dist/vue/index.cjs.map +1 -1
  66. package/dist/vue/index.mjs +3 -0
  67. package/dist/vue/index.mjs.map +1 -1
  68. package/package.json +1 -1
  69. package/src/lib/binary_writer.ts +4 -0
  70. package/src/lib/indexes.ts +1 -1
  71. package/src/lib/query.ts +30 -6
  72. package/src/lib/schema.ts +66 -24
  73. package/src/lib/table.ts +41 -9
  74. package/src/sdk/db_connection_impl.ts +38 -43
  75. package/src/sdk/table_cache.ts +14 -11
  76. package/src/sdk/websocket_decompress_adapter.ts +42 -45
  77. package/src/sdk/websocket_test_adapter.ts +3 -2
  78. package/src/server/runtime.ts +7 -3
  79. package/src/server/schema.test-d.ts +37 -0
  80. package/src/server/view.test-d.ts +2 -0
  81. 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.0.4",
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": {
@@ -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
  }
@@ -628,6 +629,11 @@ export type ValueExpr<TableDef extends TypedTableDef, Value> =
628
629
  | LiteralExpr<Value & LiteralValue>
629
630
  | ColumnExprForValue<TableDef, Value>;
630
631
 
632
+ type PredicateExpr<TableDef extends TypedTableDef> =
633
+ | BooleanExpr<TableDef>
634
+ | ColumnExprForValue<TableDef, SatsBool>
635
+ | boolean;
636
+
631
637
  type LiteralExpr<Value> = {
632
638
  type: 'literal';
633
639
  value: Value;
@@ -654,6 +660,24 @@ function normalizeValue(val: ValueInput<any>): ValueExpr<any, any> {
654
660
  return literal(val as LiteralValue);
655
661
  }
656
662
 
663
+ function normalizePredicateExpr<TableDef extends TypedTableDef>(
664
+ value: PredicateExpr<TableDef>
665
+ ): BooleanExpr<TableDef> {
666
+ if (value instanceof BooleanExpr) return value;
667
+ if (typeof value === 'boolean') {
668
+ return new BooleanExpr({
669
+ type: 'eq',
670
+ left: literal(value),
671
+ right: literal(true),
672
+ });
673
+ }
674
+ return new BooleanExpr({
675
+ type: 'eq',
676
+ left: value as ValueExpr<TableDef, any>,
677
+ right: literal(true),
678
+ });
679
+ }
680
+
657
681
  type EqExpr<Table extends TypedTableDef = any> = BooleanExpr<Table>;
658
682
 
659
683
  type BooleanExprData<Table extends TypedTableDef> = (
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;
@@ -400,6 +419,14 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
400
419
 
401
420
  // convert explicit multi‑column indexes coming from options.indexes
402
421
  for (const indexOpts of userIndexes ?? []) {
422
+ const accessor = indexOpts.accessor;
423
+ if (typeof accessor !== 'string' || accessor.length === 0) {
424
+ const tableLabel = name ?? '<unnamed>';
425
+ const indexLabel = indexOpts.name ?? '<unnamed>';
426
+ throw new TypeError(
427
+ `Index '${indexLabel}' on table '${tableLabel}' must define a non-empty 'accessor'`
428
+ );
429
+ }
403
430
  let algorithm: RawIndexAlgorithm;
404
431
  switch (indexOpts.algorithm) {
405
432
  case 'btree':
@@ -418,16 +445,19 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
418
445
  algorithm = { tag: 'Direct', value: colIds.get(indexOpts.column)! };
419
446
  break;
420
447
  }
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.
448
+
449
+ // Unnamed indexes are assigned a globally unique source name.
450
+ // `accessor` controls the TypeScript property used to access the index.
451
+ // `name` (if present) is preserved as the canonical schema name.
452
+ //
453
+ // IMPORTANT: we intentionally do not reject duplicate accessor names here.
454
+ // This preserves existing behavior for raw table definitions. Downstream
455
+ // runtime consumers decide how duplicates are resolved:
456
+ // - server runtime merges duplicate accessors onto one accessor object
457
+ // - client cache assignment is last-write-wins for duplicate accessors
428
458
  indexes.push({
429
459
  sourceName: undefined,
430
- accessorName: indexOpts.accessor,
460
+ accessorName: accessor,
431
461
  algorithm,
432
462
  canonicalName: indexOpts.name,
433
463
  });
@@ -496,7 +526,9 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
496
526
  isEvent,
497
527
  };
498
528
  },
499
- idxs: {} as OptsIndices<Opts>,
529
+ // Preserve the declared index options as runtime data so `tableToSchema`
530
+ // can expose them without type-smuggling.
531
+ idxs: userIndexes as OptsIndices<Opts>,
500
532
  constraints: constraints as OptsConstraints<Opts>,
501
533
  schedule,
502
534
  };
@@ -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(
@@ -12,7 +12,6 @@ import type {
12
12
  ReadonlyIndexes,
13
13
  ReadonlyRangedIndex,
14
14
  ReadonlyUniqueIndex,
15
- UntypedIndex,
16
15
  } from '../lib/indexes.ts';
17
16
  import type { Bound } from '../server/range.ts';
18
17
  import type { Prettify } from '../lib/type_util.ts';
@@ -84,23 +83,27 @@ export class TableCacheImpl<
84
83
  this.tableDef = tableDef;
85
84
  this.rows = new Map();
86
85
  this.emitter = new EventEmitter();
87
- // Build indexes
88
- const indexesDef = this.tableDef.indexes || [];
89
- for (const idx of indexesDef) {
90
- // TODO: don't do this. See comment in `tableToSchema` in `schema.ts`
91
- const idxDef = idx as UntypedIndex<
92
- keyof TableDefForTableName<RemoteModule, TableName>['columns'] & string
93
- >;
86
+ // Build index views from the resolved runtime index metadata.
87
+ //
88
+ // We intentionally use `resolvedIndexes` rather than `indexes`:
89
+ // - `indexes` is declarative table-level config (`IndexOpts`) used mainly for typing.
90
+ // - `resolvedIndexes` is the runtime shape (`UntypedIndex`) that includes both
91
+ // field-level and explicit table-level indexes.
92
+ for (const idxDef of this.tableDef.resolvedIndexes) {
94
93
  const index = this.#makeReadonlyIndex(this.tableDef, idxDef);
94
+ // IMPORTANT: for duplicate accessor names, client cache uses assignment
95
+ // semantics and later entries overwrite earlier ones. This matches prior
96
+ // behavior and is intentionally different from server runtime merge logic.
95
97
  (this as any)[idxDef.name] = index;
96
98
  }
97
99
  }
98
100
 
99
101
  // TODO: this just scans the whole table; we should build proper index structures
100
102
  #makeReadonlyIndex<
101
- I extends UntypedIndex<
102
- keyof TableDefForTableName<RemoteModule, TableName>['columns'] & string
103
- >,
103
+ I extends TableDefForTableName<
104
+ RemoteModule,
105
+ TableName
106
+ >['resolvedIndexes'][number],
104
107
  >(
105
108
  tableDef: TableDefForTableName<RemoteModule, TableName>,
106
109
  idx: I
@@ -1,48 +1,55 @@
1
1
  import { decompress } from './decompress';
2
2
  import { resolveWS } from './ws';
3
3
 
4
- export class WebsocketDecompressAdapter {
5
- onclose?: (...ev: any[]) => void;
6
- onopen?: (...ev: any[]) => void;
7
- onmessage?: (msg: { data: Uint8Array }) => void;
8
- onerror?: (msg: ErrorEvent) => void;
9
-
10
- #ws: WebSocket;
11
-
12
- async #handleOnMessage(msg: MessageEvent) {
13
- const buffer = new Uint8Array(msg.data);
14
- let decompressed: Uint8Array;
15
-
16
- if (buffer[0] === 0) {
17
- decompressed = buffer.slice(1);
18
- } else if (buffer[0] === 1) {
19
- throw new Error(
20
- 'Brotli Compression not supported. Please use gzip or none compression in withCompression method on DbConnection.'
21
- );
22
- } else if (buffer[0] === 2) {
23
- decompressed = await decompress(buffer.slice(1), 'gzip');
24
- } else {
25
- throw new Error(
26
- 'Unexpected Compression Algorithm. Please use `gzip` or `none`'
27
- );
28
- }
4
+ export interface WebsocketAdapter {
5
+ send(msg: Uint8Array): void;
6
+ close(): void;
7
+
8
+ set onclose(handler: (ev: CloseEvent) => void);
9
+ set onopen(handler: () => void);
10
+ set onmessage(handler: (msg: { data: Uint8Array }) => void);
11
+ set onerror(handler: (msg: ErrorEvent) => void);
12
+ }
29
13
 
30
- this.onmessage?.({ data: decompressed });
14
+ export class WebsocketDecompressAdapter implements WebsocketAdapter {
15
+ set onclose(handler: (ev: CloseEvent) => void) {
16
+ this.#ws.onclose = handler;
31
17
  }
32
-
33
- #handleOnOpen(msg: any) {
34
- this.onopen?.(msg);
18
+ set onopen(handler: () => void) {
19
+ this.#ws.onopen = handler;
35
20
  }
36
-
37
- #handleOnError(msg: any) {
38
- this.onerror?.(msg);
21
+ set onmessage(handler: (msg: { data: Uint8Array }) => void) {
22
+ this.#ws.onmessage = async (msg: MessageEvent<ArrayBuffer>) => {
23
+ const data = await this.#decompress(new Uint8Array(msg.data));
24
+ handler({ data });
25
+ };
26
+ }
27
+ set onerror(handler: (msg: ErrorEvent) => void) {
28
+ this.#ws.onerror = handler as (msg: Event) => void;
39
29
  }
40
30
 
41
- #handleOnClose(msg: any) {
42
- this.onclose?.(msg);
31
+ #ws: WebSocket;
32
+
33
+ async #decompress(buffer: Uint8Array): Promise<Uint8Array> {
34
+ const tag = buffer[0];
35
+ const data = buffer.subarray(1);
36
+ switch (tag) {
37
+ case 0:
38
+ return data;
39
+ case 1:
40
+ throw new Error(
41
+ 'Brotli Compression not supported. Please use gzip or none compression in withCompression method on DbConnection.'
42
+ );
43
+ case 2:
44
+ return await decompress(data, 'gzip');
45
+ default:
46
+ throw new Error(
47
+ 'Unexpected Compression Algorithm. Please use `gzip` or `none`'
48
+ );
49
+ }
43
50
  }
44
51
 
45
- send(msg: any): void {
52
+ send(msg: Uint8Array): void {
46
53
  this.#ws.send(msg);
47
54
  }
48
55
 
@@ -51,16 +58,6 @@ export class WebsocketDecompressAdapter {
51
58
  }
52
59
 
53
60
  constructor(ws: WebSocket) {
54
- this.onmessage = undefined;
55
- this.onopen = undefined;
56
- this.onmessage = undefined;
57
- this.onerror = undefined;
58
-
59
- ws.onmessage = this.#handleOnMessage.bind(this);
60
- ws.onerror = this.#handleOnError.bind(this);
61
- ws.onclose = this.#handleOnClose.bind(this);
62
- ws.onopen = this.#handleOnOpen.bind(this);
63
-
64
61
  ws.binaryType = 'arraybuffer';
65
62
 
66
63
  this.#ws = ws;
@@ -1,10 +1,11 @@
1
1
  import { BinaryReader, BinaryWriter } from '../';
2
2
  import { ClientMessage, ServerMessage } from './client_api/types';
3
+ import type { WebsocketAdapter } from './websocket_decompress_adapter';
3
4
 
4
- class WebsocketTestAdapter {
5
+ class WebsocketTestAdapter implements WebsocketAdapter {
5
6
  onclose: any;
6
7
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
7
- onopen!: Function;
8
+ onopen!: () => void;
8
9
  onmessage: any;
9
10
  onerror: any;
10
11
 
@@ -516,6 +516,7 @@ function makeTableView(
516
516
  ) as Table<any>;
517
517
 
518
518
  for (const indexDef of table.indexes) {
519
+ const accessorName = indexDef.accessorName!;
519
520
  const index_id = sys.index_id_from_name(indexDef.sourceName!);
520
521
 
521
522
  let column_ids: number[];
@@ -795,10 +796,13 @@ function makeTableView(
795
796
  } as RangedIndex<any, any>;
796
797
  }
797
798
 
798
- if (Object.hasOwn(tableView, indexDef.accessorName!)) {
799
- freeze(Object.assign(tableView[indexDef.accessorName!], index));
799
+ // IMPORTANT: duplicate accessor handling.
800
+ // When multiple raw indexes share the same accessor name, we merge index
801
+ // methods onto a single accessor object instead of throwing.
802
+ if (Object.hasOwn(tableView, accessorName)) {
803
+ freeze(Object.assign((tableView as any)[accessorName], index));
800
804
  } else {
801
- tableView[indexDef.accessorName!] = freeze(index) as any;
805
+ (tableView as any)[accessorName] = freeze(index);
802
806
  }
803
807
  }
804
808