UNPKG

@frak-labs/core-sdk

Version:

Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.

125 lines (111 loc) 3.98 kB
import { LruMap } from "./lruMap"; type CacheEntry<TData> = { data: TData; created: number; }; /** Global cache for in-flight promises (dedup concurrent calls) */ const promiseCache = new LruMap<Promise<unknown>>(1024); /** Global cache for resolved responses (TTL-based) */ const responseCache = new LruMap<CacheEntry<unknown>>(1024); /** Default cache time: 30 seconds */ export const DEFAULT_CACHE_TIME = 30_000; /** Short negative cache to avoid flooding on transient failures */ const NEGATIVE_CACHE_TIME = 1_000; /** Tracks recently failed keys to avoid request floods */ const failureCache = new LruMap<number>(1024); type WithCacheOptions = { /** The key to cache the data against */ cacheKey: string; /** Time in ms that cached data will remain valid. Default: 30_000 (30s). Set to 0 to disable caching. */ cacheTime?: number; }; /** * Returns the result of a given promise, and caches the result for * subsequent invocations against a provided cache key. * * Also deduplicates concurrent calls — if multiple callers request the same * cache key while the promise is pending, they share the same promise. * * @example * ```ts * // First call fetches, subsequent calls return cached data for 30s * const data = await withCache( * () => client.request({ method: "frak_getMerchantInformation" }), * { cacheKey: "merchantInfo", cacheTime: 30_000 } * ); * ``` */ export async function withCache<TData>( fn: () => Promise<TData>, { cacheKey, cacheTime = DEFAULT_CACHE_TIME }: WithCacheOptions ): Promise<TData> { // Check response cache — return immediately if fresh if (cacheTime > 0) { const cached = responseCache.get(cacheKey) as | CacheEntry<TData> | undefined; if (cached) { const age = Date.now() - cached.created; if (age < cacheTime) return cached.data; } } // Check if this key recently failed — back off briefly const lastFailure = failureCache.get(cacheKey); if (lastFailure && Date.now() - lastFailure < NEGATIVE_CACHE_TIME) { throw new Error(`Cache: ${cacheKey} recently failed, backing off`); } // Check if there's already a pending promise (dedup concurrent calls) let promise = promiseCache.get(cacheKey) as Promise<TData> | undefined; if (!promise) { promise = fn(); promiseCache.set(cacheKey, promise); } try { const data = await promise; // Store the response with a timestamp responseCache.set(cacheKey, { data, created: Date.now() }); // Clear any previous failure failureCache.delete(cacheKey); return data; } catch (error) { // Record the failure timestamp failureCache.set(cacheKey, Date.now()); throw error; } finally { // Clear the promise cache so subsequent calls can re-fetch after TTL promiseCache.delete(cacheKey); } } /** * Get a cache handle for a specific key, useful for manual invalidation. * * @example * ```ts * // Invalidate merchant info cache after a mutation * getCache("frak_getMerchantInformation").clear(); * ``` */ export function getCache(cacheKey: string) { return { /** Clear both the pending promise and the cached response */ clear: () => { promiseCache.delete(cacheKey); responseCache.delete(cacheKey); }, /** Check if a non-expired response exists */ has: (cacheTime: number = DEFAULT_CACHE_TIME) => { const cached = responseCache.get(cacheKey); if (!cached) return false; return Date.now() - cached.created < cacheTime; }, }; } /** * Clear all cached data (both pending promises and resolved responses). * Called automatically when the client is destroyed. */ export function clearAllCache() { promiseCache.clear(); responseCache.clear(); failureCache.clear(); }