cache-entanglement
Version:
Manage caches that are dependent on each other efficiently.
504 lines (496 loc) • 15.1 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
// node_modules/ms/index.js
var require_ms = __commonJS({
"node_modules/ms/index.js"(exports, module) {
var s = 1e3;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25;
module.exports = function(val, options) {
options = options || {};
var type = typeof val;
if (type === "string" && val.length > 0) {
return parse(val);
} else if (type === "number" && isFinite(val)) {
return options.long ? fmtLong(val) : fmtShort(val);
}
throw new Error(
"val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
);
};
function parse(str) {
str = String(str);
if (str.length > 100) {
return;
}
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
str
);
if (!match) {
return;
}
var n = parseFloat(match[1]);
var type = (match[2] || "ms").toLowerCase();
switch (type) {
case "years":
case "year":
case "yrs":
case "yr":
case "y":
return n * y;
case "weeks":
case "week":
case "w":
return n * w;
case "days":
case "day":
case "d":
return n * d;
case "hours":
case "hour":
case "hrs":
case "hr":
case "h":
return n * h;
case "minutes":
case "minute":
case "mins":
case "min":
case "m":
return n * m;
case "seconds":
case "second":
case "secs":
case "sec":
case "s":
return n * s;
case "milliseconds":
case "millisecond":
case "msecs":
case "msec":
case "ms":
return n;
default:
return void 0;
}
}
function fmtShort(ms2) {
var msAbs = Math.abs(ms2);
if (msAbs >= d) {
return Math.round(ms2 / d) + "d";
}
if (msAbs >= h) {
return Math.round(ms2 / h) + "h";
}
if (msAbs >= m) {
return Math.round(ms2 / m) + "m";
}
if (msAbs >= s) {
return Math.round(ms2 / s) + "s";
}
return ms2 + "ms";
}
function fmtLong(ms2) {
var msAbs = Math.abs(ms2);
if (msAbs >= d) {
return plural(ms2, msAbs, d, "day");
}
if (msAbs >= h) {
return plural(ms2, msAbs, h, "hour");
}
if (msAbs >= m) {
return plural(ms2, msAbs, m, "minute");
}
if (msAbs >= s) {
return plural(ms2, msAbs, s, "second");
}
return ms2 + " ms";
}
function plural(ms2, msAbs, n, name) {
var isPlural = msAbs >= n * 1.5;
return Math.round(ms2 / n) + " " + name + (isPlural ? "s" : "");
}
}
});
// src/CacheEntanglement.ts
var import_ms = __toESM(require_ms());
// src/utils/InvertedWeakMap.ts
var InvertedWeakMap = class {
_map;
_keepAlive;
_timeouts;
_registry;
_lifespan;
constructor(option) {
const { lifespan } = option;
this._lifespan = lifespan;
this._map = /* @__PURE__ */ new Map();
this._keepAlive = /* @__PURE__ */ new Map();
this._timeouts = /* @__PURE__ */ new Map();
this._registry = new FinalizationRegistry((key) => {
this._stopExpire(key, true);
this._map.delete(key);
});
}
clear() {
this._keepAlive.clear();
this._map.clear();
}
delete(key) {
const ref = this._map.get(key);
if (ref) {
const raw = ref.deref();
if (raw !== void 0) {
this._registry.unregister(raw);
}
}
this._stopExpire(key, true);
this._keepAlive.delete(key);
return this._map.delete(key);
}
get(key) {
return this._map.get(key)?.deref();
}
has(key) {
return this._map.has(key) && this.get(key) !== void 0;
}
set(key, value) {
this._map.set(key, new WeakRef(value));
this._registry.register(value, key);
if (this._lifespan > 0) {
this._stopExpire(key, true);
this._startExpire(key, value);
}
return this;
}
extendExpire(key) {
if (!(this._lifespan > 0)) {
return;
}
if (!this._keepAlive.has(key)) {
return;
}
this._stopExpire(key, false);
this._startExpire(key, this._keepAlive.get(key));
}
_startExpire(key, value) {
this._keepAlive.set(key, value);
this._timeouts.set(key, setTimeout(() => {
this._keepAlive.delete(key);
}, this._lifespan));
}
_stopExpire(key, removeKeepAlive) {
if (!this._timeouts.has(key)) {
return;
}
const timeout = this._timeouts.get(key);
this._timeouts.delete(key);
clearTimeout(timeout);
if (removeKeepAlive) {
this._keepAlive.delete(key);
}
}
get size() {
return this._map.size;
}
keys() {
return this._map.keys();
}
};
// src/CacheEntanglement.ts
var CacheEntanglement = class {
creation;
beforeUpdateHook;
lifespan;
dependencies;
caches;
parameters;
assignments;
dependencyProperties;
updateRequirements;
constructor(creation, option) {
option = option ?? {};
const {
dependencies,
lifespan,
beforeUpdateHook
} = option;
this.creation = creation;
this.beforeUpdateHook = beforeUpdateHook ?? (() => {
});
this.lifespan = this._normalizeMs(lifespan ?? 0);
this.assignments = [];
this.caches = new InvertedWeakMap({ lifespan: this.lifespan });
this.parameters = /* @__PURE__ */ new Map();
this.dependencies = dependencies ?? {};
this.dependencyProperties = Object.keys(this.dependencies);
this.updateRequirements = /* @__PURE__ */ new Set();
for (const name in this.dependencies) {
const dependency = this.dependencies[name];
if (!dependency.assignments.includes(this)) {
dependency.assignments.push(this);
}
}
}
_normalizeMs(time) {
if (typeof time === "string") {
return (0, import_ms.default)(time);
}
return time;
}
bubbleUpdateSignal(key) {
this.updateRequirements.add(key);
for (let i = 0, len = this.assignments.length; i < len; i++) {
const t = this.assignments[i];
const instance = t;
for (const cacheKey of instance.caches.keys()) {
if (cacheKey === key || cacheKey.startsWith(`${key}/`)) {
instance.bubbleUpdateSignal(cacheKey);
}
}
}
}
dependencyKey(key) {
const i = key.lastIndexOf("/");
if (i === -1) {
return key;
}
return key.substring(0, i);
}
/**
* Returns all keys stored in the instance.
*/
keys() {
return this.parameters.keys();
}
/**
* Deletes all cache values stored in the instance.
*/
clear() {
for (const key of this.keys()) {
this.delete(key);
}
}
/**
* Checks if there is a cache value stored in the key within the instance.
* @param key The key to search.
*/
exists(key) {
return this.parameters.has(key);
}
/**
* Checks if there is a cache value stored in the key within the instance.
* This method is an alias for `exists`.
* @param key The key to search.
*/
has(key) {
return this.exists(key);
}
/**
* Deletes the cache value stored in the key within the instance.
* @param key The key to delete.
*/
delete(key) {
this.caches.delete(key);
this.parameters.delete(key);
this.updateRequirements.delete(key);
for (let i = 0, len = this.assignments.length; i < len; i++) {
const t = this.assignments[i];
const instance = t;
for (const cacheKey of instance.keys()) {
if (cacheKey === key || cacheKey.startsWith(`${key}/`)) {
instance.delete(cacheKey);
}
}
}
}
};
// src/CacheData.ts
var CacheData = class _CacheData {
static StructuredClone = globalThis.structuredClone.bind(globalThis);
_value;
constructor(value) {
this._value = value;
}
/**
* This is cached data.
* It was generated at the time of caching, so there is a risk of modification if it's an object due to shallow copying.
* Therefore, if it's not a primitive type, please avoid using this value directly and use the `clone` method to use a copied version of the data.
*/
get raw() {
return this._value;
}
/**
* The method returns a copied value of the cached data.
* You can pass a function as a parameter to copy the value. This parameter function should return the copied value.
*
* If no parameter is passed, it defaults to using `structuredClone` function to copy the value.
* If you prefer shallow copying instead of deep copying,
* you can use the default options `array-shallow-copy`, `object-shallow-copy` and `deep-copy`,
* which are replaced with functions to shallow copy arrays and objects, respectively. This is a syntactic sugar.
* @param strategy The function that returns the copied value.
* If you want to perform a shallow copy, simply pass the strings `array-shallow-copy` or `object-shallow-copy` for easy use.
* The `array-shallow-copy` strategy performs a shallow copy of an array.
* The `object-shallow-copy` strategy performs a shallow copy of an object.
* The `deep-copy` strategy performs a deep copy of the value using `structuredClone`.
* The default is `deep-copy`.
*/
clone(strategy = "deep-copy") {
if (strategy && typeof strategy !== "string") {
return strategy(this.raw);
}
switch (strategy) {
case "array-shallow-copy":
return [].concat(this.raw);
case "object-shallow-copy":
return Object.assign({}, this.raw);
case "deep-copy":
default:
return _CacheData.StructuredClone(this.raw);
}
}
};
// src/CacheEntanglementSync.ts
var CacheEntanglementSync = class extends CacheEntanglement {
constructor(creation, option) {
super(creation, option);
}
recache(key) {
if (!this.parameters.has(key)) {
return;
}
if (!this.caches.has(key) || this.updateRequirements.has(key)) {
this.resolve(key, ...this.parameters.get(key));
}
return this.caches.get(key);
}
resolve(key, ...parameter) {
const resolved = {};
const dependencyKey = this.dependencyKey(key);
this.beforeUpdateHook(key, dependencyKey, ...parameter);
for (let i = 0, len = this.dependencyProperties.length; i < len; i++) {
const name = this.dependencyProperties[i];
const dependency = this.dependencies[name];
if (!dependency.exists(key) && !dependency.exists(dependencyKey)) {
throw new Error(`The key '${key}' or '${dependencyKey}' has not been assigned yet in dependency '${name.toString()}'.`, {
cause: {
from: this
}
});
}
const dependencyValue = dependency.recache(key) ?? dependency.recache(dependencyKey);
resolved[name] = dependencyValue;
}
const value = new CacheData(this.creation(key, resolved, ...parameter));
this.updateRequirements.delete(key);
this.parameters.set(key, parameter);
this.caches.set(key, value);
return value;
}
get(key) {
if (!this.parameters.has(key)) {
throw new Error(`Cache value not found: ${key}`);
}
return this.cache(key, ...this.parameters.get(key));
}
cache(key, ...parameter) {
if (!this.caches.has(key) || this.updateRequirements.has(key)) {
this.resolve(key, ...parameter);
} else {
this.caches.extendExpire(key);
}
return this.caches.get(key);
}
update(key, ...parameter) {
this.bubbleUpdateSignal(key);
this.resolve(key, ...parameter);
return this.caches.get(key);
}
};
// src/CacheEntanglementAsync.ts
var CacheEntanglementAsync = class extends CacheEntanglement {
constructor(creation, option) {
super(creation, option);
}
async recache(key) {
if (!this.parameters.has(key)) {
return;
}
if (!this.caches.has(key) || this.updateRequirements.has(key)) {
await this.resolve(key, ...this.parameters.get(key));
}
return this.caches.get(key);
}
async resolve(key, ...parameter) {
const resolved = {};
const dependencyKey = this.dependencyKey(key);
await this.beforeUpdateHook(key, dependencyKey, ...parameter);
for (let i = 0, len = this.dependencyProperties.length; i < len; i++) {
const name = this.dependencyProperties[i];
const dependency = this.dependencies[name];
if (!dependency.exists(key) && !dependency.exists(dependencyKey)) {
throw new Error(`The key '${key}' or '${dependencyKey}' has not been assigned yet in dependency '${name.toString()}'.`, {
cause: {
from: this
}
});
}
const dependencyValue = await dependency.recache(key) ?? await dependency.recache(dependencyKey);
resolved[name] = dependencyValue;
}
const value = new CacheData(await this.creation(key, resolved, ...parameter));
this.updateRequirements.delete(key);
this.parameters.set(key, parameter);
this.caches.set(key, value);
return value;
}
async get(key) {
if (!this.parameters.has(key)) {
throw new Error(`Cache value not found: ${key}`);
}
return this.cache(key, ...this.parameters.get(key));
}
async cache(key, ...parameter) {
if (!this.caches.has(key) || this.updateRequirements.has(key)) {
await this.update(key, ...parameter);
} else {
this.caches.extendExpire(key);
}
return this.caches.get(key);
}
async update(key, ...parameter) {
this.bubbleUpdateSignal(key);
await this.resolve(key, ...parameter);
return this.caches.get(key);
}
};
export {
CacheEntanglementAsync,
CacheEntanglementSync
};