spacetimedb 2.6.0 → 2.6.1

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 (172) hide show
  1. package/README.md +211 -211
  2. package/dist/angular/index.cjs.map +1 -1
  3. package/dist/angular/index.mjs.map +1 -1
  4. package/dist/browser/angular/index.mjs.map +1 -1
  5. package/dist/browser/react/index.mjs.map +1 -1
  6. package/dist/browser/solid/index.mjs.map +1 -1
  7. package/dist/browser/svelte/index.mjs.map +1 -1
  8. package/dist/browser/vue/index.mjs.map +1 -1
  9. package/dist/index.browser.mjs.map +1 -1
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/lib/type_builders.d.ts +18 -3
  13. package/dist/lib/type_builders.d.ts.map +1 -1
  14. package/dist/min/index.browser.mjs.map +1 -1
  15. package/dist/min/react/index.mjs.map +1 -1
  16. package/dist/min/sdk/index.browser.mjs.map +1 -1
  17. package/dist/react/index.cjs.map +1 -1
  18. package/dist/react/index.mjs.map +1 -1
  19. package/dist/sdk/db_connection_impl.d.ts.map +1 -1
  20. package/dist/sdk/event_context.d.ts +3 -3
  21. package/dist/sdk/event_context.d.ts.map +1 -1
  22. package/dist/sdk/index.browser.mjs.map +1 -1
  23. package/dist/sdk/index.cjs.map +1 -1
  24. package/dist/sdk/index.mjs.map +1 -1
  25. package/dist/sdk/procedures.d.ts +2 -2
  26. package/dist/sdk/procedures.d.ts.map +1 -1
  27. package/dist/sdk/reducers.d.ts +3 -4
  28. package/dist/sdk/reducers.d.ts.map +1 -1
  29. package/dist/sdk/table_cache.d.ts.map +1 -1
  30. package/dist/sdk/type_utils.d.ts +3 -3
  31. package/dist/sdk/type_utils.d.ts.map +1 -1
  32. package/dist/server/index.mjs.map +1 -1
  33. package/dist/server/runtime.d.ts.map +1 -1
  34. package/dist/server/views.d.ts +4 -2
  35. package/dist/server/views.d.ts.map +1 -1
  36. package/dist/solid/index.cjs.map +1 -1
  37. package/dist/solid/index.mjs.map +1 -1
  38. package/dist/svelte/index.cjs.map +1 -1
  39. package/dist/svelte/index.mjs.map +1 -1
  40. package/dist/svelte/useReducer.d.ts +2 -2
  41. package/dist/svelte/useReducer.d.ts.map +1 -1
  42. package/dist/tanstack/index.cjs.map +1 -1
  43. package/dist/tanstack/index.mjs.map +1 -1
  44. package/dist/vue/index.cjs.map +1 -1
  45. package/dist/vue/index.mjs.map +1 -1
  46. package/package.json +22 -21
  47. package/src/angular/connection_state.ts +19 -19
  48. package/src/angular/index.ts +3 -3
  49. package/src/angular/injectors/index.ts +4 -4
  50. package/src/angular/injectors/inject-reducer.ts +62 -62
  51. package/src/angular/injectors/inject-spacetimedb-connected.ts +13 -13
  52. package/src/angular/injectors/inject-spacetimedb.ts +10 -10
  53. package/src/angular/injectors/inject-table.ts +234 -234
  54. package/src/angular/providers/index.ts +1 -1
  55. package/src/angular/providers/provide-spacetimedb.ts +96 -96
  56. package/src/index.ts +16 -16
  57. package/src/lib/algebraic_type.ts +819 -819
  58. package/src/lib/algebraic_type_variants.ts +26 -26
  59. package/src/lib/algebraic_value.ts +10 -10
  60. package/src/lib/autogen/types.ts +746 -746
  61. package/src/lib/binary_reader.ts +188 -188
  62. package/src/lib/binary_writer.ts +213 -213
  63. package/src/lib/connection_id.ts +102 -102
  64. package/src/lib/constraints.ts +48 -48
  65. package/src/lib/errors.ts +26 -26
  66. package/src/lib/filter.ts +195 -195
  67. package/src/lib/identity.ts +83 -83
  68. package/src/lib/indexes.ts +251 -251
  69. package/src/lib/option.ts +34 -34
  70. package/src/lib/query.ts +1019 -1019
  71. package/src/lib/reducer_schema.ts +38 -38
  72. package/src/lib/reducers.ts +116 -116
  73. package/src/lib/result.ts +36 -36
  74. package/src/lib/schedule_at.ts +86 -86
  75. package/src/lib/schema.ts +420 -420
  76. package/src/lib/table.ts +548 -548
  77. package/src/lib/table_schema.ts +64 -64
  78. package/src/lib/time_duration.ts +77 -77
  79. package/src/lib/timestamp.ts +148 -148
  80. package/src/lib/type_builders.test-d.ts +130 -128
  81. package/src/lib/type_builders.ts +4050 -4014
  82. package/src/lib/type_util.ts +124 -124
  83. package/src/lib/util.ts +196 -196
  84. package/src/lib/uuid.ts +337 -337
  85. package/src/react/SpacetimeDBProvider.ts +84 -84
  86. package/src/react/connection_state.ts +6 -6
  87. package/src/react/index.ts +5 -5
  88. package/src/react/useProcedure.ts +60 -60
  89. package/src/react/useReducer.ts +53 -53
  90. package/src/react/useSpacetimeDB.ts +18 -18
  91. package/src/react/useTable.ts +256 -256
  92. package/src/sdk/client_api/index.ts +114 -114
  93. package/src/sdk/client_api/types/procedures.ts +8 -8
  94. package/src/sdk/client_api/types/reducers.ts +8 -8
  95. package/src/sdk/client_api/types.ts +288 -288
  96. package/src/sdk/client_cache.ts +129 -129
  97. package/src/sdk/client_table.ts +179 -179
  98. package/src/sdk/connection_manager.ts +352 -352
  99. package/src/sdk/db_connection_builder.ts +290 -290
  100. package/src/sdk/db_connection_impl.ts +1363 -1356
  101. package/src/sdk/db_context.ts +28 -28
  102. package/src/sdk/db_view.ts +12 -12
  103. package/src/sdk/decompress.ts +51 -51
  104. package/src/sdk/event.ts +18 -18
  105. package/src/sdk/event_context.ts +51 -51
  106. package/src/sdk/event_emitter.ts +32 -32
  107. package/src/sdk/index.ts +14 -14
  108. package/src/sdk/internal.ts +2 -2
  109. package/src/sdk/json_api.ts +46 -46
  110. package/src/sdk/logger.ts +134 -134
  111. package/src/sdk/message_types.ts +46 -46
  112. package/src/sdk/procedures.ts +87 -83
  113. package/src/sdk/reducer_event.ts +20 -20
  114. package/src/sdk/reducer_handle.ts +12 -12
  115. package/src/sdk/reducers.ts +158 -159
  116. package/src/sdk/schema.ts +45 -45
  117. package/src/sdk/spacetime_module.ts +28 -28
  118. package/src/sdk/subscription_builder_impl.ts +275 -275
  119. package/src/sdk/table_cache.ts +582 -581
  120. package/src/sdk/type_utils.ts +19 -19
  121. package/src/sdk/version.ts +133 -133
  122. package/src/sdk/websocket_decompress_adapter.ts +63 -63
  123. package/src/sdk/websocket_protocols.ts +25 -25
  124. package/src/sdk/websocket_test_adapter.ts +107 -107
  125. package/src/sdk/websocket_v3_frames.ts +126 -126
  126. package/src/sdk/ws.ts +105 -105
  127. package/src/server/console.ts +81 -81
  128. package/src/server/db_view.ts +21 -21
  129. package/src/server/errors.ts +138 -138
  130. package/src/server/http.test-d.ts +80 -80
  131. package/src/server/http.ts +14 -14
  132. package/src/server/http_handlers.ts +413 -413
  133. package/src/server/http_internal.ts +79 -79
  134. package/src/server/http_shared.ts +186 -186
  135. package/src/server/index.ts +37 -37
  136. package/src/server/polyfills.ts +4 -4
  137. package/src/server/procedures.ts +239 -239
  138. package/src/server/query.ts +1 -1
  139. package/src/server/range.ts +53 -53
  140. package/src/server/reducers.ts +113 -113
  141. package/src/server/rng.ts +113 -113
  142. package/src/server/runtime.ts +1102 -1102
  143. package/src/server/schema.test-d.ts +99 -99
  144. package/src/server/schema.ts +663 -663
  145. package/src/server/sys.d.ts +125 -125
  146. package/src/server/view.test-d.ts +194 -194
  147. package/src/server/views.ts +343 -340
  148. package/src/solid/SpacetimeDBProvider.ts +97 -97
  149. package/src/solid/connection_state.ts +6 -6
  150. package/src/solid/index.ts +5 -5
  151. package/src/solid/useProcedure.ts +57 -57
  152. package/src/solid/useReducer.ts +50 -50
  153. package/src/solid/useSpacetimeDB.ts +18 -18
  154. package/src/solid/useTable.ts +203 -203
  155. package/src/svelte/SpacetimeDBProvider.ts +101 -101
  156. package/src/svelte/connection_state.ts +16 -16
  157. package/src/svelte/index.ts +4 -4
  158. package/src/svelte/useReducer.ts +61 -61
  159. package/src/svelte/useSpacetimeDB.ts +22 -22
  160. package/src/svelte/useTable.ts +218 -218
  161. package/src/tanstack/SpacetimeDBQueryClient.ts +330 -330
  162. package/src/tanstack/hooks.ts +83 -83
  163. package/src/tanstack/index.ts +16 -16
  164. package/src/util-stub.ts +1 -1
  165. package/src/vue/SpacetimeDBProvider.ts +157 -157
  166. package/src/vue/connection_state.ts +19 -19
  167. package/src/vue/index.ts +5 -5
  168. package/src/vue/useProcedure.ts +62 -62
  169. package/src/vue/useReducer.ts +55 -55
  170. package/src/vue/useSpacetimeDB.ts +18 -18
  171. package/src/vue/useTable.ts +229 -229
  172. package/LICENSE.txt +0 -759
@@ -1,581 +1,582 @@
1
- import { EventEmitter } from './event_emitter.ts';
2
-
3
- import { stdbLogger } from './logger.ts';
4
- import { deepEqual, type ComparablePrimitive } from '../';
5
- import type { EventContextInterface, TableDefForTableName } from './index.ts';
6
- import type { RowType, TableIndexes, UntypedTableDef } from '../lib/table.ts';
7
- import type { ClientTableCoreImplementable } from './client_table.ts';
8
- import type { UntypedRemoteModule } from './spacetime_module.ts';
9
- import type { TableNamesOf } from '../lib/schema.ts';
10
- import type {
11
- ReadonlyIndex,
12
- ReadonlyIndexes,
13
- ReadonlyRangedIndex,
14
- ReadonlyUniqueIndex,
15
- } from '../lib/indexes.ts';
16
- import type { Bound } from '../server/range.ts';
17
- import type { Prettify } from '../lib/type_util.ts';
18
-
19
- export type Operation<
20
- RowType extends Record<string, any> = Record<string, any>,
21
- > = {
22
- type: 'insert' | 'delete';
23
- // For tables with a primary key, this is the primary key value, as a primitive or string.
24
- // Otherwise, it is an encoding of the full row.
25
- rowId: ComparablePrimitive;
26
- row: RowType;
27
- };
28
-
29
- export type TableUpdate<TableDef extends UntypedTableDef> = {
30
- tableName: string;
31
- operations: Operation<RowType<TableDef>>[];
32
- };
33
-
34
- export type PendingCallback = {
35
- type: 'insert' | 'delete' | 'update';
36
- table: string;
37
- cb: () => void;
38
- };
39
-
40
- // Strict scalar compare for index term values.
41
- const scalarCompare = (x: any, y: any): number => {
42
- if (x === y) return 0;
43
- // Compare booleans/numbers/bigints/strings with JS ordering.
44
- return x < y ? -1 : 1;
45
- };
46
-
47
- export type TableIndexView<
48
- RemoteModule extends UntypedRemoteModule,
49
- TableName extends TableNamesOf<RemoteModule>,
50
- > = ReadonlyIndexes<
51
- TableDefForTableName<RemoteModule, TableName>,
52
- TableIndexes<TableDefForTableName<RemoteModule, TableName>>
53
- >;
54
-
55
- export type TableCache<
56
- RemoteModule extends UntypedRemoteModule,
57
- TableName extends TableNamesOf<RemoteModule>,
58
- > = TableCacheImpl<RemoteModule, TableName> &
59
- TableIndexView<RemoteModule, TableName>;
60
-
61
- /**
62
- * Builder to generate calls to query a `table` in the database
63
- */
64
- export class TableCacheImpl<
65
- RemoteModule extends UntypedRemoteModule,
66
- TableName extends TableNamesOf<RemoteModule>,
67
- > implements ClientTableCoreImplementable<RemoteModule, TableName>
68
- {
69
- private readonly hasPrimaryKey: boolean;
70
- private rows: Map<
71
- ComparablePrimitive,
72
- [RowType<TableDefForTableName<RemoteModule, TableName>>, number]
73
- >;
74
- private tableDef: TableDefForTableName<RemoteModule, TableName>;
75
- private emitter: EventEmitter<'insert' | 'delete' | 'update'>;
76
-
77
- /**
78
- * @param name the table name
79
- * @param primaryKeyCol column index designated as `#[primarykey]`
80
- * @param primaryKey column name designated as `#[primarykey]`
81
- * @param entityClass the entityClass
82
- */
83
- constructor(tableDef: TableDefForTableName<RemoteModule, TableName>) {
84
- this.tableDef = tableDef;
85
- this.rows = new Map();
86
- this.emitter = new EventEmitter();
87
- this.hasPrimaryKey = Object.values(this.tableDef.columns).some(
88
- col => col.columnMetadata.isPrimaryKey === true
89
- );
90
- // Build index views from the resolved runtime index metadata.
91
- //
92
- // We intentionally use `resolvedIndexes` rather than `indexes`:
93
- // - `indexes` is declarative table-level config (`IndexOpts`) used mainly for typing.
94
- // - `resolvedIndexes` is the runtime shape (`UntypedIndex`) that includes both
95
- // field-level and explicit table-level indexes.
96
- for (const idxDef of this.tableDef.resolvedIndexes) {
97
- const index = this.#makeReadonlyIndex(this.tableDef, idxDef);
98
- // IMPORTANT: for duplicate accessor names, client cache uses assignment
99
- // semantics and later entries overwrite earlier ones. This matches prior
100
- // behavior and is intentionally different from server runtime merge logic.
101
- (this as any)[idxDef.name] = index;
102
- }
103
- }
104
-
105
- // TODO: this just scans the whole table; we should build proper index structures
106
- #makeReadonlyIndex<
107
- I extends TableDefForTableName<
108
- RemoteModule,
109
- TableName
110
- >['resolvedIndexes'][number],
111
- >(
112
- tableDef: TableDefForTableName<RemoteModule, TableName>,
113
- idx: I
114
- ): ReadonlyIndex<TableDefForTableName<RemoteModule, TableName>, I> {
115
- type TableDef = TableDefForTableName<RemoteModule, TableName>;
116
- type Row = Prettify<RowType<TableDef>>;
117
-
118
- // We do not yet support non-btree indexes
119
- if (idx.algorithm !== 'btree') {
120
- throw new Error('Only btree indexes are supported in TableCacheImpl');
121
- }
122
-
123
- const columns = idx.columns;
124
-
125
- // Extract the tuple key for this btree index (column order preserved)
126
- const getKey = (row: Row): readonly unknown[] => columns.map(c => row[c]);
127
-
128
- // The server’s ranged scan fixes all prefix cols to equality and applies
129
- // the bound only to the *last* term. We mirror that.
130
- //
131
- // rangeArg for multi-col index is:
132
- // [...prefixEqualValues, (lastTerm | Range<lastTerm>)]
133
- //
134
- // If only one element is provided, it’s the last term (scalar or Range).
135
- const matchRange = (row: Row, rangeArg: any): boolean => {
136
- const key = getKey(row);
137
-
138
- // Normalize rangeArg into an array.
139
- // With multi-col b-tree, IndexScanRangeBounds always yields at least one element.
140
- const arr = Array.isArray(rangeArg) ? rangeArg : [rangeArg];
141
-
142
- const prefixLen = Math.max(0, arr.length - 1);
143
- // Check equality over the prefix (all but the last provided element)
144
- for (let i = 0; i < prefixLen; i++) {
145
- if (!deepEqual(key[i], arr[i])) return false;
146
- }
147
-
148
- const lastProvided = arr[arr.length - 1];
149
- const kLast = key[prefixLen];
150
-
151
- // If the last provided is a Range<T>, apply bounds; otherwise equality.
152
- if (
153
- lastProvided &&
154
- typeof lastProvided === 'object' &&
155
- 'from' in lastProvided &&
156
- 'to' in lastProvided
157
- ) {
158
- // Range<T>
159
- const from = lastProvided.from as Bound<any>;
160
- const to = lastProvided.to as Bound<any>;
161
-
162
- // Lower bound
163
- if (from.tag !== 'unbounded') {
164
- const c = scalarCompare(kLast, from.value);
165
- if (c < 0) return false;
166
- if (c === 0 && from.tag === 'excluded') return false;
167
- }
168
-
169
- // Upper bound
170
- if (to.tag !== 'unbounded') {
171
- const c = scalarCompare(kLast, to.value);
172
- if (c > 0) return false;
173
- if (c === 0 && to.tag === 'excluded') return false;
174
- }
175
-
176
- // All good on last term; any remaining columns (if any) are unconstrained,
177
- // which matches server behavior for a prefix scan.
178
- return true;
179
- } else {
180
- // Equality on the last provided element
181
- if (!deepEqual(kLast, lastProvided)) return false;
182
- // Any remaining columns are unconstrained (prefix equality only).
183
- return true;
184
- }
185
- };
186
-
187
- // An index is unique if it shares all columns with a unique constraint
188
- const isUnique = tableDef.constraints.some(constraint => {
189
- if (constraint.constraint !== 'unique') {
190
- return false;
191
- }
192
- return deepEqual(constraint.columns, idx.columns);
193
- });
194
-
195
- // eslint-disable-next-line @typescript-eslint/no-this-alias
196
- const self = this;
197
- if (isUnique) {
198
- const impl: ReadonlyUniqueIndex<TableDef, I> = {
199
- find: (colVal: any): Row | null => {
200
- // For unique btree, caller supplies the *full* key (tuple if multi-col).
201
- const expected = Array.isArray(colVal) ? colVal : [colVal];
202
- for (const row of self.iter()) {
203
- if (deepEqual(getKey(row), expected)) return row;
204
- }
205
- return null;
206
- },
207
- };
208
- return impl as ReadonlyIndex<TableDef, I>;
209
- } else {
210
- const impl: ReadonlyRangedIndex<TableDef, I> = {
211
- *filter(range: any): IteratorObject<Row, undefined> {
212
- for (const row of self.iter()) {
213
- if (matchRange(row, range)) yield row;
214
- }
215
- },
216
- };
217
- return impl as ReadonlyIndex<TableDef, I>;
218
- }
219
- }
220
-
221
- /**
222
- * @returns number of rows in the table
223
- */
224
- count(): bigint {
225
- return BigInt(this.rows.size);
226
- }
227
-
228
- /**
229
- * @returns The values of the rows in the table
230
- */
231
- iter(): IteratorObject<
232
- Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
233
- undefined
234
- > {
235
- function* generator(
236
- rows: Map<
237
- ComparablePrimitive,
238
- [RowType<TableDefForTableName<RemoteModule, TableName>>, number]
239
- >
240
- ): IteratorObject<
241
- Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
242
- undefined
243
- > {
244
- for (const [row] of rows.values()) {
245
- yield row as Prettify<
246
- RowType<TableDefForTableName<RemoteModule, TableName>>
247
- >;
248
- }
249
- }
250
- return generator(this.rows);
251
- }
252
-
253
- /**
254
- * Allows iteration over the rows in the table
255
- * @returns An iterator over the rows in the table
256
- */
257
- [Symbol.iterator](): IteratorObject<
258
- Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
259
- undefined
260
- > {
261
- return this.iter();
262
- }
263
-
264
- applyOperations = (
265
- operations: Operation<
266
- RowType<TableDefForTableName<RemoteModule, TableName>>
267
- >[],
268
- ctx: EventContextInterface<RemoteModule>
269
- ): PendingCallback[] => {
270
- const pendingCallbacks: PendingCallback[] = [];
271
-
272
- // Event tables: fire on_insert callbacks but don't store rows in the cache.
273
- if (this.tableDef.isEvent) {
274
- for (const op of operations) {
275
- if (op.type === 'insert') {
276
- pendingCallbacks.push({
277
- type: 'insert',
278
- table: this.tableDef.sourceName,
279
- cb: () => {
280
- this.emitter.emit('insert', ctx, op.row);
281
- },
282
- });
283
- }
284
- }
285
- return pendingCallbacks;
286
- }
287
-
288
- if (this.hasPrimaryKey) {
289
- const insertMap = new Map<
290
- ComparablePrimitive,
291
- [
292
- Operation<RowType<TableDefForTableName<RemoteModule, TableName>>>,
293
- number,
294
- ]
295
- >();
296
- const deleteMap = new Map<
297
- ComparablePrimitive,
298
- [
299
- Operation<RowType<TableDefForTableName<RemoteModule, TableName>>>,
300
- number,
301
- ]
302
- >();
303
- for (const op of operations) {
304
- if (op.type === 'insert') {
305
- const [_, prevCount] = insertMap.get(op.rowId) || [op, 0];
306
- insertMap.set(op.rowId, [op, prevCount + 1]);
307
- } else {
308
- const [_, prevCount] = deleteMap.get(op.rowId) || [op, 0];
309
- deleteMap.set(op.rowId, [op, prevCount + 1]);
310
- }
311
- }
312
- for (const [primaryKey, [insertOp, refCount]] of insertMap) {
313
- const deleteEntry = deleteMap.get(primaryKey);
314
- if (deleteEntry) {
315
- const [_, deleteCount] = deleteEntry;
316
- // In most cases the refCountDelta will be either 0 or refCount, but if
317
- // an update moves a row in or out of the result set of different queries, then
318
- // other deltas are possible.
319
- const refCountDelta = refCount - deleteCount;
320
- const maybeCb = this.update(
321
- ctx,
322
- primaryKey,
323
- insertOp.row,
324
- refCountDelta
325
- );
326
- if (maybeCb) {
327
- pendingCallbacks.push(maybeCb);
328
- }
329
- deleteMap.delete(primaryKey);
330
- } else {
331
- const maybeCb = this.insert(ctx, insertOp, refCount);
332
- if (maybeCb) {
333
- pendingCallbacks.push(maybeCb);
334
- }
335
- }
336
- }
337
- for (const [deleteOp, refCount] of deleteMap.values()) {
338
- const maybeCb = this.delete(ctx, deleteOp, refCount);
339
- if (maybeCb) {
340
- pendingCallbacks.push(maybeCb);
341
- }
342
- }
343
- } else {
344
- for (const op of operations) {
345
- if (op.type === 'insert') {
346
- const maybeCb = this.insert(ctx, op);
347
- if (maybeCb) {
348
- pendingCallbacks.push(maybeCb);
349
- }
350
- } else {
351
- const maybeCb = this.delete(ctx, op);
352
- if (maybeCb) {
353
- pendingCallbacks.push(maybeCb);
354
- }
355
- }
356
- }
357
- }
358
- return pendingCallbacks;
359
- };
360
-
361
- update = (
362
- ctx: EventContextInterface<RemoteModule>,
363
- rowId: ComparablePrimitive,
364
- newRow: RowType<TableDefForTableName<RemoteModule, TableName>>,
365
- refCountDelta: number = 0
366
- ): PendingCallback | undefined => {
367
- const existingEntry = this.rows.get(rowId);
368
- if (!existingEntry) {
369
- // TODO: this should throw an error and kill the connection.
370
- stdbLogger(
371
- 'error',
372
- `Updating a row that was not present in the cache. Table: ${this.tableDef.sourceName}, RowId: ${rowId}`
373
- );
374
- return undefined;
375
- }
376
- const [oldRow, previousCount] = existingEntry;
377
- const refCount = Math.max(1, previousCount + refCountDelta);
378
- if (previousCount + refCountDelta <= 0) {
379
- stdbLogger(
380
- 'error',
381
- `Negative reference count for in table ${this.tableDef.sourceName} row ${rowId} (${previousCount} + ${refCountDelta})`
382
- );
383
- return undefined;
384
- }
385
- this.rows.set(rowId, [newRow, refCount]);
386
- // This indicates something is wrong, so we could arguably crash here.
387
- if (previousCount === 0) {
388
- stdbLogger(
389
- 'error',
390
- `Updating a row id in table ${this.tableDef.sourceName} which was not present in the cache (rowId: ${rowId})`
391
- );
392
- return {
393
- type: 'insert',
394
- table: this.tableDef.sourceName,
395
- cb: () => {
396
- this.emitter.emit('insert', ctx, newRow);
397
- },
398
- };
399
- }
400
- return {
401
- type: 'update',
402
- table: this.tableDef.sourceName,
403
- cb: () => {
404
- this.emitter.emit('update', ctx, oldRow, newRow);
405
- },
406
- };
407
- };
408
-
409
- insert = (
410
- ctx: EventContextInterface<RemoteModule>,
411
- operation: Operation<
412
- RowType<TableDefForTableName<RemoteModule, TableName>>
413
- >,
414
- count: number = 1
415
- ): PendingCallback | undefined => {
416
- const [_, previousCount] = this.rows.get(operation.rowId) || [
417
- operation.row,
418
- 0,
419
- ];
420
- this.rows.set(operation.rowId, [operation.row, previousCount + count]);
421
- if (previousCount === 0) {
422
- return {
423
- type: 'insert',
424
- table: this.tableDef.sourceName,
425
- cb: () => {
426
- this.emitter.emit('insert', ctx, operation.row);
427
- },
428
- };
429
- }
430
- // It's possible to get a duplicate insert because rows can be returned from multiple queries.
431
- return undefined;
432
- };
433
-
434
- delete = (
435
- ctx: EventContextInterface<RemoteModule>,
436
- operation: Operation<
437
- RowType<TableDefForTableName<RemoteModule, TableName>>
438
- >,
439
- count: number = 1
440
- ): PendingCallback | undefined => {
441
- const [_, previousCount] = this.rows.get(operation.rowId) || [
442
- operation.row,
443
- 0,
444
- ];
445
- // This should never happen.
446
- if (previousCount === 0) {
447
- stdbLogger('warn', 'Deleting a row that was not present in the cache');
448
- return undefined;
449
- }
450
- // If this was the last reference, we are actually deleting the row.
451
- if (previousCount <= count) {
452
- // TODO: Log a warning/error if previousCount is less than count.
453
- this.rows.delete(operation.rowId);
454
- return {
455
- type: 'delete',
456
- table: this.tableDef.sourceName,
457
- cb: () => {
458
- this.emitter.emit('delete', ctx, operation.row);
459
- },
460
- };
461
- }
462
- this.rows.set(operation.rowId, [operation.row, previousCount - count]);
463
- return undefined;
464
- };
465
-
466
- /**
467
- * Register a callback for when a row is newly inserted into the database.
468
- *
469
- * ```ts
470
- * ctx.db.user.onInsert((reducerEvent, user) => {
471
- * if (reducerEvent) {
472
- * console.log("New user on reducer", reducerEvent, user);
473
- * } else {
474
- * console.log("New user received during subscription update on insert", user);
475
- * }
476
- * });
477
- * ```
478
- *
479
- * @param cb Callback to be called when a new row is inserted
480
- */
481
- onInsert = (
482
- cb: (
483
- ctx: EventContextInterface<RemoteModule>,
484
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
485
- ) => void
486
- ): void => {
487
- this.emitter.on('insert', cb);
488
- };
489
-
490
- /**
491
- * Register a callback for when a row is deleted from the database.
492
- *
493
- * ```ts
494
- * ctx.db.user.onDelete((reducerEvent, user) => {
495
- * if (reducerEvent) {
496
- * console.log("Deleted user on reducer", reducerEvent, user);
497
- * } else {
498
- * console.log("Deleted user received during subscription update on update", user);
499
- * }
500
- * });
501
- * ```
502
- *
503
- * @param cb Callback to be called when a new row is inserted
504
- */
505
- onDelete = (
506
- cb: (
507
- ctx: EventContextInterface<RemoteModule>,
508
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
509
- ) => void
510
- ): void => {
511
- this.emitter.on('delete', cb);
512
- };
513
-
514
- /**
515
- * Register a callback for when a row is updated into the database.
516
- *
517
- * ```ts
518
- * ctx.db.user.onInsert((reducerEvent, oldUser, user) => {
519
- * if (reducerEvent) {
520
- * console.log("Updated user on reducer", reducerEvent, user);
521
- * } else {
522
- * console.log("Updated user received during subscription update on delete", user);
523
- * }
524
- * });
525
- * ```
526
- *
527
- * @param cb Callback to be called when a new row is inserted
528
- */
529
- onUpdate = (
530
- cb: (
531
- ctx: EventContextInterface<RemoteModule>,
532
- oldRow: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
533
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
534
- ) => void
535
- ): void => {
536
- this.emitter.on('update', cb);
537
- };
538
-
539
- /**
540
- * Remove a callback for when a row is newly inserted into the database.
541
- *
542
- * @param cb Callback to be removed
543
- */
544
- removeOnInsert = (
545
- cb: (
546
- ctx: EventContextInterface<RemoteModule>,
547
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
548
- ) => void
549
- ): void => {
550
- this.emitter.off('insert', cb);
551
- };
552
-
553
- /**
554
- * Remove a callback for when a row is deleted from the database.
555
- *
556
- * @param cb Callback to be removed
557
- */
558
- removeOnDelete = (
559
- cb: (
560
- ctx: EventContextInterface<RemoteModule>,
561
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
562
- ) => void
563
- ): void => {
564
- this.emitter.off('delete', cb);
565
- };
566
-
567
- /**
568
- * Remove a callback for when a row is updated into the database.
569
- *
570
- * @param cb Callback to be removed
571
- */
572
- removeOnUpdate = (
573
- cb: (
574
- ctx: EventContextInterface<RemoteModule>,
575
- oldRow: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
576
- row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
577
- ) => void
578
- ): void => {
579
- this.emitter.off('update', cb);
580
- };
581
- }
1
+ import { EventEmitter } from './event_emitter.ts';
2
+
3
+ import { stdbLogger } from './logger.ts';
4
+ import { deepEqual, type ComparablePrimitive } from '../';
5
+ import type { EventContextInterface, TableDefForTableName } from './index.ts';
6
+ import type { RowType, TableIndexes, UntypedTableDef } from '../lib/table.ts';
7
+ import type { ClientTableCoreImplementable } from './client_table.ts';
8
+ import type { UntypedRemoteModule } from './spacetime_module.ts';
9
+ import type { TableNamesOf } from '../lib/schema.ts';
10
+ import type {
11
+ ReadonlyIndex,
12
+ ReadonlyIndexes,
13
+ ReadonlyRangedIndex,
14
+ ReadonlyUniqueIndex,
15
+ } from '../lib/indexes.ts';
16
+ import type { Bound } from '../server/range.ts';
17
+ import type { Prettify } from '../lib/type_util.ts';
18
+
19
+ export type Operation<
20
+ RowType extends Record<string, any> = Record<string, any>,
21
+ > = {
22
+ type: 'insert' | 'delete';
23
+ // For tables with a primary key, this is the primary key value, as a primitive or string.
24
+ // Otherwise, it is an encoding of the full row.
25
+ rowId: ComparablePrimitive;
26
+ row: RowType;
27
+ };
28
+
29
+ export type TableUpdate<TableDef extends UntypedTableDef> = {
30
+ tableName: string;
31
+ operations: Operation<RowType<TableDef>>[];
32
+ };
33
+
34
+ export type PendingCallback = {
35
+ type: 'insert' | 'delete' | 'update';
36
+ table: string;
37
+ cb: () => void;
38
+ };
39
+
40
+ // Strict scalar compare for index term values.
41
+ const scalarCompare = (x: any, y: any): number => {
42
+ if (x === y) return 0;
43
+ // Compare booleans/numbers/bigints/strings with JS ordering.
44
+ return x < y ? -1 : 1;
45
+ };
46
+
47
+ export type TableIndexView<
48
+ RemoteModule extends UntypedRemoteModule,
49
+ TableName extends TableNamesOf<RemoteModule>,
50
+ > = ReadonlyIndexes<
51
+ TableDefForTableName<RemoteModule, TableName>,
52
+ TableIndexes<TableDefForTableName<RemoteModule, TableName>>
53
+ >;
54
+
55
+ export type TableCache<
56
+ RemoteModule extends UntypedRemoteModule,
57
+ TableName extends TableNamesOf<RemoteModule>,
58
+ > = TableCacheImpl<RemoteModule, TableName> &
59
+ TableIndexView<RemoteModule, TableName>;
60
+
61
+ /**
62
+ * Builder to generate calls to query a `table` in the database
63
+ */
64
+ export class TableCacheImpl<
65
+ RemoteModule extends UntypedRemoteModule,
66
+ TableName extends TableNamesOf<RemoteModule>,
67
+ > implements ClientTableCoreImplementable<RemoteModule, TableName>
68
+ {
69
+ private readonly hasPrimaryKey: boolean;
70
+ private rows: Map<
71
+ ComparablePrimitive,
72
+ [RowType<TableDefForTableName<RemoteModule, TableName>>, number]
73
+ >;
74
+ private tableDef: TableDefForTableName<RemoteModule, TableName>;
75
+ private emitter: EventEmitter<'insert' | 'delete' | 'update'>;
76
+
77
+ /**
78
+ * @param name the table name
79
+ * @param primaryKeyCol column index designated as `#[primarykey]`
80
+ * @param primaryKey column name designated as `#[primarykey]`
81
+ * @param entityClass the entityClass
82
+ */
83
+ constructor(tableDef: TableDefForTableName<RemoteModule, TableName>) {
84
+ this.tableDef = tableDef;
85
+ this.rows = new Map();
86
+ this.emitter = new EventEmitter();
87
+ this.hasPrimaryKey = Object.values(this.tableDef.columns).some(
88
+ col => col.columnMetadata.isPrimaryKey === true
89
+ );
90
+ // Build index views from the resolved runtime index metadata.
91
+ //
92
+ // We intentionally use `resolvedIndexes` rather than `indexes`:
93
+ // - `indexes` is declarative table-level config (`IndexOpts`) used mainly for typing.
94
+ // - `resolvedIndexes` is the runtime shape (`UntypedIndex`) that includes both
95
+ // field-level and explicit table-level indexes.
96
+ for (const idxDef of this.tableDef.resolvedIndexes) {
97
+ const index = this.#makeReadonlyIndex(this.tableDef, idxDef);
98
+ // IMPORTANT: for duplicate accessor names, client cache uses assignment
99
+ // semantics and later entries overwrite earlier ones. This matches prior
100
+ // behavior and is intentionally different from server runtime merge logic.
101
+ (this as any)[idxDef.name] = index;
102
+ }
103
+ }
104
+
105
+ // TODO: this just scans the whole table; we should build proper index structures
106
+ #makeReadonlyIndex<
107
+ I extends TableDefForTableName<
108
+ RemoteModule,
109
+ TableName
110
+ >['resolvedIndexes'][number],
111
+ >(
112
+ tableDef: TableDefForTableName<RemoteModule, TableName>,
113
+ idx: I
114
+ ): ReadonlyIndex<TableDefForTableName<RemoteModule, TableName>, I> {
115
+ type TableDef = TableDefForTableName<RemoteModule, TableName>;
116
+ type Row = Prettify<RowType<TableDef>>;
117
+
118
+ // We do not yet support non-btree indexes
119
+ if (idx.algorithm !== 'btree') {
120
+ throw new Error('Only btree indexes are supported in TableCacheImpl');
121
+ }
122
+
123
+ const columns = idx.columns;
124
+
125
+ // Extract the tuple key for this btree index (column order preserved)
126
+ const getKey = (row: Row): readonly unknown[] =>
127
+ columns.map(c => (row as Record<string, unknown>)[c]);
128
+
129
+ // The server’s ranged scan fixes all prefix cols to equality and applies
130
+ // the bound only to the *last* term. We mirror that.
131
+ //
132
+ // rangeArg for multi-col index is:
133
+ // [...prefixEqualValues, (lastTerm | Range<lastTerm>)]
134
+ //
135
+ // If only one element is provided, it’s the last term (scalar or Range).
136
+ const matchRange = (row: Row, rangeArg: any): boolean => {
137
+ const key = getKey(row);
138
+
139
+ // Normalize rangeArg into an array.
140
+ // With multi-col b-tree, IndexScanRangeBounds always yields at least one element.
141
+ const arr = Array.isArray(rangeArg) ? rangeArg : [rangeArg];
142
+
143
+ const prefixLen = Math.max(0, arr.length - 1);
144
+ // Check equality over the prefix (all but the last provided element)
145
+ for (let i = 0; i < prefixLen; i++) {
146
+ if (!deepEqual(key[i], arr[i])) return false;
147
+ }
148
+
149
+ const lastProvided = arr[arr.length - 1];
150
+ const kLast = key[prefixLen];
151
+
152
+ // If the last provided is a Range<T>, apply bounds; otherwise equality.
153
+ if (
154
+ lastProvided &&
155
+ typeof lastProvided === 'object' &&
156
+ 'from' in lastProvided &&
157
+ 'to' in lastProvided
158
+ ) {
159
+ // Range<T>
160
+ const from = lastProvided.from as Bound<any>;
161
+ const to = lastProvided.to as Bound<any>;
162
+
163
+ // Lower bound
164
+ if (from.tag !== 'unbounded') {
165
+ const c = scalarCompare(kLast, from.value);
166
+ if (c < 0) return false;
167
+ if (c === 0 && from.tag === 'excluded') return false;
168
+ }
169
+
170
+ // Upper bound
171
+ if (to.tag !== 'unbounded') {
172
+ const c = scalarCompare(kLast, to.value);
173
+ if (c > 0) return false;
174
+ if (c === 0 && to.tag === 'excluded') return false;
175
+ }
176
+
177
+ // All good on last term; any remaining columns (if any) are unconstrained,
178
+ // which matches server behavior for a prefix scan.
179
+ return true;
180
+ } else {
181
+ // Equality on the last provided element
182
+ if (!deepEqual(kLast, lastProvided)) return false;
183
+ // Any remaining columns are unconstrained (prefix equality only).
184
+ return true;
185
+ }
186
+ };
187
+
188
+ // An index is unique if it shares all columns with a unique constraint
189
+ const isUnique = tableDef.constraints.some(constraint => {
190
+ if (constraint.constraint !== 'unique') {
191
+ return false;
192
+ }
193
+ return deepEqual(constraint.columns, idx.columns);
194
+ });
195
+
196
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
197
+ const self = this;
198
+ if (isUnique) {
199
+ const impl: ReadonlyUniqueIndex<TableDef, I> = {
200
+ find: (colVal: any): Row | null => {
201
+ // For unique btree, caller supplies the *full* key (tuple if multi-col).
202
+ const expected = Array.isArray(colVal) ? colVal : [colVal];
203
+ for (const row of self.iter()) {
204
+ if (deepEqual(getKey(row), expected)) return row;
205
+ }
206
+ return null;
207
+ },
208
+ };
209
+ return impl as ReadonlyIndex<TableDef, I>;
210
+ } else {
211
+ const impl: ReadonlyRangedIndex<TableDef, I> = {
212
+ *filter(range: any): IteratorObject<Row, undefined> {
213
+ for (const row of self.iter()) {
214
+ if (matchRange(row, range)) yield row;
215
+ }
216
+ },
217
+ };
218
+ return impl as ReadonlyIndex<TableDef, I>;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * @returns number of rows in the table
224
+ */
225
+ count(): bigint {
226
+ return BigInt(this.rows.size);
227
+ }
228
+
229
+ /**
230
+ * @returns The values of the rows in the table
231
+ */
232
+ iter(): IteratorObject<
233
+ Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
234
+ undefined
235
+ > {
236
+ function* generator(
237
+ rows: Map<
238
+ ComparablePrimitive,
239
+ [RowType<TableDefForTableName<RemoteModule, TableName>>, number]
240
+ >
241
+ ): IteratorObject<
242
+ Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
243
+ undefined
244
+ > {
245
+ for (const [row] of rows.values()) {
246
+ yield row as Prettify<
247
+ RowType<TableDefForTableName<RemoteModule, TableName>>
248
+ >;
249
+ }
250
+ }
251
+ return generator(this.rows);
252
+ }
253
+
254
+ /**
255
+ * Allows iteration over the rows in the table
256
+ * @returns An iterator over the rows in the table
257
+ */
258
+ [Symbol.iterator](): IteratorObject<
259
+ Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
260
+ undefined
261
+ > {
262
+ return this.iter();
263
+ }
264
+
265
+ applyOperations = (
266
+ operations: Operation<
267
+ RowType<TableDefForTableName<RemoteModule, TableName>>
268
+ >[],
269
+ ctx: EventContextInterface<RemoteModule>
270
+ ): PendingCallback[] => {
271
+ const pendingCallbacks: PendingCallback[] = [];
272
+
273
+ // Event tables: fire on_insert callbacks but don't store rows in the cache.
274
+ if (this.tableDef.isEvent) {
275
+ for (const op of operations) {
276
+ if (op.type === 'insert') {
277
+ pendingCallbacks.push({
278
+ type: 'insert',
279
+ table: this.tableDef.sourceName,
280
+ cb: () => {
281
+ this.emitter.emit('insert', ctx, op.row);
282
+ },
283
+ });
284
+ }
285
+ }
286
+ return pendingCallbacks;
287
+ }
288
+
289
+ if (this.hasPrimaryKey) {
290
+ const insertMap = new Map<
291
+ ComparablePrimitive,
292
+ [
293
+ Operation<RowType<TableDefForTableName<RemoteModule, TableName>>>,
294
+ number,
295
+ ]
296
+ >();
297
+ const deleteMap = new Map<
298
+ ComparablePrimitive,
299
+ [
300
+ Operation<RowType<TableDefForTableName<RemoteModule, TableName>>>,
301
+ number,
302
+ ]
303
+ >();
304
+ for (const op of operations) {
305
+ if (op.type === 'insert') {
306
+ const [_, prevCount] = insertMap.get(op.rowId) || [op, 0];
307
+ insertMap.set(op.rowId, [op, prevCount + 1]);
308
+ } else {
309
+ const [_, prevCount] = deleteMap.get(op.rowId) || [op, 0];
310
+ deleteMap.set(op.rowId, [op, prevCount + 1]);
311
+ }
312
+ }
313
+ for (const [primaryKey, [insertOp, refCount]] of insertMap) {
314
+ const deleteEntry = deleteMap.get(primaryKey);
315
+ if (deleteEntry) {
316
+ const [_, deleteCount] = deleteEntry;
317
+ // In most cases the refCountDelta will be either 0 or refCount, but if
318
+ // an update moves a row in or out of the result set of different queries, then
319
+ // other deltas are possible.
320
+ const refCountDelta = refCount - deleteCount;
321
+ const maybeCb = this.update(
322
+ ctx,
323
+ primaryKey,
324
+ insertOp.row,
325
+ refCountDelta
326
+ );
327
+ if (maybeCb) {
328
+ pendingCallbacks.push(maybeCb);
329
+ }
330
+ deleteMap.delete(primaryKey);
331
+ } else {
332
+ const maybeCb = this.insert(ctx, insertOp, refCount);
333
+ if (maybeCb) {
334
+ pendingCallbacks.push(maybeCb);
335
+ }
336
+ }
337
+ }
338
+ for (const [deleteOp, refCount] of deleteMap.values()) {
339
+ const maybeCb = this.delete(ctx, deleteOp, refCount);
340
+ if (maybeCb) {
341
+ pendingCallbacks.push(maybeCb);
342
+ }
343
+ }
344
+ } else {
345
+ for (const op of operations) {
346
+ if (op.type === 'insert') {
347
+ const maybeCb = this.insert(ctx, op);
348
+ if (maybeCb) {
349
+ pendingCallbacks.push(maybeCb);
350
+ }
351
+ } else {
352
+ const maybeCb = this.delete(ctx, op);
353
+ if (maybeCb) {
354
+ pendingCallbacks.push(maybeCb);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ return pendingCallbacks;
360
+ };
361
+
362
+ update = (
363
+ ctx: EventContextInterface<RemoteModule>,
364
+ rowId: ComparablePrimitive,
365
+ newRow: RowType<TableDefForTableName<RemoteModule, TableName>>,
366
+ refCountDelta: number = 0
367
+ ): PendingCallback | undefined => {
368
+ const existingEntry = this.rows.get(rowId);
369
+ if (!existingEntry) {
370
+ // TODO: this should throw an error and kill the connection.
371
+ stdbLogger(
372
+ 'error',
373
+ `Updating a row that was not present in the cache. Table: ${this.tableDef.sourceName}, RowId: ${rowId}`
374
+ );
375
+ return undefined;
376
+ }
377
+ const [oldRow, previousCount] = existingEntry;
378
+ const refCount = Math.max(1, previousCount + refCountDelta);
379
+ if (previousCount + refCountDelta <= 0) {
380
+ stdbLogger(
381
+ 'error',
382
+ `Negative reference count for in table ${this.tableDef.sourceName} row ${rowId} (${previousCount} + ${refCountDelta})`
383
+ );
384
+ return undefined;
385
+ }
386
+ this.rows.set(rowId, [newRow, refCount]);
387
+ // This indicates something is wrong, so we could arguably crash here.
388
+ if (previousCount === 0) {
389
+ stdbLogger(
390
+ 'error',
391
+ `Updating a row id in table ${this.tableDef.sourceName} which was not present in the cache (rowId: ${rowId})`
392
+ );
393
+ return {
394
+ type: 'insert',
395
+ table: this.tableDef.sourceName,
396
+ cb: () => {
397
+ this.emitter.emit('insert', ctx, newRow);
398
+ },
399
+ };
400
+ }
401
+ return {
402
+ type: 'update',
403
+ table: this.tableDef.sourceName,
404
+ cb: () => {
405
+ this.emitter.emit('update', ctx, oldRow, newRow);
406
+ },
407
+ };
408
+ };
409
+
410
+ insert = (
411
+ ctx: EventContextInterface<RemoteModule>,
412
+ operation: Operation<
413
+ RowType<TableDefForTableName<RemoteModule, TableName>>
414
+ >,
415
+ count: number = 1
416
+ ): PendingCallback | undefined => {
417
+ const [_, previousCount] = this.rows.get(operation.rowId) || [
418
+ operation.row,
419
+ 0,
420
+ ];
421
+ this.rows.set(operation.rowId, [operation.row, previousCount + count]);
422
+ if (previousCount === 0) {
423
+ return {
424
+ type: 'insert',
425
+ table: this.tableDef.sourceName,
426
+ cb: () => {
427
+ this.emitter.emit('insert', ctx, operation.row);
428
+ },
429
+ };
430
+ }
431
+ // It's possible to get a duplicate insert because rows can be returned from multiple queries.
432
+ return undefined;
433
+ };
434
+
435
+ delete = (
436
+ ctx: EventContextInterface<RemoteModule>,
437
+ operation: Operation<
438
+ RowType<TableDefForTableName<RemoteModule, TableName>>
439
+ >,
440
+ count: number = 1
441
+ ): PendingCallback | undefined => {
442
+ const [_, previousCount] = this.rows.get(operation.rowId) || [
443
+ operation.row,
444
+ 0,
445
+ ];
446
+ // This should never happen.
447
+ if (previousCount === 0) {
448
+ stdbLogger('warn', 'Deleting a row that was not present in the cache');
449
+ return undefined;
450
+ }
451
+ // If this was the last reference, we are actually deleting the row.
452
+ if (previousCount <= count) {
453
+ // TODO: Log a warning/error if previousCount is less than count.
454
+ this.rows.delete(operation.rowId);
455
+ return {
456
+ type: 'delete',
457
+ table: this.tableDef.sourceName,
458
+ cb: () => {
459
+ this.emitter.emit('delete', ctx, operation.row);
460
+ },
461
+ };
462
+ }
463
+ this.rows.set(operation.rowId, [operation.row, previousCount - count]);
464
+ return undefined;
465
+ };
466
+
467
+ /**
468
+ * Register a callback for when a row is newly inserted into the database.
469
+ *
470
+ * ```ts
471
+ * ctx.db.user.onInsert((reducerEvent, user) => {
472
+ * if (reducerEvent) {
473
+ * console.log("New user on reducer", reducerEvent, user);
474
+ * } else {
475
+ * console.log("New user received during subscription update on insert", user);
476
+ * }
477
+ * });
478
+ * ```
479
+ *
480
+ * @param cb Callback to be called when a new row is inserted
481
+ */
482
+ onInsert = (
483
+ cb: (
484
+ ctx: EventContextInterface<RemoteModule>,
485
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
486
+ ) => void
487
+ ): void => {
488
+ this.emitter.on('insert', cb);
489
+ };
490
+
491
+ /**
492
+ * Register a callback for when a row is deleted from the database.
493
+ *
494
+ * ```ts
495
+ * ctx.db.user.onDelete((reducerEvent, user) => {
496
+ * if (reducerEvent) {
497
+ * console.log("Deleted user on reducer", reducerEvent, user);
498
+ * } else {
499
+ * console.log("Deleted user received during subscription update on update", user);
500
+ * }
501
+ * });
502
+ * ```
503
+ *
504
+ * @param cb Callback to be called when a new row is inserted
505
+ */
506
+ onDelete = (
507
+ cb: (
508
+ ctx: EventContextInterface<RemoteModule>,
509
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
510
+ ) => void
511
+ ): void => {
512
+ this.emitter.on('delete', cb);
513
+ };
514
+
515
+ /**
516
+ * Register a callback for when a row is updated into the database.
517
+ *
518
+ * ```ts
519
+ * ctx.db.user.onInsert((reducerEvent, oldUser, user) => {
520
+ * if (reducerEvent) {
521
+ * console.log("Updated user on reducer", reducerEvent, user);
522
+ * } else {
523
+ * console.log("Updated user received during subscription update on delete", user);
524
+ * }
525
+ * });
526
+ * ```
527
+ *
528
+ * @param cb Callback to be called when a new row is inserted
529
+ */
530
+ onUpdate = (
531
+ cb: (
532
+ ctx: EventContextInterface<RemoteModule>,
533
+ oldRow: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
534
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
535
+ ) => void
536
+ ): void => {
537
+ this.emitter.on('update', cb);
538
+ };
539
+
540
+ /**
541
+ * Remove a callback for when a row is newly inserted into the database.
542
+ *
543
+ * @param cb Callback to be removed
544
+ */
545
+ removeOnInsert = (
546
+ cb: (
547
+ ctx: EventContextInterface<RemoteModule>,
548
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
549
+ ) => void
550
+ ): void => {
551
+ this.emitter.off('insert', cb);
552
+ };
553
+
554
+ /**
555
+ * Remove a callback for when a row is deleted from the database.
556
+ *
557
+ * @param cb Callback to be removed
558
+ */
559
+ removeOnDelete = (
560
+ cb: (
561
+ ctx: EventContextInterface<RemoteModule>,
562
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
563
+ ) => void
564
+ ): void => {
565
+ this.emitter.off('delete', cb);
566
+ };
567
+
568
+ /**
569
+ * Remove a callback for when a row is updated into the database.
570
+ *
571
+ * @param cb Callback to be removed
572
+ */
573
+ removeOnUpdate = (
574
+ cb: (
575
+ ctx: EventContextInterface<RemoteModule>,
576
+ oldRow: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>,
577
+ row: Prettify<RowType<TableDefForTableName<RemoteModule, TableName>>>
578
+ ) => void
579
+ ): void => {
580
+ this.emitter.off('update', cb);
581
+ };
582
+ }