diff --git a/src/common/models/executionCache.model.ts b/src/common/models/executionCache.model.ts index 4c30ade..bd9b67d 100644 --- a/src/common/models/executionCache.model.ts +++ b/src/common/models/executionCache.model.ts @@ -24,17 +24,23 @@ export interface CacheContext { /** Unique key identifying the cache entry. */ cacheKey: string; + /** The time-to-live (TTL) for the cache entry. */ ttl: number; - /** Flag indicating whether the value is cached. */ + /** Flag indicating whether the cached value is bypassed and a fresh computation is triggered. */ + isBypassed: boolean; + + /** + * Flag indicating whether the value is found in the cache. + * @remarks: To confirm it was retrieved from cache, ensure `isBypassed` is `false`. + * */ isCached: boolean; /** The cached value, if any. */ value?: O; } - /** * Configuration options for caching behavior. */ @@ -42,6 +48,9 @@ export interface CacheOptions { /** Time-to-live (TTL) for cache items. Can be static (number) or dynamic (function that returns a number). */ ttl: number | ((params: { metadata: FunctionMetadata; inputs: unknown[] }) => number); + /** A function that returns `true` to ignore existing cache and force a fresh computation. Defaults to `false`. */ + bypass?: (params: { metadata: FunctionMetadata; inputs: unknown[] }) => boolean; + /** Function to generate a custom cache key based on method metadata and arguments. */ cacheKey?: (params: { metadata: FunctionMetadata; inputs: unknown[] }) => string; diff --git a/src/execution/cache.decorator.spec.ts b/src/execution/cache.decorator.spec.ts index f0da801..cb919b5 100644 --- a/src/execution/cache.decorator.spec.ts +++ b/src/execution/cache.decorator.spec.ts @@ -5,13 +5,13 @@ describe('cache decorator', () => { let memoizationCheckCount = 0; let memoizedCalls = 0; let totalFunctionCalls = 0; - + let bypassCache= false; class DataService { @cache({ ttl: 3000, onCacheEvent: (cacheContext) => { memoizationCheckCount++; - if (cacheContext.isCached) { + if (cacheContext.isCached && !cacheContext.isBypassed) { memoizedCalls++; } } @@ -21,6 +21,21 @@ describe('cache decorator', () => { return new Promise((resolve) => setTimeout(() => resolve(`Data for ID: ${id}`), 100)); } + @cache({ + ttl: 3000, + bypass: () => bypassCache, + onCacheEvent: (cacheContext) => { + memoizationCheckCount++; + if (cacheContext.isCached && !cacheContext.isBypassed) { + memoizedCalls++; + } + } + }) + async fetchDataWithByPassedCacheFunction(id: number): Promise { + totalFunctionCalls++; + return new Promise((resolve) => setTimeout(() => resolve(`Data for ID: ${id}`), 100)); + } + @cache({ ttl: 3000, onCacheEvent: (cacheContext) => { @@ -66,6 +81,32 @@ describe('cache decorator', () => { expect(totalFunctionCalls).toBe(2); // No extra new calls expect(memoizationCheckCount).toBe(4); // 4 checks in total + // test NO cache for a Bypassed cache function + memoizationCheckCount = 0; + memoizedCalls = 0; + totalFunctionCalls = 0; + const result21 = await service.fetchDataWithByPassedCacheFunction(2); + expect(result21).toBe('Data for ID: 2'); + expect(memoizedCalls).toBe(0); // ID 2 result is now memoized + expect(totalFunctionCalls).toBe(1); // extra new call + expect(memoizationCheckCount).toBe(1); // 5 checks in total + + bypassCache = false; + const result22 = await service.fetchDataWithByPassedCacheFunction(2); + expect(result22).toBe('Data for ID: 2'); + expect(memoizedCalls).toBe(1); // ID 2 result is now memoized + expect(totalFunctionCalls).toBe(1); // NO extra new call + expect(memoizationCheckCount).toBe(2); // 2 checks in total + + bypassCache = true; + const result23 = await service.fetchDataWithByPassedCacheFunction(2); + expect(result23).toBe('Data for ID: 2'); + expect(memoizedCalls).toBe(1); // ID 2 result is NOT RETRIEVED FROM CACHE AS THEY ARE BYPASSED + expect(totalFunctionCalls).toBe(2); // extra new call as bypassCache = true + expect(memoizationCheckCount).toBe(3); // 5 checks in total + + + // test NO cache for a throwing async method memoizationCheckCount = 0; memoizedCalls = 0; diff --git a/src/execution/cache.decorator.ts b/src/execution/cache.decorator.ts index 72890cc..b74d420 100644 --- a/src/execution/cache.decorator.ts +++ b/src/execution/cache.decorator.ts @@ -23,6 +23,7 @@ export function cache(options: CacheOptions): MethodDecorator { functionId: thisMethodMetadata.methodSignature as string, ...options, cacheKey: attachFunctionMetadata.bind(this)(options.cacheKey, thisMethodMetadata), + bypass: attachFunctionMetadata.bind(this)(options.bypass, thisMethodMetadata), ttl: attachFunctionMetadata.bind(this)(options.ttl, thisMethodMetadata), onCacheEvent: attachFunctionMetadata.bind(this)(options.onCacheEvent, thisMethodMetadata) }); diff --git a/src/execution/cache.ts b/src/execution/cache.ts index fcafb40..58f3058 100644 --- a/src/execution/cache.ts +++ b/src/execution/cache.ts @@ -21,21 +21,31 @@ export async function executeCache( ): Promise | O> { const functionMetadata = extractFunctionMetadata(blockFunction); const cacheKey = options.cacheKey?.({ metadata: functionMetadata, inputs }) ?? generateHashId(...inputs); + const bypass = typeof options.bypass === 'function' && !!options.bypass?.({ metadata: functionMetadata, inputs }); const ttl = typeof options.ttl === 'function' ? options.ttl({ metadata: functionMetadata, inputs }) : options.ttl; - let cacheStore: CacheStore | MapCacheStore; + if (options.cacheManager) { cacheStore = typeof options.cacheManager === 'function' ? options.cacheManager(this) : options.cacheManager; } else { cacheStore = new MapCacheStore(this[cacheStoreKey], options.functionId); } - const cachedValue: O = (await cacheStore.get(cacheKey)) as O; + const cachedValue: O | undefined = (await cacheStore.get(cacheKey)) as O; + if (typeof options.onCacheEvent === 'function') { - options.onCacheEvent({ ttl, metadata: functionMetadata, inputs, cacheKey, isCached: !!cachedValue, value: cachedValue }); + options.onCacheEvent({ + ttl, + metadata: functionMetadata, + inputs, + cacheKey, + isBypassed: !!bypass, + isCached: !!cachedValue, + value: cachedValue + }); } - if (cachedValue) { + if (!bypass && cachedValue) { return cachedValue; } else { return (execute.bind(this) as typeof execute)( @@ -44,7 +54,7 @@ export async function executeCache( [], (res) => { cacheStore.set(cacheKey, res as O, ttl); - if((cacheStore as MapCacheStore).fullStorage) { + if ((cacheStore as MapCacheStore).fullStorage) { this[cacheStoreKey] = (cacheStore as MapCacheStore).fullStorage; } return res; diff --git a/src/execution/trace.decorator.spec.ts b/src/execution/trace.decorator.spec.ts index b4cb4a6..926d570 100644 --- a/src/execution/trace.decorator.spec.ts +++ b/src/execution/trace.decorator.spec.ts @@ -172,8 +172,8 @@ describe('trace decorator', () => { url: string, traceContext: Record = {} ): Promise<{ - data: string; - }> { + data: string; + }> { return this.fetchDataFunction(url, traceContext); }