backend recipe
Stream a Feed Into the Data Router
Last updated 29 September 2026
A live screen is rarely one grid. It is several views moving because the same events land behind all of them: an orders grid, an alerts panel, a KPI tile, each showing only its own slice of one feed. The Data Router reads that feed once, splits it by a property on each record, and hands each attached grid or chart only the rows that belong to it, live.
The code
router.attach(grid, value) claims every record whose partition key equals
value for that grid; a record matching no route flows nowhere unless a default sink
is attached. Feeding the router is two calls: router.load(rows) for the opening
snapshot, router.apply(batch) for every delta after that, whatever transport they
arrive over.
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
import { createGrid } from '@toclocoinc/lattice-grid';
// One router, no feed-specific code in it: it only knows the shape
// { id, type, ... } and a key to partition on.
const router = createDataRouter({ key: 'type', rowKey: 'id' });
const orders = createGrid(document.querySelector('#orders'), { rowKey: 'id', columns: orderCols });
const alerts = createGrid(document.querySelector('#alerts'), { rowKey: 'id', columns: alertCols });
// Every record whose type is 'order' reaches the orders grid, and nothing else does.
router.attach(orders, 'order');
// A busier route can decline to refresh its viewer on every single record: below
// a backlog of 200 pending rows it updates as normal; past it, it coalesces the
// backlog into one refresh per 100ms rather than one per record. flushBackpressure()
// (or a trailing flush the router runs itself) always lands the latest state, so
// the alerts grid is never left showing something stale.
router.attach(alerts, 'alert', {
backpressure: { maxLag: 200, minInterval: 100 },
});
// A stand-in feed: a snapshot the moment it opens, then a burst of deltas on a
// timer, from a seeded generator - the same shape a real WebSocket delivers,
// swapped in with no other code change (new WebSocket('wss://...') in place of
// MockWebSocket below).
const socket = new MockWebSocket({ feed: opsFeed({ seed: 7 }), rate: 900, jitter: 300 });
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.kind === 'snapshot') router.load(message.rows);
else router.apply(message.changes.map((c) => c.row));
};
The feed above is MockWebSocket, a real, shipped stand-in for a live socket: a
snapshot the moment it opens, then deltas on a timer from a seeded generator, so this runs with no
server at all. See the mock socket guide for the one-line
swap to a real WebSocket, or the Data Router overview for
a live version of this same pattern with three linked views.
The held limit on a busy route
A route with no backpressure policy refreshes its grid on every change, which is
correct and usually cheap. A route whose feed is much busier than its viewer needs, an alerts
panel fed at wire speed but read by a person, can decline that: maxLag is a backlog
depth, a count of pending changes the route is holding for that viewer alone. Below it, changes
pass straight through; past it, minInterval (or maxHz) caps how often
the viewer actually refreshes, coalescing everything held in between into the one refresh that
lands. The held backlog affects only that route's own viewer, never the shared keyed store other
routes read from, and a trailing flush always lands the latest state, deletes included, so the
view converges rather than staying stale.
// The held backlog, read rather than guessed: pending is how many changes
// are currently waiting behind the alerts route's policy, coalesced is how
// many separate change-events have been folded into a deferred refresh so far.
const { backpressure } = router.metrics().routes.find((r) => r.label === 'alert') ?? {};
console.log(backpressure); // { pending: 0, coalesced: 0 } once it has caught up
// Each attached grid still runs the ordinary updates pipeline underneath: only
// the cells a delta actually changed repaint, batched to one pass per frame.
console.log(orders.updates.stats());
The warning a host sees
Attach two grids to the same literal partition value, both listening for 'order'
say, and only the first receives rows by default. The router says so once, naming the value:
Data Router: value "order" already routes to another viewer; with overlap:false only the first receives rows. Pass createDataRouter({ overlap: true }) to fan one value to several viewers.
Ports wanted
There is no separate server for this guide: the router reads whatever feed you already have, a WebSocket, server-sent events or an async iterator, so the transport is your own choice rather than something a reference repository would fix. For a request/response backend recipe with a runnable reference server instead, see the backend recipes repository, open to PHP, .NET and Java contributions under its own "Ports wanted" section.
See stream live updates into a grid for a single grid reading a feed directly with a rolling row window, or realtime applications for a fuller network-operations screen built the same way.