import { Subscribable } from './models/Subscribable' import type { CacheKey, CacheOptions } from './types' import type { ExtractPartial } from './utility-types/ExtractPartial' import { hashCacheKey } from './utils' const enum CacheStatus { Idle = 'idle', Pending = 'pending', Resolved = 'resolved', Rejected = 'rejected', } export type AllStatusCached = | { status: CacheStatus.Idle promiseToSuspend: undefined cacheKey: TCacheKey hashedCacheKey: ReturnType state: { promise: undefined data: undefined error: undefined } } | { status: CacheStatus.Pending promiseToSuspend: Promise cacheKey: TCacheKey hashedCacheKey: ReturnType state: { promise: Promise data: undefined error: undefined } } | { status: CacheStatus.Resolved promiseToSuspend: Promise cacheKey: TCacheKey hashedCacheKey: ReturnType state: { promise: Promise data: TData error: undefined } } | { status: CacheStatus.Rejected promiseToSuspend: Promise cacheKey: TCacheKey hashedCacheKey: ReturnType state: { promise: Promise data: undefined error: unknown } } export type IdleCached = ExtractPartial< AllStatusCached, { status: CacheStatus.Idle } > export type PendingCached = ExtractPartial< AllStatusCached, { status: CacheStatus.Pending } > export type ResolvedCached = ExtractPartial< AllStatusCached, { status: CacheStatus.Resolved } > export type RejectedCached = ExtractPartial< AllStatusCached, { status: CacheStatus.Rejected } > export type Cached = | IdleCached | PendingCached | ResolvedCached | RejectedCached /** * @experimental This is experimental feature. */ export class Cache extends Subscribable<() => void> { private cache = new Map, Cached>() public reset = (options?: Pick, 'cacheKey'>) => { if (typeof options?.cacheKey === 'undefined' || options.cacheKey.length === 0) { this.cache.clear() this.notify() return } const hashedCacheKey = hashCacheKey(options.cacheKey) if (this.cache.has(hashedCacheKey)) { this.cache.delete(hashedCacheKey) } this.notify() } public remove = (options: Pick, 'cacheKey'>) => { const hashedCacheKey = hashCacheKey(options.cacheKey) if (this.cache.has(hashedCacheKey)) { this.cache.delete(hashedCacheKey) } } public clearError = (options?: Pick, 'cacheKey'>) => { if (options?.cacheKey === undefined || options.cacheKey.length === 0) { this.cache.forEach((cached, hashedCacheKey, cache) => { cache.set(hashedCacheKey, { ...cached, status: CacheStatus.Idle, promiseToSuspend: undefined, state: { promise: undefined, error: undefined, data: undefined, }, }) }) return } const hashedCacheKey = hashCacheKey(options.cacheKey) const cached = this.cache.get(hashedCacheKey) if (cached) { this.cache.set(hashedCacheKey, { ...cached, status: CacheStatus.Idle, promiseToSuspend: undefined, state: { promise: undefined, error: undefined, data: undefined, }, }) } } public suspend = ({ cacheKey, cacheFn, }: CacheOptions): ResolvedCached => { const hashedCacheKey = hashCacheKey(cacheKey) const cached = this.cache.get(hashedCacheKey) if (cached && cached.status !== CacheStatus.Idle) { if (cached.status === CacheStatus.Rejected) { throw cached.state.error } if (cached.status === CacheStatus.Resolved) { return cached as ResolvedCached } // eslint-disable-next-line @typescript-eslint/only-throw-error throw cached.promiseToSuspend } const promise = cacheFn({ cacheKey }) const newCached: Cached = { cacheKey, hashedCacheKey, status: CacheStatus.Pending, state: { promise, data: undefined, error: undefined, }, promiseToSuspend: promise.then( (data) => { newCached.status = CacheStatus.Resolved newCached.state.data = data newCached.state.error = undefined }, (error: unknown) => { newCached.status = CacheStatus.Rejected newCached.state.data = undefined newCached.state.error = error } ), } this.cache.set(hashedCacheKey, newCached) // eslint-disable-next-line @typescript-eslint/only-throw-error throw newCached.promiseToSuspend } public getData = (options: Pick, 'cacheKey'>) => this.cache.get(hashCacheKey(options.cacheKey))?.state.data public getError = (options: Pick, 'cacheKey'>) => this.cache.get(hashCacheKey(options.cacheKey))?.state.error }