This page makes an aggregate readable, or writable, with no network. It extends step 7 of the aggregate reference.
You need:
- An aggregate with a working store (Add your first feature).
- The level it needs. ADR 003 (on GitHub), section 4, sets the levels, and offline-data.md lists where every list stands.
Write the store and nothing else. With no network, its read fails and the screen shows the error.
The default for a list. The device keeps the last answer and shows it when the server cannot be
reached. apps/spa/src/state/members.ts is the example.
- Add the list's key to
READ_KEYSinstate/read-keys.ts. The key names everything the answer depends on: the group, and the cursor when the list is paged. - Give the store's factory a
device: LocalReads = noLocalReadsparameter. PasslocalReadsfrommodules.tswhere the page builds the store. - Read through the copy:
device.through(key, read, early). - Add the list to
CASESinstate/device-copies.test.ts.
read must throw for a refusal, so only an unreachable server is answered by the copy. Wrap a
result object's failure in ReadRefused. early paints the copy while the screen has nothing to
show. It returns false when it painted nothing, and takes the copy back when called with
undefined.
The one CASES entry tests five promises of a level 2 list:
- The copy answers an unreachable server.
- Every answer from the server replaces the copy.
- The copy never hides a refusal.
- Nothing is kept while the module is off.
- The copy shows before the server has answered.
A list that can mislead when old, such as one that lists access, also says it is a saved copy.
The store exposes savedAt (fresh ? null : savedAt). The screen renders SavedCopyNotice from
@spy4x/preact-system always, with a title that names the list. libs/ui/api-tokens.tsx is the
example.
Only for an aggregate a person must change with no network. It costs a conflict screen.
- Copy
offline/notes-collection.ts. It is onedefineCollectionwith the aggregate's commands, its error codes (classifyByCode), the wording of its failures and its device copy. - Add it to the
collectionslist inoffline/index.ts. - Wrap the store's dependencies as
offlineNotesdoes. - Copy
offline/notes-collection.test.ts.
The queue, the keys, the lock and the conflict rules come from @spy4x/realtime/collections; do
not write them again. The device copy is a view over the changes copy (offline/device-copy.ts).
Its clear ends the database, so a write in flight at sign-out cannot put the data back.
A level 3 aggregate is read by the changes pull: a device asks only for what changed since its cursor (offline-data.md). Notes and tags take part; copy them.
- Server. Write a
ChangesAggregatebeside the repository, aslibs/server/notes/note-changes.tsdoes. It has akindand two queries.sequencesreads the live rows'change_sequenceafter the cursor.rowsreads the rows in(after, through], in the list's shape. Add it toCHANGES_AGGREGATESinapps/api/services/db-base.ts. - Tombstones. A write that makes a row leave the group, such as a delete or a move away, calls
recordTombstones(libs/server/groups/group-changes.ts) in its own transaction. A write that moves every row at once callsforgetChangesBeforeinstead. A deleted row's content never reaches a device. - SPA. Add the kind to
ChangesRowsandCHANGES_KINDSinoffline/device-copy.ts, with an arktype schema of its row and a view over the copy. The collection's list read callspullOrKeep, then reads the view. - Tests. Add a case to
tests/integration/group-changes.integration.test.tsfor its rows and tombstones, and one tooffline/device-copies.test.tsfor the view.
- Offline: the whole local data layer, and how to remove it.
- Link two aggregates