Lattice Grid Buy a licence

developer guide

Data Grid Optimistic Updates and Rollback

An edit appears the moment it is made, so the grid never feels like it is waiting on the network, and it carries a pending mark until the server answers. A refusal rolls the cell back to what it was and says why, and a conflict is surfaced rather than swallowed.

Developer guideEditing › Data Grid Optimistic Updates and Rollback

Optimistic writes and rollback

The grid has always written optimistically without calling it that: an edit lands in the model and is painted before anything else happens. What edit.commit adds is durability: whether the write reached your server, and what to put back when it did not.

Nothing changes unless you ask for it. With no commit hook the grid behaves exactly as before: the value is written, history is recorded, cell:changed fires, and there is no pending state to think about. Subscribe to cell:changed, fire your request and ignore the result: that keeps working and costs nothing.

The usual case, the promise is the answer

edit: {
  enabled: true,
  commit: async ({ key, colId, value }) => {
    const res = await fetch(`/api/rows/${key}`, {
      method: 'PATCH',
      body: JSON.stringify({ [colId]: value }),
    });
    if (!res.ok) throw new Error(await res.text());   // throw → rolled back
  },
}

Resolving confirms the write; throwing rolls it back and fires cell:reverted with your error message as reason. A synchronous hook works too: returning normally confirms, throwing reverts.

When the answer arrives elsewhere

edit: {
  enabled: true,
  confirm: 'manual',                       // the return value is ignored
  commit: ({ id, key, colId, value }) => {
    socket.send(JSON.stringify({ id, key, colId, value }));
  },
}

socket.onmessage = (m) => {
  const { id, ok, reason } = JSON.parse(m.data);
  grid.edit.settle(id, ok, reason);
};

The mode is declared, never guessed. A websocket or event-sourced backend acknowledges on a different channel from the one the write went out on, so there is no promise to resolve. confirm: 'manual' says so explicitly. The grid does not infer it from what commit returns, because then a synchronous hook that happens to return nothing would leave every cell pending for ever with nothing in your code that looks wrong. If a write does stay pending, you get a console warning naming the cell: tune the threshold with pendingTimeout.

The states a write moves through

StateMeansEvent
pendingApplied and painted, not yet acknowledged.cell:pending
confirmedThe server accepted it. Nothing is written back.cell:confirmed
revertedThe server refused it; the cell is rolled back.cell:reverted
conflictThe write was accepted but the server row had moved underneath it. Last-write-wins: your value stands and the server's truth is surfaced so you can reconcile it.cell:conflict
supersededA newer edit replaced it while it was in flight.either, with superseded: true

Rollback goes to the last confirmed value, not the previous one. This is the part that is easy to get wrong by hand. Suppose a cell holding 1 is edited to 2, then to 3, then to 4, all before any answer comes back. If the second write fails, restoring “the value before it” would put back 2, a value the server never held, and one the user has since replaced twice. So each cell remembers the newest value a confirmation has actually vouched for, and a write that a later edit has superseded reports its failure without writing anything back. You will see cell:reverted with applied: false for those.

The rejected value travels on the event as rejected, so you can offer a retry rather than losing what the user typed.

Two behaviours worth knowing. An unconfirmed edit goes through the normal pipeline, so if it changes a sorted or filtered column the row moves immediately and moves back if the write fails. And undo of an in-flight edit issues a compensating write, a fresh write back to the previous value, itself tracked: rather than pretending to cancel a request that has already gone out.

Asking what is outstanding

grid.edit.pending();              // [{ id, key, colId, value, before, state, age }]
grid.edit.status('r1', 'cap');    // 'pending' | null

Pending cells are marked with --lattice-pending-background and rolled-back ones flash --lattice-reverted-background; restyle either through the tokens. Both use the same highlight model as everything else, so the marks survive scrolling, sorting and row recycling.