cache-entanglement
Version:
Manage caches that are dependent on each other efficiently.
470 lines (464 loc) • 13.9 kB
JavaScript
// 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
};