API index and search · Build metadata
Source snapshot
packages/client/src/compare.ts
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