@toclocoinc/lattice-grid 1.63.1 → 1.63.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/API.html +393 -33
- package/docs/api-detail.html +35 -4
- package/lattice-grid.d.ts +1 -1
- package/lattice-grid.esm.min.js +83 -7
- package/lattice-grid.min.cjs +83 -7
- package/lattice-grid.min.css +1 -1
- package/lattice-grid.min.js +83 -7
- package/modules/ai.d.ts +1 -1
- package/modules/ai.esm.min.js +4 -4
- package/modules/ai.min.cjs +4 -4
- package/modules/ai.min.js +4 -4
- package/modules/angular.d.ts +1 -1
- package/modules/angular.esm.min.js +2 -2
- package/modules/angular.min.cjs +2 -2
- package/modules/angular.min.js +2 -2
- package/modules/chart-alluvial.d.ts +1 -1
- package/modules/chart-alluvial.esm.min.js +1 -1
- package/modules/chart-alluvial.min.cjs +1 -1
- package/modules/chart-alluvial.min.js +1 -1
- package/modules/chart-arc.d.ts +1 -1
- package/modules/chart-arc.esm.min.js +1 -1
- package/modules/chart-arc.min.cjs +1 -1
- package/modules/chart-arc.min.js +1 -1
- package/modules/chart-bubblemap.d.ts +1 -1
- package/modules/chart-bubblemap.esm.min.js +1 -1
- package/modules/chart-bubblemap.min.cjs +1 -1
- package/modules/chart-bubblemap.min.js +1 -1
- package/modules/chart-bump.d.ts +1 -1
- package/modules/chart-bump.esm.min.js +1 -1
- package/modules/chart-bump.min.cjs +1 -1
- package/modules/chart-bump.min.js +1 -1
- package/modules/chart-calendar.d.ts +1 -1
- package/modules/chart-calendar.esm.min.js +1 -1
- package/modules/chart-calendar.min.cjs +1 -1
- package/modules/chart-calendar.min.js +1 -1
- package/modules/chart-decomposition.d.ts +1 -1
- package/modules/chart-decomposition.esm.min.js +1 -1
- package/modules/chart-decomposition.min.cjs +1 -1
- package/modules/chart-decomposition.min.js +1 -1
- package/modules/chart-diverging.d.ts +1 -1
- package/modules/chart-diverging.esm.min.js +1 -1
- package/modules/chart-diverging.min.cjs +1 -1
- package/modules/chart-diverging.min.js +1 -1
- package/modules/chart-dumbbell.d.ts +1 -1
- package/modules/chart-dumbbell.esm.min.js +1 -1
- package/modules/chart-dumbbell.min.cjs +1 -1
- package/modules/chart-dumbbell.min.js +1 -1
- package/modules/chart-fan.d.ts +1 -1
- package/modules/chart-fan.esm.min.js +1 -1
- package/modules/chart-fan.min.cjs +1 -1
- package/modules/chart-fan.min.js +1 -1
- package/modules/chart-hexbin.d.ts +1 -1
- package/modules/chart-hexbin.esm.min.js +1 -1
- package/modules/chart-hexbin.min.cjs +1 -1
- package/modules/chart-hexbin.min.js +1 -1
- package/modules/chart-hexmap.d.ts +1 -1
- package/modules/chart-hexmap.esm.min.js +1 -1
- package/modules/chart-hexmap.min.cjs +1 -1
- package/modules/chart-hexmap.min.js +1 -1
- package/modules/chart-icicle.d.ts +1 -1
- package/modules/chart-icicle.esm.min.js +1 -1
- package/modules/chart-icicle.min.cjs +1 -1
- package/modules/chart-icicle.min.js +1 -1
- package/modules/chart-parallel.d.ts +1 -1
- package/modules/chart-parallel.esm.min.js +1 -1
- package/modules/chart-parallel.min.cjs +1 -1
- package/modules/chart-parallel.min.js +1 -1
- package/modules/chart-ridgeline.d.ts +1 -1
- package/modules/chart-ridgeline.esm.min.js +1 -1
- package/modules/chart-ridgeline.min.cjs +1 -1
- package/modules/chart-ridgeline.min.js +1 -1
- package/modules/chart-roc.d.ts +1 -1
- package/modules/chart-roc.esm.min.js +1 -1
- package/modules/chart-roc.min.cjs +1 -1
- package/modules/chart-roc.min.js +1 -1
- package/modules/chart-slope.d.ts +1 -1
- package/modules/chart-slope.esm.min.js +1 -1
- package/modules/chart-slope.min.cjs +1 -1
- package/modules/chart-slope.min.js +1 -1
- package/modules/chart-splom.d.ts +1 -1
- package/modules/chart-splom.esm.min.js +1 -1
- package/modules/chart-splom.min.cjs +1 -1
- package/modules/chart-splom.min.js +1 -1
- package/modules/chart-waffle.d.ts +1 -1
- package/modules/chart-waffle.esm.min.js +1 -1
- package/modules/chart-waffle.min.cjs +1 -1
- package/modules/chart-waffle.min.js +1 -1
- package/modules/charts.d.ts +1 -1
- package/modules/charts.esm.min.js +4 -4
- package/modules/charts.min.cjs +4 -4
- package/modules/charts.min.js +4 -4
- package/modules/data-router.d.ts +313 -30
- package/modules/data-router.esm.min.js +4 -4
- package/modules/data-router.min.cjs +4 -4
- package/modules/data-router.min.js +4 -4
- package/modules/devtools.d.ts +1 -1
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.d.ts +1 -1
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.d.ts +1 -1
- package/modules/gantt.esm.min.js +4 -4
- package/modules/gantt.min.cjs +4 -4
- package/modules/gantt.min.js +4 -4
- package/modules/geo-europe-nuts.d.ts +1 -1
- package/modules/geo-europe-nuts.esm.min.js +1 -1
- package/modules/geo-uk.d.ts +1 -1
- package/modules/geo-uk.esm.min.js +1 -1
- package/modules/geo-us-states.d.ts +1 -1
- package/modules/geo-us-states.esm.min.js +1 -1
- package/modules/geo-world-110m.d.ts +1 -1
- package/modules/geo-world-110m.esm.min.js +1 -1
- package/modules/geo-world-50m.d.ts +1 -1
- package/modules/geo-world-50m.esm.min.js +1 -1
- package/modules/htmx.d.ts +1 -1
- package/modules/htmx.esm.min.js +83 -7
- package/modules/htmx.min.cjs +83 -7
- package/modules/htmx.min.js +83 -7
- package/modules/kanban.d.ts +1 -1
- package/modules/kanban.esm.min.js +4 -4
- package/modules/kanban.min.cjs +4 -4
- package/modules/kanban.min.js +4 -4
- package/modules/kpi.d.ts +1 -1
- package/modules/kpi.esm.min.js +4 -4
- package/modules/kpi.min.cjs +4 -4
- package/modules/kpi.min.js +4 -4
- package/modules/layout.d.ts +1 -1
- package/modules/layout.esm.min.js +4 -4
- package/modules/layout.min.cjs +4 -4
- package/modules/layout.min.js +4 -4
- package/modules/mock-socket.d.ts +1 -1
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +2 -2
- package/modules/mock-socket.min.js +2 -2
- package/modules/react.d.ts +1 -1
- package/modules/react.esm.min.js +4 -4
- package/modules/react.min.cjs +4 -4
- package/modules/react.min.js +4 -4
- package/modules/svelte.d.ts +1 -1
- package/modules/svelte.esm.min.js +2 -2
- package/modules/svelte.min.cjs +2 -2
- package/modules/svelte.min.js +2 -2
- package/modules/tabs.d.ts +1 -1
- package/modules/tabs.esm.min.js +4 -4
- package/modules/tabs.min.cjs +4 -4
- package/modules/tabs.min.js +4 -4
- package/modules/vue.d.ts +1 -1
- package/modules/vue.esm.min.js +2 -2
- package/modules/vue.min.cjs +2 -2
- package/modules/vue.min.js +2 -2
- package/modules/webcomponent.d.ts +1 -1
- package/modules/webcomponent.esm.min.js +83 -7
- package/modules/webcomponent.min.cjs +83 -7
- package/modules/webcomponent.min.js +83 -7
- package/package.json +1 -1
package/docs/API.html
CHANGED
|
@@ -360,7 +360,7 @@
|
|
|
360
360
|
<div class="shell">
|
|
361
361
|
<aside class="rail">
|
|
362
362
|
<p class="rail__brand">Lattice Grid</p>
|
|
363
|
-
<p class="rail__sub">API reference · v1.63.
|
|
363
|
+
<p class="rail__sub">API reference · v1.63.3</p>
|
|
364
364
|
<nav>
|
|
365
365
|
<div class="rail__group">
|
|
366
366
|
<span class="rail__label">Start</span>
|
|
@@ -421,6 +421,7 @@
|
|
|
421
421
|
<a href="#units">Units of your own</a>
|
|
422
422
|
<a href="#chartsmodule">The charts module</a>
|
|
423
423
|
<a href="#layout">The dashboard layout</a>
|
|
424
|
+
<a href="#datarouter">The data router</a>
|
|
424
425
|
<a href="#mocksocket">The mock socket</a>
|
|
425
426
|
<a href="#charts">In-cell charts</a>
|
|
426
427
|
<a href="#formulas">Formulas</a>
|
|
@@ -443,7 +444,7 @@
|
|
|
443
444
|
</header>
|
|
444
445
|
|
|
445
446
|
<p class="chips">
|
|
446
|
-
<span class="chip">Version 1.63.
|
|
447
|
+
<span class="chip">Version 1.63.3</span>
|
|
447
448
|
<span class="chip">Zero dependencies</span>
|
|
448
449
|
<span class="chip"><a href="api-detail.html">Developer guide →</a></span>
|
|
449
450
|
</p>
|
|
@@ -1392,7 +1393,7 @@ grid.destroy();
|
|
|
1392
1393
|
<table>
|
|
1393
1394
|
<thead><tr><th>Member</th><th>Returns</th><th>Description</th></tr></thead>
|
|
1394
1395
|
<tbody>
|
|
1395
|
-
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.63.
|
|
1396
|
+
<tr><td class="sig">getVersion()</td><td class="type">string</td><td class="desc">The version this grid came from, e.g. <code>'1.63.3'</code>. Also on the module as <code>getVersion()</code>, for when you have no grid to hand.</td></tr>
|
|
1396
1397
|
<tr><td class="sig">get(key)</td><td class="type">unknown</td><td class="desc">Read any configuration key.</td></tr>
|
|
1397
1398
|
<tr><td class="sig">set(key, value)</td><td class="type">void</td><td class="desc">Write one key. Every key is live; nothing needs a rebuild.</td></tr>
|
|
1398
1399
|
<tr><td class="sig">setAll(values)</td><td class="type">void</td><td class="desc">Write several in one pass. Emits one <code>config:changed</code> for the batch, not one per key.</td></tr>
|
|
@@ -5630,12 +5631,12 @@ router.load(snapshot); <span class="cmt">// every viewer
|
|
|
5630
5631
|
<table>
|
|
5631
5632
|
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
5632
5633
|
<tbody>
|
|
5633
|
-
<tr><td class="sig">createDataRouter({ key
|
|
5634
|
+
<tr><td class="sig">createDataRouter({ key?, rowKey?, overlap?, onUnrouted?, selectionDebounce?, metricsInterval?, seq?, dedupe?, batch?, coalesce?, time?, now?, onWrite?, onConflict?, config? })</td><td class="desc">Create a router. <code>key</code> is the partition (property or <code>fn(row)</code>) — optional, since a router whose routes all use <code>fn(row)</code> predicates never reads it; <code>rowKey</code> the default identity within a grid; <code>overlap</code> fans a record to all matching routes; <code>onUnrouted</code> is a sink for unmatched records (it receives the row on <code>load</code> and <code>query</code>, and the whole delta on <code>apply</code>). <code>selectionDebounce</code> is the debounce in ms for cross-grid selection refilters (default 16; <code>0</code> refilters synchronously). <strong>v10:</strong> <code>metricsInterval</code> is the ms between periodic <code>on('metrics')</code> emits (default 1000; <code>0</code> disables the timer). <strong>v3:</strong> <code>seq</code> (a version field or <code>fn(row)</code>) turns on ordered de-duplication, <code>dedupe: false</code> opts out; <code>batch</code> (interval ms or <code>{ intervalMs }</code>) / <code>coalesce: true</code> buffer a high-frequency <code>push</code>. <strong>v4:</strong> <code>time</code> (a timestamp field or <code>fn(row)</code>) and an injectable <code>now</code> clock drive time-domain scrubbing. <strong>v8:</strong> <code>onWrite</code>/<code>onConflict</code> are the router-global write-back callbacks. <strong>v5:</strong> <code>config</code> is a declarative routing spec, desugared through <code>configure</code>.</td></tr>
|
|
5634
5635
|
<tr><td class="sig">attach(grid, predicate, { rowKey? })</td><td class="desc">Route to <code>grid</code> when <code>predicate</code> matches: a value compared to <code>key</code>, or a <code>fn(row) => boolean</code>. <code>rowKey</code> overrides the router default for this grid.</td></tr>
|
|
5635
5636
|
<tr><td class="sig">attachDefault(grid, { rowKey? })</td><td class="desc">The "rest" sink: the grid that receives every record no explicit route matched.</td></tr>
|
|
5636
5637
|
<tr><td class="sig">load(snapshot)</td><td class="desc">Apply a full snapshot as a keyed diff per grid. Returns per-route <code>{ added, updated, removed }</code> counts in attach order.</td></tr>
|
|
5637
5638
|
<tr><td class="sig">apply(deltas)</td><td class="desc">Apply <code>{ op: 'upsert' | 'delete', row }</code> deltas in place by <code>rowKey</code>.</td></tr>
|
|
5638
|
-
<tr><td class="sig">unrouted</td><td class="desc">How many records matched no route
|
|
5639
|
+
<tr><td class="sig">unrouted</td><td class="desc">How many records matched no route. Reset to zero by <code>load</code> and by <code>query</code>, then running for deltas — so read it straight after the call you care about, not at the end of a session.</td></tr>
|
|
5639
5640
|
<tr><td class="sig">link(source, target, relation)</td><td class="desc"><strong>v2:</strong> make a selection in <code>source</code> filter what <code>target</code> receives. <code>relation</code> is a key map <code>{ from, to }</code> (target rows whose <code>to</code> value is among the selected source rows' <code>from</code> values — multi-select is an IN set, ANY match) or a function <code>fn(selectedSourceRows) => (row) => boolean</code>. No selection shows the full partition; changes are debounced.</td></tr>
|
|
5640
5641
|
<tr><td class="sig">flush()</td><td class="desc"><strong>v2:</strong> apply any debounced selection refilter now, for a deterministic point (and for tests).</td></tr>
|
|
5641
5642
|
<tr><td class="sig">attach(grid, predicate, { rollup })</td><td class="desc"><strong>v3:</strong> feed the grid a grouped/summarised view — <code>rollup: { groupBy, aggregate }</code> gives one summary row per group (<code>op</code> of <code>sum</code>/<code>avg</code>/<code>min</code>/<code>max</code>/<code>count</code> over a <code>field</code>, or a <code>fn(rows)</code>). Applied by keyed diff, so only a moved group repaints.</td></tr>
|
|
@@ -5650,8 +5651,9 @@ router.load(snapshot); <span class="cmt">// every viewer
|
|
|
5650
5651
|
<tr><td class="sig">attach(grid, predicate, { writable, onWrite?, onConflict? })</td><td class="desc"><strong>v8 (BACKLOG-0000912):</strong> make a route <strong>writable</strong> — the router captures the grid's committed edits off its public edit surface (<code>grid.on('cell:changed')</code> → <code>grid.edit.setCells</code>) and routes them to <code>onWrite(change, { route, source })</code> (per-route here, or the router-global <code>onWrite</code>), reverting the cell on reject and re-entering an accepted write as a normal delta. <code>onConflict(change, { serverRow })</code> surfaces a last-write-wins conflict. A derived (<code>rollup</code>/<code>transform</code>) route cannot be writable — its edits are reverted and warned.</td></tr>
|
|
5651
5652
|
<tr><td class="sig">attach(grid, predicate, { where })</td><td class="desc"><strong>v7 (BACKLOG-0000914):</strong> a route-level <code>where</code> — a filter-wire condition <code>{ col, op, value }</code> or an <code>and</code>/<code>or</code>/<code>not</code> group — used only by query-slice routing (<code>query()</code>): the router pushes it <em>down</em> to the engine where the adapter allows and finishes the residual client-side. Distinct from <code>filter</code> (a <code>fn(row)</code> that only ever runs in the browser).</td></tr>
|
|
5652
5653
|
<tr><td class="sig">attach(grid, predicate, { label, backpressure })</td><td class="desc"><strong>v10/v13:</strong> a human <code>label</code> for the route (shown in <code>metrics()</code> and the devtools panel), and a per-route <strong>backpressure</strong> policy that throttles / coalesces / samples how that route's viewer is refreshed under load — <em>without</em> touching the keyed store or any other route. <code>backpressure: { maxHz, minInterval?, sample?, maxLag? }</code>: <code>maxLag</code> (a backlog depth) sets when it engages (below it, changes pass straight through); <code>maxHz</code>/<code>minInterval</code> cap the refresh rate; <code>sample</code> (an integer > 1) thins intermediate refreshes. A trailing flush always lands the latest state (deletes included), so the viewer converges and is never left stale.</td></tr>
|
|
5654
|
+
<tr><td class="sig">flushBackpressure()</td><td class="desc"><strong>v13 (BACKLOG-0000962):</strong> refresh every backpressured route to the latest state now. A route with a <code>backpressure</code> policy holds its viewer refresh until its rate limit or sample count allows one, so a test — or a teardown — can observe a route that has not caught up yet; this forces the deferred flush for every route at once and gives you a deterministic point. A no-op for routes without a policy or with nothing pending, and it never touches the keyed store: the rows were always current, only the refresh was held.</td></tr>
|
|
5653
5655
|
<tr><td class="sig">query(adapter, request?)</td><td class="desc"><strong>v7 (BACKLOG-0000914):</strong> source the router from a DFQL/DuckDB (or any pushdown) adapter. Runs <code>adapter.execute</code>, partitions the result across the routes and drives the grids by the same keyed diff <code>load()</code> uses; a route's <code>where</code> is planned against the adapter's capabilities (pushed down where allowed, residual finished client-side). Composes with per-route <code>transform</code>/<code>filter</code>/<code>sort</code>/<code>rollup</code> and links/graph. Async — resolves once every slice is fetched and applied.</td></tr>
|
|
5654
|
-
<tr><td class="sig">lastQueryPlan()</td><td class="desc"><strong>v7:</strong> the pushed/residual split of the last <code>query()</code>, per fetch — whether a filter reached the engine and what work was left client-side. <code>null</code> before any query. Provenance, so a slow slice is diagnosed rather than guessed.</td></tr>
|
|
5656
|
+
<tr><td class="sig">lastQueryPlan()</td><td class="desc"><strong>v7:</strong> the pushed/residual split of the last <code>query()</code>, per fetch — whether a filter reached the engine and what work was left client-side. <code>null</code> before any query. Each entry is <code>{ route, pushedFilter, residual }</code> for a <code>where</code> route — <code>route</code> the grid, <code>pushedFilter</code> whether its filter reached the engine, <code>residual</code> the work finished client-side — or <code>{ base: true, pushedFilter, residual }</code> for the single base fetch that fed every route without a <code>where</code>. Provenance, so a slow slice is diagnosed rather than guessed.</td></tr>
|
|
5655
5657
|
<tr><td class="sig">buffer({ window?, max? })</td><td class="desc"><strong>v4 (BACKLOG-0000911):</strong> turn on time-travel buffering — record the ordered, de-duplicated stream into a <strong>bounded</strong> ring (a time <code>window</code> in ms and/or a <code>max</code> delta count; eviction folds the oldest into a moving base, so memory never grows unbounded; a default cap applies if you name neither). Seeded from the current world, so it can be turned on at any time. Opt-in and off by default.</td></tr>
|
|
5656
5658
|
<tr><td class="sig">scrubTo(target, { by? })</td><td class="desc"><strong>v4:</strong> scrub the grids to a past point — the base snapshot plus the buffered deltas up to <code>target</code> (a seq when the router has one, else a timestamp; <code>{ by: 'seq' | 'time' }</code> chooses). Pushed by keyed diff, so each view keeps scroll and selection and only changed rows repaint. Live deltas keep arriving into the buffer but do not disturb the view.</td></tr>
|
|
5657
5659
|
<tr><td class="sig">replay(from, to, { speed?, by? })</td><td class="desc"><strong>v4:</strong> walk an incident — scrub to <code>from</code>, then apply each buffered delta in <code>(from, to]</code> in order, one per <code>speed</code> ms (default <code>0</code>). Returns a promise resolving when the range finishes (or is superseded); the router stays parked at <code>to</code> until <code>live()</code>.</td></tr>
|
|
@@ -5664,7 +5666,7 @@ router.load(snapshot); <span class="cmt">// every viewer
|
|
|
5664
5666
|
<tr><td class="sig">removeSource(ref) / sources()</td><td class="desc"><strong>v9:</strong> drop exactly the rows a feed contributed (by source id or handle) from every route and unregister it; and list the registered source ids.</td></tr>
|
|
5665
5667
|
<tr><td class="sig">metrics()</td><td class="desc"><strong>v10 (BACKLOG-0000932):</strong> a cheap point-in-time observability snapshot — per-route row counts and throughput (rows/sec since the previous read), per-source rates and totals (fan-in), and the global <code>unrouted</code> / <code>dropped</code> / <code>buffered</code> / <code>lag</code> figures. Throughput is sampled over the interval since the last <code>metrics()</code> call or emit. Each entry in <code>routes[]</code> also carries its <code>label</code> and, when the route declares a backpressure policy, a <code>backpressure: { pending, coalesced }</code> object — <code>pending</code> is the held backlog since the last flush (the route's lag) and <code>coalesced</code> the cumulative change-events it has absorbed into deferred refreshes (<code>null</code> when the route has no policy).</td></tr>
|
|
5666
5668
|
<tr><td class="sig">on('metrics', handler)</td><td class="desc"><strong>v10:</strong> subscribe to the periodic <code>metrics</code> emit (the <code>metricsInterval</code> ms, default 1000; <code>0</code> disables it). The timer runs only while at least one listener is registered and stops when the last is removed. Returns an unsubscribe function.</td></tr>
|
|
5667
|
-
<tr><td class="sig">mountDevtools(el
|
|
5669
|
+
<tr><td class="sig">mountDevtools(el)</td><td class="desc"><strong>v10:</strong> mount an opt-in, DOM-touching live panel (in the module's own <code>devtools.js</code>, so the core stays DOM-free) that renders <code>metrics()</code> into <code>el</code> and re-renders on each <code>on('metrics')</code> emit — so the panel's cadence is the router's <code>metricsInterval</code>, not a setting of its own. Returns a controller with <code>refresh()</code>, which forces an immediate re-render, and <code>destroy()</code>, which unsubscribes and removes the panel from the DOM. The same panel is available without a router: <code>mountRouterDevtools(router, el)</code> is a named export of <code>modules/data-router/devtools.js</code>, and <code>mountDevtools</code> is a one-line wrapper over it. Off unless called.</td></tr>
|
|
5668
5670
|
<tr><td class="sig">persist({ key?, debounce?, storage?, indexedDB?, dbName?, storeName? })</td><td class="desc"><strong>v12 (BACKLOG-0000961):</strong> turn on durable persistence — snapshot the keyed store and the time-travel ring to a durable async key/value store so an offline reload or a browser refresh resumes exactly where it left off. The default backend is <strong>IndexedDB</strong> (native, no dependency), opened lazily and guarded so private-mode or blocked storage degrades to in-memory with a one-time warning rather than throwing. Writes a coalesced snapshot after each <code>load</code>/<code>apply</code> (debounced by <code>debounce</code> ms, default 250; <code>0</code> is eager). Pass <code>storage</code> — any object with async <code>get(key)</code>/<code>set(key, value)</code> — to use another backend (a server, a test double). Opt-in and off by default.</td></tr>
|
|
5669
5671
|
<tr><td class="sig">restore()</td><td class="desc"><strong>v12:</strong> resume from the durable snapshot. Read the last persisted state and apply it — <code>load</code> the live head through the ordinary keyed diff (so grids attached before this call repaint only what differs), restore the resume checkpoint and, when the snapshot carried a time-travel ring, restore buffering and the ring so <code>scrubTo</code>/<code>replay</code>/<code>live</code> work straight after a reload. Call it once, after attaching the grids. <code>async</code>; resolves <code>true</code> when a snapshot was found and applied, <code>false</code> when persistence is off/degraded or nothing was stored.</td></tr>
|
|
5670
5672
|
<tr><td class="sig">flushPersist() / persisting</td><td class="desc"><strong>v12:</strong> flush any pending durable write now (<code>async</code>; cancels the debounce and resolves once the write settles — for a <code>beforeunload</code> handler, a deterministic checkpoint, or a test), and whether durable persistence is on and not degraded to in-memory.</td></tr>
|
|
@@ -5673,13 +5675,14 @@ router.load(snapshot); <span class="cmt">// every viewer
|
|
|
5673
5675
|
</tbody>
|
|
5674
5676
|
</table>
|
|
5675
5677
|
</div>
|
|
5678
|
+
<p class="section-note"><strong>Four things worth knowing before you build on this.</strong> <code>attachDefault</code> keeps <em>one</em> sink: calling it twice replaces the first, silently, along with whatever slice it held — attach the sink once, at setup. An <code>alert</code> has no removal: <code>detach</code> drops routes, links and graph edges but leaves alerts running against their own partition, so an alert added at setup keeps firing until <code>destroy()</code>, even after every grid is gone. <code>detach</code> does take a <code>subscribe</code> handler as well as a grid — pass the same function you handed <code>subscribe</code> and the subscription goes with it. And a <code>join</code> accepts three spellings the examples below do not use: <code>on</code> for <code>localKey</code>, <code>fromKey</code> for <code>foreignKey</code>, and <code>select</code> for <code>fields</code>; they are equivalent, and a spec the router cannot honour degrades to plain fan-in with a one-time warning rather than throwing.</p>
|
|
5676
5679
|
<p>The router keeps a small <code>Map<rowKey, row></code> per route to compute the snapshot diff. That is deliberate for v1; a future optimisation could diff against the grid's own key index rather than a shadow copy. Ordering and dedupe across a live feed are the host's to guarantee — a caller that must drop stale out-of-order deltas can carry its own version or sequence field and filter before <code>apply</code>; v1 imposes no version scheme.</p>
|
|
5677
5680
|
<p><strong>Cross-grid selection filtering (v2, BACKLOG-0000880).</strong> <code>link</code> keeps each target's <em>full partition</em> separate from what it currently shows: when the source's selection changes, the router recomputes the shown subset from the relation and re-pushes it through the same keyed-diff path, so the target grid stays dumb — it only ever receives rows, never a query or a reference to the source. Selection <em>in</em> the target survives an unrelated refilter, because the keyed path preserves it. No selection (or one the router cannot resolve to routed rows) shows the full partition, and deselecting restores it. The source grid must have selection enabled; still no grid-core change. Debounce is controlled by <code>selectionDebounce</code> (default 16 ms; <code>0</code> is synchronous), and <code>flush()</code> forces it.</p>
|
|
5678
5681
|
<h3 id="datarouter-v2-example">Cross-grid selection filtering, executed</h3>
|
|
5679
5682
|
<p class="section-note">A customers grid and an orders grid off one feed; selecting customers filters the orders
|
|
5680
5683
|
grid to their regions through the keyed-diff path, and deselecting restores the full set. Run headless
|
|
5681
5684
|
on every build.</p>
|
|
5682
|
-
<pre data-run="js" data-expect="3 | 2 | 3 | 3" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5685
|
+
<pre data-run="js" data-expect="3 | 2 | 3 | 3" data-covers="export:createDataRouter config:key config:selectionDebounce"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5683
5686
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5684
5687
|
|
|
5685
5688
|
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }, { id: 'region', field: 'region', type: 'text' }];
|
|
@@ -5744,7 +5747,7 @@ g.destroy(); router.destroy();
|
|
|
5744
5747
|
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
5745
5748
|
|
|
5746
5749
|
<p><strong>Aggregate/rollup routes (v3, BACKLOG-0000887).</strong> A route can be fed a <em>grouped, summarised</em> view of its partition instead of the raw rows — a per-category total for a chart route, say. <code>attach(grid, predicate, { rollup: { groupBy, aggregate } })</code> gives the grid one summary row per group: <code>groupBy</code> is a property, a <code>fn(row)</code> or an array of either, and each <code>aggregate</code> entry is a <code>{ op, field }</code> (<code>sum</code>, <code>avg</code>, <code>min</code>, <code>max</code>, <code>count</code>) or a <code>fn(rows) => value</code>. A route <code>filter</code> runs on the raw rows before grouping; <code>sort</code> and <code>transform</code> run on the summaries. The summary is applied by the same keyed diff, so only a group that actually moved repaints — the router owns the roll-up, the grid stays dumb.</p>
|
|
5747
|
-
<pre data-run="js" data-expect="2 | amer | 115" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5750
|
+
<pre data-run="js" data-expect="2 | amer | 115" data-covers="export:createDataRouter config:rollup"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5748
5751
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5749
5752
|
|
|
5750
5753
|
<span class="kw">const</span> cols = [{ id: 'region', field: 'region' }, { id: 'total', field: 'total', type: 'number' }];
|
|
@@ -5772,7 +5775,7 @@ chart.destroy(); router.destroy();
|
|
|
5772
5775
|
<span class="kw">return</span> [groups, top, emeaTotal].join(' | ');</code></pre>
|
|
5773
5776
|
|
|
5774
5777
|
<p><strong>The relationship graph (v3, BACKLOG-0000887).</strong> <code>relate([...])</code> is the scalable form of v2's pairwise <code>link()</code>. Each edge is <code>{ from, to, on }</code>, where <code>on</code> is a key map <code>{ from, to }</code> or a function <code>fn(sourceRows) => (row) => boolean</code>. The router resolves the whole graph on any selection change, so it handles <strong>multi-hop</strong> chains (A→B→C: a selection in A narrows B and, through B's resulting rows, C — with no selection in B), <strong>several sources into one target</strong> (their filters AND together), and <strong>mutual</strong> edges (<code>mutual: true</code> — selecting in either linked view narrows the other; requires a key-map <code>on</code>). A node's effective set is its own selection when it has one, otherwise the rows its incoming edges leave — that is what carries a selection transitively down a chain. Additive to <code>link()</code>; the two compose.</p>
|
|
5775
|
-
<pre data-run="js" data-expect="o1,o3 | l1,l3,l4" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5778
|
+
<pre data-run="js" data-expect="o1,o3 | l1,l3,l4" data-covers="export:createDataRouter config:relate"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5776
5779
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5777
5780
|
|
|
5778
5781
|
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }, { id: 'region', field: 'region', type: 'text' }, { id: 'orderId', field: 'orderId', type: 'text' }];
|
|
@@ -5797,11 +5800,11 @@ customers.destroy(); orders.destroy(); lines.destroy(); router.destroy();
|
|
|
5797
5800
|
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
5798
5801
|
|
|
5799
5802
|
<p><strong>Stream hygiene (v3, BACKLOG-0000887).</strong> A production feed arrives out of order, gets replayed, and comes faster than a grid should repaint. Configure a <code>seq</code> (a version field or <code>fn(row)</code>) and the router orders each batch by it and <strong>drops</strong> any delta not newer than the one it already applied for that record (counted in <code>router.dropped</code>) — an out-of-order or replayed feed converges to the newest state. <code>push(delta)</code> with a <code>batch</code> interval or <code>coalesce: true</code> buffers a high-frequency feed and coalesces rapid updates to one key into a single apply (flush a deterministic point with <code>flushStream()</code>). When the host's own connection drops and it reconnects, resume precisely: <code>load</code> a fresh snapshot (a keyed diff that preserves grid state) and replay from <code>lastSeq()</code>/<code>checkpoint()</code> — the deltas the router already saw are dropped by the same gate. The router does not detect or recover from the drop itself; see <a href="#datarouter-websocket-example">a live WebSocket feed</a> for the worked reconnect example. <code>seenThrough(mark)</code> primes the checkpoint from a persisted one.</p>
|
|
5800
|
-
<pre data-run="js" data-expect="30 | 1 | 4" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5803
|
+
<pre data-run="js" data-expect="30 | 1 | 4" data-covers="export:createDataRouter config:seq config:dedupe"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5801
5804
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5802
5805
|
|
|
5803
5806
|
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: [{ id: 'id', field: 'id' }, { id: 'n', field: 'n', type: 'number' }] });
|
|
5804
|
-
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', seq: 'v' });
|
|
5807
|
+
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', seq: 'v', dedupe: true });
|
|
5805
5808
|
router.attach(g, () => true);
|
|
5806
5809
|
router.load([]);
|
|
5807
5810
|
|
|
@@ -5823,12 +5826,12 @@ g.destroy(); router.destroy();
|
|
|
5823
5826
|
dropped by the <code>seq</code> checkpoint while a genuinely new one lands. Swapping in a real
|
|
5824
5827
|
<code>WebSocket</code> is a one-line constructor change — see <a href="#mocksocket">the mock socket</a>.
|
|
5825
5828
|
Run headless on every build.</p>
|
|
5826
|
-
<pre data-run="js" data-expect="A@3 | 3 | A@4 | 2" data-covers="export:createDataRouter export:MockWebSocket"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5829
|
+
<pre data-run="js" data-expect="A@3 | 3 | A@4 | 2" data-covers="export:createDataRouter config:seq config:dedupe export:MockWebSocket"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5827
5830
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5828
5831
|
<span class="kw">const</span> { MockWebSocket } = <span class="kw">await</span> import('../packages/modules/mock-socket/index.js');
|
|
5829
5832
|
|
|
5830
5833
|
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: [{ id: 'id', field: 'id' }, { id: 'label', field: 'label' }] });
|
|
5831
|
-
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', seq: 'v' });
|
|
5834
|
+
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', seq: 'v', dedupe: true });
|
|
5832
5835
|
router.attach(g, () => <span class="kw">true</span>);
|
|
5833
5836
|
|
|
5834
5837
|
<span class="cmt">// THE INTEGRATION: a snapshot message loads, a delta message applies. This is</span>
|
|
@@ -5850,7 +5853,7 @@ router.attach(g, () => <span class="kw">true</span>);
|
|
|
5850
5853
|
}
|
|
5851
5854
|
<span class="kw">const</span> socketA = <span class="kw">new</span> MockWebSocket({ feed: feedA(), rate: 5, jitter: 0, snapshotDelay: 5 });
|
|
5852
5855
|
wireRouter(router, socketA);
|
|
5853
|
-
<span class="kw">await</span> wait(
|
|
5856
|
+
<span class="kw">await</span> wait(80);
|
|
5854
5857
|
<span class="kw">const</span> beforeDrop = g.rows.value('a', 'label'); <span class="cmt">// A@3</span>
|
|
5855
5858
|
<span class="kw">const</span> resumeFrom = router.lastSeq(); <span class="cmt">// 3 — the resume cursor</span>
|
|
5856
5859
|
socketA.close();
|
|
@@ -5913,7 +5916,7 @@ orders.destroy(); invoices.destroy(); rest.destroy(); router.destroy();
|
|
|
5913
5916
|
<h3 id="datarouter-v4-example">Scrub and return to live, executed</h3>
|
|
5914
5917
|
<p class="section-note">A versioned feed buffered into a bounded ring; the view scrubs to a past seq, reads the
|
|
5915
5918
|
reconstructed value, then returns to the live head. Run headless on every build.</p>
|
|
5916
|
-
<pre data-run="js" data-expect="30 | 10 | true | 30" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5919
|
+
<pre data-run="js" data-expect="30 | 10 | true | 30" data-covers="export:createDataRouter config:buffer config:seq"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5917
5920
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5918
5921
|
|
|
5919
5922
|
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: [{ id: 'id', field: 'id' }, { id: 'n', field: 'n', type: 'number' }] });
|
|
@@ -5974,7 +5977,7 @@ big.destroy(); small.destroy(); router.destroy();
|
|
|
5974
5977
|
<h3 id="datarouter-v9-example">Fan-in from two feeds, executed</h3>
|
|
5975
5978
|
<p class="section-note">Two feeds with a colliding raw id, namespaced per source so they merge without clobbering;
|
|
5976
5979
|
removing one feed drops exactly its rows. Run headless on every build.</p>
|
|
5977
|
-
<pre data-run="js" data-expect="3 | crm,erp | 2" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5980
|
+
<pre data-run="js" data-expect="3 | crm,erp | 2 | 2" data-covers="export:createDataRouter config:id config:key config:load config:size config:remove"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
5978
5981
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
5979
5982
|
|
|
5980
5983
|
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }, { id: 'n', field: 'n', type: 'number' }];
|
|
@@ -5989,12 +5992,13 @@ crm.load([{ id: '1', type: 'order', n: 10 }, { id: '2', type: 'order', n: 20 }])
|
|
|
5989
5992
|
erp.load([{ id: '1', type: 'order', n: 99 }]); <span class="cmt">// same raw id '1' — merged, not clobbered</span>
|
|
5990
5993
|
<span class="kw">const</span> merged = g.rows.count(); <span class="cmt">// 3</span>
|
|
5991
5994
|
<span class="kw">const</span> ids = router.sources().join(','); <span class="cmt">// crm,erp</span>
|
|
5995
|
+
<span class="kw">const</span> held = crm.size; <span class="cmt">// 2 — what this one feed holds live</span>
|
|
5992
5996
|
|
|
5993
|
-
|
|
5997
|
+
erp.remove(); <span class="cmt">// drops exactly erp's row; the handle knows its own feed</span>
|
|
5994
5998
|
<span class="kw">const</span> afterRemove = g.rows.count(); <span class="cmt">// 2</span>
|
|
5995
5999
|
|
|
5996
6000
|
g.destroy(); router.destroy();
|
|
5997
|
-
<span class="kw">return</span> [merged, ids, afterRemove].join(' | ');</code></pre>
|
|
6001
|
+
<span class="kw">return</span> [merged, ids, held, afterRemove].join(' | ');</code></pre>
|
|
5998
6002
|
|
|
5999
6003
|
<p><strong>Fan-in JOIN / enrichment (v11, BACKLOG-0000957).</strong> Fan-in above <em>merges</em> feeds side by side; a <code>join</code> spec goes further and <em>enriches</em> one feed's rows with fields looked up from <em>another</em> registered source — e.g. an <code>orders</code> feed enriched with <code>name</code>/<code>tier</code> from a <code>customers</code> source keyed by <code>customerId</code>. Declared per source: <code>addSource('orders', { join: { from: 'customers', localKey: 'customerId', fields: ['name', 'tier'], missing: 'hold' } })</code>. <code>localKey</code> is the field on the enriched (left) row holding the foreign key (a field name or <code>fn(row)</code>); <code>foreignKey</code> is the field matched on the lookup row (defaults to <code>localKey</code>'s name); <code>fields</code> is what to pull — an array, a <code>{ src: dest }</code> rename map, or <code>select(lookupRow, leftRow) => object</code>. No second store is built: the lookup source <em>is</em> an ordinary fan-in source, and the join probes its existing keyed store by an index of join-key → store-id. <code>missing</code> chooses what happens when the lookup is absent or late: <code>hold</code> withholds the row from viewers until its lookup arrives, <code>passthrough</code> (the default) lets it flow unenriched, and <code>null</code> flows it with the pulled fields set to <code>null</code>. Enriched rows reach viewers through the ordinary keyed-diff path. <strong>Late lookups re-enrich:</strong> when a lookup row arrives, changes, or is deleted, every already-seated left row that references it is re-enriched and re-emitted — a held row is released, a <code>null</code>/<code>passthrough</code> row gains its fields, and a row whose lookup vanished is nulled/stripped (or, under <code>hold</code>, withheld again). Enrichment always recomputes from the untouched base row, so it is idempotent.</p>
|
|
6000
6004
|
<p><strong>Durable resume and backpressure (v12/v13, BACKLOG-0000961 / BACKLOG-0000962).</strong> <code>persist({ key })</code> turns on durability: the router snapshots its keyed store and time-travel ring to a durable async store (IndexedDB by default, or any <code>{ get, set }</code> you pass as <code>storage</code>) after each <code>load</code>/<code>apply</code>, and <code>await router.restore()</code> — called once after the grids are attached — rehydrates them through the ordinary keyed diff, so an offline reload or a browser refresh resumes exactly where it left off (blocked/private storage degrades to in-memory with a one-time warning, never a throw). Independently, a route can declare <strong>backpressure</strong> — <code>attach(grid, type, { label, backpressure: { maxHz } })</code> — to cap how often its viewer repaints under load without slowing the store or any sibling route: <code>maxHz</code>/<code>minInterval</code> rate-limit the refresh, <code>sample</code> thins intermediate ones, and <code>maxLag</code> sets the backlog depth at which throttling engages; a trailing flush always lands the latest state so the viewer converges. What it cost is observable: <code>router.metrics().routes[].backpressure</code> is <code>{ pending, coalesced }</code> — the held backlog and the cumulative change-events folded into deferred refreshes (<code>null</code> for a route with no policy).</p>
|
|
@@ -6028,15 +6032,15 @@ g.destroy(); router.destroy();
|
|
|
6028
6032
|
<h3 id="datarouter-v10-example">A metrics snapshot, executed</h3>
|
|
6029
6033
|
<p class="section-note">A snapshot fanned to a route and a sink; the metrics read reports the route's row count and the
|
|
6030
6034
|
unrouted total, and <code>on('metrics')</code> returns an unsubscribe. Run headless on every build.</p>
|
|
6031
|
-
<pre data-run="js" data-expect="2 | 1 | function" data-covers="export:createDataRouter"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6035
|
+
<pre data-run="js" data-expect="2 | 1 | function" data-covers="export:createDataRouter config:metricsInterval"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6032
6036
|
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6033
6037
|
|
|
6034
6038
|
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }];
|
|
6035
6039
|
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6036
|
-
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id' });
|
|
6040
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id', metricsInterval: 0 }); <span class="cmt">// 0: no periodic emit; read on demand</span>
|
|
6037
6041
|
router.attach(g, 'order');
|
|
6038
6042
|
|
|
6039
|
-
<span class="kw">const</span> off = router.on('metrics', () => {}); <span class="cmt">// register;
|
|
6043
|
+
<span class="kw">const</span> off = router.on('metrics', () => {}); <span class="cmt">// register; returns unsubscribe (no timer at interval 0)</span>
|
|
6040
6044
|
router.load([
|
|
6041
6045
|
{ id: 'o1', type: 'order' },
|
|
6042
6046
|
{ id: 'o2', type: 'order' },
|
|
@@ -6045,11 +6049,192 @@ router.load([
|
|
|
6045
6049
|
<span class="kw">const</span> m = router.metrics();
|
|
6046
6050
|
<span class="kw">const</span> rows = m.routes[0].rows; <span class="cmt">// 2</span>
|
|
6047
6051
|
<span class="kw">const</span> unrouted = m.unrouted; <span class="cmt">// 1</span>
|
|
6048
|
-
off(); <span class="cmt">//
|
|
6052
|
+
off(); <span class="cmt">// last listener gone</span>
|
|
6049
6053
|
|
|
6050
6054
|
g.destroy(); router.destroy();
|
|
6051
6055
|
<span class="kw">return</span> [rows, unrouted, typeof off].join(' | ');</code></pre>
|
|
6052
6056
|
|
|
6057
|
+
|
|
6058
|
+
<h3 id="datarouter-options-examples">The remaining options, each executed</h3>
|
|
6059
|
+
<p class="section-note">One short example per option family the eight above do not reach: overlap and the unrouted sink, coalesced pushes, write-back, the declarative graph, durable resume, backpressure, and time-domain scrubbing. Each is run headless on every build.</p>
|
|
6060
|
+
<h3 id="datarouter-overlap-example">Fan one value to two viewers, and log the strays, executed</h3>
|
|
6061
|
+
<p class="section-note">With <code>overlap</code> a record goes to <em>every</em> route it matches, so a grid and a second viewer can share one partition; <code>onUnrouted</code> receives what matched none, which is the right sink when a stray record is a bug to log rather than a row to show. Run headless on every build.</p>
|
|
6062
|
+
<pre data-run="js" data-expect="1 | 1 | x1 | 1" data-covers="export:createDataRouter config:overlap config:onUnrouted"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6063
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6064
|
+
|
|
6065
|
+
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }];
|
|
6066
|
+
<span class="kw">const</span> g1 = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6067
|
+
<span class="kw">const</span> g2 = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6068
|
+
<span class="kw">const</span> strays = [];
|
|
6069
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id', overlap: true, onUnrouted: (row) => strays.push(row.id) });
|
|
6070
|
+
router.attach(g1, 'order');
|
|
6071
|
+
router.attach(g2, 'order'); // a second viewer on the same value: allowed because overlap is on
|
|
6072
|
+
router.load([{ id: 'o1', type: 'order' }, { id: 'x1', type: 'ticket' }]);
|
|
6073
|
+
<span class="kw">const</span> out = [g1.rows.count(), g2.rows.count(), strays.join(','), router.unrouted];
|
|
6074
|
+
g1.destroy(); g2.destroy(); router.destroy();
|
|
6075
|
+
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
6076
|
+
<h3 id="datarouter-coalesce-example">Coalesce a burst into one repaint, executed</h3>
|
|
6077
|
+
<p class="section-note">A high-frequency feed goes through <code>push</code>; with <code>coalesce</code> (or a <code>batch</code> interval) rapid updates to one key fold into a single <code>apply</code>, and <code>flushStream</code> gives a deterministic point. Run headless on every build.</p>
|
|
6078
|
+
<pre data-run="js" data-expect="0 | 1 | 3" data-covers="export:createDataRouter config:push config:coalesce config:batch"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6079
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6080
|
+
|
|
6081
|
+
<span class="cmt">// A hand-rolled viewer, grid-shaped: the router accepts anything with rows.apply.</span>
|
|
6082
|
+
<span class="kw">const</span> rows = <span class="kw">new</span> Map(); <span class="kw">let</span> applies = 0;
|
|
6083
|
+
<span class="kw">const</span> g = { rows: { apply(change) {
|
|
6084
|
+
applies += 1;
|
|
6085
|
+
<span class="kw">for</span> (<span class="kw">const</span> row <span class="kw">of</span> [...(change.add || []), ...(change.update || [])]) rows.set(row.id, row);
|
|
6086
|
+
} } };
|
|
6087
|
+
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', coalesce: true, batch: { intervalMs: 50 } });
|
|
6088
|
+
router.attach(g, () => true);
|
|
6089
|
+
router.push({ op: 'upsert', row: { id: 'a', n: 1 } });
|
|
6090
|
+
router.push({ op: 'upsert', row: { id: 'a', n: 2 } });
|
|
6091
|
+
router.push({ op: 'upsert', row: { id: 'a', n: 3 } });
|
|
6092
|
+
<span class="kw">const</span> before = applies; // 0: nothing applied inside the batch window
|
|
6093
|
+
router.flushStream(); // one apply, carrying the latest value
|
|
6094
|
+
<span class="kw">const</span> after = applies;
|
|
6095
|
+
<span class="kw">const</span> n = rows.get('a').n;
|
|
6096
|
+
router.destroy();
|
|
6097
|
+
<span class="kw">return</span> [before, after, n].join(' | ');</code></pre>
|
|
6098
|
+
<h3 id="datarouter-writeback-example">A writable route, a conflict, executed</h3>
|
|
6099
|
+
<p class="section-note">A route attached <code>writable</code> captures the grid’s committed edits and routes them to <code>onWrite</code>; when the server answers with a conflict, <code>onConflict</code> fires with the server row and the optimistic value stands (last-write-wins, no merge engine). Run headless on every build.</p>
|
|
6100
|
+
<pre data-run="js" data-expect="1 | 7 | 99" data-covers="export:createDataRouter config:writable config:onWrite config:onConflict"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6101
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6102
|
+
|
|
6103
|
+
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }, { id: 'amt', field: 'amt', type: 'number', edit: true }];
|
|
6104
|
+
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6105
|
+
<span class="kw">const</span> conflicts = [];
|
|
6106
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id', onConflict: (change, ctx) => conflicts.push(ctx.serverRow.amt) });
|
|
6107
|
+
router.attach(g, 'a', { writable: true, onWrite: () => ({ conflict: { id: 'r1', type: 'a', amt: 7 } }) });
|
|
6108
|
+
router.load([{ id: 'r1', type: 'a', amt: 10 }]);
|
|
6109
|
+
g.edit.setCells([{ key: 'r1', colId: 'amt', value: 99 }]); // the user's edit, routed to onWrite
|
|
6110
|
+
<span class="kw">const</span> out = [conflicts.length, conflicts[0], g.rows.byKey('r1').data.amt];
|
|
6111
|
+
g.destroy(); router.destroy();
|
|
6112
|
+
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
6113
|
+
<h3 id="datarouter-configure-example">The routing graph as one declarative spec, executed</h3>
|
|
6114
|
+
<p class="section-note">The same routes and links the imperative calls would make, as data: <code>routes</code> attach a grid <code>when</code> a value matches, <code>links</code> filter one grid by another’s selection. Run headless on every build.</p>
|
|
6115
|
+
<pre data-run="js" data-expect="2 | 1" data-covers="export:createDataRouter config:routes config:links"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6116
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6117
|
+
|
|
6118
|
+
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }, { id: 'region', field: 'region' }];
|
|
6119
|
+
<span class="kw">const</span> customers = createHeadlessGrid({ rowKey: 'id', columns: cols, selection: 'multiple' });
|
|
6120
|
+
<span class="kw">const</span> orders = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6121
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id', selectionDebounce: 0 });
|
|
6122
|
+
router.configure({
|
|
6123
|
+
routes: [{ grid: customers, when: 'customer' }, { grid: orders, when: 'order' }],
|
|
6124
|
+
links: [{ from: customers, to: orders, on: { from: 'region', to: 'region' } }],
|
|
6125
|
+
});
|
|
6126
|
+
router.load([
|
|
6127
|
+
{ id: 'c1', type: 'customer', region: 'emea' },
|
|
6128
|
+
{ id: 'o1', type: 'order', region: 'emea' },
|
|
6129
|
+
{ id: 'o2', type: 'order', region: 'amer' },
|
|
6130
|
+
]);
|
|
6131
|
+
<span class="kw">const</span> all = orders.rows.count(); // 2: no selection shows the whole partition
|
|
6132
|
+
customers.selection.set(['c1']);
|
|
6133
|
+
router.flush();
|
|
6134
|
+
<span class="kw">const</span> linked = orders.rows.count(); // 1: only emea orders
|
|
6135
|
+
customers.destroy(); orders.destroy(); router.destroy();
|
|
6136
|
+
<span class="kw">return</span> [all, linked].join(' | ');</code></pre>
|
|
6137
|
+
<h3 id="datarouter-persist-example">Durable resume through a store of your own, executed</h3>
|
|
6138
|
+
<p class="section-note">The router snapshots to IndexedDB by default — <code>dbName</code> and <code>storeName</code> say where, and <code>indexedDB</code> lets you hand it a factory (here a tiny fake, so the example is deterministic) — or to any <code>storage</code> with async <code>get</code>/<code>set</code>. A second router over the same store <code>restore</code>s what the first one wrote. Run headless on every build.</p>
|
|
6139
|
+
<pre data-run="js" data-expect="demo | router | true | 2 | true | 1" data-covers="export:createDataRouter config:storage config:indexedDB config:dbName config:storeName"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6140
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6141
|
+
|
|
6142
|
+
<span class="cmt">// A fake IDBFactory: one database, one object store, put/get in a transaction.</span>
|
|
6143
|
+
<span class="kw">const</span> dbs = <span class="kw">new</span> Map(); <span class="kw">const</span> opened = [];
|
|
6144
|
+
<span class="kw">const</span> req = () => ({ onsuccess: null, onerror: null, onupgradeneeded: null, result: undefined, error: null });
|
|
6145
|
+
<span class="kw">const</span> fakeIndexedDB = { open(name) {
|
|
6146
|
+
<span class="kw">const</span> r = req(); opened.push(name);
|
|
6147
|
+
queueMicrotask(() => {
|
|
6148
|
+
<span class="kw">const</span> fresh = !dbs.has(name); <span class="kw">if</span> (fresh) dbs.set(name, <span class="kw">new</span> Map());
|
|
6149
|
+
<span class="kw">const</span> stores = dbs.get(name);
|
|
6150
|
+
r.result = {
|
|
6151
|
+
objectStoreNames: { contains: (n) => stores.has(n) },
|
|
6152
|
+
['createObjectStore'](n) { stores.set(n, <span class="kw">new</span> Map()); <span class="kw">return</span> {}; },
|
|
6153
|
+
transaction(n) {
|
|
6154
|
+
<span class="kw">const</span> store = stores.get(Array.isArray(n) ? n[0] : n); <span class="kw">const</span> tx = { oncomplete: null, onerror: null, onabort: null }; <span class="kw">const</span> ops = [];
|
|
6155
|
+
tx.objectStore = () => ({
|
|
6156
|
+
put(v, k) { <span class="kw">const</span> q = req(); ops.push(() => { store.set(k, v); <span class="kw">if</span> (q.onsuccess) q.onsuccess({ target: q }); }); <span class="kw">return</span> q; },
|
|
6157
|
+
get(k) { <span class="kw">const</span> q = req(); ops.push(() => { q.result = store.get(k); <span class="kw">if</span> (q.onsuccess) q.onsuccess({ target: q }); }); <span class="kw">return</span> q; },
|
|
6158
|
+
});
|
|
6159
|
+
queueMicrotask(() => { <span class="kw">for</span> (<span class="kw">const</span> op <span class="kw">of</span> ops) op(); <span class="kw">if</span> (tx.oncomplete) tx.oncomplete(); });
|
|
6160
|
+
<span class="kw">return</span> tx;
|
|
6161
|
+
},
|
|
6162
|
+
close() {},
|
|
6163
|
+
};
|
|
6164
|
+
<span class="kw">if</span> (fresh && r.onupgradeneeded) r.onupgradeneeded({ target: r });
|
|
6165
|
+
<span class="kw">if</span> (r.onsuccess) r.onsuccess({ target: r });
|
|
6166
|
+
});
|
|
6167
|
+
<span class="kw">return</span> r;
|
|
6168
|
+
} };
|
|
6169
|
+
|
|
6170
|
+
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'type', field: 'type' }];
|
|
6171
|
+
<span class="kw">const</span> a = createDataRouter({ key: 'type', rowKey: 'id' });
|
|
6172
|
+
<span class="kw">const</span> ga = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6173
|
+
a.attach(ga, 'order');
|
|
6174
|
+
a.persist({ indexedDB: fakeIndexedDB, dbName: 'demo', storeName: 'router', debounce: 0 });
|
|
6175
|
+
a.load([{ id: 'o1', type: 'order' }, { id: 'o2', type: 'order' }]);
|
|
6176
|
+
<span class="kw">await</span> a.flushPersist();
|
|
6177
|
+
|
|
6178
|
+
<span class="kw">const</span> b = createDataRouter({ key: 'type', rowKey: 'id' }); // "after the reload"
|
|
6179
|
+
<span class="kw">const</span> gb = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6180
|
+
b.attach(gb, 'order');
|
|
6181
|
+
b.persist({ indexedDB: fakeIndexedDB, dbName: 'demo', storeName: 'router' });
|
|
6182
|
+
<span class="kw">const</span> found = <span class="kw">await</span> b.restore();
|
|
6183
|
+
<span class="kw">const</span> store = [...dbs.get('demo').keys()][0];
|
|
6184
|
+
|
|
6185
|
+
<span class="cmt">// Or skip IndexedDB entirely: any async get/set pair is a store.</span>
|
|
6186
|
+
<span class="kw">const</span> mem = <span class="kw">new</span> Map();
|
|
6187
|
+
<span class="kw">const</span> c = createDataRouter({ key: 'type', rowKey: 'id' });
|
|
6188
|
+
c.attach(createHeadlessGrid({ rowKey: 'id', columns: cols }), 'order');
|
|
6189
|
+
c.persist({ storage: { get: <span class="kw">async</span> (k) => mem.get(k), set: <span class="kw">async</span> (k, v) => { mem.set(k, v); } }, debounce: 0 });
|
|
6190
|
+
c.load([{ id: 'o9', type: 'order' }]);
|
|
6191
|
+
<span class="kw">await</span> c.flushPersist();
|
|
6192
|
+
<span class="kw">const</span> out = [opened[0], store, found, gb.rows.count(), b.persisting, mem.size];
|
|
6193
|
+
ga.destroy(); gb.destroy(); a.destroy(); b.destroy(); c.destroy();
|
|
6194
|
+
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
6195
|
+
<h3 id="datarouter-backpressure-example">Throttle one viewer under load, executed</h3>
|
|
6196
|
+
<p class="section-note">A <code>backpressure</code> policy on one route caps how often <em>that</em> viewer is refreshed without slowing the store or any sibling; a burst inside the window is held to one leading refresh, and <code>flushBackpressure</code> lands the coalesced latest state. Run headless on every build.</p>
|
|
6197
|
+
<pre data-run="js" data-expect="1 | 2 | 9 | 3" data-covers="export:createDataRouter config:backpressure"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6198
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6199
|
+
|
|
6200
|
+
<span class="kw">let</span> clock = 0;
|
|
6201
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'type', rowKey: 'id', now: () => clock });
|
|
6202
|
+
<span class="kw">const</span> view = { refreshes: 0, rows: <span class="kw">new</span> Map() };
|
|
6203
|
+
router.subscribe('x', (change) => {
|
|
6204
|
+
view.refreshes += 1;
|
|
6205
|
+
<span class="kw">for</span> (<span class="kw">const</span> row <span class="kw">of</span> [...change.add, ...change.update]) view.rows.set(row.id, row);
|
|
6206
|
+
<span class="kw">for</span> (<span class="kw">const</span> key <span class="kw">of</span> change.remove) view.rows.delete(key);
|
|
6207
|
+
}, { backpressure: { maxHz: 100 } }); // at most one refresh per 10 ms
|
|
6208
|
+
<span class="kw">const</span> up = (id, n) => ({ op: 'upsert', row: { id, type: 'x', n } });
|
|
6209
|
+
router.apply([up('a', 1)]); // leading edge: refreshes at once
|
|
6210
|
+
router.apply([up('b', 2)]); // inside the window: held
|
|
6211
|
+
router.apply([up('c', 3)]);
|
|
6212
|
+
router.apply([up('a', 9)]); // still held; a's latest value wins
|
|
6213
|
+
<span class="kw">const</span> held = view.refreshes; // 1
|
|
6214
|
+
clock = 10;
|
|
6215
|
+
router.flushBackpressure(); // window open: one trailing refresh
|
|
6216
|
+
<span class="kw">const</span> out = [held, view.refreshes, view.rows.get('a').n, view.rows.size];
|
|
6217
|
+
router.destroy();
|
|
6218
|
+
<span class="kw">return</span> out.join(' | ');</code></pre>
|
|
6219
|
+
<h3 id="datarouter-time-example">Scrub by wall-clock time, executed</h3>
|
|
6220
|
+
<p class="section-note">When rows carry a timestamp, <code>time</code> names it and the buffer works in the time domain: <code>scrubTo</code> takes a moment rather than a seq, and <code>live</code> returns to the head. Run headless on every build.</p>
|
|
6221
|
+
<pre data-run="js" data-expect="1 | 3" data-covers="export:createDataRouter config:time config:buffer"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
|
|
6222
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
6223
|
+
|
|
6224
|
+
<span class="kw">const</span> cols = [{ id: 'id', field: 'id' }, { id: 'ts', field: 'ts', type: 'number' }];
|
|
6225
|
+
<span class="kw">const</span> g = createHeadlessGrid({ rowKey: 'id', columns: cols });
|
|
6226
|
+
<span class="kw">const</span> router = createDataRouter({ rowKey: 'id', time: 'ts' });
|
|
6227
|
+
router.attach(g, () => true);
|
|
6228
|
+
router.buffer({ window: 10000 });
|
|
6229
|
+
router.apply([{ op: 'upsert', row: { id: 'a', ts: 100 } }]);
|
|
6230
|
+
router.apply([{ op: 'upsert', row: { id: 'b', ts: 200 } }]);
|
|
6231
|
+
router.apply([{ op: 'upsert', row: { id: 'c', ts: 300 } }]);
|
|
6232
|
+
router.scrubTo(150, { by: 'time' }); // what the grid showed at t=150
|
|
6233
|
+
<span class="kw">const</span> then = g.rows.count(); // 1
|
|
6234
|
+
router.live();
|
|
6235
|
+
<span class="kw">const</span> now = g.rows.count(); // 3
|
|
6236
|
+
g.destroy(); router.destroy();
|
|
6237
|
+
<span class="kw">return</span> [then, now].join(' | ');</code></pre>
|
|
6053
6238
|
<h2 id="ganttmodule">The Gantt module</h2>
|
|
6054
6239
|
<p><code>modules/gantt</code> is a separate, opt-in project-planning module — its own bundle, imported only when you want it, changing nothing in the grid core. It turns a task list into a real schedule: a <strong>CPM (Critical Path Method) engine</strong> computes each task's early/late start and finish, its slack (total float), and the zero-float <strong>critical path</strong>, recomputing on every edit. <code>computeSchedule(tasks, deps)</code> is the pure engine; <code>createGantt(opts)</code> is a controller that holds the model, recomputes on <code>setTasks</code>/<code>setDependencies</code>/<code>applyEdit</code>, and emits <code>schedule</code> (or <code>error</code>). Dependencies are the four standard link types — <code>LINK_TYPES</code> is <code>['FS','SS','FF','SF']</code> — each with optional lag/lead. A <strong>milestone</strong> is a zero-duration task scheduled as a point; a <strong>summary</strong> task (any task named as another's <code>parent</code>) is derived from its children (start = earliest child, end = latest child, duration-weighted progress) and is not scheduled itself. Bad input never throws or loops: a dependency cycle is refused and reported with a code from <code>SCHEDULE_ERROR</code>, and <code>findViolations</code> flags any task placed earlier than its predecessors allow. <code>toISODate</code> converts an engine day-number back to a calendar date for display.</p>
|
|
6055
6240
|
<p><strong>Feed it the rows you already have.</strong> <code>fields</code> names your own task properties for the scheduler — <code>{ id: 'taskId', start: 'startDate', name: 'jobName', duration: 'dur' }</code>, each a field name or a reader <code>(row) => value</code> — so a task list arrives as it is rather than being renamed first. The vocabulary is <code>id</code>, <code>name</code>, <code>start</code>, <code>end</code>, <code>duration</code>, <code>milestone</code>, <code>percentComplete</code>, <code>parent</code>, <code>baselineStart</code>, <code>baselineEnd</code>, <code>constraint</code> and <code>constraintDate</code>; anything you leave unmapped reads its canonical name, so an existing plan is unaffected. <code>rowKey</code> reaches the scheduler too, so a row carrying <code>taskId</code> and no <code>id</code> is identified by it — a task's own <code>id</code> still wins where it has one. It is a <strong>read</strong> mapping: <code>applyEdit</code> and <code>level()</code> write the canonical property, so each says so plainly rather than writing to a field the schedule is not read from, and <code>assignee</code>/<code>cost</code>/<code>actualCost</code> belong to the resource and earned-value layers rather than to this list.</p>
|
|
@@ -9476,18 +9661,75 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
9476
9661
|
<table>
|
|
9477
9662
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
9478
9663
|
<tbody>
|
|
9479
|
-
<tr><td class="name">attach</td><td class="type">(grid: unknown, predicate: RoutePredicate, opts?: RouteOptions): DataRouter</td><td class="desc">Attach a grid behind a predicate; `opts` may reshape
|
|
9480
|
-
<tr><td class="name">attachDefault</td><td class="type">(grid: unknown, opts?: RouteOptions): DataRouter</td><td class="desc">Attach the "rest" sink for records no explicit route matched.</td></tr>
|
|
9481
|
-
<tr><td class="name">
|
|
9482
|
-
<tr><td class="name">
|
|
9483
|
-
<tr><td class="name">
|
|
9664
|
+
<tr><td class="name">attach</td><td class="type">(grid: unknown, predicate: RoutePredicate, opts?: RouteOptions): DataRouter</td><td class="desc">Attach a grid behind a predicate; `opts` may reshape, filter, sort, summarise or throttle the route.</td></tr>
|
|
9665
|
+
<tr><td class="name">attachDefault</td><td class="type">(grid: unknown, opts?: RouteOptions): DataRouter</td><td class="desc">Attach the "rest" sink for records no explicit route matched. A second call replaces the first.</td></tr>
|
|
9666
|
+
<tr><td class="name">subscribe</td><td class="type">(predicate: RoutePredicate, handler: (change: RouterChange) => void, opts?: RouteOptions): DataRouter</td><td class="desc">Route a partition slice to any non-grid view (v5): the handler receives the same keyed diff a grid would.</td></tr>
|
|
9667
|
+
<tr><td class="name">alert</td><td class="type">(predicate: RoutePredicate, condition: (rows: RouterRecord[]) => unknown, handler: (signal: unknown, rows: RouterRecord[]) => void, opts?: AlertOptions): DataRouter</td><td class="desc">Watch a slice and emit on a rising edge of `condition` rather than render (v5). Removed only by `destroy`.</td></tr>
|
|
9668
|
+
<tr><td class="name">configure</td><td class="type">(spec?: RouterConfig): DataRouter</td><td class="desc">Take the whole routing graph as one declarative spec (v5); desugars to the calls above and composes with them.</td></tr>
|
|
9484
9669
|
<tr><td class="name">link</td><td class="type">(source: unknown, target: unknown, relation: SelectionRelation): DataRouter</td><td class="desc">Link a source grid's selection to what a target grid receives (v2, BACKLOG-0000880): the target shows the subset of its partition the `relation` admits, re-pushed through the keyed-diff path. No selection shows the full partition; changes are debounced.</td></tr>
|
|
9670
|
+
<tr><td class="name">relate</td><td class="type">(edges: RouterEdge[]): DataRouter</td><td class="desc">Declare a relationship graph (v3): multi-hop, several-into-one and mutual edges — the scalable form of `link`.</td></tr>
|
|
9485
9671
|
<tr><td class="name">flush</td><td class="type">(): DataRouter</td><td class="desc">Apply any debounced selection refilter synchronously (for tests/determinism).</td></tr>
|
|
9486
|
-
<tr><td class="name">
|
|
9672
|
+
<tr><td class="name">detach</td><td class="type">(grid: unknown): DataRouter</td><td class="desc">Detach a grid — or a `subscribe` handler — and drop any link it is part of; the host still owns and destroys it.</td></tr>
|
|
9673
|
+
<tr><td class="name">load</td><td class="type">(snapshot: RouterRecord[]): RouteDiff[]</td><td class="desc">Apply a full snapshot as a keyed diff per grid; returns per-route counts. Resets `unrouted`.</td></tr>
|
|
9674
|
+
<tr><td class="name">apply</td><td class="type">(deltas: RouterDelta[]): void</td><td class="desc">Apply incremental deltas, routed and applied in place by `rowKey`; ordered and de-duplicated when `seq` is on.</td></tr>
|
|
9675
|
+
<tr><td class="name">push</td><td class="type">(delta: RouterDelta | RouterDelta[]): DataRouter</td><td class="desc">Enqueue deltas for batched or coalesced application (v3); applies at once when no batching mode is on.</td></tr>
|
|
9676
|
+
<tr><td class="name">flushStream</td><td class="type">(): DataRouter</td><td class="desc">Apply the buffered deltas now as a single `apply` (v3) — a deterministic point, and for tests.</td></tr>
|
|
9677
|
+
<tr><td class="name">flushBackpressure</td><td class="type">(): DataRouter</td><td class="desc">Refresh every backpressured route to the latest state now (v13); a no-op with nothing pending.</td></tr>
|
|
9678
|
+
<tr><td class="name">addSource</td><td class="type">(feed: string | RouterSourceOptions, opts?: RouterSourceOptions): RouterSourceHandle</td><td class="desc">Register a source feed for fan-in (v9): its rows are normalised and namespaced into the one keyed store.</td></tr>
|
|
9679
|
+
<tr><td class="name">removeSource</td><td class="type">(ref: string | RouterSourceHandle): DataRouter</td><td class="desc">Remove a source feed by id or handle (v9): delete exactly its rows from every route, then unregister it.</td></tr>
|
|
9680
|
+
<tr><td class="name">sources</td><td class="type">(): string[]</td><td class="desc">The registered source ids (v9).</td></tr>
|
|
9681
|
+
<tr><td class="name">metrics</td><td class="type">(): RouterMetrics</td><td class="desc">A cheap point-in-time snapshot of the router's runtime (v10); throughput is measured since the previous read.</td></tr>
|
|
9682
|
+
<tr><td class="name">on</td><td class="type">(event: 'metrics', handler: (snapshot: RouterMetrics) => void): () => void</td><td class="desc">Subscribe to the periodic `metrics` emit (v10) — the only event; the timer runs only while a listener is registered. Returns the unsubscribe.</td></tr>
|
|
9683
|
+
<tr><td class="name">mountDevtools</td><td class="type">(el: unknown): RouterDevtoolsPanel</td><td class="desc">Mount the live devtools panel into `el` (v10); it re-renders on each `metrics` emit.</td></tr>
|
|
9684
|
+
<tr><td class="name">unrouted</td><td class="type">number</td><td class="desc">How many records matched no route since the last `load` or `query`, running for deltas. <small>(read-only)</small></td></tr>
|
|
9685
|
+
<tr><td class="name">dropped</td><td class="type">number</td><td class="desc">How many stale or duplicate deltas the dedupe gate dropped since creation (v3). <small>(read-only)</small></td></tr>
|
|
9686
|
+
<tr><td class="name">lastSeq</td><td class="type">(): number | undefined</td><td class="desc">The highest seq applied — the resume point to request the feed from after a dropped socket (v3).</td></tr>
|
|
9687
|
+
<tr><td class="name">checkpoint</td><td class="type">(): Map<string, number></td><td class="desc">A copy of the per-record resume checkpoint: record identity → last applied seq (v3).</td></tr>
|
|
9688
|
+
<tr><td class="name">seenThrough</td><td class="type">(mark: Map<string, number> | Record<string, number>): DataRouter</td><td class="desc">Prime the resume checkpoint from a persisted one, so replayed deltas at or below those seqs are dropped (v3).</td></tr>
|
|
9689
|
+
<tr><td class="name">persist</td><td class="type">(opts?: RouterPersistOptions): DataRouter</td><td class="desc">Turn on durable persistence of the router's state (v12).</td></tr>
|
|
9690
|
+
<tr><td class="name">restore</td><td class="type">(): Promise<boolean></td><td class="desc">Resume from the durable snapshot (v12); resolves true when one was found and applied.</td></tr>
|
|
9691
|
+
<tr><td class="name">flushPersist</td><td class="type">(): Promise<DataRouter></td><td class="desc">Flush any pending durable write now (v12); resolves once it has settled.</td></tr>
|
|
9692
|
+
<tr><td class="name">persisting</td><td class="type">boolean</td><td class="desc">Whether durable persistence is on and not degraded to in-memory (v12). <small>(read-only)</small></td></tr>
|
|
9693
|
+
<tr><td class="name">buffer</td><td class="type">(opts?: { window?: number; max?: number }): DataRouter</td><td class="desc">Record the stream into a bounded ring for time travel (v4): a time `window` in ms and/or a `max` delta count.</td></tr>
|
|
9694
|
+
<tr><td class="name">scrubTo</td><td class="type">(target: number, opts?: { by?: 'seq' | 'time' }): DataRouter</td><td class="desc">Scrub the attached grids to a past seq or timestamp (v4).</td></tr>
|
|
9695
|
+
<tr><td class="name">replay</td><td class="type">(from: number, to: number, opts?: { speed?: number; by?: 'seq' | 'time' }): Promise<void></td><td class="desc">Replay a buffered range step by step (v4); resolves when it completes or is superseded.</td></tr>
|
|
9696
|
+
<tr><td class="name">pause</td><td class="type">(): DataRouter</td><td class="desc">Pause an in-flight replay at the current step (v4); a no-op when nothing is replaying.</td></tr>
|
|
9697
|
+
<tr><td class="name">resume</td><td class="type">(): DataRouter</td><td class="desc">Resume a paused replay from where it stopped (v4); a no-op when not paused.</td></tr>
|
|
9698
|
+
<tr><td class="name">live</td><td class="type">(): DataRouter</td><td class="desc">Return to live (v4): rebuild the head from the base plus every buffered delta.</td></tr>
|
|
9699
|
+
<tr><td class="name">traveling</td><td class="type">boolean</td><td class="desc">Whether the grids are currently showing a reconstructed past (v4). <small>(read-only)</small></td></tr>
|
|
9700
|
+
<tr><td class="name">buffered</td><td class="type">number</td><td class="desc">How many deltas the bounded buffer currently holds (v4). <small>(read-only)</small></td></tr>
|
|
9701
|
+
<tr><td class="name">broadcast</td><td class="type">(opts: { channel: string }): DataRouter</td><td class="desc">Mirror the ordered, de-duplicated deltas to other tabs over a BroadcastChannel (v6), with no echo loop.</td></tr>
|
|
9702
|
+
<tr><td class="name">broadcasting</td><td class="type">boolean</td><td class="desc">Whether the router is mirroring to a BroadcastChannel (v6). <small>(read-only)</small></td></tr>
|
|
9703
|
+
<tr><td class="name">query</td><td class="type">(adapter: RouterQueryAdapter, request?: Record<string, unknown>): Promise<DataRouter></td><td class="desc">Source the router from a pushdown adapter (v7): each `where` route is planned against the adapter's capabilities.</td></tr>
|
|
9704
|
+
<tr><td class="name">lastQueryPlan</td><td class="type">(): RouterQueryPlanEntry[] | null</td><td class="desc">The pushed/residual split of the last `query()` (v7), per fetch, or null before any.</td></tr>
|
|
9487
9705
|
<tr><td class="name">destroy</td><td class="type">(): void</td><td class="desc">Detach every grid and drop every link (the host destroys the grids themselves).</td></tr>
|
|
9488
9706
|
</tbody>
|
|
9489
9707
|
</table>
|
|
9490
9708
|
</div>
|
|
9709
|
+
<h3 id="type-DataRouterOptions">DataRouterOptions</h3>
|
|
9710
|
+
<p class="section-note">Options for `createDataRouter`. `key` is the partition property or `fn(row)`; optional, since a router whose routes all use `fn(row)` predicates never reads it. `rowKey` is the identity within a grid; `overlap` fans a record to every matching route (default: first match wins); `onUnrouted` receives what matched no route — the row on `load` and `query`, the whole delta on `apply`; `selectionDebounce` is the ms debounce for cross-grid selection refilters (default 16; `0` is synchronous). `seq` names the per-record version that orders and de-duplicates a feed (v3), `dedupe` (default on with `seq`) drops stale and duplicate deltas; `batch` (ms, or `{ intervalMs }`) and `coalesce` buffer a high-frequency feed for `push`; `time` reads a row's timestamp for time-domain scrubbing and `now` overrides the clock (v4); `config` is a declarative routing graph applied at construction (v5); `onWrite` and `onConflict` are the defaults for every writable route (v8); `metricsInterval` is the ms between `metrics` emits (default 1000; `0` disables the timer) (v10).</p>
|
|
9711
|
+
<div class="table-wrap">
|
|
9712
|
+
<table>
|
|
9713
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
9714
|
+
<tbody>
|
|
9715
|
+
<tr><td class="name">key</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9716
|
+
<tr><td class="name">rowKey</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9717
|
+
<tr><td class="name">overlap</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9718
|
+
<tr><td class="name">onUnrouted</td><td class="type">(item: RouterRecord | RouterDelta) => void</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9719
|
+
<tr><td class="name">selectionDebounce</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9720
|
+
<tr><td class="name">seq</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9721
|
+
<tr><td class="name">dedupe</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9722
|
+
<tr><td class="name">batch</td><td class="type">number | { intervalMs: number }</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9723
|
+
<tr><td class="name">coalesce</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9724
|
+
<tr><td class="name">time</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9725
|
+
<tr><td class="name">now</td><td class="type">() => number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9726
|
+
<tr><td class="name">config</td><td class="type">RouterConfig</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9727
|
+
<tr><td class="name">onWrite</td><td class="type">(change: RouterWrite, ctx: { route: unknown; source: unknown }) => unknown</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9728
|
+
<tr><td class="name">onConflict</td><td class="type">(change: RouterWrite, ctx: { serverRow: RouterRecord }) => void</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9729
|
+
<tr><td class="name">metricsInterval</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
9730
|
+
</tbody>
|
|
9731
|
+
</table>
|
|
9732
|
+
</div>
|
|
9491
9733
|
<h3 id="type-DatasetColumnDifference">DatasetColumnDifference</h3>
|
|
9492
9734
|
<div class="table-wrap">
|
|
9493
9735
|
<table>
|
|
@@ -12589,15 +12831,133 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
12589
12831
|
</table>
|
|
12590
12832
|
</div>
|
|
12591
12833
|
<h3 id="type-RouteOptions">RouteOptions</h3>
|
|
12592
|
-
<p class="section-note">Per-route
|
|
12834
|
+
<p class="section-note">Per-route options, shared by `attach`, `attachDefault` and `subscribe`. `rowKey` overrides the router default for this route. `transform` reshapes each row before the viewer sees it; `filter` admits a subset; `sort` orders what the viewer receives; `rollup` summarises the slice (v3). A `transform` or `rollup` route is derived and cannot be `writable`. `where` is read only by `query()` (v7). `writable` routes the grid's committed edits to `onWrite`, reverting on reject, with `onConflict` for a last- write-wins conflict (v8); both default to the router's own. `label` names the route in metrics and the devtools panel; `backpressure` throttles its refresh under load (v13).</p>
|
|
12593
12835
|
<div class="table-wrap">
|
|
12594
12836
|
<table>
|
|
12595
12837
|
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12596
12838
|
<tbody>
|
|
12597
|
-
<tr><td class="name">rowKey</td><td class="type">
|
|
12839
|
+
<tr><td class="name">rowKey</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12598
12840
|
<tr><td class="name">transform</td><td class="type">(row: RouterRecord) => RouterRecord</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12599
12841
|
<tr><td class="name">filter</td><td class="type">(row: RouterRecord) => boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12600
|
-
<tr><td class="name">sort</td><td class="type">
|
|
12842
|
+
<tr><td class="name">sort</td><td class="type">RouteSort</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12843
|
+
<tr><td class="name">rollup</td><td class="type">RouteRollup</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12844
|
+
<tr><td class="name">where</td><td class="type">RouteWhere</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12845
|
+
<tr><td class="name">writable</td><td class="type">boolean</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12846
|
+
<tr><td class="name">onWrite</td><td class="type">(change: RouterWrite, ctx: { route: unknown; source: unknown }) => unknown</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12847
|
+
<tr><td class="name">onConflict</td><td class="type">(change: RouterWrite, ctx: { serverRow: RouterRecord }) => void</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12848
|
+
<tr><td class="name">label</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12849
|
+
<tr><td class="name">backpressure</td><td class="type">RouteBackpressure</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12850
|
+
</tbody>
|
|
12851
|
+
</table>
|
|
12852
|
+
</div>
|
|
12853
|
+
<h3 id="type-RouterConfig">RouterConfig</h3>
|
|
12854
|
+
<p class="section-note">A declarative routing graph (v5, BACKLOG-0000910): the same routes, links, relationship edges and buffer the imperative calls would make, as one data spec. Desugars to those calls and composes with them.</p>
|
|
12855
|
+
<div class="table-wrap">
|
|
12856
|
+
<table>
|
|
12857
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12858
|
+
<tbody>
|
|
12859
|
+
<tr><td class="name">routes</td><td class="type">Record<string, unknown>[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12860
|
+
<tr><td class="name">links</td><td class="type">{ from: unknown; to: unknown; on?: SelectionRelation; relation?: SelectionRelation }[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12861
|
+
<tr><td class="name">relate</td><td class="type">RouterEdge[]</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12862
|
+
<tr><td class="name">buffer</td><td class="type">{ window?: number; max?: number }</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12863
|
+
</tbody>
|
|
12864
|
+
</table>
|
|
12865
|
+
</div>
|
|
12866
|
+
<h3 id="type-RouterJoin">RouterJoin</h3>
|
|
12867
|
+
<p class="section-note">A fan-in source's lookup join (v11): `from` is the lookup source's id; `localKey` (alias `on`) reads the joining value off this source's row; `foreignKey` (alias `fromKey`) reads it off the lookup row, defaulting to a string `localKey`; `fields` (alias `select`) picks the lookup fields to carry — a list, a rename map, or a function of both rows; `missing` says what to do while the lookup row has not arrived: `hold` the row back, `passthrough` it unjoined, or fill the fields with `null`.</p>
|
|
12868
|
+
<div class="table-wrap">
|
|
12869
|
+
<table>
|
|
12870
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12871
|
+
<tbody>
|
|
12872
|
+
<tr><td class="name">from</td><td class="type">string</td><td class="desc"></td></tr>
|
|
12873
|
+
<tr><td class="name">localKey</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12874
|
+
<tr><td class="name">on</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12875
|
+
<tr><td class="name">foreignKey</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12876
|
+
<tr><td class="name">fromKey</td><td class="type">RouterKey</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12877
|
+
<tr><td class="name">fields</td><td class="type">string[] | Record<string, string> | ((lookupRow: RouterRecord | null, leftRow: RouterRecord) => RouterRecord)</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12878
|
+
<tr><td class="name">select</td><td class="type">string[] | Record<string, string> | ((lookupRow: RouterRecord | null, leftRow: RouterRecord) => RouterRecord)</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12879
|
+
<tr><td class="name">missing</td><td class="type">'hold' | 'passthrough' | 'null'</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12880
|
+
</tbody>
|
|
12881
|
+
</table>
|
|
12882
|
+
</div>
|
|
12883
|
+
<h3 id="type-RouterMetrics">RouterMetrics</h3>
|
|
12884
|
+
<p class="section-note">A `metrics()` snapshot (v10): per-route and per-source counts and throughput (rows/sec since the previous read), and the global unrouted, dropped (duplicate), buffered and lag figures.</p>
|
|
12885
|
+
<div class="table-wrap">
|
|
12886
|
+
<table>
|
|
12887
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12888
|
+
<tbody>
|
|
12889
|
+
<tr><td class="name">routes</td><td class="type">RouterRouteMetrics[]</td><td class="desc"></td></tr>
|
|
12890
|
+
<tr><td class="name">sources</td><td class="type">RouterSourceMetrics[]</td><td class="desc"></td></tr>
|
|
12891
|
+
<tr><td class="name">unrouted</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12892
|
+
<tr><td class="name">dropped</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12893
|
+
<tr><td class="name">buffered</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12894
|
+
<tr><td class="name">lag</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12895
|
+
<tr><td class="name">throughput</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12896
|
+
</tbody>
|
|
12897
|
+
</table>
|
|
12898
|
+
</div>
|
|
12899
|
+
<h3 id="type-RouteRollup">RouteRollup</h3>
|
|
12900
|
+
<p class="section-note">A rollup route's summary spec (v3): one summary row per `groupBy` group, each `aggregate` a named reducer over the group's rows or a `{ op, field }` shorthand (`sum`, `avg`, `min`, `max`, `count`).</p>
|
|
12901
|
+
<div class="table-wrap">
|
|
12902
|
+
<table>
|
|
12903
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12904
|
+
<tbody>
|
|
12905
|
+
<tr><td class="name">groupBy</td><td class="type">RouterKey | RouterKey[]</td><td class="desc"></td></tr>
|
|
12906
|
+
<tr><td class="name">aggregate</td><td class="type">Record<string, ((rows: RouterRecord[]) => unknown) | { op: string; field?: string }></td><td class="desc"><small>(optional)</small></td></tr>
|
|
12907
|
+
</tbody>
|
|
12908
|
+
</table>
|
|
12909
|
+
</div>
|
|
12910
|
+
<h3 id="type-RouterPersistOptions">RouterPersistOptions</h3>
|
|
12911
|
+
<p class="section-note">Durable persistence options (v12): `key` names the snapshot, `debounce` (ms) coalesces writes, `storage` is a `{ get, set }` pair of your own, or `indexedDB` / `dbName` / `storeName` select the browser store.</p>
|
|
12912
|
+
<div class="table-wrap">
|
|
12913
|
+
<table>
|
|
12914
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12915
|
+
<tbody>
|
|
12916
|
+
<tr><td class="name">key</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12917
|
+
<tr><td class="name">debounce</td><td class="type">number</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12918
|
+
<tr><td class="name">storage</td><td class="type">{ get: (key: string) => Promise<unknown>; set: (key: string, value: unknown) => Promise<void> }</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12919
|
+
<tr><td class="name">indexedDB</td><td class="type">unknown</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12920
|
+
<tr><td class="name">dbName</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12921
|
+
<tr><td class="name">storeName</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
|
|
12922
|
+
</tbody>
|
|
12923
|
+
</table>
|
|
12924
|
+
</div>
|
|
12925
|
+
<h3 id="type-RouterQueryAdapter">RouterQueryAdapter</h3>
|
|
12926
|
+
<p class="section-note">A pushdown adapter `query()` can source the router from (v7): anything with an `execute(query, request)` returning rows, and optional `capabilities` the planner consults to decide what it may push down.</p>
|
|
12927
|
+
<div class="table-wrap">
|
|
12928
|
+
<table>
|
|
12929
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12930
|
+
<tbody>
|
|
12931
|
+
<tr><td class="name">capabilities</td><td class="type">Record<string, unknown></td><td class="desc"><small>(optional)</small></td></tr>
|
|
12932
|
+
<tr><td class="name">execute</td><td class="type">(query: Record<string, unknown>, request?: Record<string, unknown>) => Promise<{ rows: RouterRecord[]; total?: number }></td><td class="desc"></td></tr>
|
|
12933
|
+
</tbody>
|
|
12934
|
+
</table>
|
|
12935
|
+
</div>
|
|
12936
|
+
<h3 id="type-RouterRouteMetrics">RouterRouteMetrics</h3>
|
|
12937
|
+
<p class="section-note">One route's figures in a `metrics()` snapshot (v10).</p>
|
|
12938
|
+
<div class="table-wrap">
|
|
12939
|
+
<table>
|
|
12940
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12941
|
+
<tbody>
|
|
12942
|
+
<tr><td class="name">label</td><td class="type">string | null</td><td class="desc"></td></tr>
|
|
12943
|
+
<tr><td class="name">rows</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12944
|
+
<tr><td class="name">shown</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12945
|
+
<tr><td class="name">throughput</td><td class="type">number</td><td class="desc"></td></tr>
|
|
12946
|
+
</tbody>
|
|
12947
|
+
</table>
|
|
12948
|
+
</div>
|
|
12949
|
+
<h3 id="type-RouterSourceHandle">RouterSourceHandle</h3>
|
|
12950
|
+
<p class="section-note">The handle `addSource` returns for one feed (v9). Its `load` is a per-source snapshot — a keyed diff over this feed's rows only, other feeds untouched; `apply` and `push` take this feed's deltas through the router's ordinary and batched paths; `remove` deletes exactly the rows it holds and unregisters it, returning the router.</p>
|
|
12951
|
+
<div class="table-wrap">
|
|
12952
|
+
<table>
|
|
12953
|
+
<thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
|
|
12954
|
+
<tbody>
|
|
12955
|
+
<tr><td class="name">id</td><td class="type">string</td><td class="desc">The source id. <small>(read-only)</small></td></tr>
|
|
12956
|
+
<tr><td class="name">size</td><td class="type">number</td><td class="desc">How many rows this source currently holds live. <small>(read-only)</small></td></tr>
|
|
12957
|
+
<tr><td class="name">load</td><td class="type">(rows: RouterRecord[]): RouterSourceHandle</td><td class="desc">Apply a per-source snapshot: upsert its current rows, delete the ones it no longer has.</td></tr>
|
|
12958
|
+
<tr><td class="name">apply</td><td class="type">(deltas: RouterDelta[]): RouterSourceHandle</td><td class="desc">Apply per-source deltas through the router's ordinary apply path.</td></tr>
|
|
12959
|
+
<tr><td class="name">push</td><td class="type">(delta: RouterDelta | RouterDelta[]): RouterSourceHandle</td><td class="desc">Enqueue per-source deltas through the router's stream path (batching honoured).</td></tr>
|
|
12960
|
+
<tr><td class="name">remove</td><td class="type">(): DataRouter</td><td class="desc">Remove this source: delete exactly the rows it holds from every route, then unregister it.</td></tr>
|
|
12601
12961
|
</tbody>
|
|
12602
12962
|
</table>
|
|
12603
12963
|
</div>
|
|
@@ -13444,7 +13804,7 @@ return `next ${p.mean.toFixed(1)}; r2 ${f.r2.toFixed(1)}; band ${p.upper - p.low
|
|
|
13444
13804
|
<!-- END GENERATED TYPE REFERENCE -->
|
|
13445
13805
|
|
|
13446
13806
|
<footer>
|
|
13447
|
-
Lattice Grid 1.63.
|
|
13807
|
+
Lattice Grid 1.63.3 · Copyright © 2026 TOCLOCO Inc. All rights reserved.
|
|
13448
13808
|
This document describes the behaviour of the shipped library. Where this guide and the code
|
|
13449
13809
|
disagree, the code wins: please <a href="https://www.latticegrid.dev">tell us</a>.
|
|
13450
13810
|
</footer>
|