UNPKG

zephyr-agent

Version:
148 lines (147 loc) • 4.91 kB
import "node:module"; import { createHash } from "node:crypto"; import { mkdir, readFile, readdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { lock } from "@bybrave/proper-lockfile2"; import { writeFileAtomically } from "./atomic-file.mjs"; import { ZE_STORAGE_PATH, ensurePrivateFilePermissions } from "./storage-keys.mjs"; const STORAGE_ITEM_LOCK_STALE_MS = 60000; const STORAGE_ITEM_LOCK_RETRIES = { retries: 120, factor: 1.2, minTimeout: 25, maxTimeout: 500, randomize: true }; function datumPath(directory, key) { const digest = createHash('sha256').update(key).digest('hex'); return join(directory, digest); } function parseDatum(content) { try { const datum = JSON.parse(content); return 'string' == typeof datum.key && datum.key ? datum : void 0; } catch { return; } } function isExpired(datum) { return 'number' == typeof datum.ttl && datum.ttl < Date.now(); } function createPersistentStore(directory, hooks = {}) { const ready = mkdir(directory, { recursive: true, mode: 448 }).then(()=>void 0); async function withItemLock(key, action) { await ready; const release = await lock(datumPath(directory, key), { realpath: false, retries: STORAGE_ITEM_LOCK_RETRIES, stale: STORAGE_ITEM_LOCK_STALE_MS }); try { return await action(); } finally{ await release(); } } async function readDatum(key) { await ready; try { return parseDatum(await readFile(datumPath(directory, key), 'utf8')); } catch (error) { if ('ENOENT' === error.code) return; throw error; } } async function getItem(key) { let datum = await readDatum(key); if (!datum) return; if (isExpired(datum)) { datum = await removeExpiredItem(key); if (!datum) return; } return datum.value; } async function setItem(key, value, options = {}) { return withItemLock(key, async ()=>{ const file = datumPath(directory, key); const ttl = options.ttl ? Date.now() + options.ttl : void 0; await writeFileAtomically(file, JSON.stringify({ key, value, ttl })); ensurePrivateFilePermissions(file); return { file }; }); } async function removeItem(key) { await withItemLock(key, ()=>removeItemWithoutLock(key)); } async function removeItemWithoutLock(key) { await rm(datumPath(directory, key), { force: true }); } async function removeExpiredItem(key) { await hooks.beforeExpiredCleanup?.(key); return withItemLock(key, async ()=>{ const datum = await readDatum(key); if (!datum || !isExpired(datum)) return datum; await removeItemWithoutLock(key); }); } async function data() { await ready; const entries = await readdir(directory, { withFileTypes: true }); const values = []; for (const entry of entries)if (!(!entry.isFile() || entry.name.startsWith('.'))) try { let datum = parseDatum(await readFile(join(directory, entry.name), 'utf8')); if (!datum) continue; if (isExpired(datum)) { datum = await removeExpiredItem(datum.key); if (!datum) continue; } values.push(datum); } catch (error) { if ('ENOENT' !== error.code) throw error; } return values; } async function keys() { return (await data()).map(({ key })=>key); } async function forEachItem(callback) { for (const { key, value } of (await data()))await callback({ key, value }); } return { ready, getItem, setItem, removeItem, keys, forEachItem }; } const persistentStore = createPersistentStore(ZE_STORAGE_PATH); const storage = persistentStore.ready; const storage_getItem = persistentStore.getItem; const storage_setItem = persistentStore.setItem; const storage_removeItem = persistentStore.removeItem; const storage_keys = persistentStore.keys; const storage_forEachItem = persistentStore.forEachItem; async function setPrivateItem(key, value, options) { const result = await storage_setItem(key, value, options); ensurePrivateFilePermissions(result.file); } export { createPersistentStore, setPrivateItem, storage, storage_forEachItem as forEachItem, storage_getItem as getItem, storage_keys as keys, storage_removeItem as removeItem, storage_setItem as setItem }; //# sourceMappingURL=storage.mjs.map