zephyr-agent
Version:
Zephyr plugin agent
148 lines (147 loc) • 4.91 kB
JavaScript
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