UNPKG

memoize-fs

Version:

Node.js solution for memoizing/caching function results on the file system

303 lines (302 loc) 9.37 kB
import * as path from "path"; import * as crypto from "crypto"; import * as fs from "fs/promises"; import { parse } from "meriyah"; const serializer = { serialize, deserialize }; function serialize(val) { function serializeRec(value, refs = /* @__PURE__ */ new WeakSet()) { if (value && typeof value === "object" && refs.has(value)) { return; } if (typeof value === "function") { return; } if (typeof value === "object" && value !== null) { if (refs.has(value)) return; refs.add(value); if (Array.isArray(value)) { const arr = []; for (let i = 0; i < value.length; i++) { arr[i] = serializeRec(value[i], refs); } refs.delete(value); return arr; } const obj = {}; for (const key in value) { obj[key] = serializeRec(value[key], refs); } refs.delete(value); return obj; } return value; } return JSON.stringify(val, function(_name, value) { return serializeRec(value); }); } function deserialize(str) { return JSON.parse(str).data; } function getCacheFilePath(fn, args, opt) { const options = { ...serializer, ...opt }; const salt = options.salt || ""; let fnStr; if (!options.noBody) { fnStr = String(fn); if (options.astBody) { fnStr = parse(fnStr, { jsx: true, next: true }); } fnStr = options.astBody ? JSON.stringify(fnStr) : fnStr; } const argsStr = options.serialize(args); const hash = crypto.createHash("md5").update(fnStr + argsStr + salt).digest("hex"); return path.join(options.cachePath || "", options.cacheId || "", hash); } function checkOptions(allOptions) { if (allOptions.salt && typeof allOptions.salt !== "string") { throw new TypeError("salt option of type string expected, got: " + typeof allOptions.salt); } if (allOptions.cacheId && typeof allOptions.cacheId !== "string") { throw new TypeError("cacheId option of type string expected, got: " + typeof allOptions.cacheId); } if (allOptions.maxAge && typeof allOptions.maxAge !== "number") { throw new TypeError("maxAge option of type number bigger zero expected"); } if (allOptions.serialize && typeof allOptions.serialize !== "function") { throw new TypeError("serialize option of type function expected"); } if (allOptions.deserialize && typeof allOptions.deserialize !== "function") { throw new TypeError("deserialize option of type function expected"); } if (allOptions.retryOnInvalidCache && typeof allOptions.retryOnInvalidCache !== "boolean") { throw new TypeError("retryOnInvalidCache option of type boolean expected"); } } async function initCache(cachePath, cacheOptions) { try { await fs.mkdir(cachePath, { recursive: true }); } catch (err) { if (err && err.code === "EEXIST" && (cacheOptions == null ? void 0 : cacheOptions.throwError) === false) { return; } throw err; } } async function writeResult(r, cb, optExt, filePath) { let resultObj; let resultString; if (r && typeof r === "object" || typeof r === "string") { resultObj = { data: r }; resultString = optExt.serialize(resultObj); } else { resultString = '{"data":' + r + "}"; } try { await fs.writeFile(filePath, resultString); cb(); } catch (err) { cb(err); } } function parseResult(resultString, deserialize2) { try { return deserialize2(resultString); } catch (e) { return void 0; } } function isPromise(something) { return Boolean(something && typeof something.then === "function"); } async function processFn(fn, args, allOptions, filePath, resolve, reject, cacheHitObj) { let writtenResult; let result; try { result = await fn.apply(null, args); } catch (err) { cacheHitObj.cacheHit = void 0; reject(err); return; } if (isPromise(result)) { const resolved = await result; await writeResult(resolved, function() { writtenResult = resolved; }, allOptions, filePath); cacheHitObj.cacheHit = false; resolve(writtenResult); } await writeResult(result, function(err) { if (err) { throw err; } writtenResult = result; }, allOptions, filePath); cacheHitObj.cacheHit = false; resolve(writtenResult); } async function processFnAsync(fn, fnaCb, args, allOptions, filePath, resolve, reject, cacheHitObj) { args.pop(); args.push(async function() { const cbErr = arguments[0]; const cbArgs = Array.prototype.slice.call(arguments); cbArgs.shift(); if (cbErr) { cacheHitObj.cacheHit = void 0; return resolve(cbErr); } cbArgs.unshift(null); try { await writeResult(cbArgs, function() { cacheHitObj.cacheHit = false; resolve(fnaCb.apply(null, cbArgs)); }, allOptions, filePath); } catch (err) { cacheHitObj.cacheHit = void 0; reject(err); } }); fn.apply(null, args); } async function checkFileAgeAndRead(filePath, maxAge) { let fileHandle; try { fileHandle = await fs.open(filePath, "r"); if (maxAge !== void 0) { const stats = await fileHandle.stat(); const now = (/* @__PURE__ */ new Date()).getTime(); const fileAge = now - stats.mtimeMs; if (fileAge > maxAge) { return null; } } const content = await fs.readFile(filePath, { encoding: "utf8" }); return content; } catch (err) { return null; } finally { if (fileHandle) { await fileHandle.close(); } } } function buildMemoizer(memoizerOptions) { const cacheHitObj = { cacheHit: void 0 }; const promiseCache = {}; if (!memoizerOptions || memoizerOptions && typeof memoizerOptions !== "object") { throw new Error("options of type object expected"); } if (typeof memoizerOptions.cachePath !== "string") { throw new Error("option cachePath of type string expected"); } memoizerOptions = { ...serializer, ...memoizerOptions }; checkOptions(memoizerOptions); async function memoizeFn(fn, memoizeOptions) { if (memoizeOptions && typeof memoizeOptions !== "object") { throw new Error("opt of type object expected, got '" + typeof memoizeOptions + "'"); } if (typeof fn !== "function") { throw new Error("fn of type function expected"); } const allOptions = { cacheId: "./", ...memoizerOptions, ...memoizeOptions }; checkOptions(allOptions); await initCache(path.join(allOptions.cachePath, allOptions.cacheId), allOptions); return function() { const args = Array.prototype.slice.call(arguments); const fnaCb = args.length ? args[args.length - 1] : void 0; let isAsync = false; if (typeof fnaCb === "function" && fnaCb.length > 0) { isAsync = true; } const filePath = getCacheFilePathBound(fn, args, allOptions); if (filePath in promiseCache) { return promiseCache[filePath]; } promiseCache[filePath] = new Promise(function(resolve, reject) { async function cacheAndReturn() { if (isAsync) { await processFnAsync(fn, fnaCb, args, allOptions, filePath, resolve, reject, cacheHitObj); return; } await processFn(fn, args, allOptions, filePath, resolve, reject, cacheHitObj); } checkFileAgeAndRead(filePath, allOptions.maxAge).then(function(data) { if (data === null) { return cacheAndReturn(); } const parsedData = parseResult(data, allOptions.deserialize); if (allOptions.retryOnInvalidCache && parsedData === void 0) { return cacheAndReturn(); } function retrieveAndReturn() { cacheHitObj.cacheHit = true; if (isAsync) { resolve(fnaCb.apply(null, parsedData)); } else { resolve(parsedData); } } if (allOptions.force) { allOptions.force = false; cacheAndReturn(); } else { retrieveAndReturn(); } }).catch((err) => { if (err.code === "ENOENT") { return cacheAndReturn(); } else { cacheHitObj.cacheHit = void 0; reject(err); } }); }); promiseCache[filePath].finally(function() { delete promiseCache[filePath]; }).catch(() => { }); return promiseCache[filePath]; }; } async function invalidateCache(cacheId) { if (cacheId && typeof cacheId !== "string") { throw new Error("cacheId option of type string expected, got '" + typeof cacheId + "'"); } else { const cachPath = cacheId ? path.join(memoizerOptions.cachePath || "", cacheId) : memoizerOptions.cachePath || ""; await fs.rm(cachPath, { recursive: true }); } } function getCacheFilePathBound(fn, args, opt) { return getCacheFilePath(fn, args, { ...memoizerOptions, ...opt, cachePath: memoizerOptions.cachePath }); } return { fn: async function(fn, opt) { await initCache(memoizerOptions.cachePath || ""); return memoizeFn(fn, opt); }, get cacheHit() { return cacheHitObj.cacheHit; }, getCacheFilePath: getCacheFilePathBound, invalidate: invalidateCache }; } export { buildMemoizer as default, getCacheFilePath };