# Viewport streaming

> Subscribe a feed to just the rows on screen with onViewportChange and createViewportSubscriptions, so a board of thousands of symbols streams only the few dozen in view.

A board of 1,600 symbols shows 30 to 80 at a time. Streaming all of them costs the server, the network and the client for values nobody sees. The grid's `onViewportChange` reports the data rows on screen, and `createViewportSubscriptions` from `bo-grid/realtime` turns those reports into subscribe and unsubscribe calls for your feed.

The board below has 1,000 symbols, but its simulated server sends ticks only for the subscribed ones. Scroll it: once the view settles, the feed follows the rows on screen.

Example: Streaming the rows in view (viewport-streaming.svelte)

```svelte
<script lang="ts">
  import { Grid, type ColumnDef, type GridApi } from 'bo-grid';
  import { createTickStream, createViewportSubscriptions } from 'bo-grid/realtime';

  type Row = { id: number; symbol: string; price: number; volume: number };
  const rows = $state.raw<Row[]>(Array.from({ length: 1000 }, (_, id) => ({ id, symbol: `S${String(id).padStart(4, '0')}`, price: 10 + (id % 90), volume: 0 })));
  const columns: ColumnDef[] = [
    { type: 'text', key: 'symbol', header: 'Symbol', width: 110 },
    { type: 'price', key: 'price', header: 'Price', flash: 'auto' },
    { type: 'volume', key: 'volume', header: 'Volume', flex: 1 },
  ];

  let api: GridApi | undefined;
  const stream = createTickStream<number, Partial<Row>>({ apply: (batch) => api?.patchRows(batch) });

  // The "server": it sends ticks only for the symbols subscribed.
  const live = new Set<number>();
  let streaming = $state(0);
  const subs = createViewportSubscriptions<Row, number>({
    key: (row) => row.id,
    subscribe: (ids) => ids.forEach((id) => live.add(id)),
    unsubscribe: (ids) => ids.forEach((id) => live.delete(id)),
  });

  $effect(() => {
    stream.start();
    const timer = setInterval(() => {
      for (const id of live) {
        if (Math.random() > 0.15) continue;
        const r = rows[id];
        stream.push(id, { price: Math.round((r.price + (Math.random() - 0.5) * 0.2) * 100) / 100, volume: r.volume + 100 });
      }
      streaming = live.size;
    }, 100);
    return () => {
      clearInterval(timer);
      stream.stop();
      subs.clear();
    };
  });
</script>

<p class="note">Streaming {streaming} of {rows.length.toLocaleString()} symbols: scroll, and the feed follows the rows on screen.</p>
<Grid {rows} {columns} height={280} onViewportChange={subs.update} onReady={(a) => (api = a)} />

<style>
  .note {
    margin: 0 0 8px;
    font-size: 12px;
    opacity: 0.7;
  }
</style>
```

## The rows on screen

`onViewportChange` is called with the data rows on screen, plus the small buffer rendered above and below them, whenever that set changes: a scroll, a sort, a filter, new data. A tick that only changes values never calls it. Group rows, loading rows and pinned rows are not included.

```svelte
<Grid {rows} {columns} height={600} onViewportChange={(view) => console.log(view.first, view.last, view.rows.length)} />
```

`first` and `last` are the visual indexes of the first and last row reported, or -1 when no data row is on screen; `rows` are the rows themselves, in view order.

## Subscribing what is in view

```svelte
<script lang="ts">
  import { onDestroy } from 'svelte';
  import { Grid } from 'bo-grid';
  import { createViewportSubscriptions } from 'bo-grid/realtime';

  const subs = createViewportSubscriptions({
    key: (row: GridRow) => String(row.symbol),
    subscribe: (syms) => socket.send(JSON.stringify({ op: 'sub', syms })),
    unsubscribe: (syms) => socket.send(JSON.stringify({ op: 'unsub', syms })),
  });
  onDestroy(subs.clear);
</script>

<Grid {rows} {columns} onViewportChange={subs.update} height={600} />
```

- The first view subscribes at once.
- Later changes settle for `delay` ms (150 by default), so a fast scroll is one change rather than one a frame. Then it subscribes the keys that came into view and unsubscribes the ones that left. While a scroll is in progress, the previous keys stay subscribed.
- `flush()` applies a pending change at once; `clear()` unsubscribes everything and drops a pending change (on unmount, or when the socket closes); `active` is the set of keys subscribed now.
- `subscribe` and `unsubscribe` are always called with at least one key.

Pass every change to `update` without throttling it: the `delay` does the settling.

Combine it with a [tick stream](https://bonguynvan.github.io/bo-grid/docs/tick-stream/) or a [worker feed](https://bonguynvan.github.io/bo-grid/docs/worker-feed/) for the ticks that come back: the grid applies them the same way whether a row is on screen or not, and rows that scroll in show their latest values.

## Reference

- `onViewportChange`: `(view: ViewportRange) => void` — Called with the data rows on screen — the rows in view plus the small buffer rendered around them — whenever that set changes (scroll, sort, filter, new data), never for a tick that only changes values. Pass it `createViewportSubscriptions(...).update` from `bo-grid/realtime` to stream just the symbols in view.

  ```svelte
  <Grid {rows} {columns} height={480} onViewportChange={subs.update} />
  ```


`createViewportSubscriptions<R, K>(options)` returns a `ViewportSubscriptions<K>`.

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `key` | `(row: R) => K` | required | The feed key of a row (its symbol). |
| `subscribe` | `(keys: K[]) => void` | required | Starts streaming these keys. |
| `unsubscribe` | `(keys: K[]) => void` | required | Stops streaming these keys. |
| `delay` | `number` | `150` | How long the view must stay put before a change applies, in ms; `0` applies every change at once. |

| Member | What it is |
| --- | --- |
| `update(view)` | Takes a view change: pass it as the grid's `onViewportChange`. |
| `flush()` | Applies a pending change now. |
| `clear()` | Unsubscribes everything and drops a pending change. |
| `active` | The keys subscribed now. |

---
Page: https://bonguynvan.github.io/bo-grid/docs/viewport-streaming/ · All pages: https://bonguynvan.github.io/bo-grid/llms.txt
