Skip to content

Latest commit

 

History

History
82 lines (67 loc) · 3.62 KB

data.md

File metadata and controls

82 lines (67 loc) · 3.62 KB

The todos slot and the writes

This is for someone reading src/chelonia/todos.ts and lists.ts. The README says what happens; this shows the code.

Two slots are declared once, at startup. lists on the identity contract holds the IDs of the lists this account is in, and todos attaches to each of those lists:

sbp('chelonia/kv/defineSlot', {
  contractType: 'todomvc/list',
  key: 'todos',
  defaultValue: {},
  schema: todosSchema,
  match: (contractID, contractState) =>
    currentLists().includes(contractID) &&
    !!sbp('chelonia/contract/currentKeyIdByName', contractState, '#sak', true)
})

match decides which contracts a slot attaches to. The second half of it is what makes a shared list wait: a list you have just joined is in lists, but until its keys arrive there is nothing you could read or write, because the server checks the contract's #sak before serving /kv/:contractID/:key. See sharing.md for how the keys arrive.

Every write is a reducer:

sbp('chelonia/kv/update', {
  contractID,
  key: 'todos',
  updater: (prev) => ({ ...prev, [id]: { title, completed: false, createdDate } })
})

No etags, no conflict callback, no per-key event. The server rejects a write that was based on a stale value, and Chelonia responds by refetching and running the reducer again on the newer one. That is why two windows converge. Returning KV_NOOP from a reducer cancels the write, which is how toggling a todo that is already in that state avoids a pointless round trip.

Reading does not go over the network. Chelonia keeps a local copy of every declared slot at rootState._kv[contractID][key] and updates it from four places: the first load, a push from another client, our own write, and a refetch after the socket reconnects. That state object is a Vue reactive(), so a computed over it reruns on all four and the list redraws by itself.

Offline

chelonia/kv/update needs the server, so while the socket is down a write goes into Chelonia's persistent action queue instead (src/chelonia/offline.ts). The queue stores [selector, ...args] as JSON, which is why writes are named ('addTodo', 'setTitle', ...) and the reducer is looked up when the write runs. The queue lives under one localStorage key, so it survives a reload, and retryAll is called as soon as the socket is back.

Until a write lands, currentTodos applies it on top of the mirror value, so the list looks the same offline as it will once the server has it. When a write succeeds it is taken off that overlay, on PERSISTENT_ACTION_SUCCESS. When the server refuses a write it is dropped and the list says so, because retrying would only get the same refusal.

One case does not hold. A todo made while the server is away cannot be ticked off, renamed or deleted until it has landed: the change is applied to the value the server has, which does not have that todo in it, so it does nothing and is lost. test/e2e/offline.spec.ts has it, marked as a known gap.

Two things worth knowing about the queue:

  • The queue is stored once for the whole browser, but each window keeps its own copy and saves all of it at once, so two windows writing offline overwrite each other. A window closed before its writes have been sent loses them.
  • Logging out cancels every queued write, and they are never sent. It has to: the keys that would sign them are discarded with the session. The app warns you and asks whether to log out anyway.

Only individual todo items use the offline queue. Operations on a whole list, creating one, renaming it, or sharing it, all need the server to be online, so those controls stay disabled until a connection is back.