UNPKG

@naturalcycles/js-lib

Version:

Standard library for universal (browser + Node.js) javascript

117 lines (116 loc) 5.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports._AsyncMemo = void 0; exports._getAsyncMemo = _getAsyncMemo; const assert_1 = require("../error/assert"); const types_1 = require("../types"); const decorator_util_1 = require("./decorator.util"); const memo_util_1 = require("./memo.util"); /** * Like @_Memo, but allowing async MemoCache implementation. * * Implementation is more complex than @_Memo, because it needs to handle "in-flight" Promises * while waiting for cache to resolve, to prevent "async swarm" issue. * * @experimental consider normal @_Memo for most of the cases, it's stable and predictable */ // eslint-disable-next-line @typescript-eslint/naming-convention const _AsyncMemo = (opt) => (target, key, descriptor) => { (0, assert_1._assertTypeOf)(descriptor.value, 'function', 'Memoization can be applied only to methods'); const originalFn = descriptor.value; // Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance. const instanceCache = new Map(); // Cache from Instance to Map<key, Promise> // This cache is temporary, with only one purpose - to prevent "async swarm" // It only holds values that are "in-flight", until Promise is resolved // After it's resolved - it's evicted from the cache and moved to the "proper" `instanceCache` const instancePromiseCache = new Map(); const { logger = console, cacheFactory, cacheKeyFn = memo_util_1.jsonMemoSerializer } = opt; const keyStr = String(key); const methodSignature = (0, decorator_util_1._getTargetMethodSignature)(target, keyStr); // eslint-disable-next-line @typescript-eslint/promise-function-async descriptor.value = function (...args) { const ctx = this; const cacheKey = cacheKeyFn(args); let cache = instanceCache.get(ctx); let promiseCache = instancePromiseCache.get(ctx); if (!cache) { cache = cacheFactory(); instanceCache.set(ctx, cache); // here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created // UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache) } if (!promiseCache) { promiseCache = new Map(); instancePromiseCache.set(ctx, promiseCache); } let promise = promiseCache.get(cacheKey); // If there's already "in-flight" cache request - return that, to avoid "async swarm" if (promise) { // console.log('return promise', promiseCache.size) return promise; } promise = cache.get(cacheKey).then(async (value) => { if (value !== types_1.MISS) { // console.log('hit', promiseCache.size) promiseCache.delete(cacheKey); return value; } // Miss // console.log('miss', promiseCache.size) return await onMiss(); }, async (err) => { // Log the cache error and proceed "as cache Miss" logger.error(err); return await onMiss(); }); promiseCache.set(cacheKey, promise); return promise; // async function onMiss() { try { const value = await originalFn.apply(ctx, args); // Save the value in the Cache, in parallel, // not to slow down the main function execution // and not to fail on possible cache issues void (async () => { try { await cache.set(cacheKey, value); } catch (err) { logger.error(err); // log and ignore the error } finally { // Clear the "in-flight" promise cache entry, as we now have a "permanent" cache entry promiseCache.delete(cacheKey); // console.log('cache set and cleared', promiseCache!.size) } })(); return value; } catch (err) { promiseCache.delete(cacheKey); throw err; } } }; (0, types_1._objectAssign)(descriptor.value, { clear: async () => { logger.log(`${methodSignature} @_AsyncMemo.clear()`); await Promise.all([...instanceCache.values()].map(c => c.clear())); instanceCache.clear(); }, getInstanceCache: () => instanceCache, getCache: instance => instanceCache.get(instance), }); return descriptor; }; exports._AsyncMemo = _AsyncMemo; /** Call it on a method that is decorated with `@_AsyncMemo` to get access to additional functions, e.g `clear` to clear the cache, or get its underlying data. */ function _getAsyncMemo(method) { (0, assert_1._assert)(typeof method?.getInstanceCache === 'function', 'method is not an AsyncMemo instance'); return method; }