diff --git a/CHANGELOG.md b/CHANGELOG.md index e9aef08..7080114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ... +## [3.4.0] - 2024-03-21 + +### Added + +- Expose a `.retrieve()` convenience method to simply read from the cache without revalidation. + ## [3.3.0] - 2024-02-15 ### Added @@ -106,7 +112,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add emitter events for when storage get and set fails -[unreleased]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.3.0...HEAD +[unreleased]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.4.0...HEAD +[3.4.0]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.3.0...v3.4.0 [3.3.0]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.2.1...v3.3.0 [3.2.1]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/jperasmus/stale-while-revalidate-cache/compare/v3.1.3...v3.2.0 diff --git a/README.md b/README.md index 694eabe..f9ea6b2 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,18 @@ const result = await swr.persist(cacheKey, cacheValue) The value will be passed through the `serialize` method you optionally provided when you instantiated the `swr` helper. +#### Manually read from cache + +There is a convenience static method made available if you need to simply read from the underlying storage without triggering revalidation. Sometimes you just want to know if there is a value in the cache for a given key. + +```typescript +const cacheKey = 'your-cache-key' + +const resultPayload = await swr.retrieve(cacheKey) +``` + +The cached value will be passed through the `deserialize` method you optionally provided when you instantiated the `swr` helper. + #### Manually delete from cache There is a convenience static method made available if you need to manually delete a cache entry from the underlying storage. diff --git a/package-lock.json b/package-lock.json index 94ad865..db46e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stale-while-revalidate-cache", - "version": "3.2.1", + "version": "3.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "stale-while-revalidate-cache", - "version": "3.2.1", + "version": "3.3.0", "license": "MIT", "dependencies": { "emittery": "^0.9.2" diff --git a/package.json b/package.json index 0de81a8..8ca1ef6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "3.2.1", + "version": "3.3.0", "name": "stale-while-revalidate-cache", "license": "MIT", "main": "dist/index.js", diff --git a/src/index.test.ts b/src/index.test.ts index 3b04e1d..0801589 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -697,4 +697,47 @@ describe('createStaleWhileRevalidateCache', () => { expect(mockedLocalStorage.getItem(createTimeCacheKey(key))).toEqual(null) }) }) + + describe('swr.retrieve()', () => { + it('should return the cache value and metadata for given key', async () => { + const swr = createStaleWhileRevalidateCache(validConfig) + + const key = 'retrieve key' + const value = 'value' + + const now = Date.now() + + // Manually set the value in the cache + await Promise.all([ + validConfig.storage.setItem(key, value), + validConfig.storage.setItem( + createTimeCacheKey(key), + (now + 10000).toString() // 10 seconds in the future + ), + ]) + + const result = await swr.retrieve(key) + + expect(result).toMatchObject({ + cachedValue: value, + cachedAge: expect.any(Number), + cachedAt: expect.any(Number), + now: expect.any(Number), + }) + }) + + it('should return null for cachedValue if the cache value is not found', async () => { + const swr = createStaleWhileRevalidateCache(validConfig) + + const key = 'retrieve missing key' + + const result = await swr.retrieve(key) + + expect(result).toMatchObject({ + cachedValue: null, + cachedAge: 0, + now: expect.any(Number), + }) + }) + }) }) diff --git a/src/index.ts b/src/index.ts index 0e8691d..aa0bef5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import type { Config, IncomingCacheKey, ResponseEnvelope, + RetrieveCachedValueResponse, StaleWhileRevalidateCache, StaticMethods, } from '../types' @@ -82,6 +83,45 @@ export function createStaleWhileRevalidateCache( } } + async function retrieveValue({ + cacheKey, + storage, + deserialize, + }: { + cacheKey: IncomingCacheKey + storage: Config['storage'] + deserialize: NonNullable + }): Promise> { + const now = Date.now() + const key = getCacheKey(cacheKey) + const timeKey = createTimeCacheKey(key) + + try { + const [cachedValue, cachedAt] = await Promise.all([ + storage.getItem(key), + storage.getItem(timeKey), + ]) + + if ( + isNil(cachedValue) || + isNil(cachedAt) || + Number.isNaN(Number(cachedAt)) + ) { + return { cachedValue: null, cachedAge: 0, now } + } + + return { + cachedValue: deserialize(cachedValue) as CacheValue | null, + cachedAge: now - Number(cachedAt), + cachedAt: Number(cachedAt), + now, + } + } catch (error) { + emitter.emit(EmitterEvents.cacheGetFailed, { cacheKey, error }) + throw error + } + } + async function staleWhileRevalidate( cacheKey: IncomingCacheKey, fn: () => CacheValue | Promise, @@ -106,13 +146,6 @@ export function createStaleWhileRevalidateCache( let invocationCount = 0 let cacheStatus: CacheStatus = CacheResponseStatus.MISS - type RetrieveCachedValueResponse = Promise<{ - cachedValue: unknown | null - cachedAge: number - cachedAt?: number - now: number - }> - if (inFlightKeys.has(key)) { emitter.emit(EmitterEvents.cacheInFlight, { key, cacheKey }) @@ -137,7 +170,9 @@ export function createStaleWhileRevalidateCache( inFlightKeys.add(key) - async function retrieveCachedValue(): RetrieveCachedValueResponse { + async function retrieveCachedValue(): Promise< + RetrieveCachedValueResponse + > { const now = Date.now() try { @@ -290,8 +325,19 @@ export function createStaleWhileRevalidateCache( }) } + const retrieve: StaticMethods['retrieve'] = async ( + cacheKey: IncomingCacheKey + ) => { + return retrieveValue({ + cacheKey, + storage: cacheConfig.storage, + deserialize: cacheConfig.deserialize, + }) + } + staleWhileRevalidate.delete = del staleWhileRevalidate.persist = persist + staleWhileRevalidate.retrieve = retrieve return extendWithEmitterMethods(emitter, staleWhileRevalidate) } diff --git a/types/index.d.ts b/types/index.d.ts index 4632512..03b44cd 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -24,6 +24,13 @@ export type IncomingCacheKey = string | (() => string) export type CacheStatus = 'fresh' | 'stale' | 'expired' | 'miss' +export type RetrieveCachedValueResponse = { + cachedValue: CacheValue | null + cachedAge: number + cachedAt?: number + now: number +} + export type ResponseEnvelope = { value: CacheValue status: CacheStatus @@ -43,6 +50,9 @@ export type StaleWhileRevalidateCache = ( export type StaticMethods = { delete: (cacheKey: IncomingCacheKey) => Promise + retrieve: ( + cacheKey: IncomingCacheKey + ) => Promise> persist: ( cacheKey: IncomingCacheKey, cacheValue: CacheValue