diff --git a/src/adapters.ts b/src/adapters.ts index 35701ff..334b229 100644 --- a/src/adapters.ts +++ b/src/adapters.ts @@ -1,4 +1,4 @@ -import { Cache, CacheEntry } from './common'; +import { Cache, CacheEntry, totalTtl } from './common'; export interface LRUishCache extends Omit { set( @@ -12,10 +12,9 @@ export function lruCacheAdapter(lruCache: LRUishCache): Cache { return { name: lruCache.name || 'LRU', set(key, value) { + const ttl = totalTtl(value?.metadata); return lruCache.set(key, value, { - ttl: - (value?.metadata?.ttl || 0) + (value?.metadata?.swv || 0) || - undefined, + ttl: ttl === Infinity ? undefined : ttl, start: value?.metadata?.createdTime, }); }, @@ -53,7 +52,7 @@ export function redis3CacheAdapter(redisCache: Redis3LikeCache): Cache { name: redisCache.name || 'Redis3', set(key, value) { return new Promise((res, rej) => { - const ttl = (value?.metadata?.ttl || 0) + (value?.metadata?.swv || 0); + const ttl = totalTtl(value?.metadata); const createdTime = value?.metadata?.createdTime; const cb = (err: unknown) => { if (err) { @@ -62,7 +61,7 @@ export function redis3CacheAdapter(redisCache: Redis3LikeCache): Cache { res(); }; - if (ttl > 0 && typeof createdTime === 'number') { + if (ttl > 0 && ttl < Infinity && typeof createdTime === 'number') { redisCache .multi() .set(key, JSON.stringify(value)) @@ -118,15 +117,18 @@ export function redisCacheAdapter(redisCache: RedisLikeCache): Cache { return { name: redisCache.name || 'Redis', set(key, value) { - const ttl = (value?.metadata?.ttl || 0) + (value?.metadata?.swv || 0); + const ttl = totalTtl(value?.metadata); const createdTime = value?.metadata?.createdTime; - return redisCache.set(key, JSON.stringify(value), { - EXAT: - ttl > 0 && typeof createdTime === 'number' - ? (ttl + createdTime) / 1000 - : 0, - }); + return redisCache.set( + key, + JSON.stringify(value), + ttl > 0 && ttl < Infinity && typeof createdTime === 'number' + ? { + EXAT: (ttl + createdTime) / 1000, + } + : undefined, + ); }, async get(key) { const value = await redisCache.get(key); diff --git a/src/cachified.spec.ts b/src/cachified.spec.ts index fe6e7dd..b48a152 100644 --- a/src/cachified.spec.ts +++ b/src/cachified.spec.ts @@ -15,6 +15,7 @@ import { redis3CacheAdapter, redisCacheAdapter, RedisLikeCache, + totalTtl, } from './index'; import { Deferred } from './createBatch'; @@ -60,7 +61,7 @@ describe('cachified', () => { expect(value).toBe('ONE'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -68,16 +69,16 @@ describe('cachified', () => { 6. getFreshValueSuccess {value: 'ONE'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); expect(value2).toBe('ONE'); expect(report(reporter2.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: null}, value: 'ONE'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: null}, value: 'ONE'}} 4. getCachedValueSuccess {migrated: false, value: 'ONE'}" `); @@ -171,7 +172,7 @@ describe('cachified', () => { await expect(value).rejects.toMatchInlineSnapshot(`[Error: 🙈]`); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -218,7 +219,7 @@ describe('cachified', () => { ); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -246,7 +247,7 @@ describe('cachified', () => { ); expect(report(reporter2.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -282,10 +283,10 @@ describe('cachified', () => { expect(cache.get('weather')?.value).toBe('☀️'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'weather', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'weather', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: null}, value: '☁️'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: null}, value: '☁️'}} 4. getCachedValueSuccess {migrated: true, value: '☀️'}" `); @@ -314,7 +315,7 @@ describe('cachified', () => { ); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'weather', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'weather', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -462,17 +463,17 @@ describe('cachified', () => { expect(value2).toBe('ONE'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getFreshValueStart 3. getFreshValueError {error: '🤡'} 4. getCachedValueStart 5. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: null}, value: 'ONE'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: null}, value: 'ONE'}} 6. getFreshValueCacheFallback {value: 'ONE'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); }); @@ -538,7 +539,7 @@ describe('cachified', () => { expect(await getValue()).toBe('value-1'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` " 1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -548,7 +549,7 @@ describe('cachified', () => { 7. writeFreshValueError {error: '🔥'} 8. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 9. getCachedValueStart 10. getCachedValueRead 11. getCachedValueEmpty @@ -556,7 +557,7 @@ describe('cachified', () => { 13. getFreshValueSuccess {value: 'value-1'} 14. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); expect(await getValue()).toBe('value-1'); }); @@ -585,7 +586,7 @@ describe('cachified', () => { expect(await getValue()).toBe('value-1'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` " 1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: 5}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: 5}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -593,26 +594,26 @@ describe('cachified', () => { 6. getFreshValueSuccess {value: 'value-0'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: 5}, migrated: false, written: true} + {metadata: {createdTime: 0, swr: 0, ttl: 5}, migrated: false, written: true} 8. init - {key: 'test', metadata: {createdTime: 4, swv: 0, ttl: 5}} + {key: 'test', metadata: {createdTime: 4, swr: 0, ttl: 5}} 9. getCachedValueStart 10. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: 5}, value: 'value-0'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: 5}, value: 'value-0'}} 11. getCachedValueSuccess {migrated: false, value: 'value-0'} 12. init - {key: 'test', metadata: {createdTime: 6, swv: 0, ttl: 5}} + {key: 'test', metadata: {createdTime: 6, swr: 0, ttl: 5}} 13. getCachedValueStart 14. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: 5}, value: 'value-0'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: 5}, value: 'value-0'}} 15. getCachedValueOutdated - {metadata: {createdTime: 0, swv: 0, ttl: 5}, value: 'value-0'} + {metadata: {createdTime: 0, swr: 0, ttl: 5}, value: 'value-0'} 16. getFreshValueStart 17. getFreshValueSuccess {value: 'value-1'} 18. writeFreshValueSuccess - {metadata: {createdTime: 6, swv: 0, ttl: 5}, migrated: false, written: true}" + {metadata: {createdTime: 6, swr: 0, ttl: 5}, migrated: false, written: true}" `); }); @@ -636,7 +637,7 @@ describe('cachified', () => { expect(setMock).not.toHaveBeenCalled(); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: 5}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: 5}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -644,7 +645,7 @@ describe('cachified', () => { 6. getFreshValueSuccess {value: 'ONE'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: 5}, migrated: false, written: false}" + {metadata: {createdTime: 0, swr: 0, ttl: 5}, migrated: false, written: false}" `); }); @@ -672,10 +673,10 @@ describe('cachified', () => { expect(await pValue2).toBe('ONE'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` " 1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 4. getCachedValueStart 5. getCachedValueRead 6. getCachedValueRead @@ -686,7 +687,7 @@ describe('cachified', () => { 11. getFreshValueSuccess {value: 'ONE'} 12. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); }); @@ -760,7 +761,7 @@ describe('cachified', () => { expect(report(calls)).toMatchInlineSnapshot(` " 1. init - {key: 'test', metadata: {createdTime: 0, swv: 10, ttl: 5}} + {key: 'test', metadata: {createdTime: 0, swr: 10, ttl: 5}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -768,12 +769,12 @@ describe('cachified', () => { 6. getFreshValueSuccess {value: 'value-0'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 10, ttl: 5}, migrated: false, written: true} + {metadata: {createdTime: 0, swr: 10, ttl: 5}, migrated: false, written: true} 8. init - {key: 'test', metadata: {createdTime: 6, swv: 10, ttl: 5}} + {key: 'test', metadata: {createdTime: 6, swr: 10, ttl: 5}} 9. getCachedValueStart 10. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 10, ttl: 5}, value: 'value-0'}} + {entry: {metadata: {createdTime: 0, swr: 10, ttl: 5}, value: 'value-0'}} 11. getCachedValueSuccess {migrated: false, value: 'value-0'} 12. refreshValueStart @@ -782,6 +783,33 @@ describe('cachified', () => { `); }); + it('falls back to deprecated swv when swr is not present', async () => { + const cache = new Map(); + let i = 0; + const getFreshValue = jest.fn(() => `value-${i++}`); + const oldCacheEntry = createCacheEntry(`value-${i++}`, { swr: 5, ttl: 5 }); + // @ts-ignore (we actually want to create an entry with a now deprecated signature) + oldCacheEntry.metadata.swv = oldCacheEntry.metadata.swr; + delete oldCacheEntry.metadata.swr; + cache.set('test', oldCacheEntry); + + const getValue = () => + cachified({ + cache, + key: 'test', + ttl: 5, + staleWhileRevalidate: 5, + getFreshValue, + }); + + expect(await getValue()).toBe('value-0'); + currentTime = 6; + expect(await getValue()).toBe('value-0'); + await delay(1); + expect(await getValue()).toBe('value-1'); + expect(getFreshValue).toHaveBeenCalledTimes(1); + }); + it('supports infinite stale while revalidate', async () => { const cache = new Map(); let i = 0; @@ -844,7 +872,7 @@ describe('cachified', () => { expect(getFreshValue).toHaveBeenCalledTimes(3); expect(report(calls)).toMatchInlineSnapshot(` " 1. init - {key: 'test', metadata: {createdTime: 0, swv: 10, ttl: 5}} + {key: 'test', metadata: {createdTime: 0, swr: 10, ttl: 5}} 2. getCachedValueStart 3. getCachedValueRead 4. getCachedValueEmpty @@ -852,12 +880,12 @@ describe('cachified', () => { 6. getFreshValueSuccess {value: 'value-0'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 10, ttl: 5}, migrated: false, written: true} + {metadata: {createdTime: 0, swr: 10, ttl: 5}, migrated: false, written: true} 8. init - {key: 'test', metadata: {createdTime: 6, swv: 10, ttl: 5}} + {key: 'test', metadata: {createdTime: 6, swr: 10, ttl: 5}} 9. getCachedValueStart 10. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 10, ttl: 5}, value: 'value-0'}} + {entry: {metadata: {createdTime: 0, swr: 10, ttl: 5}, value: 'value-0'}} 11. getCachedValueSuccess {migrated: false, value: 'value-0'} 12. refreshValueStart @@ -887,17 +915,17 @@ describe('cachified', () => { expect(value).toBe('TWO'); expect(report(reporter.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: null}, value: 'ONE'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: null}, value: 'ONE'}} 4. checkCachedValueError {reason: 'unknown'} 5. getFreshValueStart 6. getFreshValueSuccess {value: 'TWO'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); // the following lines only exist for 100% coverage 😅 @@ -916,23 +944,23 @@ describe('cachified', () => { expect(value2).toBe('TWO'); expect(report(reporter2.mock.calls)).toMatchInlineSnapshot(` "1. init - {key: 'test', metadata: {createdTime: 0, swv: 0, ttl: null}} + {key: 'test', metadata: {createdTime: 0, swr: 0, ttl: null}} 2. getCachedValueStart 3. getCachedValueRead - {entry: {metadata: {createdTime: 0, swv: 0, ttl: null}, value: 'ONE'}} + {entry: {metadata: {createdTime: 0, swr: 0, ttl: null}, value: 'ONE'}} 4. checkCachedValueError {reason: '🖕'} 5. getFreshValueStart 6. getFreshValueSuccess {value: 'TWO'} 7. writeFreshValueSuccess - {metadata: {createdTime: 0, swv: 0, ttl: null}, migrated: false, written: true}" + {metadata: {createdTime: 0, swr: 0, ttl: null}, migrated: false, written: true}" `); }); it('supports batch-getting fresh values', async () => { const cache = new Map(); - cache.set('test-2', createCacheEntry('YOLO!', { swv: null })); + cache.set('test-2', createCacheEntry('YOLO!', { swr: null })); const getValues = jest.fn((indexes: number[]) => indexes.map((i) => `value-${i}`), ); @@ -1112,7 +1140,7 @@ describe('cachified', () => { expect(value3).toBe('THREE'); expect(cache.get('test-2')).toEqual({ - metadata: { createdTime: 2, swv: 0, ttl: null }, + metadata: { createdTime: 2, swr: 0, ttl: null }, value: 'THREE', }); }); @@ -1139,7 +1167,7 @@ describe('cachified', () => { expect(set).toHaveBeenCalledWith( 'test-3', JSON.stringify({ - metadata: { ttl: 1, swv: 0, createdTime: 0 }, + metadata: { ttl: 1, swr: 0, createdTime: 0 }, value: 'FOUR', }), { EXAT: 0.001 }, @@ -1150,7 +1178,7 @@ describe('cachified', () => { get.mockImplementationOnce(() => Promise.resolve( JSON.stringify({ - metadata: { ttl: null, swv: 0, createdTime: 0 }, + metadata: { ttl: null, swr: 0, createdTime: 0 }, value: 'FIVE', }), ), @@ -1197,11 +1225,11 @@ describe('cachified', () => { }); expect(value3).toBe('THREE'); expect(await cache.get('test-2')).toEqual({ - metadata: { createdTime: 2, swv: 0, ttl: null }, + metadata: { createdTime: 2, swr: 0, ttl: null }, value: 'THREE', }); - // handle redis failure + // handle redis get failure jest.spyOn(redis, 'get').mockImplementationOnce((_, cb) => { cb!(new Error('Nope'), null); return false; @@ -1216,6 +1244,13 @@ describe('cachified', () => { }), ).rejects.toThrowErrorMatchingInlineSnapshot(`"Nope Nope Nope"`); + // handle redis del failure + jest.spyOn(redis, 'del').mockImplementationOnce((_, cb) => { + (cb as Function)(new Error('Nope2'), null); + return false; + }); + expect(cache.delete('test-0')).rejects.toThrowErrorMatchingInlineSnapshot(`"Nope2"`); + // handle corrupt cache await new Promise((res) => redis.set('test-3', '{{{', res)); await expect(() => @@ -1280,7 +1315,7 @@ describe('verbose reporter', () => { expect(logger.print()).toMatchInlineSnapshot(` "WARN: 'check failed for cached value of test Reason: 🚔. - Deleting the cache key and trying to get a fresh value.' {metadata: {createdTime: 0, swv: 0, ttl: null}, value: 'One'} + Deleting the cache key and trying to get a fresh value.' {metadata: {createdTime: 0, swr: 0, ttl: null}, value: 'One'} LOG: 'Updated the cache value for test.' 'Getting a fresh value for this took 0ms.' 'Caching for forever in Map.'" `); }); @@ -1415,7 +1450,7 @@ describe('verbose reporter', () => { it('logs when cache is successfully revalidated', async () => { const cache = new Map(); const logger = createLogger(); - cache.set('test', createCacheEntry('ONE', { ttl: 5, swv: 10 })); + cache.set('test', createCacheEntry('ONE', { ttl: 5, swr: 10 })); currentTime = 7; await cachified({ @@ -1437,7 +1472,7 @@ describe('verbose reporter', () => { it('logs when cache revalidation fails', async () => { const cache = new Map(); const logger = createLogger(); - cache.set('test', createCacheEntry('ONE', { ttl: 5, swv: 10 })); + cache.set('test', createCacheEntry('ONE', { ttl: 5, swr: 10 })); currentTime = 7; await cachified({ @@ -1528,7 +1563,7 @@ function createCacheEntry( ): CacheEntry { return { value, - metadata: { createdTime: Date.now(), ttl: null, swv: 0, ...metadata }, + metadata: { createdTime: Date.now(), ttl: null, swr: 0, ...metadata }, }; } @@ -1548,3 +1583,9 @@ function report(calls: [event: CacheEvent][]) { }) .join('\n'); } + +describe('totalTtl helper', () => { + it('handles metadata without ttl gracefully', () => { + expect(totalTtl({ createdTime: 0, swr: 5 })).toBe(5); + }); +}); diff --git a/src/common.ts b/src/common.ts index 4aff250..8f1814a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -3,7 +3,9 @@ import type { CreateReporter, Reporter } from './reporter'; export interface CacheMetadata { createdTime: number; ttl?: number | null; - swv?: number | null; + swr?: number | null; + /** @deprecated use swr instead */ + readonly swv?: number | null; } export interface CacheEntry { @@ -177,7 +179,7 @@ export function createContext({ ...options, metadata: { ttl: ttl === Infinity ? null : ttl, - swv: staleWhileRevalidate === Infinity ? null : staleWhileRevalidate, + swr: staleWhileRevalidate === Infinity ? null : staleWhileRevalidate, createdTime: Date.now(), }, }; @@ -193,3 +195,19 @@ export function createContext({ report, }; } + +export function staleWhileRevalidate(metadata: CacheMetadata): number | null { + return ( + (typeof metadata.swr === 'undefined' ? metadata.swv : metadata.swr) || null + ); +} + +export function totalTtl(metadata?: CacheMetadata): number { + if (!metadata) { + return 0; + } + if (metadata.ttl === null) { + return Infinity; + } + return (metadata.ttl || 0) + (staleWhileRevalidate(metadata) || 0); +} diff --git a/src/index.ts b/src/index.ts index 50965f1..a398707 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export type { CacheMetadata, Context, } from './common'; +export { staleWhileRevalidate, totalTtl } from './common'; export * from './reporter'; export * from './adapters'; export { createBatch } from './createBatch'; diff --git a/src/reporter.ts b/src/reporter.ts index 45a0eef..387ba4e 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -1,4 +1,4 @@ -import { CacheMetadata, Context } from './common'; +import { CacheMetadata, Context, staleWhileRevalidate } from './common'; export type GetFreshValueStartEvent = { name: 'getFreshValueStart'; @@ -105,16 +105,19 @@ export type CreateReporter = ( const defaultFormatDuration = (ms: number) => `${Math.round(ms)}ms`; function formatCacheTime( - { ttl, swv }: CacheMetadata, + metadata: CacheMetadata, formatDuration: (duration: number) => string, ) { - if (ttl == null || swv == null) { + const swr = staleWhileRevalidate(metadata); + if (metadata.ttl == null || swr == null) { return `forever${ - ttl != null ? ` (revalidation after ${formatDuration(ttl)})` : '' + metadata.ttl != null + ? ` (revalidation after ${formatDuration(metadata.ttl)})` + : '' }`; } - return `${formatDuration(ttl)} + ${formatDuration(swv)} stale`; + return `${formatDuration(metadata.ttl)} + ${formatDuration(swr)} stale`; } interface ReporterOpts { diff --git a/src/shouldRefresh.ts b/src/shouldRefresh.ts index be9e802..a7f3291 100644 --- a/src/shouldRefresh.ts +++ b/src/shouldRefresh.ts @@ -1,11 +1,11 @@ -import type { CacheMetadata } from './common'; +import { CacheMetadata, staleWhileRevalidate } from './common'; export function shouldRefresh( metadata: CacheMetadata, ): 'now' | 'stale' | false { if (metadata.ttl !== null) { const valid = metadata.createdTime + (metadata.ttl || 0); - const stale = valid + (metadata.swv || 0); + const stale = valid + (staleWhileRevalidate(metadata) || 0); const now = Date.now(); if (now <= valid) { return false;