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, for tests and 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 are mode 0700, files 0600, and there is no central index to corrupt (a listing is a readdir plus sidecar reads). Writes stream to a .tmp, fsync, then rename, so a reader never sees a partial blob and a crash leaves an invisible orphan, never 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 belongs is refused rather than followed. Reads verify the descriptor they draw from, so no path is re-resolved between the check and the read.
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.
A claim (the pop cycle, SPEC.md 6) moves the entry into a separate claims Map with no await between the "already claimed?" check and the move, so on the single-threaded event loop that move ITSELF is the atomicity two concurrent pops race on: the first wins, the second gets RefClaimed. restore returns a claim to the live map, commit destroys it, and verify reports a claim older than claimTimeout as stale without repairing it.
The claims Map lives only in this process's heap, so a claim dies with the process and nothing persists for a later run to reclaim: there is NO cross-process crash recovery here. 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