Skip to content

Commit 8b6e634

Browse files
authored
Merge pull request #70 from tabkram/feature/cache
fix: add bypass argument to cache execution
2 parents 4f8628b + 13658aa commit 8b6e634

File tree

5 files changed

+72
-11
lines changed

5 files changed

+72
-11
lines changed

src/common/models/executionCache.model.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,33 @@ export interface CacheContext<O = unknown> {
2424
/** Unique key identifying the cache entry. */
2525
cacheKey: string;
2626

27+
2728
/** The time-to-live (TTL) for the cache entry. */
2829
ttl: number;
2930

30-
/** Flag indicating whether the value is cached. */
31+
/** Flag indicating whether the cached value is bypassed and a fresh computation is triggered. */
32+
isBypassed: boolean;
33+
34+
/**
35+
* Flag indicating whether the value is found in the cache.
36+
* @remarks: To confirm it was retrieved from cache, ensure `isBypassed` is `false`.
37+
* */
3138
isCached: boolean;
3239

3340
/** The cached value, if any. */
3441
value?: O;
3542
}
3643

37-
3844
/**
3945
* Configuration options for caching behavior.
4046
*/
4147
export interface CacheOptions<O = unknown> {
4248
/** Time-to-live (TTL) for cache items. Can be static (number) or dynamic (function that returns a number). */
4349
ttl: number | ((params: { metadata: FunctionMetadata; inputs: unknown[] }) => number);
4450

51+
/** A function that returns `true` to ignore existing cache and force a fresh computation. Defaults to `false`. */
52+
bypass?: (params: { metadata: FunctionMetadata; inputs: unknown[] }) => boolean;
53+
4554
/** Function to generate a custom cache key based on method metadata and arguments. */
4655
cacheKey?: (params: { metadata: FunctionMetadata; inputs: unknown[] }) => string;
4756

src/execution/cache.decorator.spec.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ describe('cache decorator', () => {
55
let memoizationCheckCount = 0;
66
let memoizedCalls = 0;
77
let totalFunctionCalls = 0;
8-
8+
let bypassCache= false;
99
class DataService {
1010
@cache({
1111
ttl: 3000,
1212
onCacheEvent: (cacheContext) => {
1313
memoizationCheckCount++;
14-
if (cacheContext.isCached) {
14+
if (cacheContext.isCached && !cacheContext.isBypassed) {
1515
memoizedCalls++;
1616
}
1717
}
@@ -21,6 +21,21 @@ describe('cache decorator', () => {
2121
return new Promise((resolve) => setTimeout(() => resolve(`Data for ID: ${id}`), 100));
2222
}
2323

24+
@cache({
25+
ttl: 3000,
26+
bypass: () => bypassCache,
27+
onCacheEvent: (cacheContext) => {
28+
memoizationCheckCount++;
29+
if (cacheContext.isCached && !cacheContext.isBypassed) {
30+
memoizedCalls++;
31+
}
32+
}
33+
})
34+
async fetchDataWithByPassedCacheFunction(id: number): Promise<string> {
35+
totalFunctionCalls++;
36+
return new Promise((resolve) => setTimeout(() => resolve(`Data for ID: ${id}`), 100));
37+
}
38+
2439
@cache({
2540
ttl: 3000,
2641
onCacheEvent: (cacheContext) => {
@@ -66,6 +81,32 @@ describe('cache decorator', () => {
6681
expect(totalFunctionCalls).toBe(2); // No extra new calls
6782
expect(memoizationCheckCount).toBe(4); // 4 checks in total
6883

84+
// test NO cache for a Bypassed cache function
85+
memoizationCheckCount = 0;
86+
memoizedCalls = 0;
87+
totalFunctionCalls = 0;
88+
const result21 = await service.fetchDataWithByPassedCacheFunction(2);
89+
expect(result21).toBe('Data for ID: 2');
90+
expect(memoizedCalls).toBe(0); // ID 2 result is now memoized
91+
expect(totalFunctionCalls).toBe(1); // extra new call
92+
expect(memoizationCheckCount).toBe(1); // 5 checks in total
93+
94+
bypassCache = false;
95+
const result22 = await service.fetchDataWithByPassedCacheFunction(2);
96+
expect(result22).toBe('Data for ID: 2');
97+
expect(memoizedCalls).toBe(1); // ID 2 result is now memoized
98+
expect(totalFunctionCalls).toBe(1); // NO extra new call
99+
expect(memoizationCheckCount).toBe(2); // 2 checks in total
100+
101+
bypassCache = true;
102+
const result23 = await service.fetchDataWithByPassedCacheFunction(2);
103+
expect(result23).toBe('Data for ID: 2');
104+
expect(memoizedCalls).toBe(1); // ID 2 result is NOT RETRIEVED FROM CACHE AS THEY ARE BYPASSED
105+
expect(totalFunctionCalls).toBe(2); // extra new call as bypassCache = true
106+
expect(memoizationCheckCount).toBe(3); // 5 checks in total
107+
108+
109+
69110
// test NO cache for a throwing async method
70111
memoizationCheckCount = 0;
71112
memoizedCalls = 0;

src/execution/cache.decorator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function cache(options: CacheOptions): MethodDecorator {
2323
functionId: thisMethodMetadata.methodSignature as string,
2424
...options,
2525
cacheKey: attachFunctionMetadata.bind(this)(options.cacheKey, thisMethodMetadata),
26+
bypass: attachFunctionMetadata.bind(this)(options.bypass, thisMethodMetadata),
2627
ttl: attachFunctionMetadata.bind(this)(options.ttl, thisMethodMetadata),
2728
onCacheEvent: attachFunctionMetadata.bind(this)(options.onCacheEvent, thisMethodMetadata)
2829
});

src/execution/cache.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,31 @@ export async function executeCache<O>(
2121
): Promise<Promise<O> | O> {
2222
const functionMetadata = extractFunctionMetadata(blockFunction);
2323
const cacheKey = options.cacheKey?.({ metadata: functionMetadata, inputs }) ?? generateHashId(...inputs);
24+
const bypass = typeof options.bypass === 'function' && !!options.bypass?.({ metadata: functionMetadata, inputs });
2425
const ttl = typeof options.ttl === 'function' ? options.ttl({ metadata: functionMetadata, inputs }) : options.ttl;
25-
2626
let cacheStore: CacheStore | MapCacheStore<O>;
27+
2728
if (options.cacheManager) {
2829
cacheStore = typeof options.cacheManager === 'function' ? options.cacheManager(this) : options.cacheManager;
2930
} else {
3031
cacheStore = new MapCacheStore<O>(this[cacheStoreKey], options.functionId);
3132
}
32-
const cachedValue: O = (await cacheStore.get(cacheKey)) as O;
33+
const cachedValue: O | undefined = (await cacheStore.get(cacheKey)) as O;
34+
3335

3436
if (typeof options.onCacheEvent === 'function') {
35-
options.onCacheEvent({ ttl, metadata: functionMetadata, inputs, cacheKey, isCached: !!cachedValue, value: cachedValue });
37+
options.onCacheEvent({
38+
ttl,
39+
metadata: functionMetadata,
40+
inputs,
41+
cacheKey,
42+
isBypassed: !!bypass,
43+
isCached: !!cachedValue,
44+
value: cachedValue
45+
});
3646
}
3747

38-
if (cachedValue) {
48+
if (!bypass && cachedValue) {
3949
return cachedValue;
4050
} else {
4151
return (execute.bind(this) as typeof execute)(
@@ -44,7 +54,7 @@ export async function executeCache<O>(
4454
[],
4555
(res) => {
4656
cacheStore.set(cacheKey, res as O, ttl);
47-
if((cacheStore as MapCacheStore<O>).fullStorage) {
57+
if ((cacheStore as MapCacheStore<O>).fullStorage) {
4858
this[cacheStoreKey] = (cacheStore as MapCacheStore<O>).fullStorage;
4959
}
5060
return res;

src/execution/trace.decorator.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,8 @@ describe('trace decorator', () => {
172172
url: string,
173173
traceContext: Record<string, unknown> = {}
174174
): Promise<{
175-
data: string;
176-
}> {
175+
data: string;
176+
}> {
177177
return this.fetchDataFunction(url, traceContext);
178178
}
179179

0 commit comments

Comments
 (0)