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.
- package/LICENSE.txt +2 -2
- package/dist/angular/index.cjs +5 -1
- package/dist/angular/index.cjs.map +1 -1
- package/dist/angular/index.mjs +5 -1
- package/dist/angular/index.mjs.map +1 -1
- package/dist/browser/angular/index.mjs +5 -1
- package/dist/browser/angular/index.mjs.map +1 -1
- package/dist/browser/react/index.mjs +8 -1
- package/dist/browser/react/index.mjs.map +1 -1
- package/dist/browser/svelte/index.mjs +5 -1
- package/dist/browser/svelte/index.mjs.map +1 -1
- package/dist/browser/vue/index.mjs +5 -1
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +148 -100
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +148 -100
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +148 -100
- package/dist/index.mjs.map +1 -1
- package/dist/lib/algebraic_type.d.ts.map +1 -1
- package/dist/lib/binary_writer.d.ts +1 -0
- package/dist/lib/binary_writer.d.ts.map +1 -1
- package/dist/lib/indexes.d.ts +1 -1
- package/dist/lib/indexes.d.ts.map +1 -1
- package/dist/lib/query.d.ts +14 -7
- package/dist/lib/query.d.ts.map +1 -1
- package/dist/lib/schema.d.ts +2 -0
- package/dist/lib/schema.d.ts.map +1 -1
- package/dist/lib/table.d.ts +25 -2
- package/dist/lib/table.d.ts.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/react/index.mjs +1 -1
- package/dist/min/react/index.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/react/index.cjs +8 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.mjs +8 -1
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useTable.d.ts.map +1 -1
- package/dist/sdk/db_connection_impl.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +144 -98
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +144 -98
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +144 -98
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/sdk/table_cache.d.ts.map +1 -1
- package/dist/sdk/websocket_decompress_adapter.d.ts +17 -7
- package/dist/sdk/websocket_decompress_adapter.d.ts.map +1 -1
- package/dist/sdk/websocket_test_adapter.d.ts +3 -2
- package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.mjs +88 -30
- package/dist/server/index.mjs.map +1 -1
- package/dist/svelte/index.cjs +5 -1
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.mjs +5 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/tanstack/SpacetimeDBQueryClient.d.ts +1 -0
- package/dist/tanstack/SpacetimeDBQueryClient.d.ts.map +1 -1
- package/dist/tanstack/index.cjs +26 -1
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.mjs +26 -1
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs +5 -1
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.mjs +5 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/algebraic_type.ts +5 -1
- package/src/lib/binary_writer.ts +4 -0
- package/src/lib/indexes.ts +1 -1
- package/src/lib/query.ts +90 -25
- package/src/lib/schema.ts +66 -24
- package/src/lib/table.ts +47 -10
- package/src/react/useTable.ts +5 -0
- package/src/sdk/db_connection_impl.ts +38 -43
- package/src/sdk/table_cache.ts +14 -11
- package/src/sdk/websocket_decompress_adapter.ts +42 -45
- package/src/sdk/websocket_test_adapter.ts +3 -2
- package/src/server/index.ts +1 -0
- package/src/server/runtime.ts +7 -3
- package/src/server/schema.test-d.ts +37 -0
- package/src/server/view.test-d.ts +6 -0
- package/src/tanstack/SpacetimeDBQueryClient.ts +24 -0
package/src/sdk/table_cache.ts
CHANGED
|
@@ -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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
102
|
-
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
14
|
+
export class WebsocketDecompressAdapter implements WebsocketAdapter {
|
|
15
|
+
set onclose(handler: (ev: CloseEvent) => void) {
|
|
16
|
+
this.#ws.onclose = handler;
|
|
31
17
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.onopen?.(msg);
|
|
18
|
+
set onopen(handler: () => void) {
|
|
19
|
+
this.#ws.onopen = handler;
|
|
35
20
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
#
|
|
42
|
-
|
|
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:
|
|
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!:
|
|
8
|
+
onopen!: () => void;
|
|
8
9
|
onmessage: any;
|
|
9
10
|
onerror: any;
|
|
10
11
|
|
package/src/server/index.ts
CHANGED
|
@@ -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
|
package/src/server/runtime.ts
CHANGED
|
@@ -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
|
-
|
|
799
|
-
|
|
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[
|
|
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) {
|