alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
80 lines (70 loc) • 2.25 kB
text/typescript
import { createMiddleware, type Middleware } from "alepha";
/**
* Lightweight in-process memoization middleware.
*
* Caches handler results in a plain `Map` — no external store, no serialization,
* no provider dependency. Process-local only. Entries live until eviction by capacity.
*
* ```typescript
* class Api {
* getStats = $action({
* use: [$memoize({ max: 100 })],
* handler: async () => this.repo.aggregate(),
* });
* }
* ```
*
* > For more advanced caching, use `$cache` from "alepha/cache" instead — it supports TTL, invalidation, external stores (Redis).
*/
export const $memoize = (options?: MemoizeOptions): Middleware => {
return createMiddleware({
name: "$memoize",
options: options as unknown as Record<string, unknown>,
handler: ({ next }) => {
const store = new Map<string, any>();
const maxSize = options?.max ?? 1000;
const keyFn = options?.key ?? ((...args: any[]) => JSON.stringify(args));
return async (...args) => {
const key = keyFn(...args);
if (store.has(key)) {
return store.get(key);
}
// Store the promise immediately to deduplicate concurrent calls
// for the same key (thundering herd prevention).
const promise = next(...args);
store.set(key, promise);
try {
const result = await promise;
// Replace the promise with the resolved value
store.set(key, result);
// Evict oldest if at capacity
if (store.size > maxSize) {
const firstKey = store.keys().next().value;
if (firstKey !== undefined) {
store.delete(firstKey);
}
}
return result;
} catch (error) {
// Don't cache failed results
store.delete(key);
throw error;
}
};
},
});
};
export interface MemoizeOptions {
/**
* Maximum number of entries to keep in the cache.
* When exceeded, the oldest entry is evicted (FIFO).
*
* @default 1000
*/
max?: number;
/**
* Custom key function. Receives the handler's arguments.
* By default, `JSON.stringify(args)` is used.
*/
key?: (...args: any[]) => string;
}