UNPKG

cache-entanglement

Version:

Manage caches that are dependent on each other efficiently.

470 lines (464 loc) 13.9 kB
// src/utils/LRUMap.ts var LRUMap = class { capacity; map; head = null; tail = null; /** * Creates an instance of LRUMap. * @param capacity The maximum number of items the cache can hold. */ constructor(capacity) { this.capacity = capacity; this.map = /* @__PURE__ */ new Map(); } /** * Promotes a node to the head of the linked list (marks as most recently used). * @param node The node to promote. */ promote(node) { this.extract(node); this.prepend(node); } /** * Disconnects a node from the doubly linked list. * @param node The node to extract. */ extract(node) { if (node.prev) node.prev.next = node.next; else this.head = node.next; if (node.next) node.next.prev = node.prev; else this.tail = node.prev; node.prev = null; node.next = null; } /** * Inserts a node at the head of the doubly linked list. * @param node The node to prepend. */ prepend(node) { node.next = this.head; if (this.head) this.head.prev = node; this.head = node; if (!this.tail) this.tail = node; } /** * Stores or updates a value by key. * If the capacity is exceeded, the least recently used item (tail) is removed. * @param key The key to store. * @param value The value to store. */ set(key, value) { const existing = this.map.get(key); if (existing) { existing.value = value; this.promote(existing); return; } const newNode = { key, value, prev: null, next: null }; this.map.set(key, newNode); this.prepend(newNode); if (this.map.size > this.capacity && this.tail) { this.map.delete(this.tail.key); this.extract(this.tail); } } /** * Retrieves a value by key. * Accessing the item moves it to the "most recently used" position. * @param key The key to look for. * @returns The value associated with the key, or undefined if not found. */ get(key) { const node = this.map.get(key); if (!node) return void 0; this.promote(node); return node.value; } /** * Checks if a key exists in the cache without changing its access order. * @param key The key to check. * @returns True if the key exists, false otherwise. */ has(key) { return this.map.has(key); } /** * Removes a key and its associated value from the cache. * @param key The key to remove. * @returns True if the key was found and removed, false otherwise. */ delete(key) { const node = this.map.get(key); if (!node) return false; this.extract(node); this.map.delete(key); return true; } /** * Returns an iterator of keys in the order of most recently used to least recently used. * @returns An iterable iterator of keys. */ *keys() { let current = this.head; while (current) { yield current.key; current = current.next; } } /** * Returns the current number of items in the cache. */ get size() { return this.map.size; } /** * Clears all items from the cache. */ clear() { this.map.clear(); this.head = null; this.tail = null; } }; // src/CacheEntanglement.ts var CacheEntanglement = class { creation; beforeUpdateHook; capacity; dependencies; caches; parameters; assignments; dependencyProperties; updateRequirements; constructor(creation, option) { option = option ?? {}; const { dependencies, capacity, beforeUpdateHook } = option; this.creation = creation; this.beforeUpdateHook = beforeUpdateHook ?? (() => { }); this.capacity = capacity ?? 100; this.assignments = []; this.caches = new LRUMap(this.capacity); 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); } } } 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); } 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); } return this.caches.get(key); } async update(key, ...parameter) { this.bubbleUpdateSignal(key); await this.resolve(key, ...parameter); return this.caches.get(key); } }; // src/utils/InvertedWeakMap.ts var InvertedWeakMap = class { map; registry; /** * Creates an instance of InvertedWeakMap. */ constructor() { this.map = /* @__PURE__ */ new Map(); this.registry = new FinalizationRegistry((key) => { this.map.delete(key); }); } /** * Clears all entries from the map. */ clear() { this.map.clear(); } /** * Removes an entry from the map by key. * Also unregisters the value from the finalization registry if it still exists. * @param key The key to remove. * @returns True if the entry was removed, false otherwise. */ delete(key) { const ref = this.map.get(key); if (ref) { const raw = ref.deref(); if (raw !== void 0) { this.registry.unregister(raw); } } return this.map.delete(key); } /** * Retrieves a value by key. * @param key The key to look for. * @returns The value if it exists and hasn't been garbage collected, otherwise undefined. */ get(key) { return this.map.get(key)?.deref(); } /** * Checks if a key exists in the map and its value hasn't been garbage collected. * @param key The key to check. * @returns True if the key exists and the value is still alive. */ has(key) { return this.map.has(key) && this.get(key) !== void 0; } /** * Sets a value for the given key using a weak reference. * Registers the value in the finalization registry to automatically remove the key when the value is GC'd. * @param key The key to associate with the value. * @param value The value to store weakly. * @returns This InvertedWeakMap instance. */ set(key, value) { this.map.set(key, new WeakRef(value)); this.registry.register(value, key); return this; } /** * Returns the number of entries currently in the map. * Note: This may include entries whose values have been GC'd but not yet cleaned up by the registry. */ get size() { return this.map.size; } /** * Returns an iterator of keys in the map. * @returns An iterable iterator of keys. */ keys() { return this.map.keys(); } }; export { CacheEntanglementAsync, CacheEntanglementSync, InvertedWeakMap, LRUMap };