Part 3 — reading and writing
Opening a graph
Section titled “Opening a graph”const status = await engine.openGraph('g:acme', { create: true });// { key: 'ok' | 'waiting' | 'none', epoch: number, rotationRequired: boolean }key |
means |
|---|---|
ok |
this device holds the key; read and write freely |
waiting |
the graph has a key, this device has not been granted it yet |
none |
nobody has created a key for this graph |
Draw waiting as “sealed — another device holds the key”, never as empty.
An empty screen for a graph full of data is the worst thing a local-first app
can show.
{ create: true } initialises epoch 1 if key is none. Use it for a graph
this member owns (u, or a group they just made). For a graph somebody else
owns, open without it and wait for the grant.
Writing
Section titled “Writing”const record = await engine.put('g:acme', { kind: 'note', data: { body: 'hello' },});// → { id: 'r_...', kind: 'note', data: {...}, at: 1767..., by: 'sam' }
await engine.put('g:acme', { id: record.id, kind: 'note', data: { body: 'edited' } });
await engine.putMany('g:acme', [ { kind: 'task', data: { title: 'one', done: false } }, { kind: 'task', data: { title: 'two', done: false } },]);
await engine.remove('g:acme', record.id); // a tombstone, not a holeawait engine.removeMany('g:acme', [id1, id2]);Writes are local and synchronous from the app’s point of view: the store write
happens, the id is queued, and the push goes in the background. put throws
only for a genuine programming error — a record that fails your own schema, a
record over 512KB, a graph whose key has not arrived, or a closed engine.
by defaults to this member and is proven, not claimed — a receiver checks
the signature against the roster and the claimed author against the signing
device. Setting by to somebody else produces a record every other device
rejects.
Reading
Section titled “Reading”await engine.get('g:acme', id); // GraphRecord | null (tombstones included)await engine.list('g:acme'); // non-deleted, newest firstawait engine.listKind('g:acme', 'task'); // typed by the registryawait engine.search('g:acme', 'quarterly'); // case-insensitive substringawait engine.searchAll('quarterly'); // every open graphAll local, all instant, none of them touch the network. listKind is typed
from the registry, so records[0].data.done is a boolean. get returns
tombstones too — check record.deleted when it matters.
Search is a substring scan today. FTS5 is a small change against the SQL store and a bigger one against IndexedDB — see ROADMAP.md.
Syncing
Section titled “Syncing”engine.startAutoSync(30_000); // every 30s, plus on writeengine.stopAutoSync();
await engine.sync(); // every open graph, nowawait engine.syncGraph('g:acme'); // one graph, nowawait engine.flush(); // "everything I wrote is really pushed"await engine.poke('g:acme'); // "something changed upstream" — from realtimeflush() is the one to await in a test or before closing a window. It syncs
until there is genuinely nothing left: no run in flight, no graph marked dirty
by a write that landed mid-sync, no queued record.
Closing
Section titled “Closing”await engine.close(); // waits for in-flight work, then releases the storeAfter close, writes throw and syncs are no-ops (timers and listeners can still fire once on their way out).