Skip to content

Commit d0093fb

Browse files
Added Expiring LRU Cache implementation
1 parent e50d8fd commit d0093fb

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

src/cache/abstract-assignment-cache.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getMD5Hash } from '../obfuscation';
22

3-
import { LRUCache } from './lru-cache';
3+
import {ExpiringLRUCache, LRUCache} from './lru-cache';
4+
import {max} from "lodash";
45

56
/**
67
* Assignment cache keys are only on the subject and flag level, while the entire value is used
@@ -103,6 +104,7 @@ export class NonExpiringInMemoryAssignmentCache extends AbstractAssignmentCache<
103104
* The primary use case is for server-side SDKs, where the cache is shared across
104105
* multiple users. In this case, the cache size should be set to the maximum number
105106
* of users that can be active at the same time.
107+
* @param {number} maxSize - Maximum cache size
106108
*/
107109
export class LRUInMemoryAssignmentCache extends AbstractAssignmentCache<LRUCache> {
108110
constructor(maxSize: number) {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ExpiringLRUInMemoryAssignmentCache } from './expiring-lru-in-memory-assignment-cache';
2+
3+
describe('ExpiringLRUInMemoryAssignmentCache', () => {
4+
let cache: ExpiringLRUInMemoryAssignmentCache;
5+
const defaultTimout = 60_000; // 10 minutes
6+
7+
beforeAll(() => {
8+
jest.useFakeTimers();
9+
cache = new ExpiringLRUInMemoryAssignmentCache(2);
10+
});
11+
12+
it(`assignment cache's timeout should default to 10 minutes `, () => {
13+
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' };
14+
cache.set(key1);
15+
jest.advanceTimersByTime(defaultTimout);
16+
expect(cache.has(key1)).toBeFalsy();
17+
});
18+
19+
it(`assignment cache's timeout value is used on construction`, () => {
20+
const expectedTimout = 88;
21+
cache = new ExpiringLRUInMemoryAssignmentCache(2, expectedTimout);
22+
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' };
23+
cache.set(key1);
24+
jest.advanceTimersByTime(expectedTimout);
25+
expect(cache.has(key1)).toBeFalsy();
26+
});
27+
28+
it(`cache shouldn't be invalidated before timeout`, () => {
29+
const key1 = { subjectKey: 'a', flagKey: 'b', banditKey: 'c', actionKey: 'd' };
30+
cache.set(key1);
31+
32+
expect(cache.has(key1)).toBeTruthy();
33+
34+
jest.advanceTimersByTime(defaultTimout);
35+
expect(cache.has(key1)).toBeFalsy();
36+
});
37+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { AbstractAssignmentCache } from './abstract-assignment-cache';
2+
import { ExpiringLRUCache } from './lru-cache';
3+
4+
/**
5+
* Variation of LRU caching mechanism that will automatically evict items after
6+
* set time of milliseconds.
7+
*
8+
* It is used to limit the size of the cache.
9+
*
10+
* The primary use case is for server-side SDKs, where the cache is shared across
11+
* multiple users. In this case, the cache size should be set to the maximum number
12+
* of users that can be active at the same time.
13+
* @param {number} maxSize - Maximum cache size
14+
* @param {number} timeout - Time in milliseconds after cache will expire.
15+
*/
16+
export class ExpiringLRUInMemoryAssignmentCache extends AbstractAssignmentCache<ExpiringLRUCache> {
17+
constructor(maxSize: number, timeout = 60_000) {
18+
super(new ExpiringLRUCache(maxSize, timeout));
19+
}
20+
}

src/cache/lru-cache.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LRUCache } from './lru-cache';
1+
import {ExpiringLRUCache, LRUCache} from './lru-cache';
22

33
describe('LRUCache', () => {
44
let cache: LRUCache;
@@ -62,3 +62,28 @@ describe('LRUCache', () => {
6262
expect(oneCache.get('b')).toBe('banana');
6363
});
6464
});
65+
66+
describe('Expiring LRU Cache', () => {
67+
let cache: ExpiringLRUCache;
68+
const expectedCacheTimeoutMs = 50;
69+
70+
beforeEach(async () => {
71+
cache = new ExpiringLRUCache(2, expectedCacheTimeoutMs);
72+
});
73+
74+
afterAll(async () => {
75+
jest.restoreAllMocks();
76+
});
77+
78+
it('should evict cache after timeout', async () => {
79+
jest.useFakeTimers();
80+
jest.spyOn(global, 'setTimeout');
81+
82+
cache.set('a', 'apple');
83+
jest.advanceTimersByTime(expectedCacheTimeoutMs);
84+
85+
expect(setTimeout).toHaveBeenCalledTimes(1);
86+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), expectedCacheTimeoutMs);
87+
expect(cache.get('a')).toBeUndefined();
88+
});
89+
});

src/cache/lru-cache.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,17 @@ export class LRUCache implements Map<string, string> {
9797
return this;
9898
}
9999
}
100+
101+
export class ExpiringLRUCache extends LRUCache {
102+
constructor(readonly maxSize: number, readonly timeout: number) {
103+
super(maxSize);
104+
}
105+
106+
set(key: string, value: string): this {
107+
const cache = super.set(key, value);
108+
setTimeout(() => {
109+
this.delete(key);
110+
}, this.timeout);
111+
return cache;
112+
}
113+
}

0 commit comments

Comments
 (0)