@absoluteui/services/cache
Async cache with TTL, stale-while-revalidate, and request deduplication.
import { ... } from '@absoluteui/services/cache';How to use
Section titled “How to use”An in-memory async cache for values that are expensive to load, such as API lookups. Concurrent requests for one key share a single load, values stay fresh for ttlMs, stale values can be served while a background reload runs, and the size is bounded (least recently used entries go first). Use it for reference data (departments, cost centres, holiday calendars) and per-record lookups; for data that must survive a reload, persist it with @absoluteui/services/storage.
How do I cache an API call with stale-while-revalidate?
Section titled “How do I cache an API call with stale-while-revalidate?”Wrap the loader. Within ttlMs the cached value is returned; after that, for staleWhileRevalidateMs, the old value is returned at once and a reload runs in the background.
import { createAsyncCache } from '@absoluteui/services/cache';import { createHttpClient } from '@absoluteui/services/http';import { createMockFetch } from '@absoluteui/services/http-mock';
let version = 1;const fetch = createMockFetch([{ url: '/api/departments', respond: () => ({ body: [`Finance v${String(version)}`] }) }]);const api = createHttpClient({ fetch, baseUrl: 'http://localhost', retry: false });
let clock = 0;const departments = createAsyncCache({ load: (_key: 'all', signal) => api.get('/api/departments', { signal }), ttlMs: 60_000, staleWhileRevalidateMs: 10 * 60_000, now: () => clock,});await departments.get('all'); // => ['Finance v1']version = 2;clock = 90_000; // stale nowawait departments.get('all'); // => ['Finance v1'] // served at once, reload startedawait new Promise((resolve) => setTimeout(resolve, 10));await departments.get('all'); // => ['Finance v2']fetch.calls.length; // => 2The injected now is only for the example; omit it in applications.
How do I stop duplicate requests when many components ask at once?
Section titled “How do I stop duplicate requests when many components ask at once?”Calls for the same key while a load is running join it (single-flight). Use keyFn when keys are objects.
import { createAsyncCache } from '@absoluteui/services/cache';
let loads = 0;const managers = createAsyncCache({ load: async ({ employeeId }: { employeeId: number }) => { loads++; return { employeeId, manager: 'Grace Hopper' }; }, keyFn: (k) => k.employeeId, ttlMs: 5 * 60_000,});const results = await Promise.all([managers.get({ employeeId: 42 }), managers.get({ employeeId: 42 }), managers.get({ employeeId: 42 })]);results.length; // => 3loads; // => 1managers.stats().misses; // => 3How do I refresh a value after the user edits it?
Section titled “How do I refresh a value after the user edits it?”Put the saved value in with prime, or drop entries with invalidate and invalidateWhere so the next get loads again. get(key, { force: true }) reloads immediately.
import { createAsyncCache } from '@absoluteui/services/cache';
const employees = createAsyncCache({ load: async (id: number) => ({ id, title: 'Analyst' }) });await employees.get(42); // => { id: 42, title: 'Analyst' }employees.prime(42, { id: 42, title: 'Senior Analyst' }); // the response of your save callemployees.peek(42); // => { id: 42, title: 'Senior Analyst' }await employees.get(7);employees.invalidateWhere((id) => id < 10); // => 1employees.has(7); // => falseHow do I cancel a lookup the user no longer needs?
Section titled “How do I cancel a lookup the user no longer needs?”Pass a signal to get. The caller stops waiting at once; the shared load is aborted only when every caller has left, and the loader receives that signal to pass on to fetch.
import { createAsyncCache } from '@absoluteui/services/cache';
const search = createAsyncCache({ load: (text: string, signal) => new Promise<string[]>((resolve, reject) => { const timer = setTimeout(() => { resolve([`${text}: Ada Lovelace`]); }, 1000); signal.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason); }); }),});const controller = new AbortController();const pending = search.get('ada', { signal: controller.signal });controller.abort();await pending; // => throws AbortErrorsearch.stats().loads; // => 1What goes wrong?
Section titled “What goes wrong?”- Errors are not cached by default: the next
gettries again. SeterrorTtlMsto cache failures briefly (for example a 404 lookup). - Background revalidation failures do not reject anyone; the stale value stays until it expires. Pass
onErrorto log them. - The cache is per page and per instance. Create one per data type at module level, not inside components, or every render starts from empty.
- Negative or
NaNdurations throwSVC_CACHE_INVALID_OPTION.
API reference
Section titled “API reference”Functions
Section titled “Functions”createAsyncCache
Creates an async cache. Concurrent gets for one key share one load (single-flight); stale values are
served while a background reload runs; errors are not cached unless errorTtlMs is set.
function createAsyncCache<K, V>(options: AsyncCacheOptions<K, V>): AsyncCache<K, V>;Example:
let calls = 0;const users = createAsyncCache({ load: async (id: number) => { calls++; return { id, name: 'Ada' }; }, ttlMs: 60_000, staleWhileRevalidateMs: 300_000,});await users.get(42); // => { id: 42, name: 'Ada' }await users.get(42); // => { id: 42, name: 'Ada' } // served from cachecalls; // => 1Interfaces
Section titled “Interfaces”AsyncCache
Cache returned by {@link createAsyncCache}.
interface AsyncCache<K, V> { get(key: K, options?: AsyncCacheGetOptions): Promise<V>; peek(key: K): V | undefined; has(key: K): boolean; prime(key: K, value: V): void; invalidate(key: K): void; invalidateWhere(predicate: (key: K) => boolean): number; clear(): void; stats(): AsyncCacheStats;}AsyncCacheGetOptions
Per-call options for {@link AsyncCache.get}.
interface AsyncCacheGetOptions { readonly signal?: AbortSignal; readonly force?: boolean;}AsyncCacheOptions
Options for {@link createAsyncCache}.
interface AsyncCacheOptions<K, V> { readonly load: (key: K, signal: AbortSignal) => Promise<V> | V; readonly ttlMs?: number; readonly staleWhileRevalidateMs?: number; readonly maxSize?: number; readonly keyFn?: (key: K) => unknown; readonly errorTtlMs?: number; readonly onError?: (error: Error, key: K) => void; readonly now?: () => number;}AsyncCacheStats
Counters from {@link AsyncCache.stats}.
interface AsyncCacheStats { readonly hits: number; readonly misses: number; readonly stale: number; readonly loads: number; readonly errors: number; readonly size: number;}