@interocitor/core 0.0.0-beta.3 → 0.0.0-beta.5
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/README.md +446 -91
- package/dist/adapters/cloudflare.d.ts +8 -9
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +38 -12
- package/dist/adapters/google-drive.d.ts +1 -1
- package/dist/adapters/google-drive.js +1 -2
- package/dist/adapters/memory.d.ts +4 -1
- package/dist/adapters/memory.d.ts.map +1 -1
- package/dist/adapters/memory.js +13 -2
- package/dist/adapters/webdav.d.ts +5 -0
- package/dist/adapters/webdav.d.ts.map +1 -1
- package/dist/adapters/webdav.js +18 -1
- package/dist/core/codec.d.ts +1 -1
- package/dist/core/codec.d.ts.map +1 -1
- package/dist/core/codec.js +39 -3
- package/dist/core/compaction.d.ts +9 -1
- package/dist/core/compaction.d.ts.map +1 -1
- package/dist/core/compaction.js +63 -7
- package/dist/core/crdt.d.ts +6 -3
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/crdt.js +53 -67
- package/dist/core/errors.d.ts +47 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +61 -0
- package/dist/core/flush.d.ts +3 -3
- package/dist/core/flush.d.ts.map +1 -1
- package/dist/core/flush.js +64 -7
- package/dist/core/hlc.js +0 -1
- package/dist/core/ids.d.ts +49 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +132 -0
- package/dist/core/internals.d.ts +10 -2
- package/dist/core/internals.d.ts.map +1 -1
- package/dist/core/internals.js +27 -9
- package/dist/core/manifest.d.ts +24 -5
- package/dist/core/manifest.d.ts.map +1 -1
- package/dist/core/manifest.js +80 -13
- package/dist/core/pull.d.ts +1 -1
- package/dist/core/pull.d.ts.map +1 -1
- package/dist/core/pull.js +21 -6
- package/dist/core/row-id.js +0 -1
- package/dist/core/schema-types.d.ts +22 -11
- package/dist/core/schema-types.d.ts.map +1 -1
- package/dist/core/schema-types.js +18 -9
- package/dist/core/schema-types.type-test.js +59 -5
- package/dist/core/sync-engine.d.ts +166 -12
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +1615 -219
- package/dist/core/table.d.ts +217 -17
- package/dist/core/table.d.ts.map +1 -1
- package/dist/core/table.js +376 -24
- package/dist/core/types.d.ts +413 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -1
- package/dist/crypto/encryption.d.ts.map +1 -1
- package/dist/crypto/encryption.js +6 -1
- package/dist/crypto/keys.js +0 -1
- package/dist/handshake/channel.js +0 -1
- package/dist/handshake/index.d.ts +5 -2
- package/dist/handshake/index.d.ts.map +1 -1
- package/dist/handshake/index.js +19 -2
- package/dist/handshake/qr.js +0 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/storage/credential-store.d.ts +25 -2
- package/dist/storage/credential-store.d.ts.map +1 -1
- package/dist/storage/credential-store.js +55 -8
- package/dist/storage/local-store.d.ts +4 -1
- package/dist/storage/local-store.d.ts.map +1 -1
- package/dist/storage/local-store.js +37 -21
- package/package.json +3 -3
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/google-drive.js.map +0 -1
- package/dist/adapters/memory.js.map +0 -1
- package/dist/adapters/webdav.js.map +0 -1
- package/dist/core/codec.js.map +0 -1
- package/dist/core/compaction.js.map +0 -1
- package/dist/core/crdt.js.map +0 -1
- package/dist/core/flush.js.map +0 -1
- package/dist/core/hlc.js.map +0 -1
- package/dist/core/internals.js.map +0 -1
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/pull.js.map +0 -1
- package/dist/core/row-id.js.map +0 -1
- package/dist/core/schema-types.js.map +0 -1
- package/dist/core/schema-types.type-test.js.map +0 -1
- package/dist/core/sync-engine.js.map +0 -1
- package/dist/core/table.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/crypto/encryption.js.map +0 -1
- package/dist/crypto/keys.js.map +0 -1
- package/dist/handshake/channel.js.map +0 -1
- package/dist/handshake/index.js.map +0 -1
- package/dist/handshake/qr.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/storage/credential-store.js.map +0 -1
- package/dist/storage/local-store.js.map +0 -1
package/dist/core/table.js
CHANGED
|
@@ -1,23 +1,320 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Table<T> — typed handle for a named collection within a
|
|
2
|
+
* Table<T> — typed handle for a named collection within a Interocitor.
|
|
3
3
|
*
|
|
4
4
|
* Wraps the engine's raw Row/string API with a type-safe surface.
|
|
5
5
|
* All reads return plain T objects (internal HLC metadata stripped).
|
|
6
6
|
* All writes accept Partial<T> and return T.
|
|
7
7
|
*/
|
|
8
|
+
import { createRowId } from "./row-id.js";
|
|
9
|
+
/**
|
|
10
|
+
* Thenable result of an async query.
|
|
11
|
+
*
|
|
12
|
+
* Two layers live here, kept distinct on purpose:
|
|
13
|
+
* 1. Async query identity: `descriptor` + `cacheKey`. Owned by core. Stable
|
|
14
|
+
* across `.sort()` chains. Used by any cache layer (React etc.) to dedupe
|
|
15
|
+
* loads, share in-flight promises, and keep stale data across remounts.
|
|
16
|
+
* 2. Sync derivation: `.sort(compareFn)` runs after the async load. Does not
|
|
17
|
+
* change `cacheKey`. Not part of cache identity. Pure post-processing.
|
|
18
|
+
*
|
|
19
|
+
* `then(...)` triggers a load through the engine cache. Use `load({ bypassCache })`
|
|
20
|
+
* to force a fresh fetch.
|
|
21
|
+
*/
|
|
22
|
+
export class QueryResult {
|
|
23
|
+
constructor(descriptor, _engine,
|
|
24
|
+
/** Optional sync transform stack — runs after rows are loaded. */
|
|
25
|
+
_sortChain = [],
|
|
26
|
+
/** Optional pre-resolved promise (used for synthetic / engine-less results). */
|
|
27
|
+
_explicitPromise = null) {
|
|
28
|
+
this._engine = _engine;
|
|
29
|
+
this._sortChain = _sortChain;
|
|
30
|
+
this._explicitPromise = _explicitPromise;
|
|
31
|
+
// ─── internals ───────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Memoization for the typed+sorted projection. Keyed by the raw rows
|
|
34
|
+
* reference from the engine cache. Same input ref ⇒ same output ref.
|
|
35
|
+
* Cleared automatically when the engine swaps in a fresh raw rows array
|
|
36
|
+
* (e.g. after a write invalidation).
|
|
37
|
+
*/
|
|
38
|
+
this._projectionInputRef = null;
|
|
39
|
+
this._projectionOutput = null;
|
|
40
|
+
this.descriptor = descriptor;
|
|
41
|
+
this.cacheKey = _engine
|
|
42
|
+
? _engine.getQueryCacheKey(descriptor)
|
|
43
|
+
: computeCacheKey(descriptor);
|
|
44
|
+
}
|
|
45
|
+
/** Public metadata. Mirrors `QueryMetadata` shape. */
|
|
46
|
+
get metadata() {
|
|
47
|
+
return { descriptor: this.descriptor, cacheKey: this.cacheKey };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Async load through engine cache.
|
|
51
|
+
* Pass `{ bypassCache: true }` to ignore cached snapshot and refetch.
|
|
52
|
+
*/
|
|
53
|
+
load(options) {
|
|
54
|
+
return this._loadRaw(options).then(rows => this._applySort(rows));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read cached rows synchronously, if available. Returns `undefined` only if
|
|
58
|
+
* the cache has no row snapshot. Does not start a load.
|
|
59
|
+
*
|
|
60
|
+
* Stale-while-revalidate contract: pending/error snapshots may still carry
|
|
61
|
+
* rows from the previous ready load. Expose those rows so consumers can keep
|
|
62
|
+
* stale data visible while a refresh is in flight.
|
|
63
|
+
*
|
|
64
|
+
* Stable reference contract: callers (React, useSyncExternalStore, etc.)
|
|
65
|
+
* MUST be able to compare the returned array by reference. We memoize the
|
|
66
|
+
* typed+sorted projection keyed by the raw rows reference from the engine
|
|
67
|
+
* cache. As long as the engine cache hasn't moved, this returns the exact
|
|
68
|
+
* same array instance.
|
|
69
|
+
*/
|
|
70
|
+
peekCache() {
|
|
71
|
+
if (!this._engine)
|
|
72
|
+
return undefined;
|
|
73
|
+
const snap = this._engine.readQueryCache(this.descriptor);
|
|
74
|
+
if (!snap.rows)
|
|
75
|
+
return undefined;
|
|
76
|
+
return this._project(snap.rows);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Sync status read for cache consumers (e.g. React bindings). Mirrors the
|
|
80
|
+
* cache snapshot status without exposing engine internals.
|
|
81
|
+
*/
|
|
82
|
+
peekStatus() {
|
|
83
|
+
if (!this._engine)
|
|
84
|
+
return { status: 'empty' };
|
|
85
|
+
const snap = this._engine.readQueryCache(this.descriptor);
|
|
86
|
+
return { status: snap.status, error: snap.error };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Render-time read. If the engine is ready and a cache snapshot exists,
|
|
90
|
+
* returns rows synchronously (no flash). Otherwise returns a promise.
|
|
91
|
+
* If the engine is not ready and `mode === 'cache-first'`, falls back to
|
|
92
|
+
* sync `undefined`-like behavior by returning a promise — caller decides.
|
|
93
|
+
*/
|
|
94
|
+
readForRender(policy) {
|
|
95
|
+
const cached = this.peekCache();
|
|
96
|
+
if (cached && policy?.mode !== 'bypass-cache')
|
|
97
|
+
return cached;
|
|
98
|
+
return this.load({ bypassCache: policy?.mode === 'bypass-cache' });
|
|
99
|
+
}
|
|
100
|
+
then(onfulfilled, onrejected) {
|
|
101
|
+
return this.load().then(onfulfilled, onrejected);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sort the result. Same signature as `Array.prototype.sort`.
|
|
105
|
+
* Sync-only. Does NOT change `cacheKey` — this is post-load derivation.
|
|
106
|
+
*/
|
|
107
|
+
sort(compareFn) {
|
|
108
|
+
const next = (rows) => [...rows].sort(compareFn);
|
|
109
|
+
return new QueryResult(this.descriptor, this._engine, [...this._sortChain, next], this._explicitPromise);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sort by a single field. Default: ascending.
|
|
113
|
+
*
|
|
114
|
+
* Deterministic and serializable, so it lives on the descriptor and
|
|
115
|
+
* participates in `cacheKey`. Two queries with the same shape and the
|
|
116
|
+
* same `orderBy` share a cache entry; differing only by direction or
|
|
117
|
+
* field produces a distinct cache entry.
|
|
118
|
+
*
|
|
119
|
+
* Re-applying `orderBy` overwrites the previous one (last write wins).
|
|
120
|
+
* The sync `.sort(compareFn)` chain is preserved on top of orderBy for
|
|
121
|
+
* post-load derivations.
|
|
122
|
+
*/
|
|
123
|
+
orderBy(field, dir = 'asc') {
|
|
124
|
+
const nextDescriptor = {
|
|
125
|
+
...this.descriptor,
|
|
126
|
+
orderBy: { field: field, dir },
|
|
127
|
+
};
|
|
128
|
+
return new QueryResult(nextDescriptor, this._engine, this._sortChain, this._explicitPromise);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Subscribe to changes that affect this query's table.
|
|
132
|
+
* Returns an unsubscribe function. Currently table-wide; finer-grained
|
|
133
|
+
* invalidation can be added in core later without changing this contract.
|
|
134
|
+
*/
|
|
135
|
+
subscribe(cb) {
|
|
136
|
+
if (!this._engine)
|
|
137
|
+
return () => { };
|
|
138
|
+
const engine = this._engine;
|
|
139
|
+
const tableName = this.descriptor.table;
|
|
140
|
+
return engine.on(event => {
|
|
141
|
+
if (event.type === 'change' && event.table === tableName) {
|
|
142
|
+
cb({ type: 'change', rowId: event.rowId, row: rowToTyped(event.row) });
|
|
143
|
+
}
|
|
144
|
+
else if (event.type === 'delete' && event.table === tableName) {
|
|
145
|
+
cb({ type: 'delete', rowId: event.rowId });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
_project(rawRows) {
|
|
150
|
+
if (this._projectionInputRef === rawRows && this._projectionOutput !== null) {
|
|
151
|
+
return this._projectionOutput;
|
|
152
|
+
}
|
|
153
|
+
const typed = rawRows.map(r => rowToTyped(r));
|
|
154
|
+
const sorted = this._applySort(typed);
|
|
155
|
+
this._projectionInputRef = rawRows;
|
|
156
|
+
this._projectionOutput = sorted;
|
|
157
|
+
return sorted;
|
|
158
|
+
}
|
|
159
|
+
_applySort(rows) {
|
|
160
|
+
if (this._sortChain.length === 0)
|
|
161
|
+
return rows;
|
|
162
|
+
let out = rows;
|
|
163
|
+
for (const fn of this._sortChain)
|
|
164
|
+
out = fn(out);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
async _loadRaw(options) {
|
|
168
|
+
if (this._explicitPromise)
|
|
169
|
+
return this._explicitPromise;
|
|
170
|
+
if (!this._engine)
|
|
171
|
+
return [];
|
|
172
|
+
const rawRows = await this._engine.loadQueryRows(this.descriptor, options);
|
|
173
|
+
// Use the same projection cache so post-load + peekCache yield the
|
|
174
|
+
// same reference for the same raw rows.
|
|
175
|
+
return this._project(rawRows);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Stable, deterministic cache key derived from a query descriptor.
|
|
180
|
+
*
|
|
181
|
+
* Only async-affecting fields are included. Sync derivations (custom `sort`)
|
|
182
|
+
* are intentionally excluded.
|
|
183
|
+
*/
|
|
184
|
+
export function computeCacheKey(descriptor) {
|
|
185
|
+
const parts = [`t=${descriptor.table}`];
|
|
186
|
+
if (descriptor.clause) {
|
|
187
|
+
const c = descriptor.clause;
|
|
188
|
+
parts.push(`w=${c.field}:${c.op}`);
|
|
189
|
+
if (c.value !== undefined)
|
|
190
|
+
parts.push(`v=${serializePrimitive(c.value)}`);
|
|
191
|
+
if (c.values !== undefined)
|
|
192
|
+
parts.push(`vs=${c.values.map(serializePrimitive).join(',')}`);
|
|
193
|
+
if (c.lower !== undefined)
|
|
194
|
+
parts.push(`l=${serializePrimitive(c.lower)}`);
|
|
195
|
+
if (c.upper !== undefined)
|
|
196
|
+
parts.push(`u=${serializePrimitive(c.upper)}`);
|
|
197
|
+
if (c.lowerOpen)
|
|
198
|
+
parts.push('lo=1');
|
|
199
|
+
if (c.upperOpen)
|
|
200
|
+
parts.push('uo=1');
|
|
201
|
+
}
|
|
202
|
+
if (descriptor.orderBy) {
|
|
203
|
+
parts.push(`o=${descriptor.orderBy.field}:${descriptor.orderBy.dir}`);
|
|
204
|
+
}
|
|
205
|
+
return parts.join('|');
|
|
206
|
+
}
|
|
207
|
+
function serializePrimitive(v) {
|
|
208
|
+
if (v instanceof Date)
|
|
209
|
+
return `d:${v.getTime()}`;
|
|
210
|
+
if (typeof v === 'string')
|
|
211
|
+
return `s:${v}`;
|
|
212
|
+
if (typeof v === 'number')
|
|
213
|
+
return `n:${v}`;
|
|
214
|
+
if (typeof v === 'boolean')
|
|
215
|
+
return `b:${v ? 1 : 0}`;
|
|
216
|
+
return `x:${String(v)}`;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Thenable result of a single-row read.
|
|
220
|
+
*
|
|
221
|
+
* Same contract as `QueryResult`, scaled down to one row:
|
|
222
|
+
* - identity is `descriptor` + `cacheKey`
|
|
223
|
+
* - `then(...)` / `load(...)` go through the engine row cache
|
|
224
|
+
* - `peekCache()` / `peekStatus()` are sync probes
|
|
225
|
+
* - `subscribe(cb)` fires on table events that touch the same rowId
|
|
226
|
+
*
|
|
227
|
+
* Returns `undefined` when the row is absent or deleted.
|
|
228
|
+
*/
|
|
229
|
+
export class RowResult {
|
|
230
|
+
constructor(descriptor, _engine) {
|
|
231
|
+
this._engine = _engine;
|
|
232
|
+
/**
|
|
233
|
+
* Memoization for the typed projection. Keyed by the raw row reference.
|
|
234
|
+
* Same input ref ⇒ same output ref — required by useSyncExternalStore
|
|
235
|
+
* consumers and by selector memoization in the React hooks.
|
|
236
|
+
*/
|
|
237
|
+
this._projectionInputRef = null;
|
|
238
|
+
this._projectionOutput = null;
|
|
239
|
+
this.descriptor = descriptor;
|
|
240
|
+
this.cacheKey = _engine
|
|
241
|
+
? _engine.getRowCacheKey(descriptor)
|
|
242
|
+
: `r=${descriptor.table}|id=${descriptor.rowId}`;
|
|
243
|
+
}
|
|
244
|
+
get metadata() {
|
|
245
|
+
return { descriptor: this.descriptor, cacheKey: this.cacheKey };
|
|
246
|
+
}
|
|
247
|
+
_project(rawRow) {
|
|
248
|
+
if (!rawRow)
|
|
249
|
+
return undefined;
|
|
250
|
+
if (this._projectionInputRef === rawRow && this._projectionOutput !== null) {
|
|
251
|
+
return this._projectionOutput;
|
|
252
|
+
}
|
|
253
|
+
const typed = rowToTyped(rawRow);
|
|
254
|
+
this._projectionInputRef = rawRow;
|
|
255
|
+
this._projectionOutput = typed;
|
|
256
|
+
return typed;
|
|
257
|
+
}
|
|
258
|
+
load(options) {
|
|
259
|
+
if (!this._engine)
|
|
260
|
+
return Promise.resolve(undefined);
|
|
261
|
+
return this._engine.loadRow(this.descriptor, options).then(r => this._project(r));
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Sync cached row, or `undefined` if the cache has no row. Pending/error
|
|
265
|
+
* snapshots may still carry the previous row for stale-while-revalidate.
|
|
266
|
+
*/
|
|
267
|
+
peekCache() {
|
|
268
|
+
if (!this._engine)
|
|
269
|
+
return undefined;
|
|
270
|
+
const snap = this._engine.readRowCache(this.descriptor);
|
|
271
|
+
return this._project(snap.row);
|
|
272
|
+
}
|
|
273
|
+
peekStatus() {
|
|
274
|
+
if (!this._engine)
|
|
275
|
+
return { status: 'empty' };
|
|
276
|
+
const snap = this._engine.readRowCache(this.descriptor);
|
|
277
|
+
return { status: snap.status, error: snap.error };
|
|
278
|
+
}
|
|
279
|
+
readForRender(policy) {
|
|
280
|
+
const cached = this.peekCache();
|
|
281
|
+
if (cached !== undefined && policy?.mode !== 'bypass-cache')
|
|
282
|
+
return cached;
|
|
283
|
+
return this.load({ bypassCache: policy?.mode === 'bypass-cache' });
|
|
284
|
+
}
|
|
285
|
+
then(onfulfilled, onrejected) {
|
|
286
|
+
return this.load().then(onfulfilled, onrejected);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Subscribe to changes that touch this row's `rowId`.
|
|
290
|
+
* Returns an unsubscribe function. Filtering done here so callers don't
|
|
291
|
+
* see unrelated table events.
|
|
292
|
+
*/
|
|
293
|
+
subscribe(cb) {
|
|
294
|
+
if (!this._engine)
|
|
295
|
+
return () => { };
|
|
296
|
+
const engine = this._engine;
|
|
297
|
+
const { table, rowId } = this.descriptor;
|
|
298
|
+
return engine.on(event => {
|
|
299
|
+
if (event.type === 'change' && event.table === table && event.rowId === rowId) {
|
|
300
|
+
cb({ type: 'change', rowId: event.rowId, row: rowToTyped(event.row) });
|
|
301
|
+
}
|
|
302
|
+
else if (event.type === 'delete' && event.table === table && event.rowId === rowId) {
|
|
303
|
+
cb({ type: 'delete', rowId: event.rowId });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
8
308
|
function rowToTyped(row) {
|
|
309
|
+
// Project payload only. _meta is engine-private; user types never see it.
|
|
9
310
|
const result = {};
|
|
10
|
-
for (const [key,
|
|
11
|
-
|
|
12
|
-
continue;
|
|
13
|
-
if (val !== null && val !== undefined && typeof val === 'object' && 'value' in val && 'hlc' in val) {
|
|
14
|
-
result[key] = val.value;
|
|
15
|
-
}
|
|
311
|
+
for (const [key, entry] of Object.entries(row.payload)) {
|
|
312
|
+
result[key] = entry.value;
|
|
16
313
|
}
|
|
17
314
|
return result;
|
|
18
315
|
}
|
|
19
316
|
/**
|
|
20
|
-
* Typed handle for a named collection within a {@link
|
|
317
|
+
* Typed handle for a named collection within a {@link Interocitor}.
|
|
21
318
|
*
|
|
22
319
|
* All reads return plain objects with CRDT metadata stripped away.
|
|
23
320
|
*
|
|
@@ -35,29 +332,88 @@ export class Table {
|
|
|
35
332
|
this.engine = engine;
|
|
36
333
|
this.name = name;
|
|
37
334
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Build a single-row handle. Same descriptor + cacheKey contract as
|
|
337
|
+
* `query()`, scaled down to one row. Lazy: nothing fetched until
|
|
338
|
+
* `then()` / `load()` / `readForRender()` is called.
|
|
339
|
+
*
|
|
340
|
+
* Use `await table.row(id)` for an async fetch, `table.row(id).peekCache()`
|
|
341
|
+
* for a sync cache read, or pass the handle directly to `useRow`.
|
|
342
|
+
*/
|
|
343
|
+
row(rowId) {
|
|
344
|
+
return new RowResult({ table: this.name, rowId }, this.engine);
|
|
42
345
|
}
|
|
43
346
|
/** Retrieve all live (non-deleted) records in this collection. */
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return rows.map(r => rowToTyped(r));
|
|
347
|
+
query() {
|
|
348
|
+
return new QueryResult({ table: this.name }, this.engine);
|
|
47
349
|
}
|
|
48
350
|
/** Build a field-scoped where query (Dexie-style, without string schema syntax). */
|
|
49
351
|
where(field) {
|
|
50
352
|
return new TableWhere(this.engine, this.name, field);
|
|
51
353
|
}
|
|
52
|
-
/**
|
|
53
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Patch a record — only the provided fields are updated.
|
|
356
|
+
* Omitted fields are untouched (CRDT per-field merge).
|
|
357
|
+
* Returns the full merged row.
|
|
358
|
+
*/
|
|
359
|
+
async patch(rowId, data, userId) {
|
|
54
360
|
const row = await this.engine.put(this.name, rowId, data, userId);
|
|
55
361
|
return rowToTyped(row);
|
|
56
362
|
}
|
|
363
|
+
/** Back-compat alias for older code paths. */
|
|
364
|
+
async put(rowId, data, userId) {
|
|
365
|
+
return this.patch(rowId, data, userId);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Replace a record — writes ALL fields in `data`, explicitly nulling any
|
|
369
|
+
* fields present in the existing row but absent from `data`.
|
|
370
|
+
* Returns the full row.
|
|
371
|
+
*/
|
|
372
|
+
async replace(rowId, data, userId) {
|
|
373
|
+
// Iterate ONLY existing payload keys. Meta is in a separate namespace
|
|
374
|
+
// and must never appear as a "field" to be nulled.
|
|
375
|
+
const existing = await this.engine.loadRow({ table: this.name, rowId });
|
|
376
|
+
const existingPayloadKeys = existing ? Object.keys(existing.payload) : [];
|
|
377
|
+
const nulled = Object.fromEntries(existingPayloadKeys.filter(k => !(k in data)).map(k => [k, null]));
|
|
378
|
+
const row = await this.engine.put(this.name, rowId, { ...nulled, ...data }, userId);
|
|
379
|
+
return rowToTyped(row);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Insert a new record with an auto-generated row ID.
|
|
383
|
+
* Returns the generated ID.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* const id = await table.add({ title: 'Buy milk' });
|
|
387
|
+
* const id = await table.add({ title: 'Buy milk' }, { prefix: 'task' });
|
|
388
|
+
*/
|
|
389
|
+
async add(data, opts, userId) {
|
|
390
|
+
const id = createRowId({ prefix: opts?.prefix });
|
|
391
|
+
await this.engine.put(this.name, id, data, userId);
|
|
392
|
+
return id;
|
|
393
|
+
}
|
|
57
394
|
/** Soft-delete a record. */
|
|
58
395
|
async delete(rowId, userId) {
|
|
59
396
|
return this.engine.delete(this.name, rowId, userId);
|
|
60
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Subscribe to changes in this table. Fires on every change/delete.
|
|
400
|
+
* Returns an unsubscribe function.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* const unsub = db.table('tasks').subscribe(event => {
|
|
404
|
+
* if (event.type === 'change') console.log(event.row);
|
|
405
|
+
* });
|
|
406
|
+
*/
|
|
407
|
+
subscribe(cb) {
|
|
408
|
+
return this.engine.on(event => {
|
|
409
|
+
if (event.type === 'change' && event.table === this.name) {
|
|
410
|
+
cb({ type: 'change', rowId: event.rowId, row: rowToTyped(event.row) });
|
|
411
|
+
}
|
|
412
|
+
else if (event.type === 'delete' && event.table === this.name) {
|
|
413
|
+
cb({ type: 'delete', rowId: event.rowId });
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
61
417
|
}
|
|
62
418
|
class TableWhere {
|
|
63
419
|
constructor(engine, table, field) {
|
|
@@ -65,12 +421,9 @@ class TableWhere {
|
|
|
65
421
|
this.table = table;
|
|
66
422
|
this.field = field;
|
|
67
423
|
}
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
...clause,
|
|
72
|
-
});
|
|
73
|
-
return rows.map(row => rowToTyped(row));
|
|
424
|
+
run(clause) {
|
|
425
|
+
const fullClause = { field: this.field, ...clause };
|
|
426
|
+
return new QueryResult({ table: this.table, clause: fullClause }, this.engine);
|
|
74
427
|
}
|
|
75
428
|
equals(value) {
|
|
76
429
|
return this.run({ op: 'equals', value });
|
|
@@ -103,4 +456,3 @@ class TableWhere {
|
|
|
103
456
|
return this.run({ op: 'anyOf', values });
|
|
104
457
|
}
|
|
105
458
|
}
|
|
106
|
-
//# sourceMappingURL=table.js.map
|