const fs = require("fs"); const fsp = require("fs/promises"); function createJsonStorage(options) { const dataDir = options.dataDir; return { id: "json", async init() { await fsp.mkdir(dataDir, { recursive: true }); }, async exists(key) { return fs.existsSync(key); }, async readJson(key, fallback = null) { try { const raw = await fsp.readFile(key, "utf8"); const parsed = parseJsonWithTrailingRecovery(raw); if (parsed.ok) { if (parsed.repaired) { await backupCorruptJsonFile(key, raw, "trailing-garbage"); await fsp.writeFile(key, `${JSON.stringify(parsed.value, null, 2)}\n`, "utf8"); console.warn(`[json-storage] repaired trailing JSON data in ${key}`); } return parsed.value; } if (fallback !== undefined) { await backupCorruptJsonFile(key, raw, "invalid-json"); await fsp.writeFile(key, `${JSON.stringify(fallback, null, 2)}\n`, "utf8"); console.warn(`[json-storage] invalid JSON in ${key}, restored fallback`); return fallback; } throw parsed.error; } catch (error) { if (error.code === "ENOENT") { return fallback; } throw error; } }, async writeJson(key, value) { await fsp.writeFile(key, `${JSON.stringify(value, null, 2)}\n`, "utf8"); }, async appendText(key, text) { await fsp.appendFile(key, text, "utf8"); }, async readText(key, fallback = "") { try { return await fsp.readFile(key, "utf8"); } catch (error) { if (error.code === "ENOENT") { return fallback; } throw error; } }, async writeText(key, text) { await fsp.writeFile(key, text, "utf8"); } }; } function parseJsonWithTrailingRecovery(raw) { try { return { ok: true, repaired: false, value: JSON.parse(raw) }; } catch (error) { const message = String(error && error.message ? error.message : ""); const match = message.match(/position\s+(\d+)/i); if (!match) { return { ok: false, error }; } const position = Number(match[1]); if (!Number.isFinite(position) || position <= 0 || position >= raw.length) { return { ok: false, error }; } const prefix = raw.slice(0, position).trimEnd(); if (!prefix) { return { ok: false, error }; } try { return { ok: true, repaired: true, value: JSON.parse(prefix) }; } catch { return { ok: false, error }; } } } async function backupCorruptJsonFile(key, raw, reason) { try { const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const suffix = String(reason || "corrupt").replace(/[^a-z0-9-]/gi, "-").toLowerCase(); const backupPath = `${key}.${suffix}.${stamp}.bak`; await fsp.writeFile(backupPath, raw, "utf8"); } catch { // best effort backup only } } module.exports = { createJsonStorage };