02examples
Optimistic server sync
Every change is committed to a fake server with latency and a reject switch; a rejection springs the block back to where it was.
Drag an event. It lands at once, pulses while the commit is on the wire, and
settles when the server answers. Now flip Reject writes and drag again: the
server refuses, the block springs back, and the log underneath records the
KloqChange that was sent and what came of it. The latency slider is the
round-trip; at 2000 ms you can move three things before the first one lands.
liveOptimistic server sync
loading
uses
How it works
onCommitis the seam. kloq calls it with every committed change — a drag, a resize, a create, a delete, a paste — after applying it. Return a promise and the change stands if it resolves; reject and the store reverts, FLIPping the affected blocks back with the cancel spring. That is the whole protocol: the example'sonCommitjust returnsserver.commit(change). See Optimistic sync.FakeServerindata.tsstands in for your API.commitwaitslatencymilliseconds, then throws when the switch is on. It also keeps a small state object — in-flight count, last four results — that the strip reads throughuseSyncExternalStore, so nothing here is React-specific on the server side.- The pulse is one attribute. Every block renders
data-cal-event="<id>";markSyncingsetsdata-syncingon the blocks named by the change while the promise is pending, and kloq's stylesheet animates that selector. The ids come straight off the change:event.idfor anadd,idfor anupdate, the union for abatch. Aremovehas no block left to mark. - The store is uncontrolled —
defaultEventsseeds it,persistence={false}keeps it in memory — so undo, redo and the revert all live inside kloq. The sameonCommitworks unchanged on a controlledeventsarray; there the revert arrives as a call toonEventsChangewith the previous array. See Controlled state. - One wrinkle worth knowing: kloq ignores the promise of a
reset(there is nothing to revert to), so the fake server never rejects one. Reset from the toolbar and the log showsreset okeven with the switch on.
Take it further
- Swap
server.commitfor a route handler:fetch("/api/events", { method: "POST", body: JSON.stringify(change) })andthrowwhen!res.ok. Theupdatevariant carries bothpatchandprev, so a PATCH request and anIf-Matchheader are both a field read away. - A
batchis one change and one request. Applychangesin order inside a transaction and the paste either lands whole or springs back whole. - Tell the user. The revert is visible, but a toast on the catch — before re-throwing — says why; the demo does exactly that.