Skip to content

Commit

Permalink
feat: introduce GetFreshValueContext
Browse files Browse the repository at this point in the history
in order to support dynamically changing cache-metadata while getting the value
and to allow implementation to behave differently for background refresh scenarios

thanks to @kentcdodds for bringing up these cases and testing the solution

fix #25
fix: #24
  • Loading branch information
Xiphe committed Jan 25, 2023
1 parent e0d517f commit 9c53976
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 4 deletions.
8 changes: 7 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,13 @@ interface CachifiedOptions<Value> {
*
* Can be async and must return fresh value or throw.
*
* @type {function(): Promise | Value} Required
* context looks like this:
* - context.metadata.ttl?: number
* - context.metadata.swr?: number
* - context.metadata.createdTime: number
* - context.background: boolean
*
* @type {function(context: GetFreshValueContext): Promise | Value} Required
*/
getFreshValue: GetFreshValue<Value>;
/**
Expand Down
59 changes: 59 additions & 0 deletions src/cachified.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
redisCacheAdapter,
RedisLikeCache,
totalTtl,
GetFreshValue,
} from './index';
import { Deferred } from './createBatch';
import { logKey } from './assertCacheEntry';
Expand Down Expand Up @@ -716,6 +717,56 @@ describe('cachified', () => {
expect(await pValue2).toBe('TWO');
});

it('supports extending ttl during getFreshValue operation', async () => {
const cache = new Map<string, CacheEntry>();
const reporter = createReporter();
const getValue = (
getFreshValue: CachifiedOptions<string>['getFreshValue'],
) =>
cachified({
cache,
ttl: 5,
key: 'test',
reporter,
getFreshValue,
});

const firstCallMetaDataD = new Deferred<CacheMetadata>();

const d = new Deferred<string>();
const p1 = getValue(({ metadata }) => {
metadata.ttl = 10;
// Don't do this at home kids...
firstCallMetaDataD.resolve(metadata);
return d.promise;
});

const metadata = await firstCallMetaDataD.promise;

currentTime = 6;
// First call is still ongoing and initial ttl is over, still we exceeded
// the ttl in the call so this should not be called ever
const p2 = getValue(() => {
throw new Error('Never');
});

// Further exceeding the ttl and resolving first call
metadata!.ttl = 15;
d.resolve('ONE');

expect(await p1).toBe('ONE');
expect(await p2).toBe('ONE');

// now proceed to time between first and second modification of ttl
currentTime = 13;
// we still get the cached value from first call
expect(
await getValue(() => {
throw new Error('Never2');
}),
).toBe('ONE');
});

it('resolves earlier pending values with faster responses from later calls', async () => {
const cache = new Map<string, CacheEntry>();
const getValue = (
Expand Down Expand Up @@ -777,8 +828,16 @@ describe('cachified', () => {

// next call gets the revalidated response
expect(await getValue()).toBe('value-1');

const getFreshValueCalls = getFreshValue.mock.calls as any as Parameters<
GetFreshValue<string>
>[];
expect(getFreshValue).toHaveBeenCalledTimes(2);

// Does pass info if it's a stale while revalidate call
expect(getFreshValueCalls[0][0].background).toBe(false);
expect(getFreshValueCalls[1][0].background).toBe(true);

// Does not deliver stale cache when swr is exceeded
currentTime = 30;
expect(await getValue()).toBe('value-2');
Expand Down
14 changes: 12 additions & 2 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ export interface Cache {
delete: (key: string) => unknown | Promise<unknown>;
}

interface GetFreshValueContext {
readonly metadata: CacheMetadata;
readonly background: boolean;
}
export const HANDLE = Symbol();
export type GetFreshValue<Value> = {
(): Promise<Value> | Value;
(context: GetFreshValueContext): Promise<Value> | Value;
[HANDLE]?: () => void;
};
export const MIGRATED = Symbol();
Expand Down Expand Up @@ -72,7 +76,13 @@ export interface CachifiedOptions<Value> {
*
* Can be async and must return fresh value or throw.
*
* @type {function(): Promise | Value} Required
* context looks like this:
* - context.metadata.ttl?: number
* - context.metadata.swr?: number
* - context.metadata.createdTime: number
* - context.background: boolean
*
* @type {function(context: GetFreshValueContext): Promise | Value} Required
*/
getFreshValue: GetFreshValue<Value>;
/**
Expand Down
3 changes: 3 additions & 0 deletions src/getCachedValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ export async function getCachedValue<Value>(
void cachified({
...context,
reporter: () => () => {},
getFreshValue({ metadata }) {
return context.getFreshValue({ metadata, background: true });
},
forceFresh: true,
fallbackToCache: false,
})
Expand Down
5 changes: 4 additions & 1 deletion src/getFreshValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export async function getFreshValue<Value>(
let value: unknown;
try {
report({ name: 'getFreshValueStart' });
const freshValue = await getFreshValue();
const freshValue = await getFreshValue({
metadata: context.metadata,
background: false,
});
value = freshValue;
report({ name: 'getFreshValueSuccess', value: freshValue });
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {
CacheEntry,
CacheMetadata,
Context,
GetFreshValue,
} from './common';
export { staleWhileRevalidate, totalTtl } from './common';
export * from './reporter';
Expand Down

0 comments on commit 9c53976

Please sign in to comment.