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,581 +1,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[] => 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[] => 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
+ }