Rindle

API index and search · Build metadata

Source snapshot

packages/client/src/compare.ts

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.
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 unit3// (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 engine6// switches on the `OwnedValue` variant. Bare wire values collapse Int/Float → number and7// Str/Json → string, but within a sort column values are homogeneous, and number→total_cmp8// / 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 < true13//   - string:  UTF-8 BYTEWISE (=== SQLite BINARY / Rust `&str` Ord) — NOT `localeCompare`/`<`1415import type { WireValue } from "./types.ts";1617const enc = new TextEncoder();1819/** The `compare_values` / `compare_rows` algorithm-contract version (=== the engine's20 *  `wire_schema::COMPARATOR_VERSION`). A remote subscriber hard-rejects a `hello` whose21 *  `comparatorVersion` differs — the total order is a code contract, not data, so a schema22 *  fingerprint can't cover it. Bump in lockstep with the Rust constant if the order changes.23 *24 *  v2 (design 226 Stage B): the engine's mixed Int/Float comparison became exact instead25 *  of f64-widening. No TS behavior change — every value a client can hold today is an f6426 *  `number`, on which v1 and v2 order identically (the §8 gate keeps int64 cells out of27 *  the browser until Stage E) — but the contract the version names is the engine's. */28export const COMPARATOR_VERSION = 2;2930const SIGN = 0x8000000000000000n;31const ALL = 0xffffffffffffffffn;3233/** Map an f64 to an unsigned 64-bit key whose unsigned order is IEEE-754 totalOrder34 *  (=== Rust `f64::total_cmp`): flip all bits for negatives (incl. -0 / -NaN), else set35 *  the sign bit. */36function orderedKey(x: number): bigint {37  const dv = new DataView(new ArrayBuffer(8));38  dv.setFloat64(0, x);39  const bits = dv.getBigUint64(0);40  return bits & SIGN ? bits ^ ALL : bits | SIGN;41}4243/** number compare with `f64::total_cmp` semantics (NaN deterministic & last; -0 < +0). */44export function compareNumber(a: number, b: number): -1 | 0 | 1 {45  const ka = orderedKey(a);46  const kb = orderedKey(b);47  return ka < kb ? -1 : ka > kb ? 1 : 0;48}4950/** UTF-8 bytewise string compare (=== SQLite `BINARY` / Rust `&str` Ord). Correct for51 *  supplementary-plane code points, where JS `<` / `localeCompare` (UTF-16) would disagree. */52export function compareString(a: string, b: string): -1 | 0 | 1 {53  const ba = enc.encode(a);54  const bb = enc.encode(b);55  const n = Math.min(ba.length, bb.length);56  for (let i = 0; i < n; i++) {57    if (ba[i] !== bb[i]) return ba[i] < bb[i] ? -1 : 1;58  }59  return ba.length === bb.length ? 0 : ba.length < bb.length ? -1 : 1;60}6162/** Compare two bare cells, dispatching on the runtime type (null sorts first). */63export function compareValue(a: WireValue, b: WireValue): -1 | 0 | 1 {64  const an = a === null || a === undefined;65  const bn = b === null || b === undefined;66  if (an && bn) return 0;67  if (an) return -1;68  if (bn) return 1;69  switch (typeof a) {70    case "number":71      return compareNumber(a, b as number);72    case "boolean":73      return a === b ? 0 : a ? 1 : -1;74    case "string":75      return compareString(a, b as string);76    default:77      // A parsed-JSON object in a sort column (rare): compare its text bytewise.78      return compareString(JSON.stringify(a), JSON.stringify(b));79  }80}8182/** Compare two rows by a resolved sort (`[columnIndex, ascending]` pairs). First non-equal83 *  column wins; a descending column negates. */84export function compareRows(a: WireValue[], b: WireValue[], sort: [number, boolean][]): -1 | 0 | 1 {85  for (const [col, asc] of sort) {86    const c = compareValue(a[col], b[col]);87    if (c !== 0) return asc ? c : ((-c) as -1 | 1);88  }89  return 0;90}91