Advanced (the pull-model store)
The simple store owns your data and covers most apps. Reach for
selfstore/advanced in two cases: your state already lives in its own
reactive model (Svelte runes, Redux, signals) and you want selfstore to
follow it rather than own it, or you are writing your own destination.
Everything here is the same install, one import away, and
store.advanced on a simple store IS this store, the same instance, so the
two styles compose instead of competing.
The pull-model store
You keep ownership of the data; selfstore pulls a snapshot to save and pushes one back to load.
import { createLocalStore, indexedDbCache } from 'selfstore/advanced';
const store = createLocalStore({
app: 'my-app',
schemaVersion: 1,
gather: () => myApp.toSnapshot(), // your state -> Snapshot
apply: (snap) => myApp.load(snap), // Snapshot -> your state
cache: indexedDbCache('my-app'), // or memoryCache() in tests / SSR
});
await store.init(); // hydrate from cache, restore target, converge
myApp.onEveryChange(() => store.schedule()); // debounced auto-save
window.addEventListener('pagehide', () => store.flush());
The store logs a one-time warning for a mis-keyed record here rather than throwing, because, unlike the simple store, it cannot know which of your collections you consider mergeable.
Destinations, and silent reboot
import { driveTarget, fileTarget, webdavTarget, s3Target, gisDriveAuth } from 'selfstore/advanced';
const cache = indexedDbCache('my-app');
const auth = gisDriveAuth({ clientId: GOOGLE_CLIENT_ID });
const drive = () => driveTarget.connect({ auth, kv: cache.kv, fileName: 'my-app.zip' });
const store = createLocalStore({
app: 'my-app', schemaVersion: 1, gather, apply, cache,
restoreTarget: async (kind) => (kind === 'drive' ? drive() : null), // silent reboot
});
await store.init();
// On a user click (opens Google consent the first time):
await store.attachTarget(await drive(), { strategy: 'merge', password });
// Disk file (Chromium) and WebDAV work alike:
const f = await fileTarget.connect({ kv: cache.kv, fileName: 'my-app.zip' });
const w = await webdavTarget.connect({ kv: cache.kv, config: { url, username, password } });
strategy is 'merge' (default, multi-device), 'replace-local' (load the
target over local) or 'replace-remote' (push local over the target).
Write your own destination
A BackupTarget is about six methods. The error protocol is the whole
contract: throw AuthExpiredError for a genuine loss of access (the store
raises the reconnect gate); anything else you throw reads as transient and
is retried silently.
import { AuthExpiredError, type BackupTarget } from 'selfstore/advanced';
const bucket: BackupTarget = {
kind: 's3', // any string except 'device' / 'file-manual'
label: 'my bucket',
async save(blob) {
const res = await put(KEY, blob);
if (res.status === 401 || res.status === 403) throw new AuthExpiredError();
if (!res.ok) throw new Error(`upload failed: ${res.status}`); // transient
return res.headers.get('etag'); // or null
},
async load() { return (await getObject(KEY)) ?? null; },
async isReady() { return true; }, // false = transient blip; throw AuthExpiredError = genuine
async reconnect() { return true; }, // user gesture; true if access re-established
async disconnect() {}, // forget locally; never delete remote data
abortInFlight() { controller.abort(); }, // optional (1.1.0): cut in-flight work now
};
await store.connectTarget(bucket); // on a simple store
// or advancedStore.attachTarget(bucket, { strategy: 'merge' });
Get the isReady() split right - return false for “not right now”, throw
AuthExpiredError only for “the user must act” - and your users never see a
false-alarm reconnect dialog. A complete, typechecked version ships as
examples/custom-target.ts in the repository.
The optional abortInFlight() (added in 1.1.0) lets the store cut your
target’s in-flight request the instant the user detaches it, instead of the
detach waiting behind a request stuck on a woken-from-sleep radio. Wire it to
an AbortController your save/load fetches share; skip it and detaching
simply runs those calls to their own deadlines first, as before.
Peers, groups and the merge engine
Three deeper capabilities have their own pages, all built on this store:
- Peers and groups: share a store between people over
read-only links (
store.attachPeer), including passwordless groups fromselfstore/groups(per-member keys, signed manifests). - Multi-device sync: the strategies, the conflict journal
and the honest limits; the bare merge engine is
selfstore/sync. - Testing:
memoryCache()plus a fifteen-line in-memory target drive the whole loop in plain vitest, no browser.
The subpaths
| Import | What it is |
|---|---|
selfstore |
the simple store + the fluent backup-file API. Start here. |
selfstore/advanced |
the pull-model store, caches, targets, status derivation, functional codec |
selfstore/groups |
passwordless group encryption: identities, sealed envelopes, signed manifests |
selfstore/sync |
the bare merge engine (HLC + strategies), for embedding in your own persistence |