stash
Problem
You need a typed cache with TTL expiry, stampede prevention for async factories, and an eviction callback.
Solution
Use stash(options?) to create a Stash<T, K> instance. hash defaults to String(key) so no options are needed for string keys.
ts
import { stash } from '@vielzeug/arsenal/cache';
// Simple string-keyed cache — no options required
const sessionCache = stash<User>();
sessionCache.set('user:1', { id: 1, name: 'Alice' }, { ttlMs: 30_000 });
sessionCache.get('user:1'); // { id: 1, name: 'Alice' }Global TTL and bounded size
ts
import { stash } from '@vielzeug/arsenal/cache';
const apiCache = stash<Response>({
ttlMs: 60_000, // all entries expire after 60s unless overridden
maxSize: 500, // FIFO eviction when exceeded
onEvict: (key) => console.log('evicted', key),
});
apiCache.set('data', response); // expires in 60s
apiCache.set('data', response, { ttlMs: 5_000 }); // override: 5sNon-string keys
ts
import { stash } from '@vielzeug/arsenal/cache';
const userCache = stash<User, readonly [string, number]>({
hash: (key) => JSON.stringify(key),
});
userCache.set(['user', 1], { id: 1, name: 'Alice' });
userCache.get(['user', 1]); // { id: 1, name: 'Alice' }Async getOrSet — stampede prevention
ts
import { stash } from '@vielzeug/arsenal/cache';
const cache = stash<User>();
// Concurrent calls with the same key share one in-flight Promise
const [a, b] = await Promise.all([
cache.getOrSet('user-1', () => fetchUser(1)),
cache.getOrSet('user-1', () => fetchUser(1)), // reuses first in-flight call
]);
// Bypass the cache entirely
const fresh = await cache.getOrSet('user-1', () => fetchUser(1), { forceRefresh: true });Persistence hooks
ts
import { stash } from '@vielzeug/arsenal/cache';
// Plug in localStorage or any external store
const persistentCache = stash<string>({
persistence: {
deserialize: (raw) => JSON.parse(raw) as string,
serialize: (v) => JSON.stringify(v),
},
});Pitfalls
getOrSetcachesundefined— if the factory returnsundefined, subsequent calls return it without calling the factory again.- TTL expiry is checked lazily on read — expired entries are not purged until accessed.
persistence.serializeandpersistence.deserializemust both be present; the pair is validated at the TypeScript level.