Lattice Grid Buy a licence

demo D287

A feed that survives a reload

The routed rows written to the browser’s durable store and read straight back on the next visit, beside a fast feed held to a steady repaint rate so a firehose lands smoothly

router.persist() · router.restore()

Building…
Loading a live grid…

The configuration

'data-router-persistence': () => ({
  rows: [], config: {},
  foot: [
    'the routed rows are written to the browser’s durable store, so a page reload brings the desk back where you left it',
    'reload the page: the orders return from storage before any new event arrives',
    'the fast feed is held to a fixed number of repaints a second, folding the updates in between into one',
    'the coalesced count is how many fast updates were merged away to keep the grid smooth',
  ],
  mount: (el: HTMLElement, LG: any) => {
    let a = 20270418 >>> 0;
    const rnd = () => { a = (a + 0x6d2b79f5) >>> 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
    const pick = <T,>(list: T[]) => list[Math.floor(rnd() * list.length)];
    const REGIONS = ['EMEA', 'AMER', 'APAC'];
    const money = { style: 'currency', currency: 'USD', decimals: 0 } as const;
    let orderId = 1;
    const order = () => ({ id: 'O-' + String(1000 + orderId++), type: 'order', region: pick(REGIONS), value: 200 + Math.round(rnd() * 9800) });
    const sensor = () => ({ id: 'S-' + (1 + Math.floor(rnd() * 12)), type: 'sensor', reading: Math.round(rnd() * 100) });
    const bar = document.createElement('div');
    bar.style.cssText = 'display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:12px';
    const note = document.createElement('span');
    note.style.cssText = 'font:12.5px system-ui;color:var(--ink-2)';
    note.textContent = 'starting a fresh session ...';
    const resetBtn = document.createElement('button');
    resetBtn.type = 'button';
    resetBtn.textContent = 'Clear saved state';
    resetBtn.style.cssText = 'font:inherit;padding:5px 11px;border:1px solid var(--rule);background:var(--paper);color:var(--ink);border-radius:7px;cursor:pointer';
    bar.append(note, resetBtn);
    const gridsEl = document.createElement('div');
    gridsEl.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:12px';
    const { wrap: ordersWrap, host: ordersEl } = titledGrid('Orders, persisted across reloads');
    const { wrap: sensorWrap, host: sensorEl } = titledGrid('Sensor feed, held to a steady rate');
    ordersEl.style.cssText = 'height:340px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    sensorEl.style.cssText = 'height:340px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    gridsEl.append(ordersWrap, sensorWrap);
    const metricsEl = document.createElement('p');
    metricsEl.style.cssText = 'font:12.5px var(--mono,monospace);color:var(--ink-2);margin:12px 0 0';
    el.append(bar, gridsEl, metricsEl);
    const orderCols = [
      { field: 'id', title: 'Ref', layout: { width: 110, pin: 'start' } },
      { field: 'region', title: 'Region', filter: { type: 'set' } },
      { field: 'value', title: 'Value', type: 'number', format: money, total: 'sum' },
    ];
    const sensorCols = [
      { field: 'id', title: 'Sensor', layout: { width: 120, pin: 'start' } },
      { field: 'reading', title: 'Reading', type: 'number' },
    ];
    const orders = LG.createGrid(ordersEl, { rowKey: 'id', theme: 'light', columns: orderCols, rows: [] });
    const sensors = LG.createGrid(sensorEl, { rowKey: 'id', theme: 'light', columns: sensorCols, rows: [] });
    const PERSIST_KEY = 'lattice-demo-router-persistence';
    let router: any;
    let orderTimer: any;
    let sensorTimer: any;
    let metricsTimer: any;
    loadDataRouter()
      .then(async (dr: any) => {
        router = dr.createDataRouter({ key: 'type', rowKey: 'id' });
        router.attach(orders, 'order', { label: 'orders' });
        router.attach(sensors, 'sensor', { label: 'sensors', backpressure: { maxHz: 4 } });
        router.persist({ key: PERSIST_KEY });
        let restored = 0;
        try {
          const ok = await router.restore();
          if (ok) restored = orders.rows.count();
        } catch {  }
        if (restored > 0) {
          note.textContent = `restored ${restored.toLocaleString('en-GB')} orders from your last visit - reload again and they will still be here`;
        } else {
          note.textContent = 'seeding the desk - now reload the page and the orders come straight back from storage';
          const seed: any[] = [];
          for (let i = 0; i < 24; i++) seed.push(order());
          for (let i = 1; i <= 12; i++) seed.push({ id: 'S-' + i, type: 'sensor', reading: Math.round(rnd() * 100) });
          router.load(seed);
        }
        orderTimer = setInterval(() => {
          if (document.hidden) return;
          router.apply([{ op: 'upsert', row: order() }]);
        }, 2000);
        sensorTimer = setInterval(() => {
          if (document.hidden) return;
          const batch: any[] = [];
          for (let i = 0; i < 12; i++) batch.push({ op: 'upsert', row: sensor() });
          router.apply(batch);
        }, 120);
        metricsTimer = setInterval(() => {
          const m = router.metrics();
          const sensorRoute = m.routes.find((r: any) => r.label === 'sensors');
          const coalesced = sensorRoute?.backpressure?.coalesced ?? 0;
          metricsEl.textContent =
            `orders in storage: ${orders.rows.count().toLocaleString('en-GB')}` +
            `   ·   sensor updates coalesced: ${Number(coalesced).toLocaleString('en-GB')}`;
        }, 500);
      })
      .catch((err: any) => { note.textContent = 'could not load the data router: ' + (err?.message ?? 'error'); console.error('[data-router-persistence]', err); });
    resetBtn.addEventListener('click', async () => {
      if (!router) return;
      try {
        router.load([]);
        await router.flushPersist?.();
        note.textContent = 'saved state cleared - reload the page to start a fresh session';
      } catch (err) {
        console.error('[data-router-persistence:reset]', err);
      }
    });
    return () => {
      clearInterval(orderTimer);
      clearInterval(sensorTimer);
      clearInterval(metricsTimer);
      router?.destroy?.();
      orders?.destroy?.();
      sensors?.destroy?.();
    };
  },
})

A live desk that comes back exactly where you left it

A screen built on a live feed usually starts empty and waits for the feed to refill it. Reload the page and the desk you were reading is gone until the next batch arrives. This keeps it. As routed rows arrive they are written to the browser’s own durable store, so when the page loads again the desk comes back exactly where you left it, before a single new event has landed. Reload the tab and the orders are already there.

The same router that fans one feed out to several views is the one doing the saving, so nothing extra sits between the feed and the screen. Name a key to store under and the routed rows are kept up to date as they change; ask for them back on load and they return. Where the browser will not keep durable storage, the router simply runs in memory and fills from the feed as before, so the page still works, it just does not resume.

A fast feed gets its own treatment. Point a route at a firehose and hold its view to a fixed number of repaints a second, and the updates that arrive in between are folded into the next one. The grid stays smooth under a feed that would otherwise make it stutter, and the count of updates merged away tells you how much work the view was spared. One feed, then, that both survives a reload and takes a firehose without flinching.

How do I make a live feed survive a page reload?

Turn on persistence on the data router: call router.persist with a key to store under, and the routed rows are written to the browser’s durable store as they arrive. On the next load, call router.restore before you start the feed, and the views come back filled from storage. For a fast route, pass a backpressure option when you attach its view, capping how often it repaints, and the router coalesces the updates in between into one. Read the coalesced count from the router’s own metrics to see how many updates were merged away.