Sync / foldkit-sync

What to show the user

The three milestones each have a reader.

Visible is the Model. Nothing to do.

Saved is the persist. Between the edit showing and the persist finishing, a reload would lose what the user saw, so the browser asks before the page unloads (beforeunload) until every shown edit is stored; it usually takes milliseconds. onPersistenceFailure runs when it fails, after the edit has been reverted; the Model it returns is what the user sees next. The error names why: ReplayError for a Message that cannot be durable, StorageError for IndexedDB.

Committed is the replica's status:

ts
const status = yield* replica.status
// { pending: number, cursor: Sequence, lastError: string | undefined, rejected: ReadonlyArray<Rejection> }

pending above zero means edits the server has not confirmed; lastError is the last failed exchange, cleared by the next success; rejected lists recent edits the server refused, most recent first, each { opId, reason, operation }, where reason is an Option of the server's words for the person and operation is the refused operation itself, which the replica has dropped from the outbox, so a page reads what it changed from here. replica.statusChanges is a Stream of the same, emitted now and after every submit and exchange (subscribed before its first read, so a subscriber slow to take one misses no change); replica.changes pairs it with the optimistic shared value so one subscription sees both.

A refused edit arrives as a rejection on the next exchange, not as an error. The replica drops it and replays the rest; the mount re-installs the shared slice, and the field the user edited reverts. onPersistenceFailure does not run for this, since the local save succeeded. Watch rejected to explain the revert: journalExchange gives each rejection a reason, the authorize rule's own when it gave one, and otherwise a fixed sentence ("Not a valid operation"). It never sends an error's message, which may carry internals, so a rule a person should read about belongs in authorize with a reason, not in validate.

Waiting for the server. The Model shows an edit before the server has it. Something that must not report success until the edit is committed, an agent tool for instance, waits on mounted.committed: the shared slice as the server confirmed it, with no pending edit applied. It is a source, { get, subscribe }, told after every exchange:

ts
completion: Agent.when({
  source: mounted.committed,
  predicate: (shared, request) => shared.todos.some(todo => todo.id === request.id),
})

That completes after the exchange that commits the todo and never for one the server rejects.