UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

274 lines (273 loc) • 8.2 kB
import fs from "node:fs"; import path from "node:path"; import stream from "node:stream"; import zlib from "node:zlib"; import { E_CANCELED, Mutex } from "async-mutex"; import KeyedMutex from "../async/keyedMutex.js"; import Timer from "../async/timer.js"; import FsUtil from "../utils/fsUtil.js"; class Cache { keyValues = /* @__PURE__ */ new Map(); keyedMutex = new KeyedMutex(1e3); hasChanged = false; saveToFileTimeout; filePath; fileFlushMillis; saveMutex = new Mutex(); constructor(props) { this.filePath = props?.filePath; this.fileFlushMillis = props?.fileFlushMillis; if (props?.saveOnExit) { process.once("beforeExit", async () => { await this.save(); }); } } /** * Return the file path that this cache was loaded from, and will also save to. */ getFilePath() { return this.filePath; } /** * Return if a key exists in the cache, waiting for any existing operations to complete first. */ async has(key) { return await this.keyedMutex.runExclusiveForKey(key, () => this.keyValues.has(key)); } /** * Return all the keys that exist in the cache. */ keys() { return new Set(this.keyValues.keys()); } /** * Return the count of keys in the cache. */ size() { return this.keyValues.size; } /** * Get the value of a key in the cache, waiting for any existing operations to complete first. */ async get(key) { const cached = this.getUnsafe(key); if (cached !== void 0) { return cached; } return await this.keyedMutex.runExclusiveForKey(key, () => this.getUnsafe(key)); } getUnsafe(key) { return this.keyValues.get(key); } /** * Get the value of a key in the cache if it exists, or compute a value and set it in the cache * otherwise. */ async getOrCompute(key, runnable, shouldRecompute) { const cached = this.getUnsafe(key); if (cached !== void 0 && (shouldRecompute === void 0 || !await shouldRecompute(cached))) { return cached; } return await this.keyedMutex.runExclusiveForKey(key, async () => { if (this.keyValues.has(key)) { const existingValue = this.keyValues.get(key); if (shouldRecompute === void 0 || !await shouldRecompute(existingValue)) { return existingValue; } } const val = await runnable(key); this.setUnsafe(key, val); return val; }); } /** * Get the values of all keys in the cache if they exist, or compute all values and set them * in the cache if any key is missing. */ async getOrComputeAllKeys(keys, runnable) { if (keys.length === 0) { return /* @__PURE__ */ new Map(); } const values = keys.map((key) => this.getUnsafe(key)); if (values.every((value) => value !== void 0)) { return keys.reduce((map, key, idx) => { map.set(key, values[idx]); return map; }, /* @__PURE__ */ new Map()); } await this.keyedMutex.acquireMultiple(keys); try { const values2 = keys.map((key) => this.keyValues.get(key)); if (values2.every((value) => value !== void 0)) { return keys.reduce((map, key, idx) => { map.set(key, values2[idx]); return map; }, /* @__PURE__ */ new Map()); } const computed = await runnable(); for (const [key, value] of computed) { this.setUnsafe(key, value); } return computed; } finally { this.keyedMutex.releaseMultiple(keys); } } /** * Get the value of any key in the cache if one exists, or compute a value and set it under all * keys otherwise. Assumes all keys map to the same value. */ async getOrComputeAnyKeys(keys, runnable, shouldRecompute) { if (keys.length === 0) { return void 0; } const values = keys.map((key) => this.getUnsafe(key)); const firstDefinedValue = values.find((value) => value !== void 0); if (firstDefinedValue !== void 0 && (shouldRecompute === void 0 || !await shouldRecompute(firstDefinedValue))) { return firstDefinedValue; } await this.keyedMutex.acquireMultiple(keys); try { const firstDefinedValue2 = keys.map((key) => this.keyValues.get(key)).find((value) => value !== void 0); if (firstDefinedValue2 !== void 0 && (shouldRecompute === void 0 || !await shouldRecompute(firstDefinedValue2))) { return firstDefinedValue2; } const computed = await runnable(); for (const key of keys) { this.setUnsafe(key, computed); } return computed; } finally { this.keyedMutex.releaseMultiple(keys); } } /** * Set the value of a key in the cache. */ async set(key, val) { await this.keyedMutex.runExclusiveForKey(key, () => { this.setUnsafe(key, val); }); } setUnsafe(key, val) { const oldVal = this.keyValues.get(key); this.keyValues.set(key, val); if (val !== oldVal) { this.saveWithTimeout(); } } /** * Delete a key in the cache. */ async delete(key) { let keysToDelete; if (key instanceof RegExp) { keysToDelete = [...this.keys()].filter((k) => k.match(key) !== null); } else { keysToDelete = [key]; } await this.keyedMutex.runExclusiveGlobally(() => { for (const k of keysToDelete) { this.deleteUnsafe(k); } }); } deleteUnsafe(key) { this.keyValues.delete(key); this.saveWithTimeout(); } /** * Load the cache from a file. */ async load() { if (this.filePath === void 0 || !await FsUtil.exists(this.filePath)) { return this; } try { const chunks = []; await stream.promises.pipeline( fs.createReadStream(this.filePath), zlib.createGunzip(), new stream.Writable({ write(chunk, _enc, cb) { chunks.push(chunk); cb(); } }) ); if (chunks.length === 0) { return this; } const keyValuesObject = JSON.parse(Buffer.concat(chunks).toString("utf8")); const keyValuesEntries = Object.entries(keyValuesObject); this.keyValues = new Map(keyValuesEntries); } catch { } return this; } saveWithTimeout() { this.hasChanged = true; if (this.filePath === void 0 || this.fileFlushMillis === void 0 || this.saveToFileTimeout !== void 0) { return; } this.saveToFileTimeout = Timer.setTimeout(async () => { try { await this.save(); } finally { this.saveToFileTimeout = void 0; } }, this.fileFlushMillis); } /** * Save the cache to a file. */ async save() { try { await this.saveMutex.runExclusive(async () => { if (this.saveToFileTimeout !== void 0) { this.saveToFileTimeout.cancel(); this.saveToFileTimeout = void 0; } if (this.filePath === void 0 || !this.hasChanged) { return; } const keyValuesObject = Object.fromEntries(this.keyValues); const json = JSON.stringify(keyValuesObject); this.hasChanged = false; const dirPath = path.dirname(this.filePath); if (!await FsUtil.exists(dirPath)) { await FsUtil.mkdir(dirPath, { recursive: true }); } const tempFile = await FsUtil.mktemp(this.filePath); try { await stream.promises.pipeline( stream.Readable.from([Buffer.from(json, "utf8")]), zlib.createGzip(), fs.createWriteStream(tempFile) ); const tempFileCache = await new Cache({ filePath: tempFile }).load(); if (tempFileCache.size() !== Object.keys(keyValuesObject).length) { await FsUtil.rm(tempFile, { force: true }); this.hasChanged = true; return; } await FsUtil.mv(tempFile, this.filePath); } catch { await FsUtil.rm(tempFile, { force: true }); this.hasChanged = true; return; } this.saveMutex.cancel(); }); } catch (error) { if (error !== E_CANCELED) { throw error; } } } } export { Cache as default }; //# sourceMappingURL=cache.js.map