spacetimedb 2.4.1 → 2.6.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 (194) hide show
  1. package/LICENSE.txt +759 -759
  2. package/README.md +211 -120
  3. package/dist/angular/index.cjs.map +1 -1
  4. package/dist/angular/index.mjs.map +1 -1
  5. package/dist/browser/angular/index.mjs.map +1 -1
  6. package/dist/browser/react/index.mjs +129 -57
  7. package/dist/browser/react/index.mjs.map +1 -1
  8. package/dist/browser/solid/index.mjs +1933 -0
  9. package/dist/browser/solid/index.mjs.map +1 -0
  10. package/dist/browser/svelte/index.mjs.map +1 -1
  11. package/dist/browser/vue/index.mjs.map +1 -1
  12. package/dist/index.browser.mjs +10 -2
  13. package/dist/index.browser.mjs.map +1 -1
  14. package/dist/index.cjs +10 -2
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.mjs +10 -2
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/min/index.browser.mjs +1 -1
  19. package/dist/min/index.browser.mjs.map +1 -1
  20. package/dist/min/react/index.mjs +1 -1
  21. package/dist/min/react/index.mjs.map +1 -1
  22. package/dist/min/sdk/index.browser.mjs +1 -1
  23. package/dist/min/sdk/index.browser.mjs.map +1 -1
  24. package/dist/react/index.cjs +129 -57
  25. package/dist/react/index.cjs.map +1 -1
  26. package/dist/react/index.mjs +129 -57
  27. package/dist/react/index.mjs.map +1 -1
  28. package/dist/react/useTable.d.ts.map +1 -1
  29. package/dist/sdk/connection_manager.d.ts +8 -0
  30. package/dist/sdk/connection_manager.d.ts.map +1 -1
  31. package/dist/sdk/db_connection_impl.d.ts +7 -0
  32. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  33. package/dist/sdk/index.browser.mjs +10 -2
  34. package/dist/sdk/index.browser.mjs.map +1 -1
  35. package/dist/sdk/index.cjs +10 -2
  36. package/dist/sdk/index.cjs.map +1 -1
  37. package/dist/sdk/index.mjs +10 -2
  38. package/dist/sdk/index.mjs.map +1 -1
  39. package/dist/sdk/websocket_test_adapter.d.ts +2 -1
  40. package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
  41. package/dist/server/index.mjs.map +1 -1
  42. package/dist/server/runtime.d.ts.map +1 -1
  43. package/dist/solid/SpacetimeDBProvider.d.ts +7 -0
  44. package/dist/solid/SpacetimeDBProvider.d.ts.map +1 -0
  45. package/dist/solid/connection_state.d.ts +6 -0
  46. package/dist/solid/connection_state.d.ts.map +1 -0
  47. package/dist/solid/index.cjs +1939 -0
  48. package/dist/solid/index.cjs.map +1 -0
  49. package/dist/solid/index.d.ts +6 -0
  50. package/dist/solid/index.d.ts.map +1 -0
  51. package/dist/solid/index.mjs +1933 -0
  52. package/dist/solid/index.mjs.map +1 -0
  53. package/dist/solid/useProcedure.d.ts +4 -0
  54. package/dist/solid/useProcedure.d.ts.map +1 -0
  55. package/dist/solid/useReducer.d.ts +4 -0
  56. package/dist/solid/useReducer.d.ts.map +1 -0
  57. package/dist/solid/useSpacetimeDB.d.ts +4 -0
  58. package/dist/solid/useSpacetimeDB.d.ts.map +1 -0
  59. package/dist/solid/useTable.d.ts +32 -0
  60. package/dist/solid/useTable.d.ts.map +1 -0
  61. package/dist/svelte/index.cjs.map +1 -1
  62. package/dist/svelte/index.mjs.map +1 -1
  63. package/dist/tanstack/index.cjs +120 -50
  64. package/dist/tanstack/index.cjs.map +1 -1
  65. package/dist/tanstack/index.mjs +120 -50
  66. package/dist/tanstack/index.mjs.map +1 -1
  67. package/dist/vue/index.cjs.map +1 -1
  68. package/dist/vue/index.mjs.map +1 -1
  69. package/package.json +13 -3
  70. package/src/angular/connection_state.ts +19 -19
  71. package/src/angular/index.ts +3 -3
  72. package/src/angular/injectors/index.ts +4 -4
  73. package/src/angular/injectors/inject-reducer.ts +62 -62
  74. package/src/angular/injectors/inject-spacetimedb-connected.ts +13 -13
  75. package/src/angular/injectors/inject-spacetimedb.ts +10 -10
  76. package/src/angular/injectors/inject-table.ts +234 -234
  77. package/src/angular/providers/index.ts +1 -1
  78. package/src/angular/providers/provide-spacetimedb.ts +96 -96
  79. package/src/index.ts +16 -16
  80. package/src/lib/algebraic_type.ts +819 -819
  81. package/src/lib/algebraic_type_variants.ts +26 -26
  82. package/src/lib/algebraic_value.ts +10 -10
  83. package/src/lib/autogen/types.ts +746 -746
  84. package/src/lib/binary_reader.ts +188 -188
  85. package/src/lib/binary_writer.ts +213 -213
  86. package/src/lib/connection_id.ts +102 -102
  87. package/src/lib/constraints.ts +48 -48
  88. package/src/lib/errors.ts +26 -26
  89. package/src/lib/filter.ts +195 -195
  90. package/src/lib/identity.ts +83 -83
  91. package/src/lib/indexes.ts +251 -251
  92. package/src/lib/option.ts +34 -34
  93. package/src/lib/query.ts +1019 -1019
  94. package/src/lib/reducer_schema.ts +38 -38
  95. package/src/lib/reducers.ts +116 -116
  96. package/src/lib/result.ts +36 -36
  97. package/src/lib/schedule_at.ts +86 -86
  98. package/src/lib/schema.ts +420 -420
  99. package/src/lib/table.ts +548 -548
  100. package/src/lib/table_schema.ts +64 -64
  101. package/src/lib/time_duration.ts +77 -77
  102. package/src/lib/timestamp.ts +148 -148
  103. package/src/lib/type_builders.test-d.ts +128 -128
  104. package/src/lib/type_builders.ts +4014 -4014
  105. package/src/lib/type_util.ts +124 -124
  106. package/src/lib/util.ts +196 -196
  107. package/src/lib/uuid.ts +337 -337
  108. package/src/react/SpacetimeDBProvider.ts +84 -84
  109. package/src/react/connection_state.ts +6 -6
  110. package/src/react/index.ts +5 -5
  111. package/src/react/useProcedure.ts +60 -60
  112. package/src/react/useReducer.ts +53 -53
  113. package/src/react/useSpacetimeDB.ts +18 -18
  114. package/src/react/useTable.ts +256 -251
  115. package/src/sdk/client_api/index.ts +114 -114
  116. package/src/sdk/client_api/types/procedures.ts +8 -8
  117. package/src/sdk/client_api/types/reducers.ts +8 -8
  118. package/src/sdk/client_api/types.ts +288 -288
  119. package/src/sdk/client_cache.ts +129 -129
  120. package/src/sdk/client_table.ts +179 -179
  121. package/src/sdk/connection_manager.ts +352 -237
  122. package/src/sdk/db_connection_builder.ts +290 -290
  123. package/src/sdk/db_connection_impl.ts +1356 -1347
  124. package/src/sdk/db_context.ts +28 -28
  125. package/src/sdk/db_view.ts +12 -12
  126. package/src/sdk/decompress.ts +51 -51
  127. package/src/sdk/event.ts +18 -18
  128. package/src/sdk/event_context.ts +51 -51
  129. package/src/sdk/event_emitter.ts +32 -32
  130. package/src/sdk/index.ts +14 -14
  131. package/src/sdk/internal.ts +2 -2
  132. package/src/sdk/json_api.ts +46 -46
  133. package/src/sdk/logger.ts +134 -134
  134. package/src/sdk/message_types.ts +46 -46
  135. package/src/sdk/procedures.ts +83 -83
  136. package/src/sdk/reducer_event.ts +20 -20
  137. package/src/sdk/reducer_handle.ts +12 -12
  138. package/src/sdk/reducers.ts +159 -159
  139. package/src/sdk/schema.ts +45 -45
  140. package/src/sdk/spacetime_module.ts +28 -28
  141. package/src/sdk/subscription_builder_impl.ts +275 -275
  142. package/src/sdk/table_cache.ts +581 -581
  143. package/src/sdk/type_utils.ts +19 -19
  144. package/src/sdk/version.ts +133 -133
  145. package/src/sdk/websocket_decompress_adapter.ts +63 -63
  146. package/src/sdk/websocket_protocols.ts +25 -25
  147. package/src/sdk/websocket_test_adapter.ts +107 -100
  148. package/src/sdk/websocket_v3_frames.ts +126 -126
  149. package/src/sdk/ws.ts +105 -105
  150. package/src/server/console.ts +81 -81
  151. package/src/server/db_view.ts +21 -21
  152. package/src/server/errors.ts +138 -138
  153. package/src/server/http.test-d.ts +80 -80
  154. package/src/server/http.ts +14 -14
  155. package/src/server/http_handlers.ts +413 -413
  156. package/src/server/http_internal.ts +79 -79
  157. package/src/server/http_shared.ts +186 -186
  158. package/src/server/index.ts +37 -37
  159. package/src/server/polyfills.ts +4 -4
  160. package/src/server/procedures.ts +239 -239
  161. package/src/server/query.ts +1 -1
  162. package/src/server/range.ts +53 -53
  163. package/src/server/reducers.ts +113 -113
  164. package/src/server/rng.ts +113 -113
  165. package/src/server/runtime.ts +1102 -1102
  166. package/src/server/schema.test-d.ts +99 -99
  167. package/src/server/schema.ts +663 -663
  168. package/src/server/sys.d.ts +125 -125
  169. package/src/server/view.test-d.ts +194 -194
  170. package/src/server/views.ts +340 -340
  171. package/src/solid/SpacetimeDBProvider.ts +97 -0
  172. package/src/solid/connection_state.ts +6 -0
  173. package/src/solid/index.ts +5 -0
  174. package/src/solid/useProcedure.ts +57 -0
  175. package/src/solid/useReducer.ts +50 -0
  176. package/src/solid/useSpacetimeDB.ts +18 -0
  177. package/src/solid/useTable.ts +203 -0
  178. package/src/svelte/SpacetimeDBProvider.ts +101 -101
  179. package/src/svelte/connection_state.ts +16 -16
  180. package/src/svelte/index.ts +4 -4
  181. package/src/svelte/useReducer.ts +61 -61
  182. package/src/svelte/useSpacetimeDB.ts +22 -22
  183. package/src/svelte/useTable.ts +218 -218
  184. package/src/tanstack/SpacetimeDBQueryClient.ts +330 -330
  185. package/src/tanstack/hooks.ts +83 -83
  186. package/src/tanstack/index.ts +16 -16
  187. package/src/util-stub.ts +1 -1
  188. package/src/vue/SpacetimeDBProvider.ts +157 -157
  189. package/src/vue/connection_state.ts +19 -19
  190. package/src/vue/index.ts +5 -5
  191. package/src/vue/useProcedure.ts +62 -62
  192. package/src/vue/useReducer.ts +55 -55
  193. package/src/vue/useSpacetimeDB.ts +18 -18
  194. package/src/vue/useTable.ts +229 -229
@@ -1,663 +1,663 @@
1
- import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
2
- import {
3
- CaseConversionPolicy,
4
- Lifecycle,
5
- type MethodOrAny,
6
- } from '../lib/autogen/types';
7
- import {
8
- type ParamsAsObject,
9
- type ParamsObj,
10
- type Reducer,
11
- type ReducerCtx,
12
- } from '../lib/reducers';
13
- import {
14
- ModuleContext,
15
- tableToSchema,
16
- type TablesToSchema,
17
- type UntypedSchemaDef,
18
- } from '../lib/schema';
19
- import type { UntypedTableSchema } from '../lib/table_schema';
20
- import { ColumnBuilder, TypeBuilder } from '../lib/type_builders';
21
- import {
22
- Router,
23
- type HandlerFn,
24
- type HttpHandlerExport,
25
- type HttpHandlerOpts,
26
- makeHttpHandlerExport,
27
- makeHttpRouterExport,
28
- } from './http_handlers';
29
- import {
30
- makeProcedureExport,
31
- type ProcedureExport,
32
- type ProcedureFn,
33
- type ProcedureOpts,
34
- type Procedures,
35
- } from './procedures';
36
- import {
37
- makeReducerExport,
38
- type ReducerExport,
39
- type ReducerOpts,
40
- type Reducers,
41
- } from './reducers';
42
- import { makeHooks } from './runtime';
43
-
44
- import {
45
- makeAnonViewExport,
46
- makeViewExport,
47
- type AnonViews,
48
- type AnonymousViewFn,
49
- type ViewExport,
50
- type ViewFn,
51
- type ViewOpts,
52
- type ViewReturnTypeBuilder,
53
- type ValidateViewPrimaryKey,
54
- type Views,
55
- } from './views';
56
- import type { UntypedTableDef } from '../lib/table';
57
-
58
- export class SchemaInner<
59
- S extends UntypedSchemaDef = UntypedSchemaDef,
60
- > extends ModuleContext {
61
- schemaType: S;
62
- existingFunctions = new Set<string>();
63
- existingHttpHandlers = new Set<string>();
64
- reducers: Reducers = [];
65
- procedures: Procedures = [];
66
- views: Views = [];
67
- anonViews: AnonViews = [];
68
- httpHandlers: HandlerFn[] = [];
69
- /**
70
- * Maps ReducerExport objects to the name of the reducer.
71
- * Used for resolving the reducers of scheduled tables.
72
- */
73
- functionExports: Map<
74
- | ReducerExport<UntypedSchemaDef, any>
75
- | ProcedureExport<UntypedSchemaDef, any, any>,
76
- string
77
- > = new Map();
78
- httpHandlerExports: Map<HttpHandlerExport<UntypedSchemaDef>, string> =
79
- new Map();
80
- pendingSchedules: PendingSchedule[] = [];
81
- pendingHttpRoutes: PendingHttpRoute[] = [];
82
-
83
- constructor(getSchemaType: (ctx: SchemaInner<S>) => S) {
84
- super();
85
- this.schemaType = getSchemaType(this);
86
- }
87
-
88
- defineFunction(name: string) {
89
- if (this.existingFunctions.has(name)) {
90
- throw new TypeError(
91
- `There is already a reducer, procedure, or view with the name '${name}'`
92
- );
93
- }
94
- this.existingFunctions.add(name);
95
- }
96
-
97
- defineHttpHandler(name: string) {
98
- if (this.existingHttpHandlers.has(name)) {
99
- throw new TypeError(
100
- `There is already an HTTP handler with the name '${name}'`
101
- );
102
- }
103
- this.existingHttpHandlers.add(name);
104
- }
105
-
106
- resolveSchedules() {
107
- for (const { reducer, scheduleAtCol, tableName } of this.pendingSchedules) {
108
- const functionName = this.functionExports.get(reducer());
109
- if (functionName === undefined) {
110
- const msg = `Table ${tableName} defines a schedule, but it seems like the associated function was not exported.`;
111
- throw new TypeError(msg);
112
- }
113
- this.moduleDef.schedules.push({
114
- sourceName: undefined,
115
- tableName,
116
- scheduleAtCol,
117
- functionName,
118
- });
119
- }
120
- }
121
-
122
- resolveHttpRoutes() {
123
- for (const route of this.pendingHttpRoutes) {
124
- const handlerFunction = this.httpHandlerExports.get(route.handler);
125
- if (handlerFunction === undefined) {
126
- throw new TypeError(
127
- `HTTP route for path '${route.path}' refers to a handler that was not exported.`
128
- );
129
- }
130
- this.moduleDef.httpRoutes.push({
131
- handlerFunction,
132
- method: route.method,
133
- path: route.path,
134
- });
135
- }
136
- }
137
- }
138
-
139
- type PendingSchedule = UntypedTableSchema['schedule'] & { tableName: string };
140
- type PendingHttpRoute = {
141
- handler: HttpHandlerExport<UntypedSchemaDef>;
142
- method: MethodOrAny;
143
- path: string;
144
- };
145
-
146
- /**
147
- * The Schema class represents the database schema for a SpacetimeDB application.
148
- * It encapsulates the table definitions and typespace, and provides methods to define
149
- * reducers and lifecycle hooks.
150
- *
151
- * Schema has a generic parameter S which represents the inferred schema type. This type
152
- * is automatically inferred when creating a schema using the `schema()` function and is
153
- * used to type the database view in reducer contexts.
154
- *
155
- * The methods on this class are used to register reducers and lifecycle hooks
156
- * with the SpacetimeDB runtime. Theey forward to free functions that handle the actual
157
- * registration logic, but having them as methods on the Schema class helps with type inference.
158
- *
159
- * @template S - The inferred schema type of the SpacetimeDB module.
160
- *
161
- * @example
162
- * ```typescript
163
- * const spacetimedb = schema({
164
- * user: table({}, userType),
165
- * post: table({}, postType)
166
- * });
167
- * spacetimedb.reducer(
168
- * 'create_user',
169
- * { username: t.string(), email: t.string() },
170
- * (ctx, { username, email }) => {
171
- * ctx.db.user.insert({ username, email, created_at: ctx.timestamp });
172
- * console.log(`User ${username} created by ${ctx.sender.identityId}`);
173
- * }
174
- * );
175
- * ```
176
- */
177
- // TODO(cloutiertyler): It might be nice to have a way to access the types
178
- // for the tables from the schema object, e.g. `spacetimedb.user.type` would
179
- // be the type of the user table.
180
- export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
181
- #ctx: SchemaInner<S>;
182
-
183
- constructor(ctx: SchemaInner<S>) {
184
- // TODO: TableSchema and TableDef should really be unified
185
- this.#ctx = ctx;
186
- }
187
-
188
- [moduleHooks](exports: object) {
189
- // if (!(hasOwn(exports, 'default') && exports.default instanceof Schema)) {
190
- // throw new TypeError('must export schema as default export');
191
- // }
192
- const registeredSchema = this.#ctx;
193
- for (const [name, moduleExport] of Object.entries(exports)) {
194
- if (name === 'default') continue;
195
- if (!isModuleExport(moduleExport)) {
196
- throw new TypeError(
197
- 'exporting something that is not a spacetime export'
198
- );
199
- }
200
- checkExportContext(moduleExport, registeredSchema);
201
- moduleExport[registerExport](registeredSchema, name);
202
- }
203
- registeredSchema.resolveSchedules();
204
- registeredSchema.resolveHttpRoutes();
205
- return makeHooks(registeredSchema);
206
- }
207
-
208
- get schemaType(): S {
209
- return this.#ctx.schemaType;
210
- }
211
-
212
- get moduleDef() {
213
- return this.#ctx.moduleDef;
214
- }
215
-
216
- get typespace() {
217
- return this.#ctx.typespace;
218
- }
219
-
220
- /**
221
- * Defines a SpacetimeDB reducer function.
222
- *
223
- * Reducers are the primary way to modify the state of your SpacetimeDB application.
224
- * They are atomic, meaning that either all operations within a reducer succeed,
225
- * or none of them do.
226
- *
227
- * @template S - The inferred schema type of the SpacetimeDB module.
228
- * @template Params - The type of the parameters object expected by the reducer.
229
- *
230
- * @param {Params} params - An object defining the parameters that the reducer accepts.
231
- * Each key-value pair represents a parameter name and its corresponding
232
- * {@link TypeBuilder} or {@link ColumnBuilder}.
233
- * @param {(ctx: ReducerCtx<S>, payload: ParamsAsObject<Params>) => void} fn - The reducer function itself.
234
- * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`.
235
- * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`.
236
- *
237
- * @example
238
- * ```typescript
239
- * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string)
240
- * export const create_user = spacetime.reducer(
241
- * {
242
- * username: t.string(),
243
- * email: t.string(),
244
- * },
245
- * (ctx, { username, email }) => {
246
- * // Access the 'user' table from the database view in the context
247
- * ctx.db.user.insert({ username, email, created_at: ctx.timestamp });
248
- * console.log(`User ${username} created by ${ctx.sender.identityId}`);
249
- * }
250
- * );
251
- * ```
252
- */
253
- reducer<Params extends ParamsObj>(
254
- params: Params,
255
- fn: Reducer<S, Params>
256
- ): ReducerExport<S, Params>;
257
- reducer(fn: Reducer<S, {}>): ReducerExport<S, {}>;
258
- reducer<Params extends ParamsObj>(
259
- opts: ReducerOpts,
260
- params: Params,
261
- fn: Reducer<S, Params>
262
- ): ReducerExport<S, Params>;
263
- reducer(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
264
- reducer<Params extends ParamsObj>(
265
- ...args:
266
- | [Params, Reducer<S, Params>]
267
- | [Reducer<S, {}>]
268
- | [ReducerOpts, Params, Reducer<S, Params>]
269
- | [ReducerOpts, Reducer<S, {}>]
270
- ): ReducerExport<S, Params> {
271
- let opts: ReducerOpts | undefined,
272
- params: Params = {} as Params,
273
- fn: Reducer<S, Params>;
274
- switch (args.length) {
275
- case 1:
276
- [fn] = args;
277
- break;
278
- case 2: {
279
- let arg1;
280
- [arg1, fn] = args;
281
- if (typeof arg1.name === 'string') opts = arg1 as ReducerOpts;
282
- else params = arg1 as Params;
283
- break;
284
- }
285
- case 3:
286
- [opts, params, fn] = args;
287
- break;
288
- }
289
- return makeReducerExport(this.#ctx, opts, params, fn);
290
- }
291
-
292
- /**
293
- * Registers an initialization reducer that runs when the SpacetimeDB module is published
294
- * for the first time.
295
- *
296
- * This function is useful to set up any initial state of your database that is guaranteed
297
- * to run only once, and before any other reducers or client connections.
298
- *
299
- * @template S - The inferred schema type of the SpacetimeDB module.
300
- * @param {Reducer<S, {}>} fn - The initialization reducer function.
301
- * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`.
302
- * @example
303
- * ```typescript
304
- * export const init = spacetime.init((ctx) => {
305
- * ctx.db.user.insert({ username: 'admin', email: 'admin@example.com' });
306
- * });
307
- * ```
308
- */
309
- init(fn: Reducer<S, {}>): ReducerExport<S, {}>;
310
- init(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
311
- init(
312
- ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
313
- ): ReducerExport<S, {}> {
314
- let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
315
- switch (args.length) {
316
- case 1:
317
- [fn] = args;
318
- break;
319
- case 2:
320
- [opts, fn] = args;
321
- break;
322
- }
323
- return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.Init);
324
- }
325
-
326
- /**
327
- * Registers a reducer to be called when a client connects to the SpacetimeDB module.
328
- * This function allows you to define custom logic that should execute
329
- * whenever a new client establishes a connection.
330
- * @template S - The inferred schema type of the SpacetimeDB module.
331
- *
332
- * @param fn - The reducer function to execute on client connection.
333
- *
334
- * @example
335
- * ```typescript
336
- * export const onConnect = spacetime.clientConnected(
337
- * (ctx) => {
338
- * console.log(`Client ${ctx.connectionId} connected`);
339
- * }
340
- * );
341
- */
342
- clientConnected(fn: Reducer<S, {}>): ReducerExport<S, {}>;
343
- clientConnected(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
344
- clientConnected(
345
- ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
346
- ): ReducerExport<S, {}> {
347
- let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
348
- switch (args.length) {
349
- case 1:
350
- [fn] = args;
351
- break;
352
- case 2:
353
- [opts, fn] = args;
354
- break;
355
- }
356
- return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.OnConnect);
357
- }
358
-
359
- /**
360
- * Registers a reducer to be called when a client disconnects from the SpacetimeDB module.
361
- * This function allows you to define custom logic that should execute
362
- * whenever a client disconnects.
363
- * @template S - The inferred schema type of the SpacetimeDB module.
364
- *
365
- * @param fn - The reducer function to execute on client disconnection.
366
- *
367
- * @example
368
- * ```typescript
369
- * export const onDisconnect = spacetime.clientDisconnected(
370
- * (ctx) => {
371
- * console.log(`Client ${ctx.connectionId} disconnected`);
372
- * }
373
- * );
374
- * ```
375
- */
376
- clientDisconnected(fn: Reducer<S, {}>): ReducerExport<S, {}>;
377
- clientDisconnected(
378
- opts: ReducerOpts,
379
- fn: Reducer<S, {}>
380
- ): ReducerExport<S, {}>;
381
- clientDisconnected(
382
- ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
383
- ): ReducerExport<S, {}> {
384
- let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
385
- switch (args.length) {
386
- case 1:
387
- [fn] = args;
388
- break;
389
- case 2:
390
- [opts, fn] = args;
391
- break;
392
- }
393
- return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.OnDisconnect);
394
- }
395
-
396
- view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
397
- opts: ViewOpts,
398
- ret: Ret,
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>
404
- ): ViewExport<F> {
405
- return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
406
- }
407
-
408
- // TODO: re-enable once parameterized views are supported in SQL
409
- // view<Ret extends ViewReturnTypeBuilder>(
410
- // opts: ViewOpts,
411
- // ret: Ret,
412
- // fn: ViewFn<S, {}, Ret>
413
- // ): void;
414
- // view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
415
- // opts: ViewOpts,
416
- // params: Params,
417
- // ret: Ret,
418
- // fn: ViewFn<S, {}, Ret>
419
- // ): void;
420
- // view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
421
- // opts: ViewOpts,
422
- // paramsOrRet: Ret | Params,
423
- // retOrFn: ViewFn<S, {}, Ret> | Ret,
424
- // maybeFn?: ViewFn<S, Params, Ret>
425
- // ): void {
426
- // if (typeof retOrFn === 'function') {
427
- // defineView(name, false, {}, paramsOrRet as Ret, retOrFn);
428
- // } else {
429
- // defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!);
430
- // }
431
- // }
432
-
433
- anonymousView<
434
- Ret extends ViewReturnTypeBuilder,
435
- F extends AnonymousViewFn<S, {}, Ret>,
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> {
445
- return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
446
- }
447
-
448
- // TODO: re-enable once parameterized views are supported in SQL
449
- // anonymousView<Ret extends ViewReturnTypeBuilder>(
450
- // opts: ViewOpts,
451
- // ret: Ret,
452
- // fn: AnonymousViewFn<S, {}, Ret>
453
- // ): void;
454
- // anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
455
- // opts: ViewOpts,
456
- // params: Params,
457
- // ret: Ret,
458
- // fn: AnonymousViewFn<S, {}, Ret>
459
- // ): void;
460
- // anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
461
- // opts: ViewOpts,
462
- // paramsOrRet: Ret | Params,
463
- // retOrFn: AnonymousViewFn<S, {}, Ret> | Ret,
464
- // maybeFn?: AnonymousViewFn<S, Params, Ret>
465
- // ): void {
466
- // if (typeof retOrFn === 'function') {
467
- // defineView(name, true, {}, paramsOrRet as Ret, retOrFn);
468
- // } else {
469
- // defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!);
470
- // }
471
- // }
472
-
473
- procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
474
- params: Params,
475
- ret: Ret,
476
- fn: ProcedureFn<S, Params, Ret>
477
- ): ProcedureFn<S, Params, Ret>;
478
- procedure<Ret extends TypeBuilder<any, any>>(
479
- ret: Ret,
480
- fn: ProcedureFn<S, {}, Ret>
481
- ): ProcedureFn<S, {}, Ret>;
482
- procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
483
- opts: ProcedureOpts,
484
- params: Params,
485
- ret: Ret,
486
- fn: ProcedureFn<S, Params, Ret>
487
- ): ProcedureFn<S, Params, Ret>;
488
- procedure<Ret extends TypeBuilder<any, any>>(
489
- opts: ProcedureOpts,
490
- ret: Ret,
491
- fn: ProcedureFn<S, {}, Ret>
492
- ): ProcedureFn<S, {}, Ret>;
493
- procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
494
- ...args:
495
- | [Params, Ret, ProcedureFn<S, Params, Ret>]
496
- | [Ret, ProcedureFn<S, Params, Ret>]
497
- | [ProcedureOpts, Params, Ret, ProcedureFn<S, Params, Ret>]
498
- | [ProcedureOpts, Ret, ProcedureFn<S, Params, Ret>]
499
- ): ProcedureExport<S, Params, Ret> {
500
- let opts: ProcedureOpts | undefined,
501
- params: Params = {} as Params,
502
- ret: Ret,
503
- fn: ProcedureFn<S, Params, Ret>;
504
- switch (args.length) {
505
- case 2:
506
- [ret, fn] = args;
507
- break;
508
- case 3: {
509
- let arg1;
510
- [arg1, ret, fn] = args;
511
- if (typeof arg1.name === 'string') opts = arg1 as ProcedureOpts;
512
- else params = arg1 as Params;
513
- break;
514
- }
515
- case 4:
516
- [opts, params, ret, fn] = args;
517
- break;
518
- }
519
- return makeProcedureExport(this.#ctx, opts, params, ret, fn);
520
- }
521
-
522
- httpHandler(fn: HandlerFn<S>): HttpHandlerExport<S>;
523
- httpHandler(opts: HttpHandlerOpts, fn: HandlerFn<S>): HttpHandlerExport<S>;
524
- httpHandler(
525
- ...args: [HandlerFn<S>] | [HttpHandlerOpts, HandlerFn<S>]
526
- ): HttpHandlerExport<S> {
527
- let opts: HttpHandlerOpts | undefined, fn: HandlerFn<S>;
528
- switch (args.length) {
529
- case 1:
530
- [fn] = args;
531
- break;
532
- case 2:
533
- [opts, fn] = args;
534
- break;
535
- }
536
- return makeHttpHandlerExport(this.#ctx, opts, fn);
537
- }
538
-
539
- httpRouter(router: Router): ModuleExport {
540
- return makeHttpRouterExport(this.#ctx, router);
541
- }
542
-
543
- /**
544
- * Bundle multiple reducers, procedures, etc into one value to export.
545
- * The name they will be exported with is their corresponding key in the `exports` argument.
546
- */
547
- exportGroup(exports: Record<string, ModuleExport>): ModuleExport {
548
- return {
549
- [exportContext]: this.#ctx,
550
- [registerExport](ctx, _exportName) {
551
- for (const [exportName, moduleExport] of Object.entries(exports)) {
552
- checkExportContext(moduleExport, ctx);
553
- moduleExport[registerExport](ctx, exportName);
554
- }
555
- },
556
- };
557
- }
558
-
559
- clientVisibilityFilter = {
560
- sql: (filter: string): ModuleExport => ({
561
- [exportContext]: this.#ctx,
562
- [registerExport](ctx, _exportName) {
563
- ctx.moduleDef.rowLevelSecurity.push({ sql: filter });
564
- },
565
- }),
566
- };
567
- }
568
-
569
- export const registerExport = Symbol('SpacetimeDB.registerExport');
570
- export const exportContext = Symbol('SpacetimeDB.exportContext');
571
-
572
- export interface ModuleExport {
573
- [registerExport](ctx: SchemaInner, exportName: string): void;
574
- [exportContext]?: SchemaInner;
575
- }
576
-
577
- function isModuleExport(x: unknown): x is ModuleExport {
578
- return (
579
- (typeof x === 'function' || typeof x === 'object') &&
580
- x !== null &&
581
- registerExport in x
582
- );
583
- }
584
-
585
- /** Verify that the ModuleContext that `exp` comes from is the same as `schema` */
586
- function checkExportContext(exp: ModuleExport, schema: SchemaInner) {
587
- if (exp[exportContext] != null && exp[exportContext] !== schema) {
588
- throw new TypeError('multiple schemas are not supported');
589
- }
590
- }
591
-
592
- /**
593
- * Extracts the inferred schema type from a Schema instance
594
- */
595
- export type InferSchema<SchemaDef extends Schema<any>> =
596
- SchemaDef extends Schema<infer S> ? S : never;
597
-
598
- /**
599
- * Creates a schema from table definitions
600
- * @param handles - Array of table handles created by table() function
601
- * @returns ColumnBuilder representing the complete database schema
602
- * @example
603
- * ```ts
604
- * const spacetimedb = schema({
605
- * user: table({}, userType),
606
- * post: table({}, postType)
607
- * });
608
- * ```
609
- */
610
- /**
611
- * Module-level settings that can be passed to `schema()`.
612
- */
613
- export interface ModuleSettings {
614
- /**
615
- * The case conversion policy for this module.
616
- * Defaults to `SnakeCase` if not specified.
617
- *
618
- * @example
619
- * ```ts
620
- * export default schema({
621
- * player,
622
- * }, { CASE_CONVERSION_POLICY: CaseConversionPolicy.None });
623
- * ```
624
- */
625
- CASE_CONVERSION_POLICY?: CaseConversionPolicy;
626
- }
627
-
628
- export function schema<const H extends Record<string, UntypedTableSchema>>(
629
- tables: H,
630
- moduleSettings?: ModuleSettings
631
- ): Schema<TablesToSchema<H>> {
632
- const ctx = new SchemaInner<TablesToSchema<H>>(ctx => {
633
- // Apply module settings.
634
- if (moduleSettings?.CASE_CONVERSION_POLICY != null) {
635
- ctx.setCaseConversionPolicy(moduleSettings.CASE_CONVERSION_POLICY);
636
- }
637
-
638
- const tableSchemas: Record<string, UntypedTableDef> = {};
639
- for (const [accName, table] of Object.entries(tables)) {
640
- const tableDef = table.tableDef(ctx, accName);
641
- tableSchemas[accName] = tableToSchema(accName, table, tableDef);
642
- ctx.moduleDef.tables.push(tableDef);
643
- if (table.schedule) {
644
- ctx.pendingSchedules.push({
645
- ...table.schedule,
646
- tableName: tableDef.sourceName,
647
- });
648
- }
649
- if (table.tableName) {
650
- ctx.moduleDef.explicitNames.entries.push({
651
- tag: 'Table',
652
- value: {
653
- sourceName: accName,
654
- canonicalName: table.tableName,
655
- },
656
- });
657
- }
658
- }
659
- return { tables: tableSchemas } as TablesToSchema<H>;
660
- });
661
-
662
- return new Schema(ctx);
663
- }
1
+ import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
2
+ import {
3
+ CaseConversionPolicy,
4
+ Lifecycle,
5
+ type MethodOrAny,
6
+ } from '../lib/autogen/types';
7
+ import {
8
+ type ParamsAsObject,
9
+ type ParamsObj,
10
+ type Reducer,
11
+ type ReducerCtx,
12
+ } from '../lib/reducers';
13
+ import {
14
+ ModuleContext,
15
+ tableToSchema,
16
+ type TablesToSchema,
17
+ type UntypedSchemaDef,
18
+ } from '../lib/schema';
19
+ import type { UntypedTableSchema } from '../lib/table_schema';
20
+ import { ColumnBuilder, TypeBuilder } from '../lib/type_builders';
21
+ import {
22
+ Router,
23
+ type HandlerFn,
24
+ type HttpHandlerExport,
25
+ type HttpHandlerOpts,
26
+ makeHttpHandlerExport,
27
+ makeHttpRouterExport,
28
+ } from './http_handlers';
29
+ import {
30
+ makeProcedureExport,
31
+ type ProcedureExport,
32
+ type ProcedureFn,
33
+ type ProcedureOpts,
34
+ type Procedures,
35
+ } from './procedures';
36
+ import {
37
+ makeReducerExport,
38
+ type ReducerExport,
39
+ type ReducerOpts,
40
+ type Reducers,
41
+ } from './reducers';
42
+ import { makeHooks } from './runtime';
43
+
44
+ import {
45
+ makeAnonViewExport,
46
+ makeViewExport,
47
+ type AnonViews,
48
+ type AnonymousViewFn,
49
+ type ViewExport,
50
+ type ViewFn,
51
+ type ViewOpts,
52
+ type ViewReturnTypeBuilder,
53
+ type ValidateViewPrimaryKey,
54
+ type Views,
55
+ } from './views';
56
+ import type { UntypedTableDef } from '../lib/table';
57
+
58
+ export class SchemaInner<
59
+ S extends UntypedSchemaDef = UntypedSchemaDef,
60
+ > extends ModuleContext {
61
+ schemaType: S;
62
+ existingFunctions = new Set<string>();
63
+ existingHttpHandlers = new Set<string>();
64
+ reducers: Reducers = [];
65
+ procedures: Procedures = [];
66
+ views: Views = [];
67
+ anonViews: AnonViews = [];
68
+ httpHandlers: HandlerFn[] = [];
69
+ /**
70
+ * Maps ReducerExport objects to the name of the reducer.
71
+ * Used for resolving the reducers of scheduled tables.
72
+ */
73
+ functionExports: Map<
74
+ | ReducerExport<UntypedSchemaDef, any>
75
+ | ProcedureExport<UntypedSchemaDef, any, any>,
76
+ string
77
+ > = new Map();
78
+ httpHandlerExports: Map<HttpHandlerExport<UntypedSchemaDef>, string> =
79
+ new Map();
80
+ pendingSchedules: PendingSchedule[] = [];
81
+ pendingHttpRoutes: PendingHttpRoute[] = [];
82
+
83
+ constructor(getSchemaType: (ctx: SchemaInner<S>) => S) {
84
+ super();
85
+ this.schemaType = getSchemaType(this);
86
+ }
87
+
88
+ defineFunction(name: string) {
89
+ if (this.existingFunctions.has(name)) {
90
+ throw new TypeError(
91
+ `There is already a reducer, procedure, or view with the name '${name}'`
92
+ );
93
+ }
94
+ this.existingFunctions.add(name);
95
+ }
96
+
97
+ defineHttpHandler(name: string) {
98
+ if (this.existingHttpHandlers.has(name)) {
99
+ throw new TypeError(
100
+ `There is already an HTTP handler with the name '${name}'`
101
+ );
102
+ }
103
+ this.existingHttpHandlers.add(name);
104
+ }
105
+
106
+ resolveSchedules() {
107
+ for (const { reducer, scheduleAtCol, tableName } of this.pendingSchedules) {
108
+ const functionName = this.functionExports.get(reducer());
109
+ if (functionName === undefined) {
110
+ const msg = `Table ${tableName} defines a schedule, but it seems like the associated function was not exported.`;
111
+ throw new TypeError(msg);
112
+ }
113
+ this.moduleDef.schedules.push({
114
+ sourceName: undefined,
115
+ tableName,
116
+ scheduleAtCol,
117
+ functionName,
118
+ });
119
+ }
120
+ }
121
+
122
+ resolveHttpRoutes() {
123
+ for (const route of this.pendingHttpRoutes) {
124
+ const handlerFunction = this.httpHandlerExports.get(route.handler);
125
+ if (handlerFunction === undefined) {
126
+ throw new TypeError(
127
+ `HTTP route for path '${route.path}' refers to a handler that was not exported.`
128
+ );
129
+ }
130
+ this.moduleDef.httpRoutes.push({
131
+ handlerFunction,
132
+ method: route.method,
133
+ path: route.path,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ type PendingSchedule = UntypedTableSchema['schedule'] & { tableName: string };
140
+ type PendingHttpRoute = {
141
+ handler: HttpHandlerExport<UntypedSchemaDef>;
142
+ method: MethodOrAny;
143
+ path: string;
144
+ };
145
+
146
+ /**
147
+ * The Schema class represents the database schema for a SpacetimeDB application.
148
+ * It encapsulates the table definitions and typespace, and provides methods to define
149
+ * reducers and lifecycle hooks.
150
+ *
151
+ * Schema has a generic parameter S which represents the inferred schema type. This type
152
+ * is automatically inferred when creating a schema using the `schema()` function and is
153
+ * used to type the database view in reducer contexts.
154
+ *
155
+ * The methods on this class are used to register reducers and lifecycle hooks
156
+ * with the SpacetimeDB runtime. Theey forward to free functions that handle the actual
157
+ * registration logic, but having them as methods on the Schema class helps with type inference.
158
+ *
159
+ * @template S - The inferred schema type of the SpacetimeDB module.
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const spacetimedb = schema({
164
+ * user: table({}, userType),
165
+ * post: table({}, postType)
166
+ * });
167
+ * spacetimedb.reducer(
168
+ * 'create_user',
169
+ * { username: t.string(), email: t.string() },
170
+ * (ctx, { username, email }) => {
171
+ * ctx.db.user.insert({ username, email, created_at: ctx.timestamp });
172
+ * console.log(`User ${username} created by ${ctx.sender.identityId}`);
173
+ * }
174
+ * );
175
+ * ```
176
+ */
177
+ // TODO(cloutiertyler): It might be nice to have a way to access the types
178
+ // for the tables from the schema object, e.g. `spacetimedb.user.type` would
179
+ // be the type of the user table.
180
+ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
181
+ #ctx: SchemaInner<S>;
182
+
183
+ constructor(ctx: SchemaInner<S>) {
184
+ // TODO: TableSchema and TableDef should really be unified
185
+ this.#ctx = ctx;
186
+ }
187
+
188
+ [moduleHooks](exports: object) {
189
+ // if (!(hasOwn(exports, 'default') && exports.default instanceof Schema)) {
190
+ // throw new TypeError('must export schema as default export');
191
+ // }
192
+ const registeredSchema = this.#ctx;
193
+ for (const [name, moduleExport] of Object.entries(exports)) {
194
+ if (name === 'default') continue;
195
+ if (!isModuleExport(moduleExport)) {
196
+ throw new TypeError(
197
+ 'exporting something that is not a spacetime export'
198
+ );
199
+ }
200
+ checkExportContext(moduleExport, registeredSchema);
201
+ moduleExport[registerExport](registeredSchema, name);
202
+ }
203
+ registeredSchema.resolveSchedules();
204
+ registeredSchema.resolveHttpRoutes();
205
+ return makeHooks(registeredSchema);
206
+ }
207
+
208
+ get schemaType(): S {
209
+ return this.#ctx.schemaType;
210
+ }
211
+
212
+ get moduleDef() {
213
+ return this.#ctx.moduleDef;
214
+ }
215
+
216
+ get typespace() {
217
+ return this.#ctx.typespace;
218
+ }
219
+
220
+ /**
221
+ * Defines a SpacetimeDB reducer function.
222
+ *
223
+ * Reducers are the primary way to modify the state of your SpacetimeDB application.
224
+ * They are atomic, meaning that either all operations within a reducer succeed,
225
+ * or none of them do.
226
+ *
227
+ * @template S - The inferred schema type of the SpacetimeDB module.
228
+ * @template Params - The type of the parameters object expected by the reducer.
229
+ *
230
+ * @param {Params} params - An object defining the parameters that the reducer accepts.
231
+ * Each key-value pair represents a parameter name and its corresponding
232
+ * {@link TypeBuilder} or {@link ColumnBuilder}.
233
+ * @param {(ctx: ReducerCtx<S>, payload: ParamsAsObject<Params>) => void} fn - The reducer function itself.
234
+ * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`.
235
+ * - `payload`: An object containing the arguments passed to the reducer, typed according to `params`.
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * // Define a reducer named 'create_user' that takes 'username' (string) and 'email' (string)
240
+ * export const create_user = spacetime.reducer(
241
+ * {
242
+ * username: t.string(),
243
+ * email: t.string(),
244
+ * },
245
+ * (ctx, { username, email }) => {
246
+ * // Access the 'user' table from the database view in the context
247
+ * ctx.db.user.insert({ username, email, created_at: ctx.timestamp });
248
+ * console.log(`User ${username} created by ${ctx.sender.identityId}`);
249
+ * }
250
+ * );
251
+ * ```
252
+ */
253
+ reducer<Params extends ParamsObj>(
254
+ params: Params,
255
+ fn: Reducer<S, Params>
256
+ ): ReducerExport<S, Params>;
257
+ reducer(fn: Reducer<S, {}>): ReducerExport<S, {}>;
258
+ reducer<Params extends ParamsObj>(
259
+ opts: ReducerOpts,
260
+ params: Params,
261
+ fn: Reducer<S, Params>
262
+ ): ReducerExport<S, Params>;
263
+ reducer(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
264
+ reducer<Params extends ParamsObj>(
265
+ ...args:
266
+ | [Params, Reducer<S, Params>]
267
+ | [Reducer<S, {}>]
268
+ | [ReducerOpts, Params, Reducer<S, Params>]
269
+ | [ReducerOpts, Reducer<S, {}>]
270
+ ): ReducerExport<S, Params> {
271
+ let opts: ReducerOpts | undefined,
272
+ params: Params = {} as Params,
273
+ fn: Reducer<S, Params>;
274
+ switch (args.length) {
275
+ case 1:
276
+ [fn] = args;
277
+ break;
278
+ case 2: {
279
+ let arg1;
280
+ [arg1, fn] = args;
281
+ if (typeof arg1.name === 'string') opts = arg1 as ReducerOpts;
282
+ else params = arg1 as Params;
283
+ break;
284
+ }
285
+ case 3:
286
+ [opts, params, fn] = args;
287
+ break;
288
+ }
289
+ return makeReducerExport(this.#ctx, opts, params, fn);
290
+ }
291
+
292
+ /**
293
+ * Registers an initialization reducer that runs when the SpacetimeDB module is published
294
+ * for the first time.
295
+ *
296
+ * This function is useful to set up any initial state of your database that is guaranteed
297
+ * to run only once, and before any other reducers or client connections.
298
+ *
299
+ * @template S - The inferred schema type of the SpacetimeDB module.
300
+ * @param {Reducer<S, {}>} fn - The initialization reducer function.
301
+ * - `ctx`: The reducer context, providing access to `sender`, `timestamp`, `connection_id`, and `db`.
302
+ * @example
303
+ * ```typescript
304
+ * export const init = spacetime.init((ctx) => {
305
+ * ctx.db.user.insert({ username: 'admin', email: 'admin@example.com' });
306
+ * });
307
+ * ```
308
+ */
309
+ init(fn: Reducer<S, {}>): ReducerExport<S, {}>;
310
+ init(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
311
+ init(
312
+ ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
313
+ ): ReducerExport<S, {}> {
314
+ let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
315
+ switch (args.length) {
316
+ case 1:
317
+ [fn] = args;
318
+ break;
319
+ case 2:
320
+ [opts, fn] = args;
321
+ break;
322
+ }
323
+ return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.Init);
324
+ }
325
+
326
+ /**
327
+ * Registers a reducer to be called when a client connects to the SpacetimeDB module.
328
+ * This function allows you to define custom logic that should execute
329
+ * whenever a new client establishes a connection.
330
+ * @template S - The inferred schema type of the SpacetimeDB module.
331
+ *
332
+ * @param fn - The reducer function to execute on client connection.
333
+ *
334
+ * @example
335
+ * ```typescript
336
+ * export const onConnect = spacetime.clientConnected(
337
+ * (ctx) => {
338
+ * console.log(`Client ${ctx.connectionId} connected`);
339
+ * }
340
+ * );
341
+ */
342
+ clientConnected(fn: Reducer<S, {}>): ReducerExport<S, {}>;
343
+ clientConnected(opts: ReducerOpts, fn: Reducer<S, {}>): ReducerExport<S, {}>;
344
+ clientConnected(
345
+ ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
346
+ ): ReducerExport<S, {}> {
347
+ let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
348
+ switch (args.length) {
349
+ case 1:
350
+ [fn] = args;
351
+ break;
352
+ case 2:
353
+ [opts, fn] = args;
354
+ break;
355
+ }
356
+ return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.OnConnect);
357
+ }
358
+
359
+ /**
360
+ * Registers a reducer to be called when a client disconnects from the SpacetimeDB module.
361
+ * This function allows you to define custom logic that should execute
362
+ * whenever a client disconnects.
363
+ * @template S - The inferred schema type of the SpacetimeDB module.
364
+ *
365
+ * @param fn - The reducer function to execute on client disconnection.
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * export const onDisconnect = spacetime.clientDisconnected(
370
+ * (ctx) => {
371
+ * console.log(`Client ${ctx.connectionId} disconnected`);
372
+ * }
373
+ * );
374
+ * ```
375
+ */
376
+ clientDisconnected(fn: Reducer<S, {}>): ReducerExport<S, {}>;
377
+ clientDisconnected(
378
+ opts: ReducerOpts,
379
+ fn: Reducer<S, {}>
380
+ ): ReducerExport<S, {}>;
381
+ clientDisconnected(
382
+ ...args: [Reducer<S, {}>] | [ReducerOpts, Reducer<S, {}>]
383
+ ): ReducerExport<S, {}> {
384
+ let opts: ReducerOpts | undefined, fn: Reducer<S, {}>;
385
+ switch (args.length) {
386
+ case 1:
387
+ [fn] = args;
388
+ break;
389
+ case 2:
390
+ [opts, fn] = args;
391
+ break;
392
+ }
393
+ return makeReducerExport(this.#ctx, opts, {}, fn, Lifecycle.OnDisconnect);
394
+ }
395
+
396
+ view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
397
+ opts: ViewOpts,
398
+ ret: Ret,
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>
404
+ ): ViewExport<F> {
405
+ return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
406
+ }
407
+
408
+ // TODO: re-enable once parameterized views are supported in SQL
409
+ // view<Ret extends ViewReturnTypeBuilder>(
410
+ // opts: ViewOpts,
411
+ // ret: Ret,
412
+ // fn: ViewFn<S, {}, Ret>
413
+ // ): void;
414
+ // view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
415
+ // opts: ViewOpts,
416
+ // params: Params,
417
+ // ret: Ret,
418
+ // fn: ViewFn<S, {}, Ret>
419
+ // ): void;
420
+ // view<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
421
+ // opts: ViewOpts,
422
+ // paramsOrRet: Ret | Params,
423
+ // retOrFn: ViewFn<S, {}, Ret> | Ret,
424
+ // maybeFn?: ViewFn<S, Params, Ret>
425
+ // ): void {
426
+ // if (typeof retOrFn === 'function') {
427
+ // defineView(name, false, {}, paramsOrRet as Ret, retOrFn);
428
+ // } else {
429
+ // defineView(name, false, paramsOrRet as Params, retOrFn, maybeFn!);
430
+ // }
431
+ // }
432
+
433
+ anonymousView<
434
+ Ret extends ViewReturnTypeBuilder,
435
+ F extends AnonymousViewFn<S, {}, Ret>,
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> {
445
+ return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
446
+ }
447
+
448
+ // TODO: re-enable once parameterized views are supported in SQL
449
+ // anonymousView<Ret extends ViewReturnTypeBuilder>(
450
+ // opts: ViewOpts,
451
+ // ret: Ret,
452
+ // fn: AnonymousViewFn<S, {}, Ret>
453
+ // ): void;
454
+ // anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
455
+ // opts: ViewOpts,
456
+ // params: Params,
457
+ // ret: Ret,
458
+ // fn: AnonymousViewFn<S, {}, Ret>
459
+ // ): void;
460
+ // anonymousView<Params extends ParamsObj, Ret extends ViewReturnTypeBuilder>(
461
+ // opts: ViewOpts,
462
+ // paramsOrRet: Ret | Params,
463
+ // retOrFn: AnonymousViewFn<S, {}, Ret> | Ret,
464
+ // maybeFn?: AnonymousViewFn<S, Params, Ret>
465
+ // ): void {
466
+ // if (typeof retOrFn === 'function') {
467
+ // defineView(name, true, {}, paramsOrRet as Ret, retOrFn);
468
+ // } else {
469
+ // defineView(name, true, paramsOrRet as Params, retOrFn, maybeFn!);
470
+ // }
471
+ // }
472
+
473
+ procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
474
+ params: Params,
475
+ ret: Ret,
476
+ fn: ProcedureFn<S, Params, Ret>
477
+ ): ProcedureFn<S, Params, Ret>;
478
+ procedure<Ret extends TypeBuilder<any, any>>(
479
+ ret: Ret,
480
+ fn: ProcedureFn<S, {}, Ret>
481
+ ): ProcedureFn<S, {}, Ret>;
482
+ procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
483
+ opts: ProcedureOpts,
484
+ params: Params,
485
+ ret: Ret,
486
+ fn: ProcedureFn<S, Params, Ret>
487
+ ): ProcedureFn<S, Params, Ret>;
488
+ procedure<Ret extends TypeBuilder<any, any>>(
489
+ opts: ProcedureOpts,
490
+ ret: Ret,
491
+ fn: ProcedureFn<S, {}, Ret>
492
+ ): ProcedureFn<S, {}, Ret>;
493
+ procedure<Params extends ParamsObj, Ret extends TypeBuilder<any, any>>(
494
+ ...args:
495
+ | [Params, Ret, ProcedureFn<S, Params, Ret>]
496
+ | [Ret, ProcedureFn<S, Params, Ret>]
497
+ | [ProcedureOpts, Params, Ret, ProcedureFn<S, Params, Ret>]
498
+ | [ProcedureOpts, Ret, ProcedureFn<S, Params, Ret>]
499
+ ): ProcedureExport<S, Params, Ret> {
500
+ let opts: ProcedureOpts | undefined,
501
+ params: Params = {} as Params,
502
+ ret: Ret,
503
+ fn: ProcedureFn<S, Params, Ret>;
504
+ switch (args.length) {
505
+ case 2:
506
+ [ret, fn] = args;
507
+ break;
508
+ case 3: {
509
+ let arg1;
510
+ [arg1, ret, fn] = args;
511
+ if (typeof arg1.name === 'string') opts = arg1 as ProcedureOpts;
512
+ else params = arg1 as Params;
513
+ break;
514
+ }
515
+ case 4:
516
+ [opts, params, ret, fn] = args;
517
+ break;
518
+ }
519
+ return makeProcedureExport(this.#ctx, opts, params, ret, fn);
520
+ }
521
+
522
+ httpHandler(fn: HandlerFn<S>): HttpHandlerExport<S>;
523
+ httpHandler(opts: HttpHandlerOpts, fn: HandlerFn<S>): HttpHandlerExport<S>;
524
+ httpHandler(
525
+ ...args: [HandlerFn<S>] | [HttpHandlerOpts, HandlerFn<S>]
526
+ ): HttpHandlerExport<S> {
527
+ let opts: HttpHandlerOpts | undefined, fn: HandlerFn<S>;
528
+ switch (args.length) {
529
+ case 1:
530
+ [fn] = args;
531
+ break;
532
+ case 2:
533
+ [opts, fn] = args;
534
+ break;
535
+ }
536
+ return makeHttpHandlerExport(this.#ctx, opts, fn);
537
+ }
538
+
539
+ httpRouter(router: Router): ModuleExport {
540
+ return makeHttpRouterExport(this.#ctx, router);
541
+ }
542
+
543
+ /**
544
+ * Bundle multiple reducers, procedures, etc into one value to export.
545
+ * The name they will be exported with is their corresponding key in the `exports` argument.
546
+ */
547
+ exportGroup(exports: Record<string, ModuleExport>): ModuleExport {
548
+ return {
549
+ [exportContext]: this.#ctx,
550
+ [registerExport](ctx, _exportName) {
551
+ for (const [exportName, moduleExport] of Object.entries(exports)) {
552
+ checkExportContext(moduleExport, ctx);
553
+ moduleExport[registerExport](ctx, exportName);
554
+ }
555
+ },
556
+ };
557
+ }
558
+
559
+ clientVisibilityFilter = {
560
+ sql: (filter: string): ModuleExport => ({
561
+ [exportContext]: this.#ctx,
562
+ [registerExport](ctx, _exportName) {
563
+ ctx.moduleDef.rowLevelSecurity.push({ sql: filter });
564
+ },
565
+ }),
566
+ };
567
+ }
568
+
569
+ export const registerExport = Symbol('SpacetimeDB.registerExport');
570
+ export const exportContext = Symbol('SpacetimeDB.exportContext');
571
+
572
+ export interface ModuleExport {
573
+ [registerExport](ctx: SchemaInner, exportName: string): void;
574
+ [exportContext]?: SchemaInner;
575
+ }
576
+
577
+ function isModuleExport(x: unknown): x is ModuleExport {
578
+ return (
579
+ (typeof x === 'function' || typeof x === 'object') &&
580
+ x !== null &&
581
+ registerExport in x
582
+ );
583
+ }
584
+
585
+ /** Verify that the ModuleContext that `exp` comes from is the same as `schema` */
586
+ function checkExportContext(exp: ModuleExport, schema: SchemaInner) {
587
+ if (exp[exportContext] != null && exp[exportContext] !== schema) {
588
+ throw new TypeError('multiple schemas are not supported');
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Extracts the inferred schema type from a Schema instance
594
+ */
595
+ export type InferSchema<SchemaDef extends Schema<any>> =
596
+ SchemaDef extends Schema<infer S> ? S : never;
597
+
598
+ /**
599
+ * Creates a schema from table definitions
600
+ * @param handles - Array of table handles created by table() function
601
+ * @returns ColumnBuilder representing the complete database schema
602
+ * @example
603
+ * ```ts
604
+ * const spacetimedb = schema({
605
+ * user: table({}, userType),
606
+ * post: table({}, postType)
607
+ * });
608
+ * ```
609
+ */
610
+ /**
611
+ * Module-level settings that can be passed to `schema()`.
612
+ */
613
+ export interface ModuleSettings {
614
+ /**
615
+ * The case conversion policy for this module.
616
+ * Defaults to `SnakeCase` if not specified.
617
+ *
618
+ * @example
619
+ * ```ts
620
+ * export default schema({
621
+ * player,
622
+ * }, { CASE_CONVERSION_POLICY: CaseConversionPolicy.None });
623
+ * ```
624
+ */
625
+ CASE_CONVERSION_POLICY?: CaseConversionPolicy;
626
+ }
627
+
628
+ export function schema<const H extends Record<string, UntypedTableSchema>>(
629
+ tables: H,
630
+ moduleSettings?: ModuleSettings
631
+ ): Schema<TablesToSchema<H>> {
632
+ const ctx = new SchemaInner<TablesToSchema<H>>(ctx => {
633
+ // Apply module settings.
634
+ if (moduleSettings?.CASE_CONVERSION_POLICY != null) {
635
+ ctx.setCaseConversionPolicy(moduleSettings.CASE_CONVERSION_POLICY);
636
+ }
637
+
638
+ const tableSchemas: Record<string, UntypedTableDef> = {};
639
+ for (const [accName, table] of Object.entries(tables)) {
640
+ const tableDef = table.tableDef(ctx, accName);
641
+ tableSchemas[accName] = tableToSchema(accName, table, tableDef);
642
+ ctx.moduleDef.tables.push(tableDef);
643
+ if (table.schedule) {
644
+ ctx.pendingSchedules.push({
645
+ ...table.schedule,
646
+ tableName: tableDef.sourceName,
647
+ });
648
+ }
649
+ if (table.tableName) {
650
+ ctx.moduleDef.explicitNames.entries.push({
651
+ tag: 'Table',
652
+ value: {
653
+ sourceName: accName,
654
+ canonicalName: table.tableName,
655
+ },
656
+ });
657
+ }
658
+ }
659
+ return { tables: tableSchemas } as TablesToSchema<H>;
660
+ });
661
+
662
+ return new Schema(ctx);
663
+ }