Backends
Everything a storage persists — records, the operation log, the chain, the keys, the peer registry, the catalog — goes through one small contract: an fsi, five functions over "a directory and a file name". The platform's own is the default; any other goes in through open({ fsi }).
| Function | Meaning |
|---|---|
read(dir, name) | Promise<string>; a missing file rejects (code: 'ENOENT' or name: 'NotFoundError') |
write(dir, name, content) | Creates the directory; whole-file, no rename |
exists(dir, name) | Promise<boolean> |
remove(dir, name) | A no-op when absent |
list(dir) | File names, sorted; an absent directory is [] |
sync(dir, name) (optional) | fsync — what durable: true calls after every write |
create(dir, name, content) (optional) | Exclusive create: rejects with code: 'EEXIST' when the file is there; what the directory lock uses when present |
isFsi(value) tells whether an object has the five. The check runs at open(), naming the method that is missing, and the storage never closes a backend it was handed — two storages may share one.
The four that ship
| Backend | Where | How |
|---|---|---|
node:fs/promises | Node.js | The default under Node; sync is fsync |
| OPFS | The browser | The default in a browser bundle (fsi.browser.js); durable when a writable closes, no sync |
@alexify/syncom/idb | The browser | createIdbFsi({ name, store, indexedDB }) — see Browser |
createMemoryFsi() | Anywhere | A Map; nothing survives the process |
const { open, createMemoryFsi } = require('@alexify/syncom');
const fsi = createMemoryFsi();
const storage = await open({ path: 'app', fsi }); // nothing touches the disk
await storage.insert({ a: 1 });
fsi.size; // 11 files: keys, chain, log, data, catalog, lock…
Object.keys(fsi.dump()); // ['app/lock.json', 'app/keys/public.pem', …]The memory backend is what a test, a server-side render, or a benchmark wants; two storages opened over one of it with different paths are a mesh in one process, which is how the sync-engine suite runs.
import { open } from '@alexify/syncom';
import { createIdbFsi } from '@alexify/syncom/idb';
const idb = createIdbFsi({ name: 'app' });
const storage = await open({ path: 'data', fsi: idb });
// …
await storage.close();
await idb.close(); // the backend's ownYour own
An adapter is the five functions over whatever holds bytes — a Map, a key-value store, S3 per node, a Capacitor filesystem on mobile. Two things the engine relies on: write is whole-file and visible to the next read once it resolves, and list names files only, sorted. tests/suites/fsi.js is the contract as a test — run it against yours.
Durability
Files are written with the platform's default durability. open({ durable: true }) follows every write with the backend's sync; on node:fs that is an fsync per file — the log batch, the chain block, the record — and the price is the write latency of the disk. A backend without sync (OPFS, IndexedDB, memory) is as durable as it is, and durable logs a debug line and does nothing. What a crash can tear is the newest, unannounced batch: the log is written before the record, a reopen replays its unsealed tail, and a torn open file is dropped before anyone was told of it. A record file torn the same way reads as INTEGRITY until repair(id) rebuilds it.
The log append is the commit point. A backend that fails after it — a full disk, a browser's QuotaExceededError on the record file or the chain block — does not undo the write: it resolves, the op replicates, the record is held in memory so reads see it, and the storage's error event carries code: 'UNPERSISTED'. The batch stays unapplied in the log, and the next open replays it onto the files.
One instance per directory
open() holds the directory — the Web Locks API in a browser, a lock.json with the pid elsewhere — and a second open() of it rejects with code: 'LOCKED'; lock: 'wait' waits, lock: false leaves the coordination to you. See One instance per directory for why, and for what two browser tabs should do.