UNPKG

data

Version:

reactive data for typescript — $() wraps values, chainable operators derive views, render binds to the DOM. work proportional to the path that changed.

1,378 lines (1,374 loc) 163 kB
// utils.ts function iter(o, fn) { if (isArray(o)) { for (let i = 0; i < o.length; i++) fn(i, o[i]); } else { for (const i in o) fn(i, o[i]); } } var { isArray } = Array; var identity = (d) => d; var noop = () => { }; var left = (prop) => function bisect(a, v, lo = 0, hi = a.length) { while (lo < hi) { const mid = lo + hi >>> 1; if (prop(a[mid]) < v) lo = mid + 1; else hi = mid; } return lo; }; var right = (prop) => function bisect(a, v, lo = 0, hi = a.length) { while (lo < hi) { const mid = lo + hi >>> 1; if (prop(a[mid]) > v) hi = mid; else lo = mid + 1; } return lo; }; function bisect_right(v, lo = 0, hi = this.sorted.length) { while (lo < hi) { const mid = lo + hi >>> 1; if (this.col(this.p.value[this.sorted[mid]]) < v) hi = mid; else lo = mid + 1; } return lo; } function bisect_left(v, lo = 0, hi = this.sorted.length) { while (lo < hi) { const mid = lo + hi >>> 1; if (this.col(this.p.value[this.sorted[mid]]) < v) lo = mid + 1; else hi = mid; } return lo; } function isEmpty(obj) { for (const i in obj) return false; return true; } // core.ts var value = /* @__PURE__ */ Symbol("value"); var reactive = /* @__PURE__ */ Symbol.for("reactive"); var view = /* @__PURE__ */ Symbol("view"); var Symbols = { value, view }; var sclone = (d) => d === void 0 ? void 0 : d[view] ? d[view].value : structuredClone(d); var Operators = {}; var $ = (v) => new ViewProxy(View.value(v)); $.random = (o) => crypto.randomUUID(); var _devtoolsRoots = /* @__PURE__ */ new Set(); function createOperator(source, OperatorClass, ...args) { const p = source[view]; let op = p.some_sink((sink) => sink instanceof OperatorClass && sink.matches?.(...args) ? sink : void 0); if (!op) { op = new OperatorClass(p, ...args); p.sinks.add(new WeakRef(op)); } return new ViewProxy(op.view); } var Value = class { constructor() { this.view = new View(this); } // Entry points from ViewProxy.set / .insert(...) / deleteProperty. They // dispatch on key-path length to the correct depth-suffixed verb. Setting a // proxy to another proxy is forbidden here because the resulting cycle is // ambiguous (copy or link?) — the caller must use a linked value instead // (see LinkedView). update(value2, key) { if (value2 instanceof ViewProxy) throw new Error("cannot set value to another data, use a linked value instead"); key.length === 0 ? this.XU0(value2) : key.length === 1 ? this.BU1([key[0], value2]) : this.BU2([key, value2]); } insert(value2, key, at) { if (value2 instanceof ViewProxy) throw new Error("cannot set value to another data, use a linked value instead"); at = at === void 0 ? at : `${at}`; key.length === 0 ? this.BI0([at, value2]) : this.BI2([key, value2, at]); } remove(key) { key.length === 0 ? this.XR0() : key.length === 1 ? this.BR1([key[0]]) : this.BR2([key]); } // Idempotent: a Value already at undefined emits nothing. Returns false so // callers can short-circuit when nothing happened (used by Sink chains that // skip propagation on no-ops). XR0() { if (this.view.value === void 0) return false; const value2 = this.view.value; this.view.value = void 0; this.view.XR0(value2); } // BR1A: array-aware remove-at-name. Each name is treated as a positional // index; surviving rows shift down. The downstream BR1 carries the original // (pre-shift) name so sinks can identify which element left, but the // underlying array is already spliced by the time the View dispatches. // // Splice only if this operator owns its view.value — when the value is a // reference shared with the upstream (the common case for pass-through // operators like tap, which point view.value at p.value via XU0), // upstream has already spliced the array and re-splicing here shifts // every survivor one position further than intended. BR1A(R1) { const owns = this.view.value !== this.p?.value; const NR1 = []; for (let i = 0; i < R1.length; i++) { const name = R1[i]; const value2 = this.view.value?.[name]; if (owns) this.view.value.splice(name, 1); NR1.push(name); NR1.push(value2); } this.view.BR1(NR1); } // BR1: object remove-at-name. Routes to BR1A when the underlying value is // an array so we get splice semantics and downstream V1 propagation. Skips // already-undefined slots so a remove is a true no-op rather than emitting // a phantom event. BR1(R1) { if (isArray(this.view.value)) return this.BR1A(R1); const NR1 = []; for (let i = 0; i < R1.length; i++) { const name = R1[i]; const value2 = this.view.value?.[name]; if (value2 === void 0) continue; delete this.view.value[name]; NR1.push(name); NR1.push(value2); } this.view.BR1(NR1); } BR2(R2) { const NR2 = []; loop1: for (let i = 0; i < R2.length; i++) { const key = R2[i]; const [last, ...path] = key.slice().reverse(); let vo = this.view.value; if (typeof vo !== "object") return; while (path.length) { const n = path.pop(); if (typeof vo !== "object") continue loop1; vo = vo[n]; } if (vo[last] === void 0) continue loop1; const value2 = vo[last]; if (isArray(vo)) { vo.splice(last, 1); } else { delete vo[last]; } NR2.push(key, value2); } this.view.BR2(NR2); } // Reference-equality short-circuit: if the caller passed the same object we // already hold, skip the entire dispatch. Operators that mutate in place // and re-emit (e.g. between, sort) rely on this — they swap the live // reference for a copy first to avoid this guard suppressing real changes. XU0(value2) { if (this.view.value === value2) return; this.view.value = value2; this.view.XU0(); } // BU1 doubles as an upsert: keys whose previous value was undefined become // BI0 events, keys with an existing value become BU1, and identical values // are dropped entirely. Splitting the two avoids forcing every BU1 sink to // re-derive whether the row is new or a refresh. BU1(U1) { const NU1 = []; const NI0 = []; if (typeof this.view.value !== "object") this.view.value = {}; for (let i = 0; i < U1.length; i++) { const name = U1[i++]; const value2 = U1[i]; if (this.view.value?.[name] === value2) continue; this.view.value?.[name] === void 0 ? NI0.push(name, value2) : NU1.push(name, value2); this.view.value[name] = value2; } this.view.BU1(NU1); this.view.BI0(NI0); } // Deep update along a key path. We auto-create intermediate objects so a // user can write `proxy.a.b.c = 1` without first ensuring `a.b` exists; the // alternative would force callers to reproduce immutable-update boilerplate // for what's logically one assignment. `key.slice().reverse()` then `pop()` // is just a cheap way to walk the path forward without mutating the caller's // key array. BU2(U2) { if (typeof this.view.value !== "object") this.view.value = {}; for (let i = 0; i < U2.length; i++) { const key = U2[i++]; const value2 = U2[i]; const [last, ...path] = key.slice().reverse(); let vo = this.view.value; while (path.length) { const n = path.pop(); vo = typeof vo[n] === "object" ? vo[n] : vo[n] = {}; } if (vo[last] === value2) continue; vo[last] = value2; } this.view.BU2(U2); } // BI0: object insert. If `at` is omitted we mint a random key — this lets // `arr.insert(row)` work without the caller managing IDs. Routes to BI0A // for arrays so insert-at-position carries shift semantics. BI0(I0) { if (isArray(this.view.value)) return this.BI0A(I0); if (typeof this.view.value !== "object") this.view.value = {}; for (let i = 0; i < I0.length; i++) { const at = I0[i++] ??= "" + $.random(this.view.value); const value2 = I0[i]; if (this.view.value?.[at] === value2) continue; this.view.value[at] = value2; } this.view.BI0(I0); } // BI0A: array insert-at-position. Undefined `at` means "push to end" and // we record the resulting index back into I0 so downstream sinks know // where the row landed. Defined `at` means splice — surviving elements at // that position and beyond shift up. // // Splice only if this operator owns its view.value (same shared-ref // guard as BR1A / BMV1 — see comment on BR1A). BI0A(I0) { const owns = this.view.value !== this.p?.value; for (let i = 0; i < I0.length; i += 2) { const at = I0[i]; const value2 = I0[i + 1]; if (at === void 0) { if (owns) I0[i] = "" + (this.view.value.push(value2) - 1); else I0[i] = "" + (this.view.value.length - 1); } else if (owns) { this.view.value.splice(at, 0, value2); } } this.view.BI0(I0); } // Move-at-depth-1 verb. Each [from, to] pair moves the element at // index `from` to index `to`; rows in between rotate by one. Carried as a // single 'move' for change-stream consumers that want move semantics rather // than N value-update events. (DOMSink itself treats a move as a no-op: it // renders rows index-keyed, so Value.BMV1's positional child refresh below // already updates each slot's content — see render/index.ts BMV1.) // // Splice only if this operator owns its view.value (same shared-ref // guard as BR1A / BI0A — see comment on BR1A). BMV1(M1) { if (this.view.value !== this.p?.value) { for (let i = 0; i < M1.length; i += 2) { const from = +M1[i]; const to = +M1[i + 1]; const [v] = this.view.value.splice(from, 1); this.view.value.splice(to, 0, v); } } this.view.BMV1(M1); } BI2(I2) { if (typeof this.view.value !== "object") this.view.value = {}; for (let i = 0; i < I2.length; i++) { const key = I2[i++]; const value2 = I2[i++]; const path = key.slice().reverse(); let vo = this.view.value; while (path.length) { const n = path.pop(); vo = typeof vo[n] === "object" ? vo[n] : vo[n] = {}; } if (isArray(vo)) { if (I2[i] === void 0) I2[i] ??= "" + (vo.push(value2) - 1); else vo.splice(I2[i], 0, value2); } else { const at = I2[i] ??= "" + $.random(vo); vo[at] = value2; } } this.view.BI2(I2); } }; var Operator = class extends Value { }; var View = class _View { constructor(res) { this.res = res; this.key = []; this.sinks = /* @__PURE__ */ new Set(); this.views = /* @__PURE__ */ new Map(); this.p = void 0; this.name = void 0; this.value = void 0; } // Child views are produced lazily when ViewProxy.get sees a property access. // A child stays attached to its parent's key (so writes route correctly) but // owns its own value snapshot — kept in sync by the parent's dispatch logic // calling child.XU0() / XR0() on every notification that crosses its key. static child(p, name) { const view2 = new _View(p.res); view2.p = p; view2.key = [...p.key, name]; view2.name = name; view2.XU0(p.value?.[name]); return view2; } // Two distinct entry points unified behind one factory: $(plain) builds a // fresh Value-backed View; $(otherProxy) builds a LinkedView that forwards // every read/write to the linked source. The branch matters for set/get // semantics — see LinkedView below. static value(value2) { if (value2 instanceof ViewProxy) { return new LinkedView(value2); } else { const res = new Value(); res.XU0(value2); _devtoolsRoots.add(new WeakRef(res.view)); return res.view; } } // XR0 cascades a clear: every named child loses its value too, but only if // the corresponding key actually disappeared (the second half of the OR // covers the case where a child is currently undefined and stays that way — // we still want its sinks to know). XR0(value2) { if (this.p) this.value = void 0; this.each((name, child) => { if (child.value !== value2?.[name] || child.value !== void 0) child.XR0(value2?.[name]); }); this.sink((sink) => sink.XR0(value2, this)); } // Splice-aware fan-out for object removes. For object sources we route each // R1 to the named child as an XR0 (a single key disappeared, named children // at other keys are unaffected). For array sources we instead refresh every // child whose index ≥ the smallest removed index — those rows just got // shifted to a different value. Sinks then see either the array-aware // BR1A (with shift semantics) or BR1 (treat as named delete) depending on // what they implement; the prototype check stops a sink that inherits the // default Value.BR1A from masquerading as array-aware. BR1(R1) { if (!R1.length) return; const arr = isArray(this.value); if (!arr) { for (let i = 0; i < R1.length; i += 2) this.get_named(R1[i])?.XR0(R1[i + 1]); } else if (this.views.size) { let offset = Infinity; for (let i = 0; i < R1.length; i += 2) { if (R1[i] < offset) offset = R1[i]; if (!offset) break; } this.V1(offset); } this.fanout(arr ? "BR1A" : void 0, "BR1", R1); } BR2(R2) { for (let i = 0; i < R2.length; i++) { const [name, ...rest] = R2[i++]; const value2 = R2[i]; rest.length === 1 ? this.get_named(name)?.BR1([rest[0], value2]) : this.get_named(name)?.BR2([rest, value2]); } this.sink((sink) => sink.BR2(R2, this)); } // Whole-value replacement. For child views this means: any name still // present in the new value gets a refresh (XU0), any name that vanished // gets a clear (XR0). The `if (this.p)` re-reads our slice from the parent // because XU0 on the parent already mutated `p.value`; we just mirror it. XU0() { if (this.p) this.value = this.p.value?.[this.name]; this.each((name, child) => { if (this.value?.[name] !== void 0) child.XU0(); else { if (child.value !== void 0) child.XR0(child.value); } }); this.sink((sink) => sink.XU0(this.value, this)); } BU1(U1) { if (!U1.length) return; if (this.p) this.value = this.p.value?.[this.name]; for (let i = 0; i < U1.length; i++) this.get_named(U1[i++])?.XU0(); this.sink((sink) => sink.BU1(U1, this)); } BU2(U2) { if (this.p) this.value = this.p.value?.[this.name]; for (let i = 0; i < U2.length; i++) { const [name, ...rest] = U2[i++]; const value2 = U2[i]; rest.length === 1 ? this.get_named(name)?.BU1([rest[0], value2]) : this.get_named(name)?.BU2([rest, value2]); } this.sink((sink) => sink.BU2(U2, this)); } BI0(I0) { if (!I0.length) return; if (this.p) this.value = this.p.value?.[this.name]; if (isArray(this.value)) return this.BI0A(I0); for (let i = 0; i < I0.length; i++) this.get_named(I0[i++])?.XU0(); this.sink((sink) => sink.BI0(I0, this)); } // Array insert: every existing index ≥ the smallest insert position has // shifted up, so refresh those children once before fanning out to sinks. // The prototype check guards against a sink that only inherits the default // BI0A from Value being treated as array-aware. BI0A(I0) { if (this.views.size) { let offset = Infinity; for (let i = 0; i < I0.length; i += 2) { if (I0[i] < offset) offset = I0[i]; } this.V1(offset); } this.fanout("BI0A", "BI0", I0); } // Hole remove / hole fill — the positional-stable counterparts of BR1A/BI0A. // A sparse producer (between/intersect/union/except over an ARRAY) marks an // excluded slot `undefined` WITHOUT splicing: the array length is unchanged // and survivors do NOT shift. BR1A/BI0A would wrongly splice downstream // (ghost rows / dropped survivors — the array-positional desync). Instead the // producer emits BH1/BF0: we refresh only the touched children (no V1 shift) // and route to a sink's BH1/BF0 if it has one. A sink WITHOUT them (an // aggregate, say — position-agnostic) falls back to BR1/BI0, which is correct: // it just drops/adds the row. Operator positional sinks (RowOperator, a // downstream sparse op, sort) implement BH1/BF0 to mirror the hole instead // of shifting. The DOMSink ALSO implements them (index-keyed _remove_at/ // _create_at, see render/index.ts) so a sparse producer can be bound straight // to a row template without phantom holes — the V1 content refresh we fire // here (get_named(k).XU0()) sets the touched child's value BEFORE the sink's // BH1/BF0 runs, and because the DOMSink keys nodes by index that refresh is // not double-applied (closed ISSUES.md C4). BH1/BF0 live on View only — never // on Value — so a plain Value sink never inherits one and always takes the // BR1/BI0 fallback. BH1(R1) { if (!R1.length) return; for (let i = 0; i < R1.length; i += 2) this.get_named(R1[i])?.XU0(); this.fanout("BH1", "BR1", R1); } BF0(I0) { if (!I0.length) return; for (let i = 0; i < I0.length; i += 2) this.get_named(I0[i])?.XU0(); this.fanout("BF0", "BI0", I0); } BI2(I2) { if (this.p) this.value = this.p.value?.[this.name]; for (let i = 0; i < I2.length; ) { const [name, ...rest] = I2[i++]; const value2 = I2[i++]; const at = I2[i++]; rest.length ? this.get_named(name)?.BI2([rest, value2, at]) : this.get_named(name)?.BI0([at, value2]); } this.sink((sink) => sink.BI2(I2, this)); } // Apply a batched [from, to] rotation to named children whose key falls // inside any affected range, refreshing each from the (already moved) // parent value. Sinks that don't implement BMV1 fall back to BU1 over the // affected positions so they refresh content reactively. BMV1(M1) { if (!M1.length) return; if (this.p) this.value = this.p.value?.[this.name]; if (this.views.size) { let lo = Infinity, hi = -Infinity; for (let i = 0; i < M1.length; i += 2) { const a = +M1[i], b = +M1[i + 1]; if (a < lo) lo = a; if (b < lo) lo = b; if (a > hi) hi = a; if (b > hi) hi = b; } for (let j = lo; j <= hi; j++) { const child = this.get_named(`${j}`); if (child && child.value !== this.value[j]) child.XU0(); } } for (const x of this.sinks) { const sink = x.deref(); if (!sink) { this.sinks.delete(x); continue; } if (sink.BMV1 && sink.BMV1 !== Value.prototype.BMV1) { sink.BMV1(M1, this); } else { const NU1 = []; for (let i = 0; i < M1.length; i += 2) { const a = +M1[i], b = +M1[i + 1]; const lo = a < b ? a : b; const hi = a < b ? b : a; for (let j = lo; j <= hi; j++) NU1.push("" + j, this.value[j]); } if (NU1.length) sink.BU1(NU1, this); } } } // After an array splice every index from `offset` onward may now hold a // different element. Walk all named children in that range and refresh // those whose snapshot diverged. Off-by-one (`length+1`) intentional: a // child created at the now-empty tail needs an XU0 to clear itself. V1(offset) { for (let i = offset; i < this.value.length + 1; i++) { const child = this.get_named(`${i}`); if (child && child.value !== this.value[i]) child.XU0(); } } // Iteration helpers all double as sweepers: a WeakRef whose target was GC'd // is removed from the collection on the fly, so dead subscribers don't // accumulate. `sink(fn)` is the standard fan-out; `some_sink(fn)` is the // operator-dedup helper used by createOperator and ViewProxy.apply. some_sink(fn) { let n; for (const x of this.sinks) { const sink = x.deref?.(); if (!sink) { this.sinks.delete(x); continue; } if (n = fn(sink)) return n; } } sink(fn) { for (const x of this.sinks) { const sink = x.deref?.(); if (!sink) { this.sinks.delete(x); continue; } fn(sink); } } // Array-aware fan-out: dispatch `verb` to each sink that has its OWN // implementation, else fall back to `fallback`. The four array-positional // dispatch sites (BR1→BR1A, BI0A, BH1, BF0) collapse onto this. "Has its own" // means: for BR1A/BI0A — distinct from Value.prototype's default (Value // defines those, so a bare Value sink must NOT masquerade as array-aware); // for BH1/BF0 — merely present (Value defines neither, so `proto` is undefined // and any method counts). A sink without `verb` takes `fallback` (BR1/BI0), // which is correct for position-agnostic sinks (aggregates, length). Pass // `verb = undefined` to force the fallback (object BR1 — no array variant). // `verb`/`fallback` are constant string literals at each call site, so V8 // specializes `sink[verb]` back to a fixed-offset access after inlining. fanout(verb, fallback, payload) { const proto = verb && Value.prototype[verb]; for (const x of this.sinks) { const sink = x.deref?.(); if (!sink) { this.sinks.delete(x); continue; } const m = verb && sink[verb]; m && (proto === void 0 || m !== proto) ? m.call(sink, payload, this) : sink[fallback](payload, this); } } each(fn) { for (const [name, ref] of this.views) { const res = ref.deref?.(); if (!res) { this.views.delete(name); continue; } fn(name, res); } } get_or_create_named(name) { return this.views.get(name)?.deref?.() ?? create( this.views, name, _View.child(this, name) ); } get_named(name) { const res = this.views.get(name)?.deref?.(); if (!res) this.views.delete(name); return res; } disconnect(sink) { for (const x of this.sinks) { const s = x.deref?.(); if (s === sink) { this.sinks.delete(x); break; } if (!s) { this.sinks.delete(x); continue; } } } connect(sink) { this.sinks.add(new WeakRef(sink)); } }; var Sink = class { }; var LinkedView = class extends View { constructor(p) { super(); this.src = p[Symbols.view]; this.update(this.src); } update(value2, key = []) { if (key.length) { return this.src.res.update(value2, key); } if (value2 instanceof ViewProxy) value2 = value2[Symbols.view]; if (!(value2 instanceof View)) throw new Error("cannot set linked value to non-reactive source"); this.src.disconnect(this); this.src = value2; this.src.connect(this); this.XU0(); } insert(...args) { return this.src.res.insert(...args); } remove(...args) { return this.src.res.remove(...args); } // `value` and `res` are read-through to the source — the LinkedView itself // never holds data, it's a transparent forwarder. get value() { return this.src.value; } set value(v) { } get res() { return this; } set res(v) { } }; function iter22(arr, fn) { for (let i = 0; i < arr.length; i++) fn(arr[i++], arr[i]); } function iter3(arr, fn) { for (let i = 0; i < arr.length; i++) fn(arr[i++], arr[i++], arr[i]); } var ArrSink = class { constructor(p, arr) { this.p = p; this.arr = arr; const refs = lifetimes.get(arr) ?? /* @__PURE__ */ new Set(); refs.add(this); lifetimes.set(arr, refs); this.update([], p.value); } update = (key, value2) => this.arr.push({ type: "update", key, value: sclone(value2) }); remove = (key, value2) => this.arr.push({ type: "remove", key, value: sclone(value2) }); insert = (key, value2, at) => this.arr.push({ type: "insert", key, value: sclone(value2), at }); XU0(value2) { this.update([], value2); } BU1(U1) { iter22(U1, (name, value2) => this.update([name], value2)); } BU2(U2) { iter22(U2, (key, value2) => this.update(key, value2)); } BI0(I0) { iter22(I0, (at, value2) => this.insert([], value2, at)); } BI2(I0) { iter3(I0, (key, value2, at) => this.insert(key, value2, at)); } XR0(value2) { this.remove([], value2); } BR1(R1) { iter22(R1, (name, value2) => this.remove([name], value2)); } BR2(R2) { iter22(R2, (key, value2) => this.remove(key, value2)); } move = (from, to) => this.arr.push({ type: "move", from, to }); BMV1(M1) { iter22(M1, (from, to) => this.move(+from, +to)); } R0(value2) { this.arr.push({ type: "remove", key: [], value: sclone(value2) }); } R1(name, value2) { this.arr.push({ type: "remove", key: [name], value: sclone(value2) }); } R2(key, value2) { this.arr.push({ type: "remove", key, value: sclone(value2) }); } U0(value2) { this.arr.push({ type: "update", key: [], value: sclone(value2) }); } U1(name, value2) { this.arr.push({ type: "update", key: [name], value: sclone(value2) }); } U2(key, value2) { this.arr.push({ type: "update", key, value: sclone(value2) }); } I0(value2, at) { this.arr.push({ type: "insert", value: sclone(value2), at }); } I1(name, value2, at) { this.arr.push({ type: "insert", key: [name], value: sclone(value2), at }); } I2(key, value2, at) { this.arr.push({ type: "insert", key, value: sclone(value2), at }); } }; var lifetimes = /* @__PURE__ */ new WeakMap(); var PropSink = class extends Sink { p; obj; prop; constructor(p, obj, prop) { super(); this.p = p; this.obj = obj; this.prop = prop; this.obj[prop] = p.value; const refs = lifetimes.get(obj) ?? /* @__PURE__ */ new Set(); refs.add(this); lifetimes.set(obj, refs); } XU0(value2) { this.obj[this.prop] = value2; } XR0() { this.XU0(this.p.value); } BU1() { this.XU0(this.p.value); } BR1() { this.XU0(this.p.value); } BI0() { this.XU0(this.p.value); } BU2() { this.XU0(this.p.value); } BR2() { this.XU0(this.p.value); } BI2() { this.XU0(this.p.value); } BMV1() { this.XU0(this.p.value); } }; var FunctionSink = class extends Sink { constructor(p, obj, fn) { super(); this.fn = fn; const refs = lifetimes.get(obj) ?? /* @__PURE__ */ new Set(); refs.add(this); lifetimes.set(obj, refs); fn({ type: "update", key: [], value: sclone(p.value) }); } XU0(value2) { this.fn({ type: "update", key: [], value: sclone(value2) }); } XR0(value2) { this.fn({ type: "remove", key: [], value: sclone(value2) }); } BU1(U1) { iter22(U1, (name, value2) => this.fn({ type: "update", key: [name], value: sclone(value2) })); } BU2(U2) { iter22(U2, (key, value2) => this.fn({ type: "update", key, value: sclone(value2) })); } BI0(I0) { iter22(I0, (at, value2) => this.fn({ type: "insert", key: [], value: sclone(value2), at })); } BI2(I2) { iter3(I2, (key, value2, at) => this.fn({ type: "insert", key, value: sclone(value2), at })); } BR1(R1) { iter22(R1, (name, value2) => this.fn({ type: "remove", key: [name], value: sclone(value2) })); } BR2(R2) { iter22(R2, (key, value2) => this.fn({ type: "remove", key, value: sclone(value2) })); } BMV1(M1) { iter22(M1, (from, to) => this.fn({ type: "move", from: +from, to: +to })); } }; var ViewProxy = class _ViewProxy { view; constructor(view2) { this.view = view2; return new Proxy(noop, this); } deleteProperty(target, name) { const { res, key } = this.view; const path = name === Symbols.value ? key : [...key, "" + name]; res.remove(path); return true; } set(t, name, value2) { const { res, key } = this.view; const path = name === Symbols.value ? key : [...key, name]; res.update(value2, path); return true; } // Special-cased property reads: // Symbol.toPrimitive — used by template literals and arithmetic. `hint` // is "string" | "number" | "default"; truthy hint means string context. // Symbol.iterator — lets `for (const x of proxy)` walk numeric indices. // Symbols.reactive — branding so foreign code can detect ViewProxies. // Symbols.view — internal: the underlying View object. // Symbols.value — the raw snapshot. Reading proxy.value would create // a child view named "value" instead — that's the // canonical gotcha noted in CLAUDE.md. get(t, name) { if (name === Symbol.toPrimitive) return (hint) => hint ? this.view.value?.toString() : +this.view.value; if (name === Symbol.iterator) return this.iterator; if (name === Symbols.reactive) return true; if (name === Symbols.view) return this.view; if (name === Symbols.value) return this.view.value; return new _ViewProxy(this.view.get_or_create_named(name)); } // `proxy.filter(fn)` arrives here as: get → child view named "filter" → // apply. The child view's `name` tells us which operator to construct. // `connect`, `update`, `insert`, `remove` are handled directly without // going through the operator dispatch table. apply(t, m, args) { const { p, name: type } = this.view; if (!p) throw new Error("cannot invoke a root value!"); if (type === "then" && typeof args[0] === "function") { const [onFulfilled, onRejected] = args; try { onFulfilled(p.value); } catch (e) { if (typeof onRejected === "function") onRejected(e); } return; } if (type === "connect") return connect(p, ...args); if (type === "raf") return raf(p); if (type === "patch") { const { res, key } = p; const pairs = args[0]; if (!key.length) return res.BU1(pairs); const U2 = []; for (let i = 0; i < pairs.length; i += 2) U2.push([...key, pairs[i]], pairs[i + 1]); return res.BU2(U2); } if (type === "first") return new _ViewProxy(p.get_or_create_named(firstKey(p.value))); if (type === "last") return new _ViewProxy(p.get_or_create_named(lastKey(p.value))); const OperatorClass = Operators[type]?.(...args); if (OperatorClass) { let sink = p.some_sink((sink2) => sink2 instanceof OperatorClass && sink2.matches?.(...args) ? sink2 : void 0); if (!sink) { p.sinks.add(new WeakRef(sink = new OperatorClass(p, ...args))); } return new _ViewProxy(sink.view); } const [value2, at] = args; if (type === "remove") return this.view.res.remove(p.key); if (type === "update") return this.view.res.update(value2, p.key); if (type === "insert") return this.view.res.insert(value2, p.key, at); throw new Error(`Unknown operator '${type}'. Chainable operators (.filter, .between, .length, etc.) register when you import from 'data' (the default entry) or 'data/full' (adds JSX). You're seeing this because the dispatch table is empty \u2014 likely an import from 'data/lean' (the registration-free core). Switch to 'data', or register the operators you need onto the exported 'Operators' table yourself.`); } getPrototypeOf(target) { return _ViewProxy.prototype; } // Open-ended counter — relies on the consumer to break out (typically // `.slice()` or destructuring with a fixed length). The reactive view // doesn't know its own length without resolving `value` first. *iterator(i = 0) { while (true) { yield this[i++]; } } }; function create(views, name, res) { views.set(name, new WeakRef(res)); return res; } function connect(p, a, b) { if (isArray(a)) { const sink = new ArrSink(p, a); p.sinks.add(new WeakRef(sink)); return a; } if (typeof a === "object" && typeof b === "string") { const sink = new PropSink(p, a, b); p.sinks.add(new WeakRef(sink)); return a; } if (typeof a === "object" && typeof b === "function") { const sink = new FunctionSink(p, a, b); p.sinks.add(new WeakRef(sink)); return a; } if (typeof a === "function") throw new Error( "connect(fn) isn't supported: a bare function can't act as a sink. Use connect(anchor, fn) to receive change records (the anchor object keeps the subscription alive past GC), connect([]) to collect events into an array, or connect(obj, 'prop') to mirror the value onto a property." ); p.sinks.add(new WeakRef(a)); return a; } function firstKey(v) { if (v == null || typeof v !== "object") return "0"; if (isArray(v)) return "0"; for (const k in v) return k; return "0"; } function lastKey(v) { if (v == null || typeof v !== "object") return "0"; if (isArray(v)) return String(Math.max(0, v.length - 1)); let last = "0"; for (const k in v) last = k; return last; } function raf(p) { let pending; let scheduled = false; const schedule = (cb) => typeof globalThis.requestAnimationFrame === "function" ? globalThis.requestAnimationFrame(cb) : setTimeout(cb, 16); const writer = (v) => { pending = v; if (scheduled) return; scheduled = true; schedule(() => { if (!scheduled) return; scheduled = false; p.res.update(pending, p.key); }); }; writer.flush = () => { if (!scheduled) return; scheduled = false; p.res.update(pending, p.key); }; return writer; } // row.ts var RowOperator = class extends Operator { process() { throw new Error("not implemented, process:", this.name); } // Generic loop body shared by every BU1/BU2/BI0/BI2/BR2 entrypoint. `inc` // is the stride (2 for flat name/value, 3 for keyed insert with `at`); // `inner` distinguishes nested-key arrays (BU2/BI2/BR2 carry [key, ...] as // the first slot) from flat ones. We classify each row as upsert/insert/ // remove based on whether `process` returned a value before *and* now, then // batch the resulting deltas into a single set of downstream events. loop(C, inc, inner) { const NU1 = [], NI0 = [], NR1 = []; for (let i = 0; i < C.length; i += inc) { const name = inner ? C[i][0] : C[i]; const old_val = this.view.value?.[name]; const now_val = this.process(this.p.value[name], name, old_val); const old = old_val !== void 0; const now = now_val !== void 0; if (old && now) { NU1.push(name, now_val); this.view.value[name] = now_val; } else if (!old && now) { NI0.push(name, now_val); this.view.value[name] = now_val; } else if (old && !now) { NR1.push(name, old_val); delete this.view.value[name]; } } this.view.BU1(NU1); if (isArray(this.view.value)) { this.view.BF0(NI0); this.view.BH1(NR1); } else { this.view.BI0(NI0); this.view.BR1(NR1); } } // Whole-value reset: rebuild the snapshot from scratch. Non-object values // collapse the operator to undefined since per-row semantics don't apply // (e.g. setting the source to a primitive). Array-vs-object shape is // mirrored from the source so `for...in` iteration stays consistent. XU0(value2) { if (typeof value2 !== "object") return this.view.XU0(this.view.value = void 0); const n = isArray(value2) ? [] : {}; for (const i in value2) { const v = this.process(value2[i], i, this.view.value?.[i]); if (v !== void 0) n[i] = v; } this.view.XU0(this.view.value = n); } BU1(U1) { this.loop(U1, 2, false); } BU2(U2) { this.loop(U2, 2, true); } BI0(I0) { this.loop(I0, 2, false); } BI2(I2) { this.loop(I2, 3, true); } BR2(R2) { this.loop(R2, 2, true); } XR0() { super.XR0(); } // Removes can't be derived from `process` (the row is already gone // upstream), so this branch is a straight propagation: drop from our // snapshot and forward the delta if the row was actually held. // // Array sources need extra care: by the time BR1 fires the source has // already spliced its array, so every surviving position shifted down by // one for each removed entry below it. Our `view.value` is the same // array shape; if we don't splice in lockstep the layouts diverge, // subsequent BU2 events misclassify (read a hole, insert "new"), and // any downstream operator keying off positions (sort/za, between) gets // stale indices. So we always splice for arrays — even if our predicate // had excluded the row — and propagate a `[name, undefined]` pair so // downstream array-aware operators can apply their own shift bookkeeping. // The `value !== undefined` guard is preserved for object sources where // there's no shift to track. BR1(R1) { const isArr = isArray(this.view.value); const NR1 = []; for (let i = 0; i < R1.length; i++) { const name = R1[i++]; const value2 = this.view.value?.[name]; if (isArr) { this.view.value.splice(name, 1); NR1.push(name, value2); } else if (value2 !== void 0) { delete this.view.value[name]; NR1.push(name, value2); } } this.view.BR1(NR1); } // Array-positional insert (the array-aware counterpart of BR1). By the time // this fires the upstream has already spliced the row in at `at` — a row // rotating into a windowed sort, or a mid-array `insert(row, at)`. Our // `view.value` is the parallel array; we MUST splice in lockstep. The plain // BI0 path (loop) would instead read `view.value[at]` — the occupant the // insert displaced — as the row's "old" value, classify the insert as an // *update* of that slot, overwrite the occupant, and never shift it down: // the displaced row vanishes (the windowed-sort drop, C2). So process the // row, splice the result in at `at` (a `delete` afterwards turns an excluded // row into a proper hole, matching the rest of RowOperator's array // convention so for-in skips it), and forward a positional BI0A so our own // array-aware sinks shift too. Object upstreams never reach here — core only // routes array inserts through BI0A — so the object path is untouched. BI0A(I0) { const NI0 = []; for (let i = 0; i < I0.length; i += 2) { const at = I0[i]; const now_val = this.process(this.p.value[at], at, void 0); this.view.value.splice(at, 0, now_val); if (now_val === void 0) delete this.view.value[at]; NI0.push(at, now_val); } this.view.BI0A(NI0); } // Hole remove (counterpart of BR1, for a sparse producer that marked a slot // undefined WITHOUT splicing). The row simply left our view too: clear our // slot to a hole, keeping length and positions aligned with the upstream — do // NOT splice (that would shift survivors the producer never moved). Forward a // BH1 so our own positional sinks mirror the hole rather than shifting. BH1(R1) { const NR1 = []; for (let i = 0; i < R1.length; i++) { const name = R1[i++]; const value2 = this.view.value?.[name]; if (value2 !== void 0) { delete this.view.value[name]; NR1.push(name, value2); } } this.view.BH1(NR1); } // Hole fill (counterpart of BI0A). The producer re-admitted a row into a // previously-holed position — length unchanged, no shift. Re-run `process` // and fill our slot in place if the row passes (otherwise leave it a hole). // Forward a BF0 so downstream fills in place too. BF0(I0) { const NF0 = []; for (let i = 0; i < I0.length; i += 2) { const name = I0[i]; const now_val = this.process(this.p.value[name], name, void 0); if (now_val !== void 0) { this.view.value[name] = now_val; NF0.push(name, now_val); } } this.view.BF0(NF0); } }; // operators/filter/index.ts function get(k, r) { const p = k.concat([]); while (p.length) r = r?.[p.shift()]; return r; } function match(actual, expected) { if (typeof expected !== "object") return actual === expected; else return Object.entries(expected).every(([k, v]) => match(actual?.[k], v)); } var FilterValue = class extends RowOperator { constructor(p, fn) { super(); this.p = p; this.fn = fn; this.XU0(this.p.value); } process(value2, name, old_val) { return this.fn(value2, name, old_val) ? value2 : void 0; } }; var FilterObjectValue = class extends FilterValue { constructor(p, obj) { super(p, (r) => match(r, obj)); } }; var FilterStringValue = class extends FilterValue { constructor(p, name, value2) { super( p, value2 === void 0 ? (r) => !!r[name] : (r) => r[name] === value2 ); } }; var FilterColumnValue = class extends FilterValue { constructor(p, name, value2) { const key = [].concat(name); super( p, value2 === void 0 ? (r) => !!get(key, r) : (r) => get(key, r) === value2 ); } }; // operators/between/index.ts var BetweenValue = class extends Operator { // Dedup helper — when two charts brush over the same column with the same // bounds, share a single Between sink. matches(col, [lo, hi]) { return this.col === col && this.plo === lo && this.phi === hi; } constructor(p, col, arg) { super(); this.p = p; this.col = col; this.plo = arg[0]; this.phi = arg[1]; this.sorted = []; this.find = left((d) => { return this.p.value[d][col]; }); this.findHi = right((d) => { return this.p.value[d][col]; }); if (arg instanceof ViewProxy) { arg.connect(this, "extent"); } else { this._loSrc = arg[0] instanceof ViewProxy ? arg[0] : $(arg[0]); this._hiSrc = arg[1] instanceof ViewProxy ? arg[1] : $(arg[1]); this._loSrc.connect(this, "lo"); this._hiSrc.connect(this, "hi"); } this.XU0(p.value); } // Single-bound setters auto-sort so lo always ends up ≤ hi. This is what // keeps the resize handles working when the user drags one past the other. set lo(v) { this.extent = v > this.hi_val ? [this.hi_val, v] : [v, this.hi_val]; } set hi(v) { this.extent = v < this.lo_val ? [v, this.lo_val] : [this.lo_val, v]; } // Whole-extent setter — the hot path. Each branch handles one of the // common bound transitions: // • full domain (-∞, ∞) → unfiltered, share the source array directly // • collapsed (lo === hi) → empty result // • shrink/expand → walk sorted from the old boundary to the new one and // emit incremental BI0/BR1 events instead of resnapshotting. // The `value === p.value` check is the unfilter fast path: when we // previously aliased the source we have to fork it before mutating, or our // `value[ti] = undefined` writes would hit the user's data. set extent([a = -Infinity, b = Infinity]) { if (this.sortedDirty) this._resort(); a = +a; b = +b; const new_lo = a < b ? a : b; const new_hi = a < b ? b : a; if (!this.view.value) return [this.lo_val, this.hi_val] = [new_lo, new_hi]; if (new_lo === -Infinity && new_hi === Infinity) { this.hi_index = this.lo_index = void 0; [this.lo_val, this.hi_val] = [new_lo, new_hi]; return this.view.XU0(this.view.value = this.p.value); } if (this.view.value === this.p.value) { this.view.value = isArray(this.p.value) ? [...this.p.value] : { ...this.p.value }; } const I0 = [], R1 = []; this.lo_index ??= this.find(this.sorted, this.lo_val); this.hi_index ??= this.findHi(this.sorted, this.hi_val); let ti, tv; if (new_hi < this.hi_val) { while ((tv = this.p.value[ti = this.sorted[this.hi_index - 1]]) && tv[this.col] > new_hi) { this.hi_index--; if (this.view.value[ti] !== void 0) { R1.push(ti, tv); this.view.value[ti] = void 0; } } if (this.lo_index > this.hi_index) this.lo_index = this.hi_index; } if (new_lo > this.lo_val) { while ((tv = this.p.value[ti = this.sorted[this.lo_index]]) && tv[this.col] < new_lo) { this.lo_index++; if (this.view.value[ti] !== void 0) { R1.push(ti, tv); this.view.value[ti] = void 0; } } if (this.hi_index < this.lo_index) this.hi_index = this.lo_index; } if (new_hi > this.hi_val) { while ((tv = this.p.value[ti = this.sorted[this.hi_index]]) && tv[this.col] <= new_hi) { this.hi_index++; if (this.view.value[ti] === void 0) { I0.push(ti, tv); this.view.value[ti] = tv; } } } if (new_lo < this.lo_val) { while ((tv = this.p.value[ti = this.sorted[this.lo_index - 1]]) && tv[this.col] >= new_lo) { this.lo_index--; if (this.view.value[ti] === void 0) { I0.push(ti, tv); this.view.value[ti] = tv; } } } this.lo_val = new_lo; this.hi_val = new_hi; if (R1.length) this.isArr ? this.view.BH1(R1) : this.view.BR1(R1); if (I0.length) this.isArr ? this.view.BF0(I0) : this.view.BI0(I0); } // Whole-source replacement: rebuild `sorted` and seed `new_value` with // rows already inside the bounds. The bound indexes are wiped so the next // `extent` setter recomputes them from scratch (cheaper than tracking // them through this rebuild). XU0(value2) { const { col } = this; this.lo_index = void 0; this.hi_index = void 0; if (typeof value2 !== "object") return super.XU0(); this.isArr = isArray(value2); const new_value = this.isArr ? [] : {}; this.sorted = []; iter(value2, (i, v) => { if (v === void 0) return; this.sorted.push("" + i); if (v[col] >= this.lo_val && v[col] <= this.hi_val) new_value[i] = value2[i]; }); this.sorted.sort((a, b) => { const va = value2[a]?.[col]; const vb = value2[b]?.[col]; return va > vb ? 1 : va < vb ? -1 : 0; }); super.XU0(new_value); } // ─── Source-mutation handlers ───────────────────────────────────────────── // The bound-walk in `set extent` relies on `sorted` being current and on // `lo_index`/`hi_index` matching the bounds. These handlers keep `sorted` // synced with source mutations and invalidate the cached indexes — the // next bound change recomputes them lazily via the `??=` in `set extent`. // // Unfilter mode (view.value aliases source) is a fast path: every row is // trivially in range, so we don't need to fork or maintain membership — // we just relay the upstream verb to our sinks. _inRange(v) { return v >= this.lo_val && v <= this.hi_val; } // Membership transition for a single row whose row-value or col-value // changed. `name` may or may not currently be in `sorted`/view; we emit // BU1/BI0/BR1 based on the before/after membership and mark the sorted // index dirty. The dirty flag is honoured the next time `set extent` // runs (which is rare relative to BU2 ticks — bounds change on user // brush, attribute updates happen on every data tick) so each BU2 stays // O(1) instead of paying O(N) splice + indexOf to maintain `sorted`. _replaceRow(name, row, newCol) { const wasIn = this.view.value[name] !== void 0; const isIn = this._inRange(newCol); if (wasIn && isIn) { this.view.value[name] = row; this.view.BU1([name, row]); } else if (!wasIn && isIn) { this.view.value[name] = row; this.isArr ? this.view.BF0([name, row]) : this.view.BI0([name, row]); } else if (wasIn && !isIn) { const oldVal = this.view.value[name]; if (this.isArr) { this.view.value[name] = void 0; this.view.BH1([name, oldVal]); } else { delete this.view.value[name]; this.view.BR1([name, oldVal]); } } this.sortedDirty = true; this.lo_index = void 0; this.hi_index = void 0; } // Rebuild `sorted` from the current `p.value`. Called lazily by // `set extent` when `sortedDirty` is set — amortizes the cost of many // BU2/BU1 attribute updates into a single O(N log N) sort that fires // only when the user actually brushes new bounds. _resort() { const v = this.p.value; if (!v || typeof v !== "object") return; this.sorted = []; iter(v, (i, row) => { if (row !== void 0) this.sorted.push("" + i); }); const col = this.col; this.sorted.sort((a, b) => { const va = v[a]?.[col]; const vb = v[b]?.[col]; return va > vb ? 1 : va < vb ? -1 : 0; }); this.sortedDirty = false; } BU1(U1) { if (this.view.value === this.p.value) return this.view.BU1(U1); for (let i = 0; i < U1.length; i += 2) { const name = U1[i]; const row = U1[i + 1]; this._replaceRow(name, row, row?.[this.col]); } } BU2(U2) { if (this.view.value === this.p.value) return this.view.BU2(U2); for (let i = 0; i < U2.length; i += 2) { const key = U2[i]; const value2 = U2[i + 1]; const [name, ...rest] = key; if (rest.length && rest[0] === this.col) { const row = this.p.value?.[name]; if (row !== void 0) this._replaceRow(name, row, value2); } else if (this.view.value[name] !== void 0) { this.view.BU2([key, value2]); } } } // Insert/remove DEFER `sorted` maintenance via the same dirty-flag // amortization `_replaceRow` (BU2/BU1) already uses. `sorted` is read ONLY by // `set extent`, which calls `_resort()` when `sortedDirty` is set — so an // insert/remove only needs the membership decision (lo_val/hi_val, not // `sorted`) plus the view.value write, then marks `sorted` dirty. A stream of // inserts/removes between two brushes is therefore O(1) each (object: // dropping the O(N) indexOf+splice; array: dropping the O(N) key-shift loop // and the O(N²) batch-remove key-shift recompute) instead of O(N) per row. // The next brush pays one O(N log N) `_resort` — the births/deaths workload // (object-keyed population, frequent inserts/removes, occasional brush). For // arrays the view.value splice that mirrors the source's positional shift is //