Skip to content

Instantly share code, notes, and snippets.

@max-lt
Last active February 22, 2024 17:03
Show Gist options
  • Save max-lt/37cee8b99557787f27d98bcf87b04a18 to your computer and use it in GitHub Desktop.
Save max-lt/37cee8b99557787f27d98bcf87b04a18 to your computer and use it in GitHub Desktop.
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