spacetimedb 2.4.0 → 2.5.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/README.md +91 -0
- package/dist/browser/solid/index.mjs +1863 -0
- package/dist/browser/solid/index.mjs.map +1 -0
- package/dist/index.browser.mjs +14 -0
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +14 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +14 -0
- package/dist/index.mjs.map +1 -1
- package/dist/lib/autogen/types.d.ts +17 -0
- package/dist/lib/autogen/types.d.ts.map +1 -1
- package/dist/lib/schema.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/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.browser.mjs +14 -0
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +14 -0
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +14 -0
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/server/index.mjs +46 -2
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/schema.d.ts +3 -3
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/views.d.ts +17 -1
- package/dist/server/views.d.ts.map +1 -1
- package/dist/solid/SpacetimeDBProvider.d.ts +7 -0
- package/dist/solid/SpacetimeDBProvider.d.ts.map +1 -0
- package/dist/solid/connection_state.d.ts +6 -0
- package/dist/solid/connection_state.d.ts.map +1 -0
- package/dist/solid/index.cjs +1869 -0
- package/dist/solid/index.cjs.map +1 -0
- package/dist/solid/index.d.ts +6 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.mjs +1863 -0
- package/dist/solid/index.mjs.map +1 -0
- package/dist/solid/useProcedure.d.ts +4 -0
- package/dist/solid/useProcedure.d.ts.map +1 -0
- package/dist/solid/useReducer.d.ts +4 -0
- package/dist/solid/useReducer.d.ts.map +1 -0
- package/dist/solid/useSpacetimeDB.d.ts +4 -0
- package/dist/solid/useSpacetimeDB.d.ts.map +1 -0
- package/dist/solid/useTable.d.ts +32 -0
- package/dist/solid/useTable.d.ts.map +1 -0
- package/package.json +13 -3
- package/src/lib/autogen/types.ts +9 -0
- package/src/lib/schema.ts +7 -0
- package/src/server/schema.ts +15 -2
- package/src/server/view.test-d.ts +22 -0
- package/src/server/views.ts +126 -0
- package/src/solid/SpacetimeDBProvider.ts +97 -0
- package/src/solid/connection_state.ts +6 -0
- package/src/solid/index.ts +5 -0
- package/src/solid/useProcedure.ts +57 -0
- package/src/solid/useReducer.ts +50 -0
- package/src/solid/useSpacetimeDB.ts +18 -0
- package/src/solid/useTable.ts +203 -0
package/src/server/schema.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
type ViewFn,
|
|
51
51
|
type ViewOpts,
|
|
52
52
|
type ViewReturnTypeBuilder,
|
|
53
|
+
type ValidateViewPrimaryKey,
|
|
53
54
|
type Views,
|
|
54
55
|
} from './views';
|
|
55
56
|
import type { UntypedTableDef } from '../lib/table';
|
|
@@ -395,7 +396,11 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
395
396
|
view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
|
|
396
397
|
opts: ViewOpts,
|
|
397
398
|
ret: Ret,
|
|
398
|
-
fn: F
|
|
399
|
+
fn: F,
|
|
400
|
+
// Compile-time-only guard: this rest parameter is `[]` for valid return
|
|
401
|
+
// builders, but becomes a required error tuple when a returned row builder
|
|
402
|
+
// marks more than one column with `.primaryKey()`.
|
|
403
|
+
..._: ValidateViewPrimaryKey<Ret>
|
|
399
404
|
): ViewExport<F> {
|
|
400
405
|
return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
|
|
401
406
|
}
|
|
@@ -428,7 +433,15 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
|
|
|
428
433
|
anonymousView<
|
|
429
434
|
Ret extends ViewReturnTypeBuilder,
|
|
430
435
|
F extends AnonymousViewFn<S, {}, Ret>,
|
|
431
|
-
>(
|
|
436
|
+
>(
|
|
437
|
+
opts: ViewOpts,
|
|
438
|
+
ret: Ret,
|
|
439
|
+
fn: F,
|
|
440
|
+
// Compile-time-only guard: this rest parameter is `[]` for valid return
|
|
441
|
+
// builders, but becomes a required error tuple when a returned row builder
|
|
442
|
+
// marks more than one column with `.primaryKey()`.
|
|
443
|
+
..._: ValidateViewPrimaryKey<Ret>
|
|
444
|
+
): ViewExport<F> {
|
|
432
445
|
return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
|
|
433
446
|
}
|
|
434
447
|
|
|
@@ -73,11 +73,33 @@ const spacetime = schema({
|
|
|
73
73
|
|
|
74
74
|
const arrayRetValue = t.array(person.rowType);
|
|
75
75
|
const optionalPerson = t.option(person.rowType);
|
|
76
|
+
const multiplePrimaryKeyRows = t.array(
|
|
77
|
+
t.row('MultiplePrimaryKeyRows', {
|
|
78
|
+
id: t.u32().primaryKey(),
|
|
79
|
+
name: t.string().primaryKey(),
|
|
80
|
+
})
|
|
81
|
+
);
|
|
76
82
|
|
|
77
83
|
spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
|
|
78
84
|
return ctx.from.person.build();
|
|
79
85
|
});
|
|
80
86
|
|
|
87
|
+
// @ts-expect-error views can have at most one primaryKey column on the returned row type.
|
|
88
|
+
spacetime.anonymousView(
|
|
89
|
+
{ name: 'multiplePrimaryRows', public: true },
|
|
90
|
+
multiplePrimaryKeyRows,
|
|
91
|
+
() => []
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// @ts-expect-error the same multiple-primary-key check also applies to query-builder views.
|
|
95
|
+
spacetime.anonymousView(
|
|
96
|
+
{ name: 'multiplePrimaryRowsQuery', public: true },
|
|
97
|
+
multiplePrimaryKeyRows,
|
|
98
|
+
ctx => {
|
|
99
|
+
return ctx.from.person;
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
81
103
|
spacetime.anonymousView(
|
|
82
104
|
{ name: 'optionalPerson', public: true },
|
|
83
105
|
optionalPerson,
|
package/src/server/views.ts
CHANGED
|
@@ -10,10 +10,15 @@ import type { OptionAlgebraicType } from '../lib/option';
|
|
|
10
10
|
import type { ParamsObj } from '../lib/reducers';
|
|
11
11
|
import { type UntypedSchemaDef } from '../lib/schema';
|
|
12
12
|
import {
|
|
13
|
+
ArrayBuilder,
|
|
14
|
+
OptionBuilder,
|
|
13
15
|
RowBuilder,
|
|
16
|
+
type ColumnBuilder,
|
|
17
|
+
type ColumnMetadata,
|
|
14
18
|
type Infer,
|
|
15
19
|
type InferSpacetimeTypeOfTypeBuilder,
|
|
16
20
|
type InferTypeOfRow,
|
|
21
|
+
type RowObj,
|
|
17
22
|
type TypeBuilder,
|
|
18
23
|
} from '../lib/type_builders';
|
|
19
24
|
import { bsatnBaseSize, toPascalCase } from '../lib/util';
|
|
@@ -90,6 +95,77 @@ export type ViewOpts = {
|
|
|
90
95
|
|
|
91
96
|
type FlattenedArray<T> = T extends readonly (infer E)[] ? E : never;
|
|
92
97
|
|
|
98
|
+
// Compile-time mirror of `viewReturnRow` below. Views currently return either
|
|
99
|
+
// `array(row(...))` or `option(row(...))`; this extracts the row object type
|
|
100
|
+
// from those builders so we can inspect column metadata at the type level.
|
|
101
|
+
// Non-row returns collapse to `never`, which makes the primary-key validation
|
|
102
|
+
// below a no-op for unsupported shapes.
|
|
103
|
+
type ViewReturnRow<Ret extends ViewReturnTypeBuilder> =
|
|
104
|
+
Ret extends ArrayBuilder<infer Element>
|
|
105
|
+
? Element extends RowBuilder<infer Row>
|
|
106
|
+
? Row
|
|
107
|
+
: never
|
|
108
|
+
: Ret extends OptionBuilder<infer Value>
|
|
109
|
+
? Value extends RowBuilder<infer Row>
|
|
110
|
+
? Row
|
|
111
|
+
: never
|
|
112
|
+
: never;
|
|
113
|
+
|
|
114
|
+
// Produces a union of the returned row's column names marked with
|
|
115
|
+
// `.primaryKey()`. For example, `{ id: t.u32().primaryKey(), name: t.string() }`
|
|
116
|
+
// becomes `"id"`, while two marked columns becomes `"id" | "name"`.
|
|
117
|
+
type PrimaryKeyColumnNames<Row extends RowObj> = {
|
|
118
|
+
[K in keyof Row & string]: Row[K] extends ColumnBuilder<any, any, infer M>
|
|
119
|
+
? M extends { isPrimaryKey: true }
|
|
120
|
+
? K
|
|
121
|
+
: never
|
|
122
|
+
: never;
|
|
123
|
+
}[keyof Row & string];
|
|
124
|
+
|
|
125
|
+
// Standard conditional-type trick for distinguishing a single type from a
|
|
126
|
+
// union. We use it because zero or one primary-key column is valid, but a union
|
|
127
|
+
// of two or more column names means the row builder marked multiple primary
|
|
128
|
+
// keys.
|
|
129
|
+
type IsUnion<T, U = T> = [T] extends [never]
|
|
130
|
+
? false
|
|
131
|
+
: T extends any
|
|
132
|
+
? [U] extends [T]
|
|
133
|
+
? false
|
|
134
|
+
: true
|
|
135
|
+
: false;
|
|
136
|
+
|
|
137
|
+
// In generic code, row keys may widen from literal names like "id" | "name"
|
|
138
|
+
// to plain `string`. That means "unknown column name", not "multiple primary
|
|
139
|
+
// keys", so avoid a false-positive type error and rely on the runtime check.
|
|
140
|
+
type HasMultiplePrimaryKeys<Row extends RowObj> =
|
|
141
|
+
string extends PrimaryKeyColumnNames<Row>
|
|
142
|
+
? false
|
|
143
|
+
: IsUnion<PrimaryKeyColumnNames<Row>>;
|
|
144
|
+
|
|
145
|
+
type MultiplePrimaryKeyColumns<Ret extends ViewReturnTypeBuilder> =
|
|
146
|
+
PrimaryKeyColumnNames<ViewReturnRow<Ret>>;
|
|
147
|
+
|
|
148
|
+
type ERROR_view_return_type_can_have_at_most_one_primaryKey<
|
|
149
|
+
Columns extends string,
|
|
150
|
+
> = {
|
|
151
|
+
_primaryKeyColumns: Columns;
|
|
152
|
+
_fix: 'Remove primaryKey() from all but one column on the returned row type';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Used as a rest parameter type on `Schema.view` and `Schema.anonymousView`.
|
|
156
|
+
// Valid return builders produce `[]`, so callers pass no extra arguments. If
|
|
157
|
+
// the returned row has multiple `.primaryKey()` columns, this becomes a
|
|
158
|
+
// one-element tuple containing an explanatory error type, which makes the
|
|
159
|
+
// normal three-argument call fail to type-check.
|
|
160
|
+
export type ValidateViewPrimaryKey<Ret extends ViewReturnTypeBuilder> =
|
|
161
|
+
HasMultiplePrimaryKeys<ViewReturnRow<Ret>> extends true
|
|
162
|
+
? [
|
|
163
|
+
error: ERROR_view_return_type_can_have_at_most_one_primaryKey<
|
|
164
|
+
MultiplePrimaryKeyColumns<Ret>
|
|
165
|
+
>,
|
|
166
|
+
]
|
|
167
|
+
: [];
|
|
168
|
+
|
|
93
169
|
// // If we allowed functions to return either.
|
|
94
170
|
// type ViewReturn<Ret extends ViewReturnTypeBuilder> =
|
|
95
171
|
// | Infer<Ret>
|
|
@@ -164,6 +240,22 @@ export function registerView<
|
|
|
164
240
|
returnType,
|
|
165
241
|
});
|
|
166
242
|
|
|
243
|
+
// Runtime counterpart to `ValidateViewPrimaryKey`: the type-level check gives
|
|
244
|
+
// users an early diagnostic in normal code, but this still protects dynamic
|
|
245
|
+
// or widened builders and is the source of the raw module-def metadata.
|
|
246
|
+
const primaryKeyColumns = viewPrimaryKeyColumns(ret);
|
|
247
|
+
if (primaryKeyColumns.length > 1) {
|
|
248
|
+
throw new TypeError(
|
|
249
|
+
`View '${exportName}' can have at most one primaryKey() column on its returned row type; found ${primaryKeyColumns.join(', ')}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (primaryKeyColumns.length === 1) {
|
|
253
|
+
ctx.moduleDef.viewPrimaryKeys.push({
|
|
254
|
+
viewSourceName: exportName,
|
|
255
|
+
columns: primaryKeyColumns,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
167
259
|
if (opts.name != null) {
|
|
168
260
|
ctx.moduleDef.explicitNames.entries.push({
|
|
169
261
|
tag: 'Function',
|
|
@@ -194,6 +286,40 @@ export function registerView<
|
|
|
194
286
|
});
|
|
195
287
|
}
|
|
196
288
|
|
|
289
|
+
// Inspect the returned row builder and collect the column property names marked
|
|
290
|
+
// with `.primaryKey()`. These names are the TypeScript row-builder keys, which
|
|
291
|
+
// are also the raw column names in the module definition emitted by the TS SDK.
|
|
292
|
+
function viewPrimaryKeyColumns(ret: ViewReturnTypeBuilder): string[] {
|
|
293
|
+
const row = viewReturnRow(ret);
|
|
294
|
+
if (row == null) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return Object.entries(row.row)
|
|
299
|
+
.filter(
|
|
300
|
+
(
|
|
301
|
+
entry
|
|
302
|
+
): entry is [string, ColumnBuilder<any, any, ColumnMetadata<any>>] =>
|
|
303
|
+
entry[1].columnMetadata.isPrimaryKey === true
|
|
304
|
+
)
|
|
305
|
+
.map(([name]) => name);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Views can return either `array(row(...))` or `option(row(...))`. The primary
|
|
309
|
+
// key marker lives on the inner `RowBuilder`, so unwrap those two supported
|
|
310
|
+
// shapes and ignore anything else.
|
|
311
|
+
function viewReturnRow(
|
|
312
|
+
ret: ViewReturnTypeBuilder
|
|
313
|
+
): RowBuilder<any> | undefined {
|
|
314
|
+
if (ret instanceof ArrayBuilder && ret.element instanceof RowBuilder) {
|
|
315
|
+
return ret.element;
|
|
316
|
+
}
|
|
317
|
+
if (ret instanceof OptionBuilder && ret.value instanceof RowBuilder) {
|
|
318
|
+
return ret.value;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
197
323
|
type ViewInfo<F> = {
|
|
198
324
|
fn: F;
|
|
199
325
|
deserializeParams: Deserializer<any>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DbConnectionBuilder,
|
|
3
|
+
type DbConnectionImpl,
|
|
4
|
+
} from '../sdk/db_connection_impl';
|
|
5
|
+
import { onCleanup, createMemo, createComputed } from 'solid-js';
|
|
6
|
+
import { createStore } from 'solid-js/store';
|
|
7
|
+
import { SpacetimeDBContext } from './useSpacetimeDB';
|
|
8
|
+
import type { ConnectionState } from './connection_state';
|
|
9
|
+
import { ConnectionId } from '../lib/connection_id';
|
|
10
|
+
import {
|
|
11
|
+
ConnectionManager,
|
|
12
|
+
type ConnectionState as ManagerConnectionState,
|
|
13
|
+
} from '../sdk/connection_manager';
|
|
14
|
+
|
|
15
|
+
export interface SpacetimeDBProviderProps<
|
|
16
|
+
DbConnection extends DbConnectionImpl<any>,
|
|
17
|
+
> {
|
|
18
|
+
connectionBuilder: DbConnectionBuilder<DbConnection>;
|
|
19
|
+
children?: any;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SpacetimeDBProvider<DbConnection extends DbConnectionImpl<any>>(
|
|
23
|
+
props: SpacetimeDBProviderProps<DbConnection>
|
|
24
|
+
) {
|
|
25
|
+
const uri = () => props.connectionBuilder.getUri();
|
|
26
|
+
const moduleName = () => props.connectionBuilder.getModuleName();
|
|
27
|
+
|
|
28
|
+
const key = createMemo(() => ConnectionManager.getKey(uri(), moduleName()));
|
|
29
|
+
|
|
30
|
+
const fallbackState: ManagerConnectionState = {
|
|
31
|
+
isActive: false,
|
|
32
|
+
identity: undefined,
|
|
33
|
+
token: undefined,
|
|
34
|
+
connectionId: ConnectionId.random(),
|
|
35
|
+
connectionError: undefined,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const [state, setState] = createStore<ManagerConnectionState>(fallbackState);
|
|
39
|
+
|
|
40
|
+
// Subscribe to ConnectionManager state changes
|
|
41
|
+
createComputed(() => {
|
|
42
|
+
const currentKey = key();
|
|
43
|
+
|
|
44
|
+
const unsubscribe = ConnectionManager.subscribe(currentKey, () => {
|
|
45
|
+
const snapshot =
|
|
46
|
+
ConnectionManager.getSnapshot(currentKey) ?? fallbackState;
|
|
47
|
+
setState(snapshot);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Load initial snapshot
|
|
51
|
+
const snapshot = ConnectionManager.getSnapshot(currentKey) ?? fallbackState;
|
|
52
|
+
setState(snapshot);
|
|
53
|
+
|
|
54
|
+
onCleanup(() => {
|
|
55
|
+
unsubscribe();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const getConnection = () =>
|
|
60
|
+
ConnectionManager.getConnection<DbConnection>(key());
|
|
61
|
+
|
|
62
|
+
const contextValue: ConnectionState = {
|
|
63
|
+
get isActive() {
|
|
64
|
+
return state.isActive;
|
|
65
|
+
},
|
|
66
|
+
get identity() {
|
|
67
|
+
return state.identity;
|
|
68
|
+
},
|
|
69
|
+
get token() {
|
|
70
|
+
return state.token;
|
|
71
|
+
},
|
|
72
|
+
get connectionId() {
|
|
73
|
+
return state.connectionId;
|
|
74
|
+
},
|
|
75
|
+
get connectionError() {
|
|
76
|
+
return state.connectionError;
|
|
77
|
+
},
|
|
78
|
+
getConnection,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Retain / release lifecycle
|
|
82
|
+
createComputed(() => {
|
|
83
|
+
const currentKey = key();
|
|
84
|
+
ConnectionManager.retain(currentKey, props.connectionBuilder);
|
|
85
|
+
|
|
86
|
+
onCleanup(() => {
|
|
87
|
+
ConnectionManager.release(currentKey);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return SpacetimeDBContext.Provider({
|
|
92
|
+
value: contextValue,
|
|
93
|
+
get children() {
|
|
94
|
+
return props.children;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DbConnectionImpl } from '../sdk/db_connection_impl';
|
|
2
|
+
import type { ConnectionState as ManagerConnectionState } from '../sdk/connection_manager';
|
|
3
|
+
|
|
4
|
+
export type ConnectionState = ManagerConnectionState & {
|
|
5
|
+
getConnection(): DbConnectionImpl<any> | null;
|
|
6
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createEffect } from 'solid-js';
|
|
2
|
+
import type { UntypedProcedureDef } from '../sdk/procedures';
|
|
3
|
+
import { useSpacetimeDB } from './useSpacetimeDB';
|
|
4
|
+
import type {
|
|
5
|
+
ProcedureParamsType,
|
|
6
|
+
ProcedureReturnType,
|
|
7
|
+
} from '../sdk/type_utils';
|
|
8
|
+
|
|
9
|
+
export function useProcedure<ProcedureDef extends UntypedProcedureDef>(
|
|
10
|
+
procedureDef: ProcedureDef
|
|
11
|
+
): (
|
|
12
|
+
...params: ProcedureParamsType<ProcedureDef>
|
|
13
|
+
) => Promise<ProcedureReturnType<ProcedureDef>> {
|
|
14
|
+
const { getConnection, isActive } = useSpacetimeDB();
|
|
15
|
+
const procedureName = procedureDef.accessorName;
|
|
16
|
+
|
|
17
|
+
// Holds calls made before the connection exists
|
|
18
|
+
const queue: {
|
|
19
|
+
params: ProcedureParamsType<ProcedureDef>;
|
|
20
|
+
resolve: (val: any) => void;
|
|
21
|
+
reject: (err: unknown) => void;
|
|
22
|
+
}[] = [];
|
|
23
|
+
|
|
24
|
+
// Flush when we finally have a connection
|
|
25
|
+
createEffect(() => {
|
|
26
|
+
if (!isActive) return;
|
|
27
|
+
|
|
28
|
+
const conn = getConnection();
|
|
29
|
+
if (!conn) return;
|
|
30
|
+
|
|
31
|
+
const fn = (conn.procedures as any)[procedureName] as (
|
|
32
|
+
...p: ProcedureParamsType<ProcedureDef>
|
|
33
|
+
) => Promise<ProcedureReturnType<ProcedureDef>>;
|
|
34
|
+
|
|
35
|
+
if (queue.length) {
|
|
36
|
+
const pending = queue.splice(0);
|
|
37
|
+
for (const item of pending) {
|
|
38
|
+
fn(...item.params).then(item.resolve, item.reject);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (...params: ProcedureParamsType<ProcedureDef>) => {
|
|
44
|
+
const conn = getConnection();
|
|
45
|
+
if (!conn) {
|
|
46
|
+
return new Promise<ProcedureReturnType<ProcedureDef>>(
|
|
47
|
+
(resolve, reject) => {
|
|
48
|
+
queue.push({ params, resolve, reject });
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const fn = (conn.procedures as any)[procedureName] as (
|
|
53
|
+
...p: ProcedureParamsType<ProcedureDef>
|
|
54
|
+
) => Promise<ProcedureReturnType<ProcedureDef>>;
|
|
55
|
+
return fn(...params);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createEffect } from 'solid-js';
|
|
2
|
+
import type { UntypedReducerDef } from '../sdk/reducers';
|
|
3
|
+
import { useSpacetimeDB } from './useSpacetimeDB';
|
|
4
|
+
import type { ParamsType } from '../sdk';
|
|
5
|
+
|
|
6
|
+
export function useReducer<ReducerDef extends UntypedReducerDef>(
|
|
7
|
+
reducerDef: ReducerDef
|
|
8
|
+
): (...params: ParamsType<ReducerDef>) => Promise<void> {
|
|
9
|
+
const { getConnection, isActive } = useSpacetimeDB();
|
|
10
|
+
const reducerName = reducerDef.accessorName;
|
|
11
|
+
|
|
12
|
+
// Holds calls made before the connection exists
|
|
13
|
+
const queue: {
|
|
14
|
+
params: ParamsType<ReducerDef>;
|
|
15
|
+
resolve: () => void;
|
|
16
|
+
reject: (err: unknown) => void;
|
|
17
|
+
}[] = [];
|
|
18
|
+
|
|
19
|
+
// Flush when we finally have a connection
|
|
20
|
+
createEffect(() => {
|
|
21
|
+
if (!isActive) return;
|
|
22
|
+
|
|
23
|
+
const conn = getConnection();
|
|
24
|
+
if (!conn) return;
|
|
25
|
+
|
|
26
|
+
const fn = (conn.reducers as any)[reducerName] as (
|
|
27
|
+
...p: ParamsType<ReducerDef>
|
|
28
|
+
) => Promise<void>;
|
|
29
|
+
|
|
30
|
+
if (queue.length) {
|
|
31
|
+
const pending = queue.splice(0);
|
|
32
|
+
for (const item of pending) {
|
|
33
|
+
fn(...item.params).then(item.resolve, item.reject);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return (...params: ParamsType<ReducerDef>) => {
|
|
39
|
+
const conn = getConnection();
|
|
40
|
+
if (!conn) {
|
|
41
|
+
return new Promise<void>((resolve, reject) => {
|
|
42
|
+
queue.push({ params, resolve, reject });
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const fn = (conn.reducers as any)[reducerName] as (
|
|
46
|
+
...p: ParamsType<ReducerDef>
|
|
47
|
+
) => Promise<void>;
|
|
48
|
+
return fn(...params);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from 'solid-js';
|
|
2
|
+
import type { ConnectionState } from './connection_state';
|
|
3
|
+
|
|
4
|
+
export const SpacetimeDBContext = createContext<ConnectionState | undefined>(
|
|
5
|
+
undefined
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
// Throws an error if used outside of a SpacetimeDBProvider
|
|
9
|
+
// Error is caught by other hooks like useTable so they can provide better error messages
|
|
10
|
+
export function useSpacetimeDB(): ConnectionState {
|
|
11
|
+
const context = useContext(SpacetimeDBContext) as ConnectionState | undefined;
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'useSpacetimeDB must be used within a SpacetimeDBProvider component. Did you forget to add a `SpacetimeDBProvider` to your component tree?'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createSignal, onCleanup, createMemo, createComputed } from 'solid-js';
|
|
2
|
+
import { useSpacetimeDB } from './useSpacetimeDB';
|
|
3
|
+
import { type EventContextInterface } from '../sdk/db_connection_impl';
|
|
4
|
+
import type { UntypedRemoteModule } from '../sdk/spacetime_module';
|
|
5
|
+
import type { RowType, UntypedTableDef } from '../lib/table';
|
|
6
|
+
import type { Prettify } from '../lib/type_util';
|
|
7
|
+
import {
|
|
8
|
+
type Query,
|
|
9
|
+
type BooleanExpr,
|
|
10
|
+
toSql,
|
|
11
|
+
evaluateBooleanExpr,
|
|
12
|
+
getQueryAccessorName,
|
|
13
|
+
getQueryWhereClause,
|
|
14
|
+
} from '../lib/query';
|
|
15
|
+
import { createStore, reconcile } from 'solid-js/store';
|
|
16
|
+
|
|
17
|
+
export interface UseTableCallbacks<RowType> {
|
|
18
|
+
onInsert?: (row: RowType) => void;
|
|
19
|
+
onDelete?: (row: RowType) => void;
|
|
20
|
+
onUpdate?: (oldRow: RowType, newRow: RowType) => void;
|
|
21
|
+
/** Whether the subscription is active. Defaults to `true`. */
|
|
22
|
+
enabled?: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type MembershipChange = 'enter' | 'leave' | 'stayIn' | 'stayOut';
|
|
26
|
+
|
|
27
|
+
function classifyMembership(
|
|
28
|
+
whereExpr: BooleanExpr<any> | undefined,
|
|
29
|
+
oldRow: Record<string, any>,
|
|
30
|
+
newRow: Record<string, any>
|
|
31
|
+
): MembershipChange {
|
|
32
|
+
if (!whereExpr) return 'stayIn';
|
|
33
|
+
const oldIn = evaluateBooleanExpr(whereExpr, oldRow);
|
|
34
|
+
const newIn = evaluateBooleanExpr(whereExpr, newRow);
|
|
35
|
+
if (oldIn && !newIn) return 'leave';
|
|
36
|
+
if (!oldIn && newIn) return 'enter';
|
|
37
|
+
if (oldIn && newIn) return 'stayIn';
|
|
38
|
+
return 'stayOut';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* SolidJS primitive to subscribe to a table in SpacetimeDB and receive live updates.
|
|
43
|
+
*
|
|
44
|
+
* Accepts a query builder expression as the first argument:
|
|
45
|
+
* - `tables.user` — subscribe to all rows
|
|
46
|
+
* - `tables.user.where(r => r.online.eq(true))` — subscribe with a filter
|
|
47
|
+
*
|
|
48
|
+
* @param query - A query builder expression (table reference or filtered query).
|
|
49
|
+
* @param callbacks - Optional callbacks for row insert, delete, and update events.
|
|
50
|
+
* @returns A tuple of [rows, isReady].
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* const [rows, isReady] = useTable(tables.user);
|
|
55
|
+
* const [onlineUsers, isReady] = useTable(
|
|
56
|
+
* tables.user.where(r => r.online.eq(true)),
|
|
57
|
+
* { onInsert: (row) => console.log('New user:', row) }
|
|
58
|
+
* );
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function useTable<TableDef extends UntypedTableDef>(
|
|
62
|
+
query: () => Query<TableDef>,
|
|
63
|
+
callbacks?: UseTableCallbacks<Prettify<RowType<TableDef>>>
|
|
64
|
+
): [readonly Prettify<RowType<TableDef>>[], () => boolean] {
|
|
65
|
+
type UseTableRowType = RowType<TableDef>;
|
|
66
|
+
const enabled = callbacks?.enabled ?? (() => true);
|
|
67
|
+
const q = createMemo(query);
|
|
68
|
+
const accessorName = createMemo(() => getQueryAccessorName(q()));
|
|
69
|
+
const whereExpr = createMemo(() => getQueryWhereClause(q()));
|
|
70
|
+
const querySql = createMemo(() => toSql(q()));
|
|
71
|
+
|
|
72
|
+
let connectionState: import('./connection_state').ConnectionState;
|
|
73
|
+
try {
|
|
74
|
+
connectionState = useSpacetimeDB();
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Could not find SpacetimeDB client! Did you forget to add a ' +
|
|
78
|
+
'`SpacetimeDBProvider`? `useTable` must be used in the SolidJS component tree ' +
|
|
79
|
+
'under a `SpacetimeDBProvider` component.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const [rows, setRows] = createStore<readonly Prettify<UseTableRowType>[]>([]);
|
|
84
|
+
|
|
85
|
+
let latestTransactionEventId: string | null = null;
|
|
86
|
+
|
|
87
|
+
const computeSnapshot = (): readonly Prettify<UseTableRowType>[] => {
|
|
88
|
+
if (!enabled()) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const connection = connectionState.getConnection();
|
|
93
|
+
if (!connection) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const table = connection.db[accessorName()];
|
|
97
|
+
const result: readonly Prettify<UseTableRowType>[] = whereExpr()
|
|
98
|
+
? (Array.from(table.iter()).filter(row =>
|
|
99
|
+
evaluateBooleanExpr(whereExpr()!, row as Record<string, any>)
|
|
100
|
+
) as Prettify<UseTableRowType>[])
|
|
101
|
+
: (Array.from(table.iter()) as Prettify<UseTableRowType>[]);
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const [isReady, setIsReady] = createSignal(false);
|
|
106
|
+
|
|
107
|
+
createComputed(() => {
|
|
108
|
+
if (!enabled()) {
|
|
109
|
+
setIsReady(false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const connection = connectionState.getConnection();
|
|
114
|
+
if (!connectionState.isActive || !connection) {
|
|
115
|
+
setIsReady(false);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cancel = connection
|
|
120
|
+
.subscriptionBuilder()
|
|
121
|
+
.onApplied(() => {
|
|
122
|
+
setIsReady(true);
|
|
123
|
+
})
|
|
124
|
+
.subscribe(querySql());
|
|
125
|
+
|
|
126
|
+
onCleanup(() => {
|
|
127
|
+
cancel.unsubscribe();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Bind to table events
|
|
131
|
+
const table = connection.db[accessorName()];
|
|
132
|
+
|
|
133
|
+
const onInsert = (
|
|
134
|
+
ctx: EventContextInterface<UntypedRemoteModule>,
|
|
135
|
+
row: any
|
|
136
|
+
) => {
|
|
137
|
+
if (whereExpr() && !evaluateBooleanExpr(whereExpr()!, row)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
callbacks?.onInsert?.(row);
|
|
141
|
+
if (ctx.event.id !== latestTransactionEventId) {
|
|
142
|
+
latestTransactionEventId = ctx.event.id;
|
|
143
|
+
setRows(reconcile(computeSnapshot()));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const onDelete = (
|
|
148
|
+
ctx: EventContextInterface<UntypedRemoteModule>,
|
|
149
|
+
row: any
|
|
150
|
+
) => {
|
|
151
|
+
if (whereExpr() && !evaluateBooleanExpr(whereExpr()!, row)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
callbacks?.onDelete?.(row);
|
|
155
|
+
if (ctx.event.id !== latestTransactionEventId) {
|
|
156
|
+
latestTransactionEventId = ctx.event.id;
|
|
157
|
+
setRows(reconcile(computeSnapshot()));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const onUpdate = (
|
|
162
|
+
ctx: EventContextInterface<UntypedRemoteModule>,
|
|
163
|
+
oldRow: any,
|
|
164
|
+
newRow: any
|
|
165
|
+
) => {
|
|
166
|
+
const change = classifyMembership(whereExpr(), oldRow, newRow);
|
|
167
|
+
|
|
168
|
+
switch (change) {
|
|
169
|
+
case 'leave':
|
|
170
|
+
callbacks?.onDelete?.(oldRow);
|
|
171
|
+
break;
|
|
172
|
+
case 'enter':
|
|
173
|
+
callbacks?.onInsert?.(newRow);
|
|
174
|
+
break;
|
|
175
|
+
case 'stayIn':
|
|
176
|
+
callbacks?.onUpdate?.(oldRow, newRow);
|
|
177
|
+
break;
|
|
178
|
+
case 'stayOut':
|
|
179
|
+
return; // no-op
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (ctx.event.id !== latestTransactionEventId) {
|
|
183
|
+
latestTransactionEventId = ctx.event.id;
|
|
184
|
+
setRows(reconcile(computeSnapshot()));
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
table.onInsert(onInsert);
|
|
189
|
+
table.onDelete(onDelete);
|
|
190
|
+
table.onUpdate?.(onUpdate);
|
|
191
|
+
|
|
192
|
+
// Load initial snapshot
|
|
193
|
+
setRows(reconcile(computeSnapshot()));
|
|
194
|
+
|
|
195
|
+
onCleanup(() => {
|
|
196
|
+
table.removeOnInsert(onInsert);
|
|
197
|
+
table.removeOnDelete(onDelete);
|
|
198
|
+
table.removeOnUpdate?.(onUpdate);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return [rows, isReady];
|
|
203
|
+
}
|