@daisugi/kintsugi
Version:
Kintsugi is a set of utilities to help build a fault tolerant services.
112 lines (102 loc) • 2.9 kB
text/typescript
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;
};
}