flatdb API reference
A file-backed JSON document store. Load a JSON file once into a value-semantic handle, query and edit it in memory through JSON Pointer, and write it back with a crash-atomic whole-file replace (temp file + rename). It is deliberately NOT a database engine: crash-atomic snapshotting of small data. Atomicity is whole-file (temp + rename); durability is only as strong as the OS's buffering of the rename. For a real database, use a client over net (e.g. redis), not this. It is a thin file-lifecycle + ergonomics layer over the json write surface, not a re-implementation of it. Runs on both binaries (pure json + fs, no network).
CONCURRENT WRITERS: crash-atomic is not the same as write-safe. Two writers that each open, edit different parts, and save would otherwise silently lose one update (last rename wins). Two guards address that: save fails loudly (a catchable flatdb error) if the file changed since it was opened, so a lost update can never be silent; and update(path, transform) brackets the whole read-modify-write in a cross-process advisory lock, so concurrent writers serialize and no update is lost. Prefer update when more than one writer (separate processes, or spawned tasks) can touch one store.
Import with import "flatdb.j" as flatdb;. See the flatdb guide for prose and examples.
Functions
flatdb.append(db as DB, pointer as string, value as json.Value)
Push value onto the list addressed by pointer, returning a new DB. The list must already exist (create it first with set($db, ptr, json.list())).
Parameters
db{DB}- the store to editpointer{string}- the JSON Pointer to a listvalue{json.Value}- the value to append
Returns {DB} - a new store with the element appended
Throws
{Error}- when pointer does not resolve to an existing list
flatdb.get(db as DB, pointer as string)
Return the sub-document at pointer (the whole document for "").
Parameters
db{DB}- the store to readpointer{string}- the JSON Pointer
Returns {json.Value} - the node at pointer
Throws
{Error}- when pointer does not resolve
flatdb.has(db as DB, pointer as string)
Report whether pointer resolves to an existing node.
Parameters
db{DB}- the store to readpointer{string}- the JSON Pointer
Returns {bool} - true when the node exists
flatdb.keys(db as DB, pointer as string)
List the keys of the object at pointer, in document order.
Parameters
db{DB}- the store to readpointer{string}- the JSON Pointer to an object
Returns {list of string} - the object's keys
Throws
{Error}- when pointer does not resolve to an object
flatdb.length(db as DB, pointer as string)
Return the element count of a list, or entry count of an object, at pointer.
Parameters
db{DB}- the store to readpointer{string}- the JSON Pointer to a list or object
Returns {int} - the element or entry count
Throws
{Error}- when pointer does not resolve to a list or object
flatdb.lock(dbPath as string)
Take the advisory write lock for the store backed by dbPath, blocking (with a short poll) until it is free or LOCK_WAIT_MS elapses. Built on fs.writeNew (an atomic exclusive create), so it coordinates across processes, not just spawned tasks. A lock leaked by a crashed writer is broken after LOCK_STALE_SECONDS. Pair with unlock - or, better, use update, which brackets the whole read-modify-write and releases on every path.
Parameters
dbPath{string}- the store's backing file path
Returns {Lock} - the held lock (pass to unlock)
Throws
{Error}- kind "flatdb" if the lock cannot be taken within LOCK_WAIT_MS, or at once if the lock file cannot be created at all (no holder exists: a missing directory, a read-only mount, no write permission)
flatdb.open(path as string)
Load the JSON document at path into a DB. A missing file yields an empty document (an empty object), so open never fails on a first run.
Parameters
path{string}- the backing file path
Returns {DB} - the loaded store
flatdb.openString(text as string)
Load a DB from an in-memory JSON string instead of a file - for a database fetched over the network (http.get(url, {}).body), embedded in the program, or built elsewhere. The returned DB has no backing path, so it is read-only: every reader verb works and the mutating verbs still return a fresh in-memory DB, but save throws (there is nowhere to write). Whitespace-only text yields an empty document, matching open on a missing file. This keeps flatdb transport-agnostic: it never imports http / net, so it stays fs-only and builds on both binaries; the caller brings the bytes.
Parameters
text{string}- the JSON document
Returns {DB} - a read-only store
flatdb.remove(db as DB, pointer as string)
Drop the key or element at pointer, returning a new DB.
Parameters
db{DB}- the store to editpointer{string}- the JSON Pointer to remove
Returns {DB} - a new store with the node removed
Throws
{Error}- when pointer does not resolve
flatdb.save(db as DB)
Write the document back to its own backing file, crash-atomically (temp + rename), and return a fresh DB rebound to the just-written file. A read-only DB (from openString, no backing file) has nowhere to write - use saveAs.
Conflict check: if the file changed since this DB was opened (a concurrent writer got there first), save throws a flatdb error instead of silently clobbering their write. Re-open and retry, or - to serialize writers without a possible conflict at all - use update. Because save returns the rebound DB, a program that saves the same handle repeatedly should thread it through ($db = flatdb.save($db);) so the next save checks against the file it wrote.
Parameters
db{DB}- the store to persist
Returns {DB} - a fresh DB bound to the written file (its version is the new file's)
Throws
{Error}- kind "flatdb" if the DB is read-only, if the document changed since it was opened, or on a filesystem write / rename failure
flatdb.saveAs(db as DB, path as string)
Write the document to path (not necessarily its own) and return a fresh DB bound to that path - the passed-in db is unchanged (value semantics, like set / append / remove). Two uses: the first dump to disk of a read-only openString DB, and copying / forking an on-disk store to a new file (a snapshot or a new version). Which store is "current" for the next save is whichever handle you keep - reassign ($db = flatdb.saveAs($db, path);) to make the new file current, or hold both to keep the original too. Same crash-atomic temp+rename as save.
Parameters
db{DB}- the store whose data to writepath{string}- the destination file path
Returns {DB} - a new DB bound to path (its data equals db's)
Throws
{Error}- kind "flatdb" ifpathis empty, or on a filesystem failure
flatdb.set(db as DB, pointer as string, value as json.Value)
Write value at pointer (upsert an object key / replace a list index), returning a new DB. Strict: intermediate containers must already exist.
Parameters
db{DB}- the store to editpointer{string}- the JSON Pointer to writevalue{json.Value}- the value to write (build scalars withjson.decode, objects and lists withjson.map/json.list)
Returns {DB} - a new store with the write applied
Throws
{Error}- when an intermediate container does not exist
flatdb.unlock(lk as Lock)
Release a lock taken by lock, but only if this holder still owns it: if a stale-break already handed the lock to another writer, unlock does nothing (it never removes a lock it no longer holds).
Parameters
lk{Lock}- the lock returned by lock
Returns {null}
flatdb.update(path as string, transform as func)
Run a locked read-modify-write against the store at path: take the write lock, open the current document under it, apply transform, save, and release the lock - on every exit path (via defer). This is the safe way to update a store that concurrent writers (separate processes, or spawned tasks) may also write: because the read and the write are one locked span, no update is lost and save cannot conflict. transform is a func value func(db as DB) -> DB - edit the DB it receives with set / append / remove and return the result.
Parameters
path{string}- the store's backing file pathtransform{func}- func(db as DB) -> DB, the edit to apply under the lock
Returns {DB} - the saved DB (its version is the freshly-written file's)
Throws
{Error}- kind "flatdb" if the lock cannot be taken, or on a write failure
Structs
flatdb.DB
The value the caller holds: the file path plus the decoded document. A module holds no mutable state and spawn deep-copies scope, so a store cannot be a shared open connection - it is a value. Mutating verbs return a fresh DB; save is the only side effect.
| Field | Type | Description |
|---|---|---|
path | string | the backing file path (empty for a read-only DB from openString) |
data | json.Value | the decoded in-memory document |
version | string | an opaque token of the file's on-disk state at open, so save can detect a concurrent write (empty when there was no backing file) |
flatdb.Lock
A held advisory lock, returned by lock and passed to unlock. Carries the lock file path and this holder's unique id, so unlock removes only a lock this holder still owns (never one a stale-break has handed to another writer).
| Field | Type | Description |
|---|---|---|
path | string | the lock file path |
holder | string | this holder's unique id ("<uuid> <unix-seconds>") |