@rindle/client 0.1.6 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/ast.d.ts +11 -0
- package/dist/ast.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +5 -0
- package/dist/operators.d.ts.map +1 -1
- package/dist/operators.js +5 -0
- package/dist/operators.js.map +1 -1
- package/dist/query.d.ts +155 -34
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +213 -8
- package/dist/query.js.map +1 -1
- package/dist/resolve.d.ts +40 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +109 -0
- package/dist/resolve.js.map +1 -0
- package/dist/store.d.ts +58 -15
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +221 -26
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +41 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts +43 -5
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +109 -31
- package/dist/view.js.map +1 -1
- package/package.json +1 -1
- package/src/ast.ts +11 -0
- package/src/index.ts +5 -0
- package/src/operators.ts +5 -0
- package/src/query.ts +505 -52
- package/src/resolve.ts +136 -0
- package/src/store.ts +227 -35
- package/src/types.ts +40 -3
- package/src/view.ts +127 -30
package/src/view.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// and untouched subtrees keep their object identity (React/Solid memoize on it).
|
|
10
10
|
|
|
11
11
|
import { compareRows } from "./compare.ts";
|
|
12
|
-
import type { ColType, FlatChange, FlatOp, ResultType, WireNode, WireSchema, WireValue } from "./types.ts";
|
|
12
|
+
import type { ColType, FlatChange, FlatOp, QueryId, ResultType, WireNode, WireSchema, WireValue } from "./types.ts";
|
|
13
13
|
|
|
14
14
|
/** Per-level column types (parallel to the WireSchema), used to JSON.parse json columns on
|
|
15
15
|
* projection. Built by the Store from the typed schema; absent ⇒ no parsing (bare values). */
|
|
@@ -22,6 +22,15 @@ export interface ViewTypes {
|
|
|
22
22
|
export interface ArrayView<R> {
|
|
23
23
|
/** The current materialized result (reference-stable where data is unchanged). */
|
|
24
24
|
readonly data: readonly R[];
|
|
25
|
+
/** The engine query id the Store assigned this view (1:1 with the view). The same `qid` the raw
|
|
26
|
+
* change stream ({@link Store.subscribeChanges}) tags each frame with, so a consumer can
|
|
27
|
+
* correlate this query with its `ChangeEvent`s (e.g. to bind a narrator) straight off
|
|
28
|
+
* `materialize(query).qid` — no separate handle needed. */
|
|
29
|
+
readonly qid: QueryId;
|
|
30
|
+
/** The query's view `WireSchema` (the position→name source for {@link resolveChange}), captured
|
|
31
|
+
* from its `hello` frame. `null` while PENDING — a remote backend's `hello` arrives async; an
|
|
32
|
+
* in-process backend (wasm/replica) populates it synchronously during `materialize`. */
|
|
33
|
+
readonly schema: WireSchema | null;
|
|
25
34
|
/** The query's SERVER-CHANNEL state (`unknown` while loading, `complete` once server-authoritative;
|
|
26
35
|
* the `error` variant is reserved and currently unproduced). A pending optimistic mutation no
|
|
27
36
|
* longer moves this — it is a separate axis (FOLDED-MUTATIONS-DESIGN §7). `complete` for backends
|
|
@@ -40,6 +49,10 @@ export interface ArrayView<R> {
|
|
|
40
49
|
export interface SingularArrayView<R> {
|
|
41
50
|
/** The single current row, or `null` when the query matches nothing. */
|
|
42
51
|
readonly data: R | null;
|
|
52
|
+
/** The engine query id — see {@link ArrayView.qid}. */
|
|
53
|
+
readonly qid: QueryId;
|
|
54
|
+
/** The query's view `WireSchema` — see {@link ArrayView.schema}. */
|
|
55
|
+
readonly schema: WireSchema | null;
|
|
43
56
|
/** The query's lifecycle state — see {@link ArrayView.resultType}. */
|
|
44
57
|
readonly resultType: ResultType;
|
|
45
58
|
/** Subscribe; fires immediately with the current row, then after each applied batch (and after
|
|
@@ -62,6 +75,14 @@ export class SingularView<R> implements SingularArrayView<R> {
|
|
|
62
75
|
return this.inner.data[0] ?? null;
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
get qid(): QueryId {
|
|
79
|
+
return this.inner.qid;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get schema(): WireSchema | null {
|
|
83
|
+
return this.inner.schema;
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
get resultType(): ResultType {
|
|
66
87
|
return this.inner.resultType;
|
|
67
88
|
}
|
|
@@ -124,12 +145,36 @@ function cloneNode(n: Node): Node {
|
|
|
124
145
|
return { row: n.row.slice(), rc: n.rc, rels: n.rels.map((r) => r.map(cloneNode)), out: null };
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
/** Reconstruct a positional {@link WireNode} (row + its populated child slots) from a maintained
|
|
149
|
+
* {@link Node} — the inverse of {@link buildNode}. Used to attach a removed subtree to a `remove`
|
|
150
|
+
* op so a change consumer (a narrator) can resolve its nested subs, which a bare positional remove
|
|
151
|
+
* (just the leaving row) cannot carry. Only in-view (`child !== null`), non-empty slots are emitted,
|
|
152
|
+
* matching how an `add` node ships only populated relationships. */
|
|
153
|
+
function toWireNode(node: Node, schema: WireSchema): WireNode {
|
|
154
|
+
const rels: { rel: number; children: WireNode[] }[] = [];
|
|
155
|
+
for (const rel of schema.relationships) {
|
|
156
|
+
if (rel.child === null) continue;
|
|
157
|
+
const childList = node.rels[rel.slot] ?? [];
|
|
158
|
+
if (childList.length === 0) continue;
|
|
159
|
+
rels.push({ rel: rel.slot, children: childList.map((c) => toWireNode(c, rel.child as WireSchema)) });
|
|
160
|
+
}
|
|
161
|
+
return { row: node.row, rels };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function rowsEqual(a: WireValue[], b: WireValue[]): boolean {
|
|
165
|
+
return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
|
|
166
|
+
}
|
|
167
|
+
|
|
127
168
|
const EMPTY: readonly never[] = Object.freeze([]);
|
|
128
169
|
|
|
129
170
|
export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
171
|
+
// The engine query id (Store-assigned, 1:1 with this view), exposed read-only via {@link qid}.
|
|
172
|
+
// `0` ⇒ unbound (a bare view never registered with a Store); the Store passes it at construction.
|
|
173
|
+
private readonly _qid: QueryId;
|
|
130
174
|
// `null` ⇒ PENDING (no schema yet): a remote backend's `hello` arrives async, so the view
|
|
131
175
|
// exists (and reads as `[]`) before its schema lands. Set by `reset` (first hello / re-hydrate).
|
|
132
|
-
|
|
176
|
+
// Exposed read-only via the {@link schema} getter (the position→name source for `resolveChange`).
|
|
177
|
+
private _schema: WireSchema | null;
|
|
133
178
|
private types?: ViewTypes;
|
|
134
179
|
// SSR first-paint seed (SSR-DESIGN.md §6): pre-projected rows installed by `seed`, returned by
|
|
135
180
|
// `data` WHILE pending (no live schema yet). Cleared on `reset` — the first live `hello` —
|
|
@@ -144,9 +189,18 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
144
189
|
private rt: ResultType = "complete";
|
|
145
190
|
private readonly listeners = new Set<(data: readonly R[]) => void>();
|
|
146
191
|
|
|
147
|
-
constructor(schema?: WireSchema, types?: ViewTypes) {
|
|
148
|
-
this.
|
|
192
|
+
constructor(schema?: WireSchema, types?: ViewTypes, qid: QueryId = 0) {
|
|
193
|
+
this._schema = schema ?? null;
|
|
149
194
|
this.types = types;
|
|
195
|
+
this._qid = qid;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get qid(): QueryId {
|
|
199
|
+
return this._qid;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get schema(): WireSchema | null {
|
|
203
|
+
return this._schema;
|
|
150
204
|
}
|
|
151
205
|
|
|
152
206
|
get resultType(): ResultType {
|
|
@@ -167,7 +221,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
167
221
|
* the caller's view reference and its subscribers survive a re-subscribe. Does NOT notify —
|
|
168
222
|
* the snapshot that follows (`applyChanges`) does, avoiding an empty-then-filled flicker. */
|
|
169
223
|
reset(schema: WireSchema, types?: ViewTypes): void {
|
|
170
|
-
this.
|
|
224
|
+
this._schema = schema;
|
|
171
225
|
this.types = types;
|
|
172
226
|
this.seeded = null; // live data takes over; the seed was first-paint only
|
|
173
227
|
this.top = [];
|
|
@@ -186,18 +240,41 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
186
240
|
|
|
187
241
|
/** Apply a batch (the hydrate snapshot or one transaction's events) in order, then
|
|
188
242
|
* notify subscribers once. Order is significant (FLAT-CHANGES-DESIGN.md §5.4). A no-op
|
|
189
|
-
* while pending (changes never precede the `hello` that resets the schema).
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
243
|
+
* while pending (changes never precede the `hello` that resets the schema).
|
|
244
|
+
*
|
|
245
|
+
* `enrichRemoves` ⇒ before a removed node is dropped, reconstruct its full subtree and attach it
|
|
246
|
+
* to the `remove` op's `node` (in place, so the same event object the Store fans out to its
|
|
247
|
+
* change subscribers carries it). Off by default — paid only when a consumer asked for it, and
|
|
248
|
+
* only on a real eviction (an rc-decrement that keeps the row leaves `node` absent).
|
|
249
|
+
*
|
|
250
|
+
* `deferNotify` ⇒ fold but do NOT notify; the caller is responsible for calling {@link flush}
|
|
251
|
+
* later. The Store uses this to fold every view in one commit before notifying any subscriber
|
|
252
|
+
* (cross-view-atomic notification — `Store.onCommitBoundary`); standalone use leaves it off, so
|
|
253
|
+
* a bare view still fires its subscribers after each applied batch.
|
|
254
|
+
*
|
|
255
|
+
* Returns whether the batch changed the view (so a deferring caller knows it must be flushed). */
|
|
256
|
+
applyChanges(events: FlatChange[], enrichRemoves = false, deferNotify = false): boolean {
|
|
257
|
+
if (this._schema === null) return false;
|
|
258
|
+
let changed = false;
|
|
259
|
+
for (const e of events) changed = this.applyAt(this.top, this._schema, e.path, e.op, 0, enrichRemoves) || changed;
|
|
260
|
+
if (!changed) return false;
|
|
193
261
|
this.dirty = true;
|
|
262
|
+
if (!deferNotify) this.notify();
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Notify subscribers with the current data. The deferred half of {@link applyChanges} (when
|
|
267
|
+
* `deferNotify` was set): the Store calls this at the commit-notify barrier — after every view
|
|
268
|
+
* touched by the same commit has folded — so a subscriber that re-reads a sibling view inside
|
|
269
|
+
* its callback observes post-commit data, never a torn mid-commit state. */
|
|
270
|
+
flush(): void {
|
|
194
271
|
this.notify();
|
|
195
272
|
}
|
|
196
273
|
|
|
197
274
|
get data(): readonly R[] {
|
|
198
|
-
if (this.
|
|
275
|
+
if (this._schema === null) return this.seeded ?? (EMPTY as readonly R[]);
|
|
199
276
|
if (!this.dirty && this.cached !== null) return this.cached;
|
|
200
|
-
this.cached = this.top.map((n) => this.project(n, this.
|
|
277
|
+
this.cached = this.top.map((n) => this.project(n, this._schema as WireSchema, this.types)) as R[];
|
|
201
278
|
this.dirty = false;
|
|
202
279
|
return this.cached;
|
|
203
280
|
}
|
|
@@ -217,41 +294,60 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
217
294
|
|
|
218
295
|
// --- apply -------------------------------------------------------------------
|
|
219
296
|
|
|
220
|
-
private applyAt(list: Node[], schema: WireSchema, path: FlatChange["path"], op: FlatOp, depth: number):
|
|
297
|
+
private applyAt(list: Node[], schema: WireSchema, path: FlatChange["path"], op: FlatOp, depth: number, enrichRemoves: boolean): boolean {
|
|
221
298
|
if (depth === path.length) {
|
|
222
|
-
this.applyOp(list, schema, op);
|
|
223
|
-
return;
|
|
299
|
+
return this.applyOp(list, schema, op, enrichRemoves);
|
|
224
300
|
}
|
|
225
301
|
const seg = path[depth];
|
|
226
302
|
const child = schema.relationships[seg.rel]?.child;
|
|
227
|
-
if (!child) return; // in-view gate: a gating slot drops the whole change
|
|
303
|
+
if (!child) return false; // in-view gate: a gating slot drops the whole change
|
|
228
304
|
const { found, index } = binarySearch(list, seg.parentRow, schema.sort);
|
|
229
305
|
if (!found) throw new Error("flat ArrayView: parent not found at path hop (inconsistent stream)");
|
|
230
306
|
const node = list[index];
|
|
231
|
-
|
|
232
|
-
|
|
307
|
+
const changed = this.applyAt(node.rels[seg.rel], child, path, op, depth + 1, enrichRemoves);
|
|
308
|
+
if (changed) node.out = null; // a descendant changed → this node must re-project its rel array
|
|
309
|
+
return changed;
|
|
233
310
|
}
|
|
234
311
|
|
|
235
|
-
private applyOp(list: Node[], schema: WireSchema, op: FlatOp):
|
|
236
|
-
if (op.tag === "add") this.applyAdd(list, schema, op.node);
|
|
237
|
-
|
|
238
|
-
|
|
312
|
+
private applyOp(list: Node[], schema: WireSchema, op: FlatOp, enrichRemoves: boolean): boolean {
|
|
313
|
+
if (op.tag === "add") return this.applyAdd(list, schema, op.node);
|
|
314
|
+
if (op.tag === "remove") {
|
|
315
|
+
const removed = this.applyRemove(list, schema, op.row);
|
|
316
|
+
// Attach the full removed subtree (in place) for an opted-in consumer; absent when the row
|
|
317
|
+
// only lost a reference (rc > 1) and so did not actually leave the result.
|
|
318
|
+
if (removed && enrichRemoves) op.node = toWireNode(removed, schema);
|
|
319
|
+
return removed !== null;
|
|
320
|
+
}
|
|
321
|
+
return this.applyEdit(list, schema, op.old, op.new);
|
|
239
322
|
}
|
|
240
323
|
|
|
241
|
-
private applyAdd(list: Node[], schema: WireSchema, wnode: WireNode):
|
|
324
|
+
private applyAdd(list: Node[], schema: WireSchema, wnode: WireNode): boolean {
|
|
242
325
|
const at = binarySearch(list, wnode.row, schema.sort);
|
|
243
|
-
if (at.found)
|
|
244
|
-
|
|
326
|
+
if (at.found) {
|
|
327
|
+
list[at.index].rc += 1; // duplicate path to an existing row; ignore subtree
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
list.splice(at.index, 0, buildNode(wnode, schema));
|
|
331
|
+
return true;
|
|
245
332
|
}
|
|
246
333
|
|
|
247
|
-
|
|
334
|
+
/** Drop one path to `row`. Returns the evicted {@link Node} (its full subtree intact) when the
|
|
335
|
+
* last path went away — `null` when another path still holds it (rc decremented, nothing left
|
|
336
|
+
* the result). */
|
|
337
|
+
private applyRemove(list: Node[], schema: WireSchema, row: WireValue[]): Node | null {
|
|
248
338
|
const at = binarySearch(list, row, schema.sort);
|
|
249
339
|
if (!at.found) throw new Error("flat ArrayView: remove of a non-existent node");
|
|
250
|
-
|
|
251
|
-
|
|
340
|
+
const node = list[at.index];
|
|
341
|
+
if (node.rc === 1) {
|
|
342
|
+
list.splice(at.index, 1);
|
|
343
|
+
return node;
|
|
344
|
+
}
|
|
345
|
+
node.rc -= 1;
|
|
346
|
+
return null;
|
|
252
347
|
}
|
|
253
348
|
|
|
254
|
-
private applyEdit(list: Node[], schema: WireSchema, oldRow: WireValue[], newRow: WireValue[]):
|
|
349
|
+
private applyEdit(list: Node[], schema: WireSchema, oldRow: WireValue[], newRow: WireValue[]): boolean {
|
|
350
|
+
if (rowsEqual(oldRow, newRow)) return false;
|
|
255
351
|
const sort = schema.sort;
|
|
256
352
|
if (compareRows(oldRow, newRow, sort) === 0) {
|
|
257
353
|
// Sort key unchanged → edit in place (keeps position + children + rc).
|
|
@@ -259,7 +355,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
259
355
|
if (!at.found) throw new Error("flat ArrayView: edit of a non-existent node");
|
|
260
356
|
list[at.index].row = newRow;
|
|
261
357
|
list[at.index].out = null;
|
|
262
|
-
return;
|
|
358
|
+
return true;
|
|
263
359
|
}
|
|
264
360
|
|
|
265
361
|
// Sort key changed → the row may move; rc may be > 1.
|
|
@@ -276,7 +372,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
276
372
|
if (oldRc === 1 && (pos === oldPos || pos - 1 === oldPos)) {
|
|
277
373
|
list[oldPos].row = newRow;
|
|
278
374
|
list[oldPos].out = null;
|
|
279
|
-
return;
|
|
375
|
+
return true;
|
|
280
376
|
}
|
|
281
377
|
|
|
282
378
|
// General move.
|
|
@@ -301,6 +397,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
|
|
|
301
397
|
oldEntry.rc = 1;
|
|
302
398
|
list.splice(adjusted, 0, oldEntry);
|
|
303
399
|
}
|
|
400
|
+
return true;
|
|
304
401
|
}
|
|
305
402
|
|
|
306
403
|
// --- projection (memoized, structurally shared) -----------------------------
|