Backends
Stash holds policy. A backend never validates lifecycle, never interprets meta, and never decides destruction -- it stores what it is handed under the id it is handed, computes size and the digest as the bytes stream through (with the algorithm named by the entry's self-describing digest), and reports what it holds. The same conformance suite runs against every backend, unmodified.
Two implementations ship. The memory backend is Map-backed -- no persistence, no pretense; for tests and legitimately process-lifetime stashes. The disk backend is sidecar-file storage: one blob and one JSON sidecar per entry, no central index to corrupt, atomic tmp-fsync-rename writes, 0700/0600 modes, and realpath containment that refuses a planted symlink instead of following it.
stash.backends.DiskBackend
new DiskBackend(opts) -> DiskBackend
Construct the sidecar-file disk backend over opts.root. The layout is blobs/<id> + meta/<id>.json (plus the claims and tombstones directories the pop cycle and replication use), directories mode 0700 and files 0600, with no central index to corrupt: a listing is a readdir plus sidecar reads. Writes stream to a .tmp, fsync, then rename -- a reader never sees a partial blob, and a crash leaves an invisible orphan rather than a half-entry. The constructor does no I/O; the layout appears on first use.
Containment is the backend's own job, not the sandbox's: the root is realpath-pinned at init, every operation re-asserts that its directory still resolves inside it, and a symlink where a blob should be is refused as corruption rather than followed. Blob and sidecar reads open a descriptor and verify it there -- fstat gates the shape, the read draws from the same handle -- so the path is never re-resolved between the check and the read, and a file swapped in behind the check cannot redirect it.
Example
import { Stash } from "@blamejs/stash";
import { DiskBackend } from "@blamejs/stash/backends/disk";
const stash = new Stash({ backend: new DiskBackend({ root: "./.stash" }) });
References
stash.backends.MemoryBackend
new MemoryBackend() -> MemoryBackend
Construct the in-memory backend. Pass it as backend to new Stash(). Storage is a private Map from id to { entry, chunks }; nothing touches the filesystem, so the permission-model posture costs nothing here.
Claim semantics (the pop cycle, SPEC.md 6) stand in for the disk backend's filesystem primitives without a filesystem. A claim atomically moves the entry out of the live Map into a separate claims Map; that move is synchronous within one method body -- no await between the "already claimed?" check and the move -- so on the single-threaded event loop the move ITSELF is the atomicity two concurrent pops race on: the first wins, the second sees the id already claimed and gets RefClaimed. That is the role the disk backend gives an atomic rename/link. restore returns a claim to the live map; commit destroys it. Claim freshness is a claimedAt wall-clock stamp on the in-memory claim record -- the analogue of the disk claim file's mtime -- and verify reports a claim older than claimTimeout as a stale claim, never repairing it (resolving one is recovery's job).
The claims Map lives only in this process's heap, so a claim is process-lifetime: it dies with the process. A crash takes the whole store -- live entries, claims, and tombstones alike -- with it, so unlike the disk backend there is NO cross-process crash recovery here: nothing persists for a later run's recovery scan to reclaim by age. Use this backend only for a stash whose lifetime is the process's; reach for the disk backend when a claim, or the data, must survive a restart.
Example
import { Stash } from "@blamejs/stash";
import { MemoryBackend } from "@blamejs/stash/backends/memory";
const stash = new Stash({ backend: new MemoryBackend() });
const ref = await stash.push("hello");
References
- spec SPEC.md 9