@stainless-code/persist
Version:
Hydration-aware persistence for any reactive store — zero-dep persistSource core; codecs, backends, cross-tab transport, source + framework hydration adapters ship as opt-in recipes
55 lines (54 loc) • 1.78 kB
JavaScript
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
//#region src/adapters/backends/node-fs.ts
const hashKey = (s) => {
let h = 5381;
for (let i = 0; i < s.length; i++) h = h * 33 ^ s.charCodeAt(i);
return (h >>> 0).toString(16);
};
/**
* Node `fs` storage backend — one file per key under `dir` (async `fs.promises`).
* Keys are sanitized to filename-safe segments (`app:prefs:v1` → `app_prefs_v1`).
* Compose with `createStorage(() => nodeFsStateStorage({ dir }), codec)`.
*
* @example
* ```ts
* import { createStorage } from "@stainless-code/persist";
* import { serovalCodec } from "@stainless-code/persist/codecs/seroval";
* import { nodeFsStateStorage } from "@stainless-code/persist/backends/node-fs";
*
* const storage = createStorage<Prefs>(() => nodeFsStateStorage({ dir: "./.persist" }), serovalCodec());
* ```
*/
function nodeFsStateStorage(options) {
const { dir } = options;
const pathFor = (name) => {
const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
if (safe === ".." || safe === "." || safe === "") throw new Error(`[nodeFsStateStorage] key "${name}" sanitizes to "${safe}" — refusing to resolve outside dir`);
return join(dir, `${safe}.${hashKey(name)}`);
};
return {
getItem: async (name) => {
try {
return await readFile(pathFor(name), "utf8");
} catch (err) {
if (err.code === "ENOENT") return null;
throw err;
}
},
setItem: async (name, value) => {
await mkdir(dir, { recursive: true });
await writeFile(pathFor(name), value, "utf8");
},
removeItem: async (name) => {
try {
await unlink(pathFor(name));
} catch (err) {
if (err.code === "ENOENT") return;
throw err;
}
}
};
}
//#endregion
export { nodeFsStateStorage };