@twin.org/core 0.9.1-next.6 → 0.9.1-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -27,7 +27,9 @@ export class AsyncCache {
|
|
|
27
27
|
*/
|
|
28
28
|
static _CLEANUP_INTERVAL_MS = 5000;
|
|
29
29
|
/**
|
|
30
|
-
* Execute an async request and cache the result.
|
|
30
|
+
* Execute an async request and cache the result. A result that resolves to
|
|
31
|
+
* undefined or null is treated as a cache miss for later callers, who re-run
|
|
32
|
+
* requestMethod themselves instead of waiting on the empty value.
|
|
31
33
|
* @param key The key for the entry in the cache.
|
|
32
34
|
* @param ttlMs The TTL of the entry in the cache.
|
|
33
35
|
* @param requestMethod The method to call if not cached.
|
|
@@ -56,6 +58,18 @@ export class AsyncCache {
|
|
|
56
58
|
// If the cache has already resulted in an error, reject it
|
|
57
59
|
return Promise.reject(cachedEntry.error);
|
|
58
60
|
}
|
|
61
|
+
else if (!cachedEntry.inProgress) {
|
|
62
|
+
// The request already settled with an empty/nullish result. Both result
|
|
63
|
+
// and error are empty here, which would otherwise look identical to
|
|
64
|
+
// "still in progress" - but inProgress tells us the one-time settlement
|
|
65
|
+
// callback below already fired, so nothing would ever drain a new queue
|
|
66
|
+
// entry for this stale record. Treat it as a miss and re-run fresh,
|
|
67
|
+
// mirroring how an uncached failure is handled just below.
|
|
68
|
+
if (cache[key] === cachedEntry) {
|
|
69
|
+
delete cache[key];
|
|
70
|
+
}
|
|
71
|
+
return AsyncCache.exec(key, ttlMs, requestMethod, cacheFailures);
|
|
72
|
+
}
|
|
59
73
|
// Otherwise create a promise to return and store the resolver
|
|
60
74
|
// and rejector in the cache entry, so that we can call then
|
|
61
75
|
// when the request is done
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asyncCache.js","sourceRoot":"","sources":["../../../src/utils/asyncCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\n\n/**\n * Cache the results from asynchronous requests.\n */\nexport class AsyncCache {\n\t/**\n\t * Cache key for the shared cache object in the SharedStore.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_KEY = \"asyncCache\";\n\n\t/**\n\t * Cache key for the entry key of the soonest-expiring cache entry, used to optimize cleanup.\n\t * @internal\n\t */\n\tprivate static readonly _NEXT_EXPIRY_CACHE_KEY = \"asyncCacheNextExpiryKey\";\n\n\t/**\n\t * Cache key for the timestamp of the last full cleanup scan.\n\t * @internal\n\t */\n\tprivate static readonly _LAST_CLEANUP_KEY = \"asyncCacheLastCleanup\";\n\n\t/**\n\t * Minimum interval in ms between full cleanup scans.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_INTERVAL_MS = 5000;\n\n\t/**\n\t * Execute an async request and cache the result.\n\t * @param key The key for the entry in the cache.\n\t * @param ttlMs The TTL of the entry in the cache.\n\t * @param requestMethod The method to call if not cached.\n\t * @param cacheFailures Cache failure results, defaults to false.\n\t * @returns The response.\n\t */\n\tpublic static async exec<T = unknown>(\n\t\tkey: string,\n\t\tttlMs: number | undefined,\n\t\trequestMethod: () => Promise<T>,\n\t\tcacheFailures?: boolean\n\t): Promise<T> {\n\t\tconst cacheEnabled = Is.integer(ttlMs) && ttlMs > 0;\n\t\tif (!cacheEnabled) {\n\t\t\t// No caching, just execute the request method\n\t\t\treturn requestMethod();\n\t\t}\n\n\t\tAsyncCache.cleanupExpired();\n\t\t// Cleanup will not necessarily remove the entry for the key we are requesting\n\t\t// as it is throttled, so we also check and evict if expired here to ensure we don't return stale data.\n\t\tAsyncCache.evictIfExpired(key);\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst cachedEntry = cache[key];\n\n\t\t// Do we have a cache entry for the key\n\t\tif (cachedEntry) {\n\t\t\tif (!Is.empty(cachedEntry.result)) {\n\t\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\t\treturn Promise.resolve(cachedEntry.result);\n\t\t\t} else if (!Is.empty(cachedEntry.error)) {\n\t\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\t\treturn Promise.reject(cachedEntry.error);\n\t\t\t}\n\n\t\t\t// Otherwise create a promise to return and store the resolver\n\t\t\t// and rejector in the cache entry, so that we can call then\n\t\t\t// when the request is done\n\n\t\t\tlet storedResolve: ((value: T | PromiseLike<T>) => void) | undefined;\n\t\t\tlet storedReject: ((reason?: unknown) => void) | undefined;\n\t\t\tconst wait = new Promise<T>((resolve, reject) => {\n\t\t\t\tstoredResolve = resolve;\n\t\t\t\tstoredReject = reject;\n\t\t\t});\n\t\t\tif (!Is.empty(storedResolve) && !Is.empty(storedReject)) {\n\t\t\t\tcachedEntry.promiseQueue.push({\n\t\t\t\t\trequestMethod,\n\t\t\t\t\tresolve: storedResolve,\n\t\t\t\t\treject: storedReject\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn wait;\n\t\t}\n\n\t\t// If we don't have a cache entry, create a new one\n\t\tconst expires = Date.now() + ttlMs;\n\t\tconst cacheEntry: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t} = {\n\t\t\tinProgress: true,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tcache[key] = cacheEntry;\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\n\t\t// Return a promise that wraps the original request method\n\t\t// so that we can store any results or errors in the cache\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Call the request method and store the result\n\t\t\trequestMethod()\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then(res => {\n\t\t\t\t\t// If the request was successful, store the result\n\t\t\t\t\tcacheEntry.inProgress = false;\n\t\t\t\t\tcacheEntry.result = res;\n\n\t\t\t\t\t// and resolve both this promise and all the waiters\n\t\t\t\t\tresolve(res);\n\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\twait.resolve(res);\n\t\t\t\t\t}\n\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\treturn res;\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\t// Reject the promise\n\t\t\t\t\treject(err);\n\t\t\t\t\tcacheEntry.inProgress = false;\n\n\t\t\t\t\t// Handle the waiters based on the cacheFailures flag\n\t\t\t\t\tif (cacheFailures ?? false) {\n\t\t\t\t\t\t// If we are caching failures, store the error and reject the waiters\n\t\t\t\t\t\tcacheEntry.error = err;\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\twait.reject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear the waiters so we don't call them again\n\t\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If not caching failures for any queued requests we\n\t\t\t\t\t\t// have no value to either resolve or reject, so we\n\t\t\t\t\t\t// just resolve with the original request method\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-floating-promises\n\t\t\t\t\t\t\tAsyncCache.resolveWaiter(wait.requestMethod, wait.resolve, wait.reject);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cache[key] === cacheEntry) {\n\t\t\t\t\t\t\tdelete cache[key];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Get an entry from the cache.\n\t * @param key The key to get from the cache.\n\t * @returns The item from the cache if it exists, or undefined if the key is missing, expired, or\n\t * its request is still in-progress. Throws if a cached failure exists for the key.\n\t */\n\tpublic static async get<T = unknown>(key: string): Promise<T | undefined> {\n\t\tif (AsyncCache.evictIfExpired(key)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst entry = cache[key];\n\n\t\tif (!Is.empty(entry?.result)) {\n\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\treturn entry.result;\n\t\t}\n\n\t\tif (!Is.empty(entry?.error)) {\n\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\tthrow entry.error as Error;\n\t\t}\n\t}\n\n\t/**\n\t * Set an entry into the cache.\n\t * @param key The key to set in the cache.\n\t * @param value The value to set in the cache.\n\t * @param ttlMs The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).\n\t * @returns A promise that resolves when the entry has been stored.\n\t */\n\tpublic static async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n\t\tconst expires = Date.now() + (ttlMs ?? 1000);\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tcache[key] = {\n\t\t\tresult: value,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * @param key The key to remove from the cache.\n\t */\n\tpublic static remove(key: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tdelete cache[key];\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (nextKey === key) {\n\t\t\tAsyncCache.recalculateNextKey();\n\t\t}\n\t}\n\n\t/**\n\t * Clear the cache.\n\t * @param prefix Optional prefix to clear only entries with that prefix.\n\t */\n\tpublic static clearCache(prefix?: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tif (Is.stringValue(prefix)) {\n\t\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\t\tlet nextKeyRemoved = false;\n\t\t\tfor (const entry in cache) {\n\t\t\t\tif (entry.startsWith(prefix)) {\n\t\t\t\t\tif (entry === nextKey) {\n\t\t\t\t\t\tnextKeyRemoved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdelete cache[entry];\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (nextKeyRemoved) {\n\t\t\t\tAsyncCache.recalculateNextKey();\n\t\t\t}\n\t\t} else {\n\t\t\tSharedStore.set(AsyncCache._CACHE_KEY, {});\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, Date.now());\n\t\t}\n\t}\n\n\t/**\n\t * Perform a cleanup of the expired entries in the cache.\n\t */\n\tpublic static cleanupExpired(): void {\n\t\tconst now = Date.now();\n\t\tconst lastCleanup = SharedStore.get<number>(AsyncCache._LAST_CLEANUP_KEY) ?? 0;\n\t\tif (now - lastCleanup < AsyncCache._CLEANUP_INTERVAL_MS) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry > now) {\n\t\t\t// Nothing is expired yet. Stamp the time so the throttle suppresses further checks\n\t\t\t// for the next interval. Per-key freshness is still guaranteed because exec() and\n\t\t\t// get() call evictIfExpired() directly before reading the cache.\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\t\treturn;\n\t\t}\n\n\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\tAsyncCache.recalculateNextKey();\n\t}\n\n\t/**\n\t * Get the shared cache.\n\t * @returns The shared cache.\n\t * @internal\n\t */\n\tprivate static getSharedCache<T = unknown>(): {\n\t\t[url: string]: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t};\n\t} {\n\t\treturn SharedStore.get<{\n\t\t\t[url: string]: {\n\t\t\t\tresult?: T;\n\t\t\t\terror?: unknown;\n\t\t\t\tinProgress?: boolean;\n\t\t\t\tpromiseQueue: {\n\t\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t\t}[];\n\t\t\t\texpires: number;\n\t\t\t};\n\t\t}>(AsyncCache._CACHE_KEY, () => ({}));\n\t}\n\n\t/**\n\t * Resolve a waiter by re-running its request method safely.\n\t * @param requestMethod The method to execute.\n\t * @param resolve The resolver for the waiter.\n\t * @param reject The rejector for the waiter.\n\t * @internal\n\t */\n\tprivate static async resolveWaiter<T>(\n\t\trequestMethod: () => Promise<T>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void\n\t): Promise<void> {\n\t\ttry {\n\t\t\tresolve(await requestMethod());\n\t\t} catch (waitErr) {\n\t\t\treject(waitErr);\n\t\t}\n\t}\n\n\t/**\n\t * Scan all cache entries, delete any that have expired, and record the key of the\n\t * soonest-expiring remaining entry.\n\t * @internal\n\t */\n\tprivate static recalculateNextKey(): void {\n\t\tconst now = Date.now();\n\t\tconst cache = AsyncCache.getSharedCache();\n\n\t\tif (Object.keys(cache).length === 0) {\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\treturn;\n\t\t}\n\n\t\tlet newNextKey: string | undefined;\n\t\tlet newNextExpiry = Number.MAX_SAFE_INTEGER;\n\t\tfor (const entryKey in cache) {\n\t\t\tconst { expires, inProgress } = cache[entryKey];\n\t\t\tif (expires > 0 && expires < now && inProgress !== true) {\n\t\t\t\tdelete cache[entryKey];\n\t\t\t} else if (expires > 0 && expires < newNextExpiry) {\n\t\t\t\tnewNextExpiry = expires;\n\t\t\t\tnewNextKey = entryKey;\n\t\t\t}\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, newNextKey);\n\t}\n\n\t/**\n\t * Update the tracked next-expiry entry key if the given entry expires sooner than the current one.\n\t * @param key The cache entry key.\n\t * @param expires The expiry timestamp of the entry.\n\t * @internal\n\t */\n\tprivate static updateNextExpiryKey(key: string, expires: number): void {\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry <= expires) {\n\t\t\treturn;\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, key);\n\t}\n\n\t/**\n\t * Deletes the given key from the cache if it has expired and is not in-progress.\n\t * Returns true if the entry was evicted.\n\t * @param key The cache entry key to check.\n\t * @returns True if the expired entry was removed; otherwise, false.\n\t * @internal\n\t */\n\tprivate static evictIfExpired(key: string): boolean {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst entry = cache[key];\n\t\tif (!Is.empty(entry) && entry.expires > 0 && entry.expires < Date.now() && !entry.inProgress) {\n\t\t\tdelete cache[key];\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Returns the expiry timestamp of the tracked next-expiry entry, or undefined if there\n\t * is no tracked entry or it is no longer present in the cache.\n\t * @returns The next tracked expiry timestamp, if one is still available.\n\t * @internal\n\t */\n\tprivate static getNextExpiry(): number | undefined {\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (Is.empty(nextKey)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst nextEntry = cache[nextKey];\n\t\treturn Is.empty(nextEntry) ? undefined : nextEntry.expires;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asyncCache.js","sourceRoot":"","sources":["../../../src/utils/asyncCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\n\n/**\n * Cache the results from asynchronous requests.\n */\nexport class AsyncCache {\n\t/**\n\t * Cache key for the shared cache object in the SharedStore.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_KEY = \"asyncCache\";\n\n\t/**\n\t * Cache key for the entry key of the soonest-expiring cache entry, used to optimize cleanup.\n\t * @internal\n\t */\n\tprivate static readonly _NEXT_EXPIRY_CACHE_KEY = \"asyncCacheNextExpiryKey\";\n\n\t/**\n\t * Cache key for the timestamp of the last full cleanup scan.\n\t * @internal\n\t */\n\tprivate static readonly _LAST_CLEANUP_KEY = \"asyncCacheLastCleanup\";\n\n\t/**\n\t * Minimum interval in ms between full cleanup scans.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_INTERVAL_MS = 5000;\n\n\t/**\n\t * Execute an async request and cache the result. A result that resolves to\n\t * undefined or null is treated as a cache miss for later callers, who re-run\n\t * requestMethod themselves instead of waiting on the empty value.\n\t * @param key The key for the entry in the cache.\n\t * @param ttlMs The TTL of the entry in the cache.\n\t * @param requestMethod The method to call if not cached.\n\t * @param cacheFailures Cache failure results, defaults to false.\n\t * @returns The response.\n\t */\n\tpublic static async exec<T = unknown>(\n\t\tkey: string,\n\t\tttlMs: number | undefined,\n\t\trequestMethod: () => Promise<T>,\n\t\tcacheFailures?: boolean\n\t): Promise<T> {\n\t\tconst cacheEnabled = Is.integer(ttlMs) && ttlMs > 0;\n\t\tif (!cacheEnabled) {\n\t\t\t// No caching, just execute the request method\n\t\t\treturn requestMethod();\n\t\t}\n\n\t\tAsyncCache.cleanupExpired();\n\t\t// Cleanup will not necessarily remove the entry for the key we are requesting\n\t\t// as it is throttled, so we also check and evict if expired here to ensure we don't return stale data.\n\t\tAsyncCache.evictIfExpired(key);\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst cachedEntry = cache[key];\n\n\t\t// Do we have a cache entry for the key\n\t\tif (cachedEntry) {\n\t\t\tif (!Is.empty(cachedEntry.result)) {\n\t\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\t\treturn Promise.resolve(cachedEntry.result);\n\t\t\t} else if (!Is.empty(cachedEntry.error)) {\n\t\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\t\treturn Promise.reject(cachedEntry.error);\n\t\t\t} else if (!cachedEntry.inProgress) {\n\t\t\t\t// The request already settled with an empty/nullish result. Both result\n\t\t\t\t// and error are empty here, which would otherwise look identical to\n\t\t\t\t// \"still in progress\" - but inProgress tells us the one-time settlement\n\t\t\t\t// callback below already fired, so nothing would ever drain a new queue\n\t\t\t\t// entry for this stale record. Treat it as a miss and re-run fresh,\n\t\t\t\t// mirroring how an uncached failure is handled just below.\n\t\t\t\tif (cache[key] === cachedEntry) {\n\t\t\t\t\tdelete cache[key];\n\t\t\t\t}\n\t\t\t\treturn AsyncCache.exec(key, ttlMs, requestMethod, cacheFailures);\n\t\t\t}\n\n\t\t\t// Otherwise create a promise to return and store the resolver\n\t\t\t// and rejector in the cache entry, so that we can call then\n\t\t\t// when the request is done\n\n\t\t\tlet storedResolve: ((value: T | PromiseLike<T>) => void) | undefined;\n\t\t\tlet storedReject: ((reason?: unknown) => void) | undefined;\n\t\t\tconst wait = new Promise<T>((resolve, reject) => {\n\t\t\t\tstoredResolve = resolve;\n\t\t\t\tstoredReject = reject;\n\t\t\t});\n\t\t\tif (!Is.empty(storedResolve) && !Is.empty(storedReject)) {\n\t\t\t\tcachedEntry.promiseQueue.push({\n\t\t\t\t\trequestMethod,\n\t\t\t\t\tresolve: storedResolve,\n\t\t\t\t\treject: storedReject\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn wait;\n\t\t}\n\n\t\t// If we don't have a cache entry, create a new one\n\t\tconst expires = Date.now() + ttlMs;\n\t\tconst cacheEntry: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t} = {\n\t\t\tinProgress: true,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tcache[key] = cacheEntry;\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\n\t\t// Return a promise that wraps the original request method\n\t\t// so that we can store any results or errors in the cache\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Call the request method and store the result\n\t\t\trequestMethod()\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then(res => {\n\t\t\t\t\t// If the request was successful, store the result\n\t\t\t\t\tcacheEntry.inProgress = false;\n\t\t\t\t\tcacheEntry.result = res;\n\n\t\t\t\t\t// and resolve both this promise and all the waiters\n\t\t\t\t\tresolve(res);\n\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\twait.resolve(res);\n\t\t\t\t\t}\n\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\treturn res;\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\t// Reject the promise\n\t\t\t\t\treject(err);\n\t\t\t\t\tcacheEntry.inProgress = false;\n\n\t\t\t\t\t// Handle the waiters based on the cacheFailures flag\n\t\t\t\t\tif (cacheFailures ?? false) {\n\t\t\t\t\t\t// If we are caching failures, store the error and reject the waiters\n\t\t\t\t\t\tcacheEntry.error = err;\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\twait.reject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear the waiters so we don't call them again\n\t\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If not caching failures for any queued requests we\n\t\t\t\t\t\t// have no value to either resolve or reject, so we\n\t\t\t\t\t\t// just resolve with the original request method\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-floating-promises\n\t\t\t\t\t\t\tAsyncCache.resolveWaiter(wait.requestMethod, wait.resolve, wait.reject);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cache[key] === cacheEntry) {\n\t\t\t\t\t\t\tdelete cache[key];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Get an entry from the cache.\n\t * @param key The key to get from the cache.\n\t * @returns The item from the cache if it exists, or undefined if the key is missing, expired, or\n\t * its request is still in-progress. Throws if a cached failure exists for the key.\n\t */\n\tpublic static async get<T = unknown>(key: string): Promise<T | undefined> {\n\t\tif (AsyncCache.evictIfExpired(key)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst entry = cache[key];\n\n\t\tif (!Is.empty(entry?.result)) {\n\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\treturn entry.result;\n\t\t}\n\n\t\tif (!Is.empty(entry?.error)) {\n\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\tthrow entry.error as Error;\n\t\t}\n\t}\n\n\t/**\n\t * Set an entry into the cache.\n\t * @param key The key to set in the cache.\n\t * @param value The value to set in the cache.\n\t * @param ttlMs The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).\n\t * @returns A promise that resolves when the entry has been stored.\n\t */\n\tpublic static async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n\t\tconst expires = Date.now() + (ttlMs ?? 1000);\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tcache[key] = {\n\t\t\tresult: value,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * @param key The key to remove from the cache.\n\t */\n\tpublic static remove(key: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tdelete cache[key];\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (nextKey === key) {\n\t\t\tAsyncCache.recalculateNextKey();\n\t\t}\n\t}\n\n\t/**\n\t * Clear the cache.\n\t * @param prefix Optional prefix to clear only entries with that prefix.\n\t */\n\tpublic static clearCache(prefix?: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tif (Is.stringValue(prefix)) {\n\t\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\t\tlet nextKeyRemoved = false;\n\t\t\tfor (const entry in cache) {\n\t\t\t\tif (entry.startsWith(prefix)) {\n\t\t\t\t\tif (entry === nextKey) {\n\t\t\t\t\t\tnextKeyRemoved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdelete cache[entry];\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (nextKeyRemoved) {\n\t\t\t\tAsyncCache.recalculateNextKey();\n\t\t\t}\n\t\t} else {\n\t\t\tSharedStore.set(AsyncCache._CACHE_KEY, {});\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, Date.now());\n\t\t}\n\t}\n\n\t/**\n\t * Perform a cleanup of the expired entries in the cache.\n\t */\n\tpublic static cleanupExpired(): void {\n\t\tconst now = Date.now();\n\t\tconst lastCleanup = SharedStore.get<number>(AsyncCache._LAST_CLEANUP_KEY) ?? 0;\n\t\tif (now - lastCleanup < AsyncCache._CLEANUP_INTERVAL_MS) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry > now) {\n\t\t\t// Nothing is expired yet. Stamp the time so the throttle suppresses further checks\n\t\t\t// for the next interval. Per-key freshness is still guaranteed because exec() and\n\t\t\t// get() call evictIfExpired() directly before reading the cache.\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\t\treturn;\n\t\t}\n\n\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\tAsyncCache.recalculateNextKey();\n\t}\n\n\t/**\n\t * Get the shared cache.\n\t * @returns The shared cache.\n\t * @internal\n\t */\n\tprivate static getSharedCache<T = unknown>(): {\n\t\t[url: string]: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t};\n\t} {\n\t\treturn SharedStore.get<{\n\t\t\t[url: string]: {\n\t\t\t\tresult?: T;\n\t\t\t\terror?: unknown;\n\t\t\t\tinProgress?: boolean;\n\t\t\t\tpromiseQueue: {\n\t\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t\t}[];\n\t\t\t\texpires: number;\n\t\t\t};\n\t\t}>(AsyncCache._CACHE_KEY, () => ({}));\n\t}\n\n\t/**\n\t * Resolve a waiter by re-running its request method safely.\n\t * @param requestMethod The method to execute.\n\t * @param resolve The resolver for the waiter.\n\t * @param reject The rejector for the waiter.\n\t * @internal\n\t */\n\tprivate static async resolveWaiter<T>(\n\t\trequestMethod: () => Promise<T>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void\n\t): Promise<void> {\n\t\ttry {\n\t\t\tresolve(await requestMethod());\n\t\t} catch (waitErr) {\n\t\t\treject(waitErr);\n\t\t}\n\t}\n\n\t/**\n\t * Scan all cache entries, delete any that have expired, and record the key of the\n\t * soonest-expiring remaining entry.\n\t * @internal\n\t */\n\tprivate static recalculateNextKey(): void {\n\t\tconst now = Date.now();\n\t\tconst cache = AsyncCache.getSharedCache();\n\n\t\tif (Object.keys(cache).length === 0) {\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\treturn;\n\t\t}\n\n\t\tlet newNextKey: string | undefined;\n\t\tlet newNextExpiry = Number.MAX_SAFE_INTEGER;\n\t\tfor (const entryKey in cache) {\n\t\t\tconst { expires, inProgress } = cache[entryKey];\n\t\t\tif (expires > 0 && expires < now && inProgress !== true) {\n\t\t\t\tdelete cache[entryKey];\n\t\t\t} else if (expires > 0 && expires < newNextExpiry) {\n\t\t\t\tnewNextExpiry = expires;\n\t\t\t\tnewNextKey = entryKey;\n\t\t\t}\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, newNextKey);\n\t}\n\n\t/**\n\t * Update the tracked next-expiry entry key if the given entry expires sooner than the current one.\n\t * @param key The cache entry key.\n\t * @param expires The expiry timestamp of the entry.\n\t * @internal\n\t */\n\tprivate static updateNextExpiryKey(key: string, expires: number): void {\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry <= expires) {\n\t\t\treturn;\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, key);\n\t}\n\n\t/**\n\t * Deletes the given key from the cache if it has expired and is not in-progress.\n\t * Returns true if the entry was evicted.\n\t * @param key The cache entry key to check.\n\t * @returns True if the expired entry was removed; otherwise, false.\n\t * @internal\n\t */\n\tprivate static evictIfExpired(key: string): boolean {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst entry = cache[key];\n\t\tif (!Is.empty(entry) && entry.expires > 0 && entry.expires < Date.now() && !entry.inProgress) {\n\t\t\tdelete cache[key];\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Returns the expiry timestamp of the tracked next-expiry entry, or undefined if there\n\t * is no tracked entry or it is no longer present in the cache.\n\t * @returns The next tracked expiry timestamp, if one is still available.\n\t * @internal\n\t */\n\tprivate static getNextExpiry(): number | undefined {\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (Is.empty(nextKey)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst nextEntry = cache[nextKey];\n\t\treturn Is.empty(nextEntry) ? undefined : nextEntry.expires;\n\t}\n}\n"]}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export declare class AsyncCache {
|
|
5
5
|
/**
|
|
6
|
-
* Execute an async request and cache the result.
|
|
6
|
+
* Execute an async request and cache the result. A result that resolves to
|
|
7
|
+
* undefined or null is treated as a cache miss for later callers, who re-run
|
|
8
|
+
* requestMethod themselves instead of waiting on the empty value.
|
|
7
9
|
* @param key The key for the entry in the cache.
|
|
8
10
|
* @param ttlMs The TTL of the entry in the cache.
|
|
9
11
|
* @param requestMethod The method to call if not cached.
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.1-next.7](https://github.com/iotaledger/twin-framework/compare/core-v0.9.1-next.6...core-v0.9.1-next.7) (2026-07-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* settle AsyncCache waiters correctly when a request resolves undefined or null ([#401](https://github.com/iotaledger/twin-framework/issues/401)) ([6ae3f8a](https://github.com/iotaledger/twin-framework/commit/6ae3f8aca688ca76d93ec3964178a3b1a8895453))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/nameof bumped from 0.9.1-next.6 to 0.9.1-next.7
|
|
16
|
+
* devDependencies
|
|
17
|
+
* @twin.org/nameof-transformer bumped from 0.9.1-next.6 to 0.9.1-next.7
|
|
18
|
+
* @twin.org/nameof-vitest-plugin bumped from 0.9.1-next.6 to 0.9.1-next.7
|
|
19
|
+
|
|
3
20
|
## [0.9.1-next.6](https://github.com/iotaledger/twin-framework/compare/core-v0.9.1-next.5...core-v0.9.1-next.6) (2026-07-03)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -18,7 +18,9 @@ Cache the results from asynchronous requests.
|
|
|
18
18
|
|
|
19
19
|
> `static` **exec**\<`T`\>(`key`, `ttlMs`, `requestMethod`, `cacheFailures?`): `Promise`\<`T`\>
|
|
20
20
|
|
|
21
|
-
Execute an async request and cache the result.
|
|
21
|
+
Execute an async request and cache the result. A result that resolves to
|
|
22
|
+
undefined or null is treated as a cache miss for later callers, who re-run
|
|
23
|
+
requestMethod themselves instead of waiting on the empty value.
|
|
22
24
|
|
|
23
25
|
#### Type Parameters
|
|
24
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/core",
|
|
3
|
-
"version": "0.9.1-next.
|
|
3
|
+
"version": "0.9.1-next.7",
|
|
4
4
|
"description": "Helper methods/classes for data type checking/validation/guarding/error handling",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@twin.org/nameof": "0.9.1-next.
|
|
17
|
+
"@twin.org/nameof": "0.9.1-next.7",
|
|
18
18
|
"intl-messageformat": "11.2.8",
|
|
19
19
|
"rfc6902": "5.2.0"
|
|
20
20
|
},
|