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
JavaScript
// 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
//