Last active
February 22, 2024 17:03
-
-
Save max-lt/37cee8b99557787f27d98bcf87b04a18 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Observable, merge, mergeMap, shareReplay } from 'rxjs'; | |
interface ICacheElement<T> { | |
res$: Observable<T>; | |
exp: number; | |
} | |
const DEFAULT_KEY = Symbol('default_cache_key'); | |
type CacheKey = string | number | symbol; | |
type Cacheable<T> = (arg?: CacheKey, ...args: any[]) => Observable<T>; | |
type DecoratorReturn<T> = (target: object, name: string, desc: TypedPropertyDescriptor<Cacheable<T>>) => void; | |
/** | |
* Cache decorator | |
* @param ttl time to live in ms | |
* @param allowStale if true, allow to use stale cache while refreshing | |
*/ | |
export function Cache<T>(ttl: number, allowStale = true): DecoratorReturn<T> { | |
return (target, name, desc) => { | |
// Keeping original method. | |
const fn = desc.value; | |
if (!fn) { | |
throw new Error('desc.value is undefined'); | |
} | |
// Setup cache for method | |
const origin = [target.constructor.name, name].join('.'); | |
const cache = new Map<CacheKey, ICacheElement<T>>(); | |
console.debug('Init cache', origin, { ttl, allowStale }); | |
desc.value = function (arg?: CacheKey, ...args: any[]): Observable<T> { | |
// If we use first arg as key, we ensure that it is a string, number or symbol | |
if (arg && typeof arg !== 'string' && typeof arg !== 'number' && typeof arg !== 'symbol') { | |
console.warn('Invalid argument for cache', origin, { arg }); | |
return fn.call(this, arg, ...args); | |
} | |
const key = arg || DEFAULT_KEY; | |
const now = Date.now(); | |
const cached = cache.get(key); | |
// Cache HIT | |
if (cached && now < cached.exp) { | |
console.debug('CACHE HIT', origin, key); | |
return cached.res$; | |
} | |
// Call original method & pipe with shareReplay | |
const origin$ = fn.call(this, arg, ...args).pipe(shareReplay(1)) as Observable<T>; | |
// Cache STALE; use cached value and merge with origin$ | |
if (cached && allowStale) { | |
console.debug('CACHE STALE', origin, key); | |
const stale$ = merge([cached.res$, origin$]).pipe( | |
mergeMap((e) => e), | |
shareReplay({ refCount: true, bufferSize: 1 }) | |
); | |
// Cache stale value, we do not want to call origin again | |
cache.set(key, { res$: stale$, exp: now + ttl }); | |
return stale$; | |
} | |
// Cache MISS; use origin$ | |
console.debug('CACHE MISS', origin, key); | |
cache.set(key || DEFAULT_KEY, { res$: origin$, exp: now + ttl }); | |
return origin$; | |
}; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment