import type { Awaitable } from '../types/misc.ts'; import type { GetOptions, Store } from './store.ts'; export interface GetCachedOptions { signal?: AbortSignal; noCache?: boolean; allowStale?: boolean; } export interface GetterOptions { signal?: AbortSignal; noCache: boolean; } export type Getter = (key: K, options: GetterOptions, storedValue: V | undefined) => Awaitable; export interface CachedGetterOptions { isStale?(key: K, value: V): Awaitable; onStoreError?(err: unknown, key: K, value: V): Awaitable; deleteOnError?(err: unknown, key: K, value: V): Awaitable; } type PendingItem = Promise<{ value: V; fresh: boolean }>; const returnTrue = () => true; const returnFalse = () => false; export class CachedGetter { #pending = new Map>(); readonly getter: Getter; readonly store: Store; readonly options: CachedGetterOptions; constructor(getter: Getter, store: Store, options: CachedGetterOptions = {}) { this.getter = getter; this.store = store; this.options = options; } async get(key: K, options: GetCachedOptions = {}): Promise { const { signal, allowStale = false, noCache = false } = options; const { isStale, deleteOnError } = this.options; signal?.throwIfAborted(); const allowStored: (value: V) => Awaitable = noCache ? returnFalse : allowStale || isStale == null ? returnTrue : async (value: V) => !(await isStale(key, value)); let promise: PendingItem | undefined; // wait for the previous request for the same key to finish while ((promise = this.#pending.get(key)) !== undefined) { try { const { value, fresh } = await promise; if (fresh) { return value; } if (await allowStored(value)) { return value; } } catch { // ignore errors from previous requests } signal?.throwIfAborted(); } // now we start our own. promise = (async (): PendingItem => { try { const storedValue = await this.getStored(key, { signal }); if (storedValue !== undefined && (await allowStored(storedValue))) { return { fresh: false, value: storedValue }; } let value: V; try { const options: GetterOptions = { signal, noCache }; value = await (0, this.getter)(key, options, storedValue); } catch (err) { if (storedValue !== undefined && deleteOnError !== undefined) { try { if (await deleteOnError(err, key, storedValue)) { await this.deleteStored(key, err); } } catch (error) { // oxlint-disable-next-line preserve-caught-error -- errors preserved in first arg throw new AggregateError([err, error], `error while deleting stored value`); } } throw err; } await this.setStored(key, value); return { fresh: true, value: value }; } finally { this.#pending.delete(key); } })(); this.#pending.set(key, promise); const { value } = await promise; return value; } async getStored(key: K, options?: GetOptions): Promise { try { return await this.store.get(key, options); } catch { return undefined; } } async setStored(key: K, value: V): Promise { try { await this.store.set(key, value); } catch (err) { const onStoreError = this.options?.onStoreError; await onStoreError?.(err, key, value); } } async deleteStored(key: K, _cause?: unknown): Promise { await this.store.delete(key); } }