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
@@ -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
 
@@ -21,5 +21,6 @@ export { toCamelCase } from '../lib/util';
21
21
  export type { Uuid } from '../lib/uuid';
22
22
  export type { Random } from './rng';
23
23
  export type { ViewExport, ViewCtx, AnonymousViewCtx } from './views';
24
+ export { Range, type Bound } from './range';
24
25
 
25
26
  import './polyfills'; // Ensure polyfills are loaded
@@ -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
 
@@ -60,3 +60,40 @@ spacetimedb.init(ctx => {
60
60
  // @ts-expect-error update() is NOT allowed on non-PK unique indexes
61
61
  const _update = ctx.db.person.name2.update;
62
62
  });
63
+
64
+ /**
65
+ * Regression coverage for the declared-vs-resolved index split:
66
+ * - declared table-level indexes must still produce typed accessors
67
+ * - field-level indexes must still produce typed accessors
68
+ * - non-indexed fields must not accidentally become index accessors
69
+ */
70
+ const account = table(
71
+ {
72
+ indexes: [
73
+ {
74
+ accessor: 'byEmailAndOrg',
75
+ algorithm: 'btree',
76
+ columns: ['email', 'orgId'] as const,
77
+ },
78
+ ] as const,
79
+ },
80
+ {
81
+ accountId: t.u32(),
82
+ email: t.string().index('hash'),
83
+ orgId: t.u32(),
84
+ nickname: t.string(),
85
+ }
86
+ );
87
+
88
+ const spacetimedbIndexSplit = schema({ account });
89
+
90
+ spacetimedbIndexSplit.init(ctx => {
91
+ // Explicit table-level index accessor from `table({ indexes: [...] })`.
92
+ ctx.db.account.byEmailAndOrg.filter(['a@example.com', 1]);
93
+
94
+ // Field-level index accessor derived from column metadata.
95
+ ctx.db.account.email.filter('a@example.com');
96
+
97
+ // @ts-expect-error `nickname` is not indexed, so no index accessor should exist.
98
+ const _nickname = ctx.db.account.nickname;
99
+ });
@@ -7,6 +7,7 @@ const person = table(
7
7
  name: 'person',
8
8
  indexes: [
9
9
  {
10
+ accessor: 'name_id_idx',
10
11
  name: 'name_id_idx',
11
12
  algorithm: 'btree',
12
13
  columns: ['name', 'id'] as const,
@@ -48,6 +49,7 @@ const order = table(
48
49
  name: 'order',
49
50
  indexes: [
50
51
  {
52
+ accessor: 'id_person_id',
51
53
  name: 'id_person_id', // We are adding this to make sure `person_id` still isn't considered indexed.
52
54
  algorithm: 'btree',
53
55
  columns: ['id', 'person_id'] as const,
@@ -159,6 +161,10 @@ spacetime.anonymousView({ name: 'v5', public: true }, arrayRetValue, ctx => {
159
161
  .where(row => row.id.eq(5))
160
162
  .leftSemijoin(ctx.from.order, (p, o) => p.name.eq(o.person_name))
161
163
  .build();
164
+ const _mixedScopeAndInJoinPredicate = ctx.from.person
165
+ // @ts-expect-error semijoin on(...) only supports one table scope for and/or clauses.
166
+ .leftSemijoin(ctx.from.order, (p, o) => p.id.eq(o.id).and(o.id.eq(5)))
167
+ .build();
162
168
  return ctx.from.person
163
169
  .where(row => row.id.eq(5))
164
170
  .leftSemijoin(ctx.from.order, (p, o) => p.id.eq(o.id))
@@ -96,6 +96,7 @@ export class SpacetimeDBQueryClient {
96
96
  setConnection(connection: SpacetimeConnection): void {
97
97
  this.connection = connection;
98
98
  this.processPendingQueries();
99
+ this.hydrateSubscriptions();
99
100
  }
100
101
 
101
102
  connect(queryClient: QueryClient): void {
@@ -289,6 +290,29 @@ export class SpacetimeDBQueryClient {
289
290
  }
290
291
  }
291
292
 
293
+ // subscribe to queries with SSR cached data but no active subscription
294
+ private hydrateSubscriptions(): void {
295
+ if (!this.connection || !this.queryClient) {
296
+ return;
297
+ }
298
+
299
+ for (const [querySql, { accessorName, whereExpr }] of queryRegistry) {
300
+ const queryKey = ['spacetimedb', accessorName, querySql] as const;
301
+ const keyStr = JSON.stringify(queryKey);
302
+
303
+ if (this.subscriptions.has(keyStr)) {
304
+ continue;
305
+ }
306
+ if (this.queryClient.getQueryData(queryKey) === undefined) {
307
+ continue;
308
+ }
309
+
310
+ this.setupSubscription(queryKey, accessorName, querySql, whereExpr).catch(
311
+ () => {}
312
+ );
313
+ }
314
+ }
315
+
292
316
  // clean up all subscriptions and disconnect
293
317
  disconnect(): void {
294
318
  if (this.cacheUnsubscribe) {