@rindle/client 0.1.5 → 0.2.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/src/types.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  // Cells cross the boundary BARE (the host has a typed schema, so per-column types are
4
4
  // known here, not per-cell). JSON columns cross as their raw JSON string.
5
5
 
6
+ import type { Ast } from "./ast.ts";
7
+
6
8
  /** A bare wire cell. (JSON columns arrive as their raw JSON string.) */
7
9
  export type WireValue = number | string | boolean | null;
8
10
 
@@ -143,8 +145,9 @@ export interface Backend {
143
145
  unregisterQuery(queryId: QueryId): void;
144
146
  /** Optional split path for local-first backends: retain/release a named remote footprint
145
147
  * without creating another local materialized view. `localQueryId`, when provided, names
146
- * the local AST view this remote footprint feeds. */
147
- retainRemoteQuery?(queryId: QueryId, remote: RemoteQuery, localQueryId?: QueryId): void;
148
+ * the local AST view this remote footprint feeds. `ast`, when provided, gives the backend
149
+ * the local schema context for sync-only retains that still need aggregate/table setup. */
150
+ retainRemoteQuery?(queryId: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast): void;
148
151
  releaseRemoteQuery?(queryId: QueryId): void;
149
152
  /** Local: applies now (changes flow back on the stream). Remote: sends to the server. */
150
153
  mutate(mutations: Mutation[]): Promise<void>;
package/src/view.ts CHANGED
@@ -124,6 +124,10 @@ function cloneNode(n: Node): Node {
124
124
  return { row: n.row.slice(), rc: n.rc, rels: n.rels.map((r) => r.map(cloneNode)), out: null };
125
125
  }
126
126
 
127
+ function rowsEqual(a: WireValue[], b: WireValue[]): boolean {
128
+ return a.length === b.length && a.every((v, i) => Object.is(v, b[i]));
129
+ }
130
+
127
131
  const EMPTY: readonly never[] = Object.freeze([]);
128
132
 
129
133
  export class FlatArrayView<R = unknown> implements ArrayView<R> {
@@ -189,7 +193,9 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
189
193
  * while pending (changes never precede the `hello` that resets the schema). */
190
194
  applyChanges(events: FlatChange[]): void {
191
195
  if (this.schema === null) return;
192
- for (const e of events) this.applyAt(this.top, this.schema, e.path, e.op, 0);
196
+ let changed = false;
197
+ for (const e of events) changed = this.applyAt(this.top, this.schema, e.path, e.op, 0) || changed;
198
+ if (!changed) return;
193
199
  this.dirty = true;
194
200
  this.notify();
195
201
  }
@@ -217,41 +223,50 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
217
223
 
218
224
  // --- apply -------------------------------------------------------------------
219
225
 
220
- private applyAt(list: Node[], schema: WireSchema, path: FlatChange["path"], op: FlatOp, depth: number): void {
226
+ private applyAt(list: Node[], schema: WireSchema, path: FlatChange["path"], op: FlatOp, depth: number): boolean {
221
227
  if (depth === path.length) {
222
- this.applyOp(list, schema, op);
223
- return;
228
+ return this.applyOp(list, schema, op);
224
229
  }
225
230
  const seg = path[depth];
226
231
  const child = schema.relationships[seg.rel]?.child;
227
- if (!child) return; // in-view gate: a gating slot drops the whole change
232
+ if (!child) return false; // in-view gate: a gating slot drops the whole change
228
233
  const { found, index } = binarySearch(list, seg.parentRow, schema.sort);
229
234
  if (!found) throw new Error("flat ArrayView: parent not found at path hop (inconsistent stream)");
230
235
  const node = list[index];
231
- node.out = null; // a descendant changed this node must re-project its rel array
232
- this.applyAt(node.rels[seg.rel], child, path, op, depth + 1);
236
+ const changed = this.applyAt(node.rels[seg.rel], child, path, op, depth + 1);
237
+ if (changed) node.out = null; // a descendant changed → this node must re-project its rel array
238
+ return changed;
233
239
  }
234
240
 
235
- private applyOp(list: Node[], schema: WireSchema, op: FlatOp): void {
236
- if (op.tag === "add") this.applyAdd(list, schema, op.node);
237
- else if (op.tag === "remove") this.applyRemove(list, schema, op.row);
238
- else this.applyEdit(list, schema, op.old, op.new);
241
+ private applyOp(list: Node[], schema: WireSchema, op: FlatOp): boolean {
242
+ if (op.tag === "add") return this.applyAdd(list, schema, op.node);
243
+ if (op.tag === "remove") return this.applyRemove(list, schema, op.row);
244
+ return this.applyEdit(list, schema, op.old, op.new);
239
245
  }
240
246
 
241
- private applyAdd(list: Node[], schema: WireSchema, wnode: WireNode): void {
247
+ private applyAdd(list: Node[], schema: WireSchema, wnode: WireNode): boolean {
242
248
  const at = binarySearch(list, wnode.row, schema.sort);
243
- if (at.found) list[at.index].rc += 1; // duplicate path to an existing row; ignore subtree
244
- else list.splice(at.index, 0, buildNode(wnode, schema));
249
+ if (at.found) {
250
+ list[at.index].rc += 1; // duplicate path to an existing row; ignore subtree
251
+ return false;
252
+ }
253
+ list.splice(at.index, 0, buildNode(wnode, schema));
254
+ return true;
245
255
  }
246
256
 
247
- private applyRemove(list: Node[], schema: WireSchema, row: WireValue[]): void {
257
+ private applyRemove(list: Node[], schema: WireSchema, row: WireValue[]): boolean {
248
258
  const at = binarySearch(list, row, schema.sort);
249
259
  if (!at.found) throw new Error("flat ArrayView: remove of a non-existent node");
250
- if (list[at.index].rc === 1) list.splice(at.index, 1);
251
- else list[at.index].rc -= 1;
260
+ if (list[at.index].rc === 1) {
261
+ list.splice(at.index, 1);
262
+ return true;
263
+ }
264
+ list[at.index].rc -= 1;
265
+ return false;
252
266
  }
253
267
 
254
- private applyEdit(list: Node[], schema: WireSchema, oldRow: WireValue[], newRow: WireValue[]): void {
268
+ private applyEdit(list: Node[], schema: WireSchema, oldRow: WireValue[], newRow: WireValue[]): boolean {
269
+ if (rowsEqual(oldRow, newRow)) return false;
255
270
  const sort = schema.sort;
256
271
  if (compareRows(oldRow, newRow, sort) === 0) {
257
272
  // Sort key unchanged → edit in place (keeps position + children + rc).
@@ -259,7 +274,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
259
274
  if (!at.found) throw new Error("flat ArrayView: edit of a non-existent node");
260
275
  list[at.index].row = newRow;
261
276
  list[at.index].out = null;
262
- return;
277
+ return true;
263
278
  }
264
279
 
265
280
  // Sort key changed → the row may move; rc may be > 1.
@@ -276,7 +291,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
276
291
  if (oldRc === 1 && (pos === oldPos || pos - 1 === oldPos)) {
277
292
  list[oldPos].row = newRow;
278
293
  list[oldPos].out = null;
279
- return;
294
+ return true;
280
295
  }
281
296
 
282
297
  // General move.
@@ -301,6 +316,7 @@ export class FlatArrayView<R = unknown> implements ArrayView<R> {
301
316
  oldEntry.rc = 1;
302
317
  list.splice(adjusted, 0, oldEntry);
303
318
  }
319
+ return true;
304
320
  }
305
321
 
306
322
  // --- projection (memoized, structurally shared) -----------------------------