spacetimedb 2.5.0 → 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.
- package/LICENSE.txt +759 -759
- package/README.md +211 -211
- package/dist/angular/index.cjs.map +1 -1
- package/dist/angular/index.mjs.map +1 -1
- package/dist/browser/angular/index.mjs.map +1 -1
- package/dist/browser/react/index.mjs +129 -57
- package/dist/browser/react/index.mjs.map +1 -1
- package/dist/browser/solid/index.mjs +120 -50
- package/dist/browser/solid/index.mjs.map +1 -1
- package/dist/browser/svelte/index.mjs.map +1 -1
- package/dist/browser/vue/index.mjs.map +1 -1
- package/dist/index.browser.mjs +10 -2
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +10 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/index.mjs.map +1 -1
- package/dist/min/index.browser.mjs +1 -1
- package/dist/min/index.browser.mjs.map +1 -1
- package/dist/min/react/index.mjs +1 -1
- package/dist/min/react/index.mjs.map +1 -1
- package/dist/min/sdk/index.browser.mjs +1 -1
- package/dist/min/sdk/index.browser.mjs.map +1 -1
- package/dist/react/index.cjs +129 -57
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.mjs +129 -57
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useTable.d.ts.map +1 -1
- package/dist/sdk/connection_manager.d.ts +8 -0
- package/dist/sdk/connection_manager.d.ts.map +1 -1
- package/dist/sdk/db_connection_impl.d.ts +7 -0
- package/dist/sdk/db_connection_impl.d.ts.map +1 -1
- package/dist/sdk/index.browser.mjs +10 -2
- package/dist/sdk/index.browser.mjs.map +1 -1
- package/dist/sdk/index.cjs +10 -2
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.mjs +10 -2
- package/dist/sdk/index.mjs.map +1 -1
- package/dist/sdk/websocket_test_adapter.d.ts +2 -1
- package/dist/sdk/websocket_test_adapter.d.ts.map +1 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/solid/index.cjs +120 -50
- package/dist/solid/index.cjs.map +1 -1
- package/dist/solid/index.mjs +120 -50
- package/dist/solid/index.mjs.map +1 -1
- package/dist/svelte/index.cjs.map +1 -1
- package/dist/svelte/index.mjs.map +1 -1
- package/dist/tanstack/index.cjs +120 -50
- package/dist/tanstack/index.cjs.map +1 -1
- package/dist/tanstack/index.mjs +120 -50
- package/dist/tanstack/index.mjs.map +1 -1
- package/dist/vue/index.cjs.map +1 -1
- package/dist/vue/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/angular/connection_state.ts +19 -19
- package/src/angular/index.ts +3 -3
- package/src/angular/injectors/index.ts +4 -4
- package/src/angular/injectors/inject-reducer.ts +62 -62
- package/src/angular/injectors/inject-spacetimedb-connected.ts +13 -13
- package/src/angular/injectors/inject-spacetimedb.ts +10 -10
- package/src/angular/injectors/inject-table.ts +234 -234
- package/src/angular/providers/index.ts +1 -1
- package/src/angular/providers/provide-spacetimedb.ts +96 -96
- package/src/index.ts +16 -16
- package/src/lib/algebraic_type.ts +819 -819
- package/src/lib/algebraic_type_variants.ts +26 -26
- package/src/lib/algebraic_value.ts +10 -10
- package/src/lib/autogen/types.ts +746 -746
- package/src/lib/binary_reader.ts +188 -188
- package/src/lib/binary_writer.ts +213 -213
- package/src/lib/connection_id.ts +102 -102
- package/src/lib/constraints.ts +48 -48
- package/src/lib/errors.ts +26 -26
- package/src/lib/filter.ts +195 -195
- package/src/lib/identity.ts +83 -83
- package/src/lib/indexes.ts +251 -251
- package/src/lib/option.ts +34 -34
- package/src/lib/query.ts +1019 -1019
- package/src/lib/reducer_schema.ts +38 -38
- package/src/lib/reducers.ts +116 -116
- package/src/lib/result.ts +36 -36
- package/src/lib/schedule_at.ts +86 -86
- package/src/lib/schema.ts +420 -420
- package/src/lib/table.ts +548 -548
- package/src/lib/table_schema.ts +64 -64
- package/src/lib/time_duration.ts +77 -77
- package/src/lib/timestamp.ts +148 -148
- package/src/lib/type_builders.test-d.ts +128 -128
- package/src/lib/type_builders.ts +4014 -4014
- package/src/lib/type_util.ts +124 -124
- package/src/lib/util.ts +196 -196
- package/src/lib/uuid.ts +337 -337
- package/src/react/SpacetimeDBProvider.ts +84 -84
- package/src/react/connection_state.ts +6 -6
- package/src/react/index.ts +5 -5
- package/src/react/useProcedure.ts +60 -60
- package/src/react/useReducer.ts +53 -53
- package/src/react/useSpacetimeDB.ts +18 -18
- package/src/react/useTable.ts +256 -251
- package/src/sdk/client_api/index.ts +114 -114
- package/src/sdk/client_api/types/procedures.ts +8 -8
- package/src/sdk/client_api/types/reducers.ts +8 -8
- package/src/sdk/client_api/types.ts +288 -288
- package/src/sdk/client_cache.ts +129 -129
- package/src/sdk/client_table.ts +179 -179
- package/src/sdk/connection_manager.ts +352 -237
- package/src/sdk/db_connection_builder.ts +290 -290
- package/src/sdk/db_connection_impl.ts +1356 -1347
- package/src/sdk/db_context.ts +28 -28
- package/src/sdk/db_view.ts +12 -12
- package/src/sdk/decompress.ts +51 -51
- package/src/sdk/event.ts +18 -18
- package/src/sdk/event_context.ts +51 -51
- package/src/sdk/event_emitter.ts +32 -32
- package/src/sdk/index.ts +14 -14
- package/src/sdk/internal.ts +2 -2
- package/src/sdk/json_api.ts +46 -46
- package/src/sdk/logger.ts +134 -134
- package/src/sdk/message_types.ts +46 -46
- package/src/sdk/procedures.ts +83 -83
- package/src/sdk/reducer_event.ts +20 -20
- package/src/sdk/reducer_handle.ts +12 -12
- package/src/sdk/reducers.ts +159 -159
- package/src/sdk/schema.ts +45 -45
- package/src/sdk/spacetime_module.ts +28 -28
- package/src/sdk/subscription_builder_impl.ts +275 -275
- package/src/sdk/table_cache.ts +581 -581
- package/src/sdk/type_utils.ts +19 -19
- package/src/sdk/version.ts +133 -133
- package/src/sdk/websocket_decompress_adapter.ts +63 -63
- package/src/sdk/websocket_protocols.ts +25 -25
- package/src/sdk/websocket_test_adapter.ts +107 -100
- package/src/sdk/websocket_v3_frames.ts +126 -126
- package/src/sdk/ws.ts +105 -105
- package/src/server/console.ts +81 -81
- package/src/server/db_view.ts +21 -21
- package/src/server/errors.ts +138 -138
- package/src/server/http.test-d.ts +80 -80
- package/src/server/http.ts +14 -14
- package/src/server/http_handlers.ts +413 -413
- package/src/server/http_internal.ts +79 -79
- package/src/server/http_shared.ts +186 -186
- package/src/server/index.ts +37 -37
- package/src/server/polyfills.ts +4 -4
- package/src/server/procedures.ts +239 -239
- package/src/server/query.ts +1 -1
- package/src/server/range.ts +53 -53
- package/src/server/reducers.ts +113 -113
- package/src/server/rng.ts +113 -113
- package/src/server/runtime.ts +1102 -1102
- package/src/server/schema.test-d.ts +99 -99
- package/src/server/schema.ts +663 -663
- package/src/server/sys.d.ts +125 -125
- package/src/server/view.test-d.ts +194 -194
- package/src/server/views.ts +340 -340
- package/src/solid/SpacetimeDBProvider.ts +97 -97
- package/src/solid/connection_state.ts +6 -6
- package/src/solid/index.ts +5 -5
- package/src/solid/useProcedure.ts +57 -57
- package/src/solid/useReducer.ts +50 -50
- package/src/solid/useSpacetimeDB.ts +18 -18
- package/src/solid/useTable.ts +203 -203
- package/src/svelte/SpacetimeDBProvider.ts +101 -101
- package/src/svelte/connection_state.ts +16 -16
- package/src/svelte/index.ts +4 -4
- package/src/svelte/useReducer.ts +61 -61
- package/src/svelte/useSpacetimeDB.ts +22 -22
- package/src/svelte/useTable.ts +218 -218
- package/src/tanstack/SpacetimeDBQueryClient.ts +330 -330
- package/src/tanstack/hooks.ts +83 -83
- package/src/tanstack/index.ts +16 -16
- package/src/util-stub.ts +1 -1
- package/src/vue/SpacetimeDBProvider.ts +157 -157
- package/src/vue/connection_state.ts +19 -19
- package/src/vue/index.ts +5 -5
- package/src/vue/useProcedure.ts +62 -62
- package/src/vue/useReducer.ts +55 -55
- package/src/vue/useSpacetimeDB.ts +18 -18
- package/src/vue/useTable.ts +229 -229
package/src/sdk/table_cache.ts
CHANGED
|
@@ -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
|
+
}
|