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
JavaScript
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