@rindle/client 0.1.0-rc.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.
Files changed (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +65 -0
  3. package/dist/ast.d.ts +83 -0
  4. package/dist/ast.d.ts.map +1 -0
  5. package/dist/ast.js +5 -0
  6. package/dist/ast.js.map +1 -0
  7. package/dist/compare.d.ts +17 -0
  8. package/dist/compare.d.ts.map +1 -0
  9. package/dist/compare.js +81 -0
  10. package/dist/compare.js.map +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +13 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/key.d.ts +2 -0
  16. package/dist/key.d.ts.map +1 -0
  17. package/dist/key.js +26 -0
  18. package/dist/key.js.map +1 -0
  19. package/dist/operators.d.ts +39 -0
  20. package/dist/operators.d.ts.map +1 -0
  21. package/dist/operators.js +45 -0
  22. package/dist/operators.js.map +1 -0
  23. package/dist/query.d.ts +280 -0
  24. package/dist/query.d.ts.map +1 -0
  25. package/dist/query.js +348 -0
  26. package/dist/query.js.map +1 -0
  27. package/dist/schema.d.ts +111 -0
  28. package/dist/schema.d.ts.map +1 -0
  29. package/dist/schema.js +92 -0
  30. package/dist/schema.js.map +1 -0
  31. package/dist/ssr.d.ts +73 -0
  32. package/dist/ssr.d.ts.map +1 -0
  33. package/dist/ssr.js +66 -0
  34. package/dist/ssr.js.map +1 -0
  35. package/dist/store.d.ts +90 -0
  36. package/dist/store.d.ts.map +1 -0
  37. package/dist/store.js +225 -0
  38. package/dist/store.js.map +1 -0
  39. package/dist/types.d.ts +250 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +20 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/view.d.ts +88 -0
  44. package/dist/view.d.ts.map +1 -0
  45. package/dist/view.js +294 -0
  46. package/dist/view.js.map +1 -0
  47. package/package.json +36 -0
  48. package/src/ast.ts +94 -0
  49. package/src/compare.ts +85 -0
  50. package/src/index.ts +57 -0
  51. package/src/key.ts +23 -0
  52. package/src/operators.ts +68 -0
  53. package/src/query.ts +734 -0
  54. package/src/schema.ts +181 -0
  55. package/src/ssr.ts +115 -0
  56. package/src/store.ts +279 -0
  57. package/src/types.ts +234 -0
  58. package/src/view.ts +348 -0
package/dist/view.js ADDED
@@ -0,0 +1,294 @@
1
+ // The JS ArrayView — the materialized, typed result tree a backend's ChangeEvent stream
2
+ // folds into (WASM-CLIENT-DESIGN.md §7, §8). A faithful port of the View's `apply_change`
3
+ // (src/view.rs / src/flat_receiver.rs), the §6 receiver contract of FLAT-CHANGES-DESIGN.md:
4
+ // descend the path with the in-view gate, then Add / Remove / Edit (rc + the edit-move
5
+ // ghost/adjusted-position/merge protocol) located by value-driven binary search.
6
+ //
7
+ // Reads are cheap and reference-stable: each node memoizes its projected output (`out`),
8
+ // invalidated only along the root→mutation spine, so `.data` re-projects only what changed
9
+ // and untouched subtrees keep their object identity (React/Solid memoize on it).
10
+ import { compareRows } from "./compare.js";
11
+ /** Wrap a plural {@link FlatArrayView} as a {@link SingularArrayView} for a `.one()` query
12
+ * (the engine caps it to `limit = 1`, so the top list holds at most one node). */
13
+ export class SingularView {
14
+ inner;
15
+ constructor(inner) {
16
+ this.inner = inner;
17
+ }
18
+ get data() {
19
+ return this.inner.data[0] ?? null;
20
+ }
21
+ get resultType() {
22
+ return this.inner.resultType;
23
+ }
24
+ subscribe(listener) {
25
+ return this.inner.subscribe((d) => listener(d[0] ?? null));
26
+ }
27
+ destroy() {
28
+ this.inner.destroy();
29
+ }
30
+ }
31
+ function binarySearch(list, row, sort) {
32
+ let lo = 0;
33
+ let hi = list.length;
34
+ while (lo < hi) {
35
+ const mid = (lo + hi) >>> 1;
36
+ const c = compareRows(list[mid].row, row, sort);
37
+ if (c < 0)
38
+ lo = mid + 1;
39
+ else if (c > 0)
40
+ hi = mid;
41
+ else
42
+ return { found: true, index: mid };
43
+ }
44
+ return { found: false, index: lo };
45
+ }
46
+ /** Build a fresh node (rc = 1) with its inline subtree, mirroring `init_rels_for_new_entry`:
47
+ * one list per declared slot; fill in-view slots from the payload (children inserted in the
48
+ * order shipped — NEVER re-sorted — folding same-sort-key dups to rc += 1). */
49
+ function buildNode(wnode, schema) {
50
+ const rels = schema.relationships.map(() => []);
51
+ for (const { rel, children } of wnode.rels) {
52
+ const child = schema.relationships[rel]?.child;
53
+ if (!child)
54
+ continue; // join-only / gating slot — not materialized
55
+ const built = [];
56
+ for (const c of children) {
57
+ const at = binarySearch(built, c.row, child.sort);
58
+ if (at.found)
59
+ built[at.index].rc += 1;
60
+ else
61
+ built.splice(at.index, 0, buildNode(c, child));
62
+ }
63
+ rels[rel] = built;
64
+ }
65
+ return { row: wnode.row, rc: 1, rels, out: null };
66
+ }
67
+ function cloneNode(n) {
68
+ return { row: n.row.slice(), rc: n.rc, rels: n.rels.map((r) => r.map(cloneNode)), out: null };
69
+ }
70
+ const EMPTY = Object.freeze([]);
71
+ export class FlatArrayView {
72
+ // `null` ⇒ PENDING (no schema yet): a remote backend's `hello` arrives async, so the view
73
+ // exists (and reads as `[]`) before its schema lands. Set by `reset` (first hello / re-hydrate).
74
+ schema;
75
+ types;
76
+ // SSR first-paint seed (SSR-DESIGN.md §6): pre-projected rows installed by `seed`, returned by
77
+ // `data` WHILE pending (no live schema yet). Cleared on `reset` — the first live `hello` —
78
+ // so the maintained tree takes over and the live snapshot reconciles. `null` ⇒ no seed.
79
+ seeded = null;
80
+ top = [];
81
+ dirty = true;
82
+ cached = null;
83
+ // `complete` by default: a backend with no server lifecycle (the in-process engine) never pushes
84
+ // a resultType, so its views read as authoritative. The Store overrides this for backends that do
85
+ // (the optimistic backend: `unknown` until hydrated, etc.).
86
+ rt = "complete";
87
+ listeners = new Set();
88
+ constructor(schema, types) {
89
+ this.schema = schema ?? null;
90
+ this.types = types;
91
+ }
92
+ get resultType() {
93
+ return this.rt;
94
+ }
95
+ /** Set the query's lifecycle state (the Store routes the backend's per-query signal here).
96
+ * Notifies subscribers on a change so a status-bound listener (React `useQueryStatus`) re-reads,
97
+ * WITHOUT re-projecting data (it is unchanged). */
98
+ setResultType(rt) {
99
+ if (this.rt === rt)
100
+ return;
101
+ this.rt = rt;
102
+ for (const l of this.listeners)
103
+ l(this.data);
104
+ }
105
+ /** (Re)bind to a schema and clear the tree IN PLACE. The first `hello` (pending → ready)
106
+ * and a re-hydrate (gap → new epoch — FLAT-CHANGES-DESIGN.md §2.3) both go through here, so
107
+ * the caller's view reference and its subscribers survive a re-subscribe. Does NOT notify —
108
+ * the snapshot that follows (`applyChanges`) does, avoiding an empty-then-filled flicker. */
109
+ reset(schema, types) {
110
+ this.schema = schema;
111
+ this.types = types;
112
+ this.seeded = null; // live data takes over; the seed was first-paint only
113
+ this.top = [];
114
+ this.cached = null;
115
+ this.dirty = true;
116
+ }
117
+ /** Install a pre-projected SSR first-paint snapshot (SSR-DESIGN.md §6). The rows are already
118
+ * in result shape (json columns parsed, relationships nested), so a view with no live backend
119
+ * (the server one-shot Store) reads them directly, and a browser view shows them until its
120
+ * first live `hello` (`reset`) swaps in the maintained tree. Does NOT notify — it is set at
121
+ * materialize time, before any subscriber, and the live snapshot that follows notifies. */
122
+ seed(rows) {
123
+ this.seeded = rows;
124
+ }
125
+ /** Apply a batch (the hydrate snapshot or one transaction's events) in order, then
126
+ * notify subscribers once. Order is significant (FLAT-CHANGES-DESIGN.md §5.4). A no-op
127
+ * while pending (changes never precede the `hello` that resets the schema). */
128
+ applyChanges(events) {
129
+ if (this.schema === null)
130
+ return;
131
+ for (const e of events)
132
+ this.applyAt(this.top, this.schema, e.path, e.op, 0);
133
+ this.dirty = true;
134
+ this.notify();
135
+ }
136
+ get data() {
137
+ if (this.schema === null)
138
+ return this.seeded ?? EMPTY;
139
+ if (!this.dirty && this.cached !== null)
140
+ return this.cached;
141
+ this.cached = this.top.map((n) => this.project(n, this.schema, this.types));
142
+ this.dirty = false;
143
+ return this.cached;
144
+ }
145
+ subscribe(listener) {
146
+ this.listeners.add(listener);
147
+ listener(this.data);
148
+ return () => {
149
+ this.listeners.delete(listener);
150
+ };
151
+ }
152
+ destroy() {
153
+ this.listeners.clear();
154
+ this.top = [];
155
+ }
156
+ // --- apply -------------------------------------------------------------------
157
+ applyAt(list, schema, path, op, depth) {
158
+ if (depth === path.length) {
159
+ this.applyOp(list, schema, op);
160
+ return;
161
+ }
162
+ const seg = path[depth];
163
+ const child = schema.relationships[seg.rel]?.child;
164
+ if (!child)
165
+ return; // in-view gate: a gating slot drops the whole change
166
+ const { found, index } = binarySearch(list, seg.parentRow, schema.sort);
167
+ if (!found)
168
+ throw new Error("flat ArrayView: parent not found at path hop (inconsistent stream)");
169
+ const node = list[index];
170
+ node.out = null; // a descendant changed → this node must re-project its rel array
171
+ this.applyAt(node.rels[seg.rel], child, path, op, depth + 1);
172
+ }
173
+ applyOp(list, schema, op) {
174
+ if (op.tag === "add")
175
+ this.applyAdd(list, schema, op.node);
176
+ else if (op.tag === "remove")
177
+ this.applyRemove(list, schema, op.row);
178
+ else
179
+ this.applyEdit(list, schema, op.old, op.new);
180
+ }
181
+ applyAdd(list, schema, wnode) {
182
+ const at = binarySearch(list, wnode.row, schema.sort);
183
+ if (at.found)
184
+ list[at.index].rc += 1; // duplicate path to an existing row; ignore subtree
185
+ else
186
+ list.splice(at.index, 0, buildNode(wnode, schema));
187
+ }
188
+ applyRemove(list, schema, row) {
189
+ const at = binarySearch(list, row, schema.sort);
190
+ if (!at.found)
191
+ throw new Error("flat ArrayView: remove of a non-existent node");
192
+ if (list[at.index].rc === 1)
193
+ list.splice(at.index, 1);
194
+ else
195
+ list[at.index].rc -= 1;
196
+ }
197
+ applyEdit(list, schema, oldRow, newRow) {
198
+ const sort = schema.sort;
199
+ if (compareRows(oldRow, newRow, sort) === 0) {
200
+ // Sort key unchanged → edit in place (keeps position + children + rc).
201
+ const at = binarySearch(list, oldRow, sort);
202
+ if (!at.found)
203
+ throw new Error("flat ArrayView: edit of a non-existent node");
204
+ list[at.index].row = newRow;
205
+ list[at.index].out = null;
206
+ return;
207
+ }
208
+ // Sort key changed → the row may move; rc may be > 1.
209
+ const oldAt = binarySearch(list, oldRow, sort);
210
+ if (!oldAt.found)
211
+ throw new Error("flat ArrayView: edit old node does not exist");
212
+ const oldPos = oldAt.index;
213
+ const oldRc = list[oldPos].rc;
214
+ const raw = binarySearch(list, newRow, sort);
215
+ const found = raw.found;
216
+ const pos = raw.index;
217
+ const oldEntry = cloneNode(list[oldPos]); // capture (with children) BEFORE mutating
218
+ // Fast path: rc==1 and the row lands in the same slot after removal → edit in place.
219
+ if (oldRc === 1 && (pos === oldPos || pos - 1 === oldPos)) {
220
+ list[oldPos].row = newRow;
221
+ list[oldPos].out = null;
222
+ return;
223
+ }
224
+ // General move.
225
+ const newRc = oldRc - 1;
226
+ let adjusted;
227
+ if (newRc === 0) {
228
+ list.splice(oldPos, 1);
229
+ adjusted = oldPos < pos ? pos - 1 : pos;
230
+ }
231
+ else {
232
+ list[oldPos].rc = newRc; // ghost survives for the other path(s); its row is unchanged
233
+ adjusted = pos;
234
+ }
235
+ if (found) {
236
+ // Merge into the existing destination entry (keep its children); bump its rc.
237
+ const existingRc = list[adjusted].rc;
238
+ list[adjusted].row = newRow;
239
+ list[adjusted].rc = existingRc + 1;
240
+ list[adjusted].out = null;
241
+ }
242
+ else {
243
+ // Move the (edited) old entry — keeping its children — to the new position.
244
+ oldEntry.row = newRow;
245
+ oldEntry.rc = 1;
246
+ list.splice(adjusted, 0, oldEntry);
247
+ }
248
+ }
249
+ // --- projection (memoized, structurally shared) -----------------------------
250
+ project(node, schema, types) {
251
+ if (node.out !== null)
252
+ return node.out;
253
+ const obj = {};
254
+ for (let i = 0; i < schema.columns.length; i++) {
255
+ const v = node.row[i];
256
+ // A projected query carries an ABSENT cell (`undefined`, PROJECTION-SUPPORT-DESIGN.md
257
+ // §5.3) for a column it did not sync; omit it from the result object so a query never
258
+ // resolves a row from columns it did not select (§6). A present-and-null cell is `null`
259
+ // and is kept. For a `'*'` query no cell is ever `undefined`, so this is inert (§7).
260
+ if (v === undefined)
261
+ continue;
262
+ // convert-once: a json column's raw string is parsed to an object on store (cached in `out`).
263
+ obj[schema.columns[i]] = types?.columnTypes[i] === "json" && typeof v === "string" ? JSON.parse(v) : v;
264
+ }
265
+ for (const rel of schema.relationships) {
266
+ if (rel.child === null)
267
+ continue; // gating slot — not part of .data
268
+ const childList = node.rels[rel.slot] ?? [];
269
+ const ct = types?.rels[rel.slot];
270
+ if (rel.project) {
271
+ // A scalar-projected relationship aggregate (REDUCE-DESIGN.md §9): unwrap the one-row
272
+ // child to a bare scalar (its project.col cell), substituting the aggregate identity
273
+ // when empty (a childless parent). A json-typed projected column is parsed like a column.
274
+ const cell = childList.length > 0 ? childList[0].row[rel.project.col] : rel.project.identity;
275
+ const projType = ct?.columnTypes[rel.project.col];
276
+ obj[rel.name] = projType === "json" && typeof cell === "string" ? JSON.parse(cell) : cell;
277
+ continue;
278
+ }
279
+ obj[rel.name] = rel.child.singular
280
+ ? childList.length > 0
281
+ ? this.project(childList[0], rel.child, ct)
282
+ : null
283
+ : childList.map((c) => this.project(c, rel.child, ct));
284
+ }
285
+ node.out = obj;
286
+ return obj;
287
+ }
288
+ notify() {
289
+ const d = this.data;
290
+ for (const l of this.listeners)
291
+ l(d);
292
+ }
293
+ }
294
+ //# sourceMappingURL=view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view.js","sourceRoot":"","sources":["../src/view.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@rindle/client",
3
+ "version": "0.1.0-rc.5",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/rindle-sh/rindle.git",
8
+ "directory": "packages/client"
9
+ },
10
+ "type": "module",
11
+ "description": "Backend-agnostic flat-change client core: typed schema, query builder, ArrayView, comparator, Backend seam.",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "sideEffects": false,
15
+ "exports": {
16
+ ".": {
17
+ "@rindle/source": "./src/index.ts",
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "devDependencies": {
28
+ "@types/node": "^22.10.0",
29
+ "typescript": "^5.7.0"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "test": "tsc --noEmit && node --test test/*.test.ts",
34
+ "typecheck": "tsc --noEmit"
35
+ }
36
+ }
package/src/ast.ts ADDED
@@ -0,0 +1,94 @@
1
+ // The Zero-wire query AST — the exact JSON shape the Rust `Ast` deserializes
2
+ // (src/ast.rs). The builder emits these; the wasm `Db.query` (and a remote server) parse
3
+ // them. camelCase keys, `type`-tagged unions, ops as SQL-ish strings, untagged literals.
4
+
5
+ export type Dir = "asc" | "desc";
6
+
7
+ /** One `(field, direction)` ordering — serializes as the 2-tuple `["id", "asc"]`. */
8
+ export type OrderPart = [field: string, dir: Dir];
9
+
10
+ /** An untagged literal (`null | bool | number | string | array`). Numbers are one f64. */
11
+ export type LitValue = null | boolean | number | string | LitValue[];
12
+
13
+ /** A column reference or a literal — `type`-tagged. */
14
+ export type ValuePosition =
15
+ | { type: "column"; name: string }
16
+ | { type: "literal"; value: LitValue };
17
+
18
+ /** The wire comparison operators (exact strings, `src/ast.rs` `Op`). */
19
+ export type SimpleOp =
20
+ | "="
21
+ | "!="
22
+ | "<"
23
+ | "<="
24
+ | ">"
25
+ | ">="
26
+ | "IS"
27
+ | "IS NOT"
28
+ | "LIKE"
29
+ | "NOT LIKE"
30
+ | "ILIKE"
31
+ | "NOT ILIKE"
32
+ | "IN"
33
+ | "NOT IN";
34
+
35
+ export type ExistsOp = "EXISTS" | "NOT EXISTS";
36
+
37
+ /** The filter tree — `type`-tagged; recursive. There is no generic NOT node (negation is
38
+ * via the negated operators and `NOT EXISTS`, matching `src/ast.rs` `Condition`). */
39
+ export type Condition =
40
+ | { type: "simple"; op: SimpleOp; left: ValuePosition; right: ValuePosition }
41
+ | { type: "and"; conditions: Condition[] }
42
+ | { type: "or"; conditions: Condition[] }
43
+ | { type: "correlatedSubquery"; related: CorrelatedSubquery; op: ExistsOp; flip?: boolean; scalar?: boolean };
44
+
45
+ export interface Correlation {
46
+ parentField: string[];
47
+ childField: string[];
48
+ }
49
+
50
+ /** A paging lower bound (`Bound`, `src/ast.rs`). `row` is a *partial* wire row — the bound
51
+ * columns by name (only the sort columns are read by the engine's `Skip` comparator).
52
+ * `exclusive` ⇒ start *after* the bound row (`Basis::After`); else *at* it (`Basis::At`). */
53
+ export interface Bound {
54
+ row: Record<string, LitValue>;
55
+ exclusive: boolean;
56
+ }
57
+
58
+ export interface CorrelatedSubquery {
59
+ correlation: Correlation;
60
+ subquery: Ast;
61
+ system?: "client" | "permissions" | "test";
62
+ }
63
+
64
+ /** An aggregate over a (correlated) subquery's rows (`REDUCE-DESIGN.md`). v1: `count(*)`.
65
+ * Set on a `related` subquery (`Ast.aggregate`), it marks that relationship a count
66
+ * aggregate the builder lowers to a scalar-projected singular relationship (§9). */
67
+ export type Aggregate = "count";
68
+
69
+ /** The query AST. `table` is the only required field; the builder omits empty/false fields
70
+ * (matching the Rust `skip_serializing_if`), which the deserializer treats as absent. */
71
+ export interface Ast {
72
+ table: string;
73
+ alias?: string;
74
+ /** Projection (PROJECTION-SUPPORT-DESIGN.md §6). Absent ⇒ select all columns; present ⇒
75
+ * project to just these (drives what syncs + what the view reports). Serializes only when
76
+ * set, matching the Rust `Ast.select`'s `skip_serializing_if`. */
77
+ select?: string[];
78
+ where?: Condition;
79
+ related?: CorrelatedSubquery[];
80
+ start?: Bound;
81
+ limit?: number;
82
+ one?: boolean;
83
+ /** Aggregate this (sub)query's rows instead of materializing them (`REDUCE-DESIGN.md`
84
+ * §9). `count` on a `related` subquery surfaces a scalar `commentCount` field; the
85
+ * builder lowers it to a scalar-projected singular relationship. Absent ⇒ ordinary rows. */
86
+ aggregate?: Aggregate;
87
+ /** The {@link Ast.aggregate} value is **precomputed** — supplied as rows of a (synthetic)
88
+ * source table rather than reduced from child rows. Set by the normalized client's AST
89
+ * rewrite (`AGGREGATE-SYNC-DESIGN.md` §3.3) so the local engine reads the server's count
90
+ * with a plain singular join + the same projection instead of a `reduce`. Only meaningful
91
+ * with `aggregate`; absent ⇒ `false`. */
92
+ aggregatePrecomputed?: boolean;
93
+ orderBy?: OrderPart[];
94
+ }
package/src/compare.ts ADDED
@@ -0,0 +1,85 @@
1
+ // The comparator — a faithful port of the engine's `compare_values` / `compare_rows`
2
+ // (src/value.rs §4 of FLAT-CHANGES-DESIGN.md), the single most correctness-critical unit
3
+ // (WASM-CLIENT-DESIGN.md §8.1). One fixed total order, NO collation.
4
+ //
5
+ // It is **value-driven** — it switches on the runtime JS type, exactly as the engine
6
+ // switches on the `OwnedValue` variant. Bare wire values collapse Int/Float → number and
7
+ // Str/Json → string, but within a sort column values are homogeneous, and number→total_cmp
8
+ // / string→bytewise cover both, so this matches the engine without needing column types:
9
+ //
10
+ // - null sorts FIRST (null < anything; null == null)
11
+ // - number: IEEE-754 totalOrder (=== Rust `f64::total_cmp`) — NOT `<` / `-`
12
+ // - boolean: false < true
13
+ // - string: UTF-8 BYTEWISE (=== SQLite BINARY / Rust `&str` Ord) — NOT `localeCompare`/`<`
14
+
15
+ import type { WireValue } from "./types.ts";
16
+
17
+ const enc = new TextEncoder();
18
+
19
+ /** The `compare_values` / `compare_rows` algorithm-contract version (=== the engine's
20
+ * `wire_schema::COMPARATOR_VERSION`). A remote subscriber hard-rejects a `hello` whose
21
+ * `comparatorVersion` differs — the total order is a code contract, not data, so a schema
22
+ * fingerprint can't cover it. Bump in lockstep with the Rust constant if the order changes. */
23
+ export const COMPARATOR_VERSION = 1;
24
+
25
+ const SIGN = 0x8000000000000000n;
26
+ const ALL = 0xffffffffffffffffn;
27
+
28
+ /** Map an f64 to an unsigned 64-bit key whose unsigned order is IEEE-754 totalOrder
29
+ * (=== Rust `f64::total_cmp`): flip all bits for negatives (incl. -0 / -NaN), else set
30
+ * the sign bit. */
31
+ function orderedKey(x: number): bigint {
32
+ const dv = new DataView(new ArrayBuffer(8));
33
+ dv.setFloat64(0, x);
34
+ const bits = dv.getBigUint64(0);
35
+ return bits & SIGN ? bits ^ ALL : bits | SIGN;
36
+ }
37
+
38
+ /** number compare with `f64::total_cmp` semantics (NaN deterministic & last; -0 < +0). */
39
+ export function compareNumber(a: number, b: number): -1 | 0 | 1 {
40
+ const ka = orderedKey(a);
41
+ const kb = orderedKey(b);
42
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
43
+ }
44
+
45
+ /** UTF-8 bytewise string compare (=== SQLite `BINARY` / Rust `&str` Ord). Correct for
46
+ * supplementary-plane code points, where JS `<` / `localeCompare` (UTF-16) would disagree. */
47
+ export function compareString(a: string, b: string): -1 | 0 | 1 {
48
+ const ba = enc.encode(a);
49
+ const bb = enc.encode(b);
50
+ const n = Math.min(ba.length, bb.length);
51
+ for (let i = 0; i < n; i++) {
52
+ if (ba[i] !== bb[i]) return ba[i] < bb[i] ? -1 : 1;
53
+ }
54
+ return ba.length === bb.length ? 0 : ba.length < bb.length ? -1 : 1;
55
+ }
56
+
57
+ /** Compare two bare cells, dispatching on the runtime type (null sorts first). */
58
+ export function compareValue(a: WireValue, b: WireValue): -1 | 0 | 1 {
59
+ const an = a === null || a === undefined;
60
+ const bn = b === null || b === undefined;
61
+ if (an && bn) return 0;
62
+ if (an) return -1;
63
+ if (bn) return 1;
64
+ switch (typeof a) {
65
+ case "number":
66
+ return compareNumber(a, b as number);
67
+ case "boolean":
68
+ return a === b ? 0 : a ? 1 : -1;
69
+ case "string":
70
+ return compareString(a, b as string);
71
+ default:
72
+ // A parsed-JSON object in a sort column (rare): compare its text bytewise.
73
+ return compareString(JSON.stringify(a), JSON.stringify(b));
74
+ }
75
+ }
76
+
77
+ /** Compare two rows by a resolved sort (`[columnIndex, ascending]` pairs). First non-equal
78
+ * column wins; a descending column negates. */
79
+ export function compareRows(a: WireValue[], b: WireValue[], sort: [number, boolean][]): -1 | 0 | 1 {
80
+ for (const [col, asc] of sort) {
81
+ const c = compareValue(a[col], b[col]);
82
+ if (c !== 0) return asc ? c : ((-c) as -1 | 1);
83
+ }
84
+ return 0;
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ // @rindle/client — the backend-agnostic flat-change client core: typed schema, query builder,
2
+ // comparator, Backend seam, and the ArrayView contract. (The ArrayView folding impl + the
3
+ // Store/backend wiring land in later slices.)
4
+
5
+ export * from "./schema.ts"; // table/columns/primaryKey, createSchema, string/number/boolean/json, Col, RowOf, Row, TableDef, tableSpec, SCHEMA
6
+ export * from "./operators.ts"; // eq/ne/gt/ge/lt/le/like/notLike/ilike/notIlike/inList/notInList/is/isNot, and/or, Pred, Cond, Arg
7
+ export * from "./query.ts"; // Query, QueryRoot, queries/newQueryBuilder, defineQuery/NamedQuery, exists, notExists, defineFragment/Fragment/FragmentRef/isFragment
8
+ export * from "./view.ts"; // ArrayView, SingularArrayView, FlatArrayView, SingularView, ViewTypes
9
+ export * from "./store.ts"; // Store, WriteTx, AssembledNode, DehydratedQuery, DehydratedState
10
+ export * from "./ssr.ts"; // createServerStore, ServerStore, OneShotBackend, OneShotQueryFn, OneShotResult, ServerStoreOptions
11
+
12
+ export { stableKey } from "./key.ts"; // canonical viewKey for an AST (shared with @rindle/react)
13
+
14
+ export { COMPARATOR_VERSION, compareNumber, compareRows, compareString, compareValue } from "./compare.ts";
15
+
16
+ export type {
17
+ Backend,
18
+ ChangeEvent,
19
+ ColType,
20
+ FlatChange,
21
+ FlatOp,
22
+ Mutation,
23
+ MutationEnvelope,
24
+ NormalizedEvent,
25
+ NormalizedOp,
26
+ NormalizedSource,
27
+ NormalizedTableSchema,
28
+ OptimisticSource,
29
+ PathSeg,
30
+ ProgressFrame,
31
+ QueryId,
32
+ RemoteQuery,
33
+ ResultType,
34
+ WireNode,
35
+ WireProjection,
36
+ WireRel,
37
+ WireSchema,
38
+ WireValue,
39
+ } from "./types.ts";
40
+
41
+ export { CLIENT_MUTATIONS_SCHEMA, CLIENT_MUTATIONS_TABLE, LMID_QUERY_NAME } from "./types.ts";
42
+
43
+ // The Zero-wire AST types (for backends / advanced use).
44
+ export type {
45
+ Aggregate,
46
+ Ast,
47
+ Bound,
48
+ Condition,
49
+ CorrelatedSubquery,
50
+ Correlation,
51
+ Dir,
52
+ ExistsOp,
53
+ LitValue,
54
+ OrderPart,
55
+ SimpleOp,
56
+ ValuePosition,
57
+ } from "./ast.ts";
package/src/key.ts ADDED
@@ -0,0 +1,23 @@
1
+ // A stable, canonical JSON string for a query AST — the `viewKey` shared by the React cache
2
+ // (one cached view per AST) and the SSR dehydrate/hydrate map (seed lookup by AST). Both sides
3
+ // MUST agree byte-for-byte, so the one canonical serializer lives here. Object keys are sorted;
4
+ // `undefined` is encoded distinctly (so `{a:undefined}` ≠ `{}`); cycles throw.
5
+
6
+ export function stableKey(value: unknown, seen = new WeakSet<object>()): string {
7
+ if (value === undefined) return '{"$undefined":true}';
8
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
9
+ if (seen.has(value)) throw new TypeError("Rindle query keys must be acyclic JSON values");
10
+ seen.add(value);
11
+ if (Array.isArray(value)) {
12
+ const out = `[${value.map((v) => stableKey(v, seen)).join(",")}]`;
13
+ seen.delete(value);
14
+ return out;
15
+ }
16
+ const obj = value as Record<string, unknown>;
17
+ const out = `{${Object.keys(obj)
18
+ .sort()
19
+ .map((k) => `${JSON.stringify(k)}:${stableKey(obj[k], seen)}`)
20
+ .join(",")}}`;
21
+ seen.delete(value);
22
+ return out;
23
+ }