UNPKG

@daisugi/kintsugi

Version:

Kintsugi is a set of utilities to help build a fault tolerant services.

112 lines (102 loc) 2.9 kB
import type { AnzenAnyResult, AnzenResultFn, } from '@daisugi/anzen'; import { Ayamari } from '@daisugi/ayamari'; import { encToFNV1A } from './enc_to_fnv1a.js'; import { randomBetween } from './random_between.js'; import { SimpleMemoryStore } from './simple_memory_store.js'; import { stringifyArgs } from './stringify_args.js'; interface WithCacheOpts { version?: string; maxAgeMs?: number; cacheStore?: CacheStore; buildCacheKey?( fnHash: number, version: string, args: any[], ): string; calculateCacheMaxAgeMs?(maxAgeMs: number): number; shouldCache?(response: AnzenAnyResult<any, any>): boolean; shouldInvalidateCache?(args: any[]): boolean; } const defaultMaxAgeMs = 1000 * 60 * 60 * 4; // 4h. const defaultVersion = 'v1'; export interface CacheStore { get( cacheKey: string, ): | AnzenAnyResult<any, any> | Promise<AnzenAnyResult<any, any>>; set( cacheKey: string, value: any, maxAgeMs: number, ): | AnzenAnyResult<any, any> | Promise<AnzenAnyResult<any, any>>; } export function buildCacheKey( fnHash: number, version: string, args: any[], ) { return `${fnHash}:${version}:${stringifyArgs(args)}`; } export function calculateCacheMaxAgeMs(maxAgeMs: number) { return randomBetween(maxAgeMs * 0.75, maxAgeMs); } export function shouldInvalidateCache() { return false; } export function shouldCache( response: AnzenAnyResult<any, any>, ) { if (response.isSuccess) { return true; } // Cache NotFound by default. // https://docs.fastly.com/en/guides/http-code-codes-cached-by-default if ( response.isFailure && response.getError().code === Ayamari.errCode.NotFound ) { return true; } return false; } export function withCache( fn: AnzenResultFn<any, any>, opts: WithCacheOpts = {}, ) { const cacheStore = opts.cacheStore || new SimpleMemoryStore(); const version = opts.version || defaultVersion; const maxAgeMs = opts.maxAgeMs || defaultMaxAgeMs; const _buildCacheKey = opts.buildCacheKey || buildCacheKey; const _calculateCacheMaxAgeMs = opts.calculateCacheMaxAgeMs || calculateCacheMaxAgeMs; const _shouldCache = opts.shouldCache || shouldCache; const _shouldInvalidateCache = opts.shouldInvalidateCache || shouldInvalidateCache; const fnHash = encToFNV1A(fn.toString()); return async function (this: unknown, ...args: any[]) { const cacheKey = _buildCacheKey(fnHash, version, args); if (!_shouldInvalidateCache(args)) { const cacheResponse = await cacheStore.get(cacheKey); if (cacheResponse.isSuccess) { return cacheResponse.getValue(); } } const response = await fn.apply(this, args); if (_shouldCache(response)) { cacheStore.set( cacheKey, response, _calculateCacheMaxAgeMs(maxAgeMs), ); // Silent fail. } return response; }; }