Skip to content

Commit d76ddf0

Browse files
committed
WIP
1 parent b01265e commit d76ddf0

File tree

4 files changed

+101
-10
lines changed

4 files changed

+101
-10
lines changed

src/defaultWhileMiss.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ export type Configuration<K, V> = {
66
/**
77
* Handler for background revalidation errors
88
*/
9-
errorHandler?: (error: Error) => void,
9+
errorHandler?: (key: K, error: Error) => void,
1010
};
1111

1212
export class DefaultWhileMissCache<K, V> implements CacheProvider<K, V> {
1313
private readonly provider: Configuration<K, V>['provider'];
1414

1515
private readonly defaultValue: V;
1616

17-
private readonly errorHandler: (error: Error) => void;
17+
private readonly errorHandler: (key: K, error: Error) => void;
1818

1919
public constructor(config: Configuration<K, V>) {
2020
this.provider = config.provider;
@@ -24,7 +24,7 @@ export class DefaultWhileMissCache<K, V> implements CacheProvider<K, V> {
2424

2525
public get(key: K, loader: CacheLoader<K, V>): Promise<V> {
2626
return this.provider.get(key, innerKey => {
27-
loader(innerKey).catch(this.errorHandler);
27+
loader(innerKey).catch(error => this.errorHandler(key, error));
2828

2929
return Promise.resolve(this.defaultValue);
3030
});

src/staleWhileRevalidate.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type Configuration<K, V> = {
2323
* evaluated in the background on subsequent gets, but the stale value
2424
* will still be served until the revalidation is complete.
2525
*/
26-
freshPeriod: number,
26+
freshPeriod: number | ((key: K, value: V) => number),
2727

2828
/**
2929
* The clock to use for time-related operations.
@@ -44,17 +44,17 @@ type Configuration<K, V> = {
4444
*
4545
* The most common use case for this handler is logging.
4646
*/
47-
errorHandler?: (error: Error) => void,
47+
errorHandler?: (key: K, error: Error) => void,
4848
};
4949

5050
export class StaleWhileRevalidateCache<K, V> implements CacheProvider<K, V> {
5151
private readonly cacheProvider: Configuration<K, V>['cacheProvider'];
5252

53-
private readonly freshPeriod: number;
53+
private readonly freshPeriod: number | ((key: K, value: V) => number);
5454

5555
private readonly clock: Clock;
5656

57-
private readonly errorHandler: (error: Error) => void;
57+
private readonly errorHandler: (key: K, error: Error) => void;
5858

5959
public constructor(config: Configuration<K, V>) {
6060
this.cacheProvider = config.cacheProvider;
@@ -79,9 +79,13 @@ export class StaleWhileRevalidateCache<K, V> implements CacheProvider<K, V> {
7979

8080
const possiblyStaleEntry = await this.cacheProvider.get(key, retrieveAndSave);
8181

82-
if (now.isAfter(possiblyStaleEntry.timestamp.plusSeconds(this.freshPeriod))) {
82+
const freshPeriod = typeof this.freshPeriod === 'function'
83+
? this.freshPeriod(key, possiblyStaleEntry.value)
84+
: this.freshPeriod;
85+
86+
if (now.isAfter(possiblyStaleEntry.timestamp.plusSeconds(freshPeriod))) {
8387
// If expired revalidate on the background and return cached value
84-
retrieveAndSave().catch(this.errorHandler);
88+
retrieveAndSave().catch(error => this.errorHandler(key, error));
8589
}
8690

8791
return possiblyStaleEntry.value;

test/defaultWhileMiss.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('A cache provider that returns a default value while loading in the bac
4444
expect(loader).toHaveBeenCalledWith('key');
4545

4646
expect(errorHandler).toHaveBeenCalledTimes(1);
47-
expect(errorHandler).toHaveBeenCalledWith(error);
47+
expect(errorHandler).toHaveBeenCalledWith('key', error);
4848
});
4949

5050
it('should delegate setting a value to the underlying provider', async () => {

test/staleWhileRevalidate.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,93 @@ describe('A cache provider that uses stale values while revalidating the cache',
9090
expect(mockCache.set).toHaveBeenCalledWith('key', expectedEntry);
9191
});
9292

93+
it('should expire entries according to the specified logic', async () => {
94+
const now = Instant.ofEpochMilli(12345);
95+
const clock = FixedClock.of(now, TimeZone.UTC);
96+
97+
const cachedEntry: TimestampedCacheEntry<string> = {
98+
value: 'cachedValue',
99+
timestamp: now.plusSeconds(-11),
100+
};
101+
102+
mockCache.get.mockResolvedValue(cachedEntry);
103+
104+
let resolveSet: () => void = jest.fn();
105+
const setPromise = new Promise<void>(resolve => { resolveSet = resolve; });
106+
107+
mockCache.set.mockImplementation(() => {
108+
resolveSet();
109+
110+
return setPromise;
111+
});
112+
113+
let resolveloader: (value: string) => any = jest.fn();
114+
115+
const loader = jest.fn().mockReturnValueOnce(
116+
new Promise(resolve => { resolveloader = resolve; }),
117+
);
118+
119+
const cache = new StaleWhileRevalidateCache({
120+
cacheProvider: mockCache,
121+
freshPeriod: (): number => 10,
122+
clock: clock,
123+
});
124+
125+
await expect(cache.get('key', loader)).resolves.toBe('cachedValue');
126+
127+
expect(mockCache.get).toHaveBeenCalledWith('key', expect.any(Function));
128+
129+
expect(loader).toHaveBeenCalledWith('key');
130+
131+
expect(mockCache.set).not.toHaveBeenCalled();
132+
133+
const expectedEntry: TimestampedCacheEntry<string> = {
134+
value: 'loaderValue',
135+
timestamp: now,
136+
};
137+
138+
resolveloader('loaderValue');
139+
140+
await setPromise;
141+
142+
expect(mockCache.set).toHaveBeenCalledWith('key', expectedEntry);
143+
});
144+
145+
it('should handle errors while revalidating expired entries', async () => {
146+
const now = Instant.ofEpochMilli(12345);
147+
const clock = FixedClock.of(now, TimeZone.UTC);
148+
149+
const cachedEntry: TimestampedCacheEntry<string> = {
150+
value: 'cachedValue',
151+
timestamp: now.plusSeconds(-11),
152+
};
153+
154+
mockCache.get.mockResolvedValue(cachedEntry);
155+
156+
const error = new Error('error');
157+
158+
const loader = jest.fn().mockRejectedValueOnce(error);
159+
160+
const errorHandler = jest.fn();
161+
162+
const cache = new StaleWhileRevalidateCache({
163+
cacheProvider: mockCache,
164+
freshPeriod: 10,
165+
clock: clock,
166+
errorHandler: errorHandler,
167+
});
168+
169+
await expect(cache.get('key', loader)).resolves.toBe('cachedValue');
170+
171+
expect(mockCache.get).toHaveBeenCalledWith('key', expect.any(Function));
172+
173+
expect(loader).toHaveBeenCalledWith('key');
174+
175+
expect(mockCache.set).not.toHaveBeenCalled();
176+
177+
expect(errorHandler).toHaveBeenCalledWith('key', error);
178+
});
179+
93180
it('should returned non-expired entries', async () => {
94181
const now = Instant.ofEpochMilli(12345);
95182
const clock = FixedClock.of(now, TimeZone.UTC);

0 commit comments

Comments
 (0)