UNPKG

@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
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 };