Incoming changes

The server's changes as your app receives them, applied as the server's or held for a person to look at first.

Your app receives the server's changes some way (a socket, a relay, a poll of its own). api.data.stream() is where they come into the grid. They apply as the server's, or they wait while someone works and apply when that person chooses. The grid needs no collaboration service, and the stream brings no transport of its own.

import type { GridApi } from '@witbybit/wit-grid-react';

type Invoice = { id: string; status: string; amount: number };
declare const api: GridApi<Invoice>;
declare const socket: { on(event: 'change', listener: (message: { id: string; amount: number; version: number }) => void): void };

const incoming = api.data.stream();
socket.on('change', ({ id, amount, version }) => incoming.push({ patched: [{ id, fields: { amount }, version }] }));

push takes the shape a pull returns: upserted (whole records), patched (changed fields, by path) and removed, each with an optional version. The server's changes have one shape, however they arrive.

The server's, not an edit

Applying a change is one commit from the server:

  • No setters, parse, capabilities or rules decide it. The rules mark what arrives, as they mark any row that arrives, and never refuse it. A server's value is never reinterpreted as if a person had typed it.
  • Nothing is queued to save, nothing is sent back, and undo does not step through it.
  • With data.save, it takes the pull's own path:
    • A field changed only here keeps its queued value until the server answers that change.
    • A field changed on both sides is decided by data.save.resolve (by default, a conflict for a person).
    • The record's version is kept, and this device's own change coming back is recognised as such.
  • Without data.save, a field someone is editing, or one already in conflict, keeps this side's value, and the server's value waits as a conflict.
  • A draft on the record keeps its values. A value the server changed under one of them shows in the draft's conflicts.

Versions in order

By default changes apply in the order they arrive, and a change repeating its record's last version is ignored. When your server's versions order, say how, once, and every server change of a record is judged by it: the stream, the pull, a save's answer and (with data.save) a load.

import { createClientGrid } from '@witbybit/wit-grid-react';

const api = createClientGrid<{ id: string; amount: number }>({
	rows: [],
	columns: [{ field: 'amount', header: 'Amount' }],
	getRowId: (row) => row.id,
	data: { order: { compare: 'number' } }, // or (a, b) => a < b ? -1 : a > b ? 1 : 0, or null when they cannot compare
});
  • An older version than the record's here is stale: never applied, and it changes nothing remembered.
  • The same version applies once. A change without a version applies in arrival order.
  • A version that cannot be compared (a string under 'number', null from your comparator) is incomparable: not applied. Text is never ordered as text: an ETag says which version, not which is newer.
  • A removal with a version is remembered (tombstones, default 10,000), so an older change cannot bring the record back; a newer one does. When room runs out the oldest is forgotten (forgotten in the status) — replaying older events past that window needs a reload.
  • Replacing the records (setRows, a new datasource) forgets what was remembered.
  • getStatus().data.incoming counts stale, incomparable and forgotten beside repeated.

Holding

import type { GridApi } from '@witbybit/wit-grid-react';
declare const api: GridApi;

const incoming = api.data.stream({ hold: true }); // or incoming.hold() at any time
incoming.held().records; // per record: kind, version, arrivedAt, and each field's current and incoming value
await incoming.apply(['INV-1042']); // these records' changes now, still holding
incoming.discard(['INV-1043']); // dropped: the server's record still changed, so reload to catch up
await incoming.release(); // everything waiting, in arrival order; from then on changes apply as they come
api.getStatus().data.incoming; // { holding, held, applied, repeated, refreshNeeded, revision }
  • Against the work here: each held field has its current value, the incoming one, and local. local is 'draft' when a draft holds a value for that field, 'unsaved' when a change to it waits to be saved, and null otherwise. Applying keeps unsaved work: the server's answer decides it.
  • Per record, in arrival order:
    • Patches to one record merge.
    • A removal replaces anything before it, and an add after a removal is an add.
    • A patch after an add patches the added record.
    • A change that repeats the record's last version is ignored and counted in repeated.
    • Within one push, whole records apply first, then patches, then removals.
  • Records are independent. Applying some records' changes never reorders a record's own. If your records depend on each other, apply them all.
  • Bounded: at most maxHeld records wait (10,000 by default). Past that limit the stream keeps no more and sets refreshNeeded. What already waits still applies; reload the records and open a new stream. Changes are never dropped silently, and a release applies 500 records per frame instead of all at once.
  • destroy() drops what waits without applying it. Opening a new stream closes the one before.

What holding does not hold

Holding covers only what this stream receives. These are never held:

  • the server's answers to a save
  • refused changes
  • the rules and the capabilities
  • the connection

data.save.pull is not routed through the stream either: pulled changes apply as they come.

Edit on GitHub

Last updated on

On this page