spacetimedb 2.0.2 → 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 (95) 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 +6 -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 +133 -100
  15. package/dist/index.browser.mjs.map +1 -1
  16. package/dist/index.cjs +133 -100
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.mjs +133 -100
  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/lib/util.d.ts +11 -11
  31. package/dist/lib/util.d.ts.map +1 -1
  32. package/dist/min/index.browser.mjs +1 -1
  33. package/dist/min/index.browser.mjs.map +1 -1
  34. package/dist/min/react/index.mjs +1 -1
  35. package/dist/min/react/index.mjs.map +1 -1
  36. package/dist/min/sdk/index.browser.mjs +1 -1
  37. package/dist/min/sdk/index.browser.mjs.map +1 -1
  38. package/dist/react/index.cjs +6 -0
  39. package/dist/react/index.cjs.map +1 -1
  40. package/dist/react/index.mjs +6 -0
  41. package/dist/react/index.mjs.map +1 -1
  42. package/dist/react/useTable.d.ts.map +1 -1
  43. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  44. package/dist/sdk/index.browser.mjs +133 -100
  45. package/dist/sdk/index.browser.mjs.map +1 -1
  46. package/dist/sdk/index.cjs +133 -100
  47. package/dist/sdk/index.cjs.map +1 -1
  48. package/dist/sdk/index.mjs +133 -100
  49. package/dist/sdk/index.mjs.map +1 -1
  50. package/dist/sdk/procedures.d.ts +1 -1
  51. package/dist/sdk/procedures.d.ts.map +1 -1
  52. package/dist/sdk/reducers.d.ts +2 -3
  53. package/dist/sdk/reducers.d.ts.map +1 -1
  54. package/dist/sdk/table_cache.d.ts.map +1 -1
  55. package/dist/sdk/websocket_decompress_adapter.d.ts +17 -7
  56. package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
  57. package/dist/sdk/websocket_test_adapter.d.ts +3 -2
  58. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  59. package/dist/server/index.mjs +72 -29
  60. package/dist/server/index.mjs.map +1 -1
  61. package/dist/svelte/index.cjs +3 -0
  62. package/dist/svelte/index.cjs.map +1 -1
  63. package/dist/svelte/index.mjs +3 -0
  64. package/dist/svelte/index.mjs.map +1 -1
  65. package/dist/tanstack/SpacetimeDBQueryClient.d.ts +3 -3
  66. package/dist/tanstack/SpacetimeDBQueryClient.d.ts.map +1 -1
  67. package/dist/tanstack/hooks.d.ts +3 -6
  68. package/dist/tanstack/hooks.d.ts.map +1 -1
  69. package/dist/tanstack/index.cjs +28 -1
  70. package/dist/tanstack/index.cjs.map +1 -1
  71. package/dist/tanstack/index.mjs +28 -1
  72. package/dist/tanstack/index.mjs.map +1 -1
  73. package/dist/vue/index.cjs +3 -0
  74. package/dist/vue/index.cjs.map +1 -1
  75. package/dist/vue/index.mjs +3 -0
  76. package/dist/vue/index.mjs.map +1 -1
  77. package/package.json +1 -1
  78. package/src/lib/binary_writer.ts +4 -0
  79. package/src/lib/indexes.ts +1 -1
  80. package/src/lib/query.ts +30 -6
  81. package/src/lib/schema.ts +66 -24
  82. package/src/lib/table.ts +41 -9
  83. package/src/lib/util.ts +18 -21
  84. package/src/react/useTable.ts +7 -0
  85. package/src/sdk/db_connection_impl.ts +38 -43
  86. package/src/sdk/procedures.ts +1 -3
  87. package/src/sdk/reducers.ts +5 -7
  88. package/src/sdk/table_cache.ts +14 -11
  89. package/src/sdk/websocket_decompress_adapter.ts +42 -45
  90. package/src/sdk/websocket_test_adapter.ts +3 -2
  91. package/src/server/runtime.ts +7 -3
  92. package/src/server/schema.test-d.ts +37 -0
  93. package/src/server/view.test-d.ts +2 -0
  94. package/src/tanstack/SpacetimeDBQueryClient.ts +28 -2
  95. package/src/tanstack/hooks.ts +3 -2
@@ -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
 
@@ -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,
@@ -4,13 +4,15 @@ import type {
4
4
  QueryFunction,
5
5
  } from '@tanstack/react-query';
6
6
  import {
7
+ type Query,
8
+ toSql,
7
9
  type BooleanExpr,
8
10
  evaluateBooleanExpr,
9
11
  getQueryAccessorName,
10
12
  getQueryWhereClause,
11
13
  } from '../lib/query';
12
14
 
13
- type QueryInput = { toSql(): string } & Record<string, any>;
15
+ type QueryInput = Query<any>;
14
16
 
15
17
  const queryRegistry = new Map<
16
18
  string,
@@ -51,7 +53,7 @@ export function spacetimeDBQuery(
51
53
  const query = queryOrSkip;
52
54
  const accessorName = getQueryAccessorName(query);
53
55
  const whereExpr = getQueryWhereClause(query);
54
- const querySql = query.toSql();
56
+ const querySql = toSql(query);
55
57
 
56
58
  queryRegistry.set(querySql, { accessorName, whereExpr });
57
59
 
@@ -94,6 +96,7 @@ export class SpacetimeDBQueryClient {
94
96
  setConnection(connection: SpacetimeConnection): void {
95
97
  this.connection = connection;
96
98
  this.processPendingQueries();
99
+ this.hydrateSubscriptions();
97
100
  }
98
101
 
99
102
  connect(queryClient: QueryClient): void {
@@ -287,6 +290,29 @@ export class SpacetimeDBQueryClient {
287
290
  }
288
291
  }
289
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
+
290
316
  // clean up all subscriptions and disconnect
291
317
  disconnect(): void {
292
318
  if (this.cacheUnsubscribe) {
@@ -6,6 +6,7 @@ import type {
6
6
  UseSuspenseQueryResult,
7
7
  } from '@tanstack/react-query';
8
8
  import type { UntypedTableDef, RowType } from '../lib/table';
9
+ import type { Query } from '../lib/query';
9
10
  import { spacetimeDBQuery } from './SpacetimeDBQueryClient';
10
11
 
11
12
  export type UseSpacetimeDBQueryResult<T> = [
@@ -29,7 +30,7 @@ export type UseSpacetimeDBSuspenseQueryResult<T> = [
29
30
  // useSpacetimeDBQuery(tables.user.where(r => r.online.eq(true)))
30
31
  // useSpacetimeDBQuery(condition ? tables.user : 'skip')
31
32
  export function useSpacetimeDBQuery<TableDef extends UntypedTableDef>(
32
- queryOrSkip: ({ toSql(): string } & Record<string, any>) | 'skip',
33
+ queryOrSkip: Query<TableDef> | 'skip',
33
34
  // any useQuery option (e.g. enabled, refetchInterval, select, placeholderData),
34
35
  // except queryKey, queryFn, and meta (managed internally)
35
36
  options?: Omit<
@@ -60,7 +61,7 @@ export function useSpacetimeDBQuery<TableDef extends UntypedTableDef>(
60
61
  // until data is ready, a parent <Suspense fallback={…}> handles the loading UI.
61
62
  // does not support 'skip' because useSuspenseQuery must always resolve
62
63
  export function useSpacetimeDBSuspenseQuery<TableDef extends UntypedTableDef>(
63
- query: { toSql(): string } & Record<string, any>,
64
+ query: Query<TableDef>,
64
65
  options?: Omit<
65
66
  UseSuspenseQueryOptions<
66
67
  RowType<TableDef>[],