data
Version:
reactive data for typescript — $() wraps values, chainable operators derive views, render binds to the DOM. work proportional to the path that changed.
231 lines (228 loc) • 9.81 kB
TypeScript
/**
* Symbol key for reading/writing a proxy's raw underlying value.
*
* Use `proxy[value]` to read the snapshot and `proxy[value] = next` to replace
* it. Read with the symbol, **not** `proxy.value` — any string-named access on a
* ViewProxy creates a child view, so `proxy.value` would make a child named
* "value" rather than returning the data.
*
* @example
* import { $, value } from 'data'
* const n = $(41)
* n[value] // 41
* n[value] = 42 // now 42
*/
declare const value: unique symbol;
declare const reactive: unique symbol;
declare const view: unique symbol;
/**
* Operator dispatch table: maps an operator name to a factory that picks the
* implementation class by argument shape. Populated by the default `data`
* entry (and `data/full`) on import; the `data/lean` entry leaves it empty.
* Register onto it directly only if you import `data/lean` and want a
* hand-picked subset of operators: `Operators['filter'] = () => FilterValue`.
*/
declare const Operators: Record<string, (...args: any[]) => any>;
/**
* Wrap a value or collection in a reactive `ViewProxy`.
*
* Read the raw value with `proxy[value]` (the {@link value} symbol). Mutate by
* assignment — `proxy.foo = 1`, `proxy[0].done = true`, `delete proxy[1]`,
* `proxy[value] = next` — including nested paths; the right update cascade
* fires automatically. Derive reactive views with chainable operators
* (`filter`, `between`, `map`, `length`, `sum`, …), which are registered when
* you import from `data` or `data/full`.
*
* @example
* import { $, value } from 'data'
* const rows = $([{ n: 1 }, { n: 5 }, { n: 9 }])
* const big = rows.filter(d => d.n > 3).length()
* big[value] // 2
* rows[0].n = 10 // views update incrementally
*/
declare const $: {
<T>(v: T): Data<T>;
random(o: any): string | number;
};
/**
* Low-level escape hatch for building a derived view from a custom `Operator`
* subclass without going through the named dispatch table — `createOperator(src,
* MyOperatorClass, ...args)`. Most code should use the chainable operators
* (`src.filter(...)`) instead; reach for this only when authoring a new
* operator or wiring one that isn't registered. See operators/README.md.
*/
declare function createOperator(source: any, OperatorClass: any, ...args: any[]): ViewProxy;
type RowOf<T> = T extends Record<any, infer R> ? R : never;
type ChangeRecord = {
type: 'update' | 'insert' | 'remove';
key: string[];
value: any;
at?: any;
};
type Data<T = any> = {
[k in keyof T]: Data<T[k]>;
} & {
[value]?: T;
/**
* Subscribe to this view. Three forms:
*
* - `connect([])` — pushes each change record `{ type, key, value, at? }` into
* the array and returns it. Best for tests and seeing what flows through.
* - `connect(anchor, 'prop')` — mirrors the current value onto `anchor.prop`
* (e.g. `connect(el, 'textContent')`); returns `anchor`.
* - `connect(anchor, fn)` — calls `fn(change)` on every event. `anchor` is the
* lifetime handle: sinks are held weakly, so the subscription lives only as
* long as `anchor` is reachable.
*
* There is **no single-argument `connect(fn)` form** — a lone function is
* attached as a raw sink with no initial emit and throws on the first insert.
* Use the two-argument `connect(anchor, fn)`.
*
* @example
* const events = rows.filter('done', false).length().connect([])
* count.connect(document.body, 'textContent')
* rows.connect(controller, change => redraw())
*/
connect<A extends any[]>(events: A): A;
connect<O extends object>(anchor: O, prop: string): O;
connect<O extends object>(anchor: O, fn: (change: ChangeRecord) => void): O;
/**
* @deprecated `connect(fn)` (a lone function) is **not** a supported form — it
* attaches with no initial emit and throws on the first insert. Use the
* two-argument `connect(anchor, fn)` instead.
*/
connect(fn: (change: ChangeRecord) => void): never;
raf(): ((value: T) => void) & {
flush(): void;
};
first(): Data<RowOf<T>>;
last(): Data<RowOf<T>>;
update(value: T): undefined;
update(value: any, key: string[]): undefined;
insert(value: RowOf<T>): undefined;
insert(value: any, key: string[]): undefined;
remove(key?: string[]): undefined;
/**
* Rows matching a predicate. Four shapes: a `(row, key) => boolean` function,
* a `key, value` pair, a `string[]` path + value, or a partial-shape object.
* The predicate is captured once — it re-runs when a row mutates, not when
* outside state changes; for a reactive predicate derive a view and chain
* `between`/`intersect`.
* @example rows.filter(d => d.active) // rows.filter('done', false) // rows.filter({ done: false })
*/
filter(arg: object): Data<T>;
filter(key: string, value: any): Data<T>;
filter(fn: (row: RowOf<T>) => boolean): Data<T>;
/**
* Rows whose `key` column falls within `[lo, hi]` (sort-indexed). Pass
* ViewProxy bounds for a reactive range (a moving brush); plain numbers are
* static. For a single moving threshold prefer `gt`/`lt`/`gte`/`lte`.
* @example trades.between('pnl', [-1e6, 1e6])
*/
between(key: string, [lo, hi]: [number, number]): Data<T>;
/**
* Whole-value transform — maps the entire snapshot, rebuilding on change.
* @example count.to(n => n * 2)
*/
to<R>(fn: (value: T) => R): Data<R>;
/**
* Per-row transform; each row maps independently (only the changed row re-runs).
* @example rows.map(r => r.qty * r.price)
*/
map<R>(fn: (row: RowOf<T>) => R): Data<Record<string, R>>;
/**
* Row count (`length()`), or grouped counts (`length(fn)`). Grouped counts
* store each bucket as `{ value: count }` — read a count via
* `counts[key].value`, **not** `counts[key]`. Emptied buckets persist at
* `{ value: 0 }` (fixed-keyspace histograms); use `group` for enter/leave.
* @example rows.length() // rows.length(r => r.region) → { east: { value: 4 }, … }
*/
length(): Data<number>;
length<R>(fn: (row: RowOf<T>) => R): Data<Record<R, number>>;
/**
* Scalar aggregate over a column (or row values if `col` omitted). `sum`/`avg`
* are O(1) per change; `max`/`min` recompute O(n). Empty set → `undefined`.
* @example orders.sum('amount') // orders.avg('amount')
*/
sum(col?: string): Data<number>;
avg(col?: string): Data<number>;
max(col?: string): Data<any>;
min(col?: string): Data<any>;
/**
* Scalar boolean — does any (`some`) / every (`every`) row match the predicate.
* @example alerts.some(a => a.level >= 3)
*/
some(fn: (row: RowOf<T>) => boolean): Data<boolean>;
every(fn: (row: RowOf<T>) => boolean): Data<boolean>;
/**
* Passthrough side effect. A 1+-arg `fn(change)` fires once per row with a
* cloned change record; a 0-arg `fn()` fires once per emit (no clone) — for
* cheap "re-read and redraw" callbacks.
* @example view.tap(() => redraw())
*/
tap(fn: (change: ChangeRecord) => void): Data<T>;
/**
* First-seen unique rows, by an optional projection.
* @example trades.distinct(t => t.symbol)
*/
distinct<K = RowOf<T>>(fn?: (row: RowOf<T>) => K): Data<RowOf<T>[]>;
/**
* Fold. `reduce(fn, init)` is the general (rebuild-on-change) form; the 3-arg
* `reduce(add, remove, init)` threads inserts/removes through in O(Δ).
* @example rows.reduce((acc, r) => acc + r.n, 0)
*/
reduce<R>(fn: (acc: R, row: RowOf<T>, key: string) => R, init: R): Data<R>;
/**
* Rows present in ANY source (value taken from the first source containing it).
* @example a.union(b, c)
*/
union(...sources: Data[]): Data<T>;
/**
* Rows in this view but not in `other`.
* @example all.except(archived)
*/
except(other: Data): Data<T>;
/** Current `Object.keys` as a reactive array. */
keys(): Data<string[]>;
/** Current `Object.values` as a reactive array. */
values(): Data<RowOf<T>[]>;
/** Array order flipped. */
reverse(): Data<RowOf<T>[]>;
/**
* Descending sort (`za`) by `column`, optionally windowed to the top `max` —
* a bounded top-K, cheaper than `za(col).limit(n)`. `az` is ascending;
* `top`/`limit` window without re-sorting.
* @example trades.za('pnl', 50) // rows.az('name')
*/
za(column: string, max?: number): Data<T>;
za(max?: number): Data<T>;
az(column: string, max?: number): Data<T>;
az(max?: number): Data<T>;
top(max?: number): Data<T>;
limit(max: number): Data<T>;
/**
* Rows present in ALL source views — set intersection (one bitmask bit per row,
* O(1) per membership flip). Tracks reactive sources live.
* @example a.intersect(b, c)
*/
intersect(...sources: Data[]): Data<T>;
/**
* Rows nested under a computed key. Prunes emptied groups (enter/leave
* semantics) — use `length(fn)` when you want zero-count buckets to persist.
* @example sales.group(s => s.region)
*/
group<R>(fn: (value: RowOf<T>) => R): Data<Record<R, RowOf<T>>>;
};
declare class Sink {
}
declare class ViewProxy {
view: any;
constructor(view: any);
deleteProperty(target: any, name: any): boolean;
set(t: any, name: any, value: any): boolean;
get(t: any, name: any): any;
apply(t: any, m: any, args: any): any;
getPrototypeOf(target: any): ViewProxy;
iterator(i?: number): Generator<any, void, unknown>;
}
export { $, Operators as O, Sink as S, view as a, createOperator as c, reactive as r, value as v };