memo
The memo utility creates a memoized version of a function that caches its results based on the provided arguments. It is highly configurable, featuring support for Time-To-Live (TTL) expiration and a maximum cache size with LRU (Least Recently Used) eviction.
Source Code
View Source Code
ts
import type { Fn } from '../types';
type MemoOptions<T extends Fn> = {
key?: (...args: Parameters<T>) => PropertyKey;
maxSize?: number;
ttl?: number;
};
type CacheEntry<T> = {
expiresAt: number;
value: T;
};
const UNDEFINED_SENTINEL = '\x00undefined\x00';
const defaultKey = (args: unknown[]): string => {
try {
return JSON.stringify(args, (_, value) => (value === undefined ? UNDEFINED_SENTINEL : value));
} catch (error) {
const reason = error instanceof Error && error.message ? ` Reason: ${error.message}` : '';
throw new TypeError(
`[toolkit/memo] Failed to serialize memo arguments. Provide options.key for non-serializable arguments.${reason}`,
{ cause: error },
);
}
};
const isPromise = (value: unknown): value is Promise<unknown> =>
typeof value === 'object' &&
value !== null &&
'then' in value &&
typeof (value as Promise<unknown>).then === 'function';
/**
* Creates a function that memoizes the result of the provided function.
* Supports sync and async functions, including in-flight deduplication for async calls.
*
* @example
* ```ts
* const add = (x, y) => x + y;
* const memoizedAdd = memo(add, { ttl: 5000, maxSize: 10 });
*
* memoizedAdd(1, 2); // 3 (caches the result)
* memoizedAdd(1, 2); // 3 (from cache)
* ```
*
* @param fn - The function to memorize.
* @param options - Memoization options.
* @param [options.ttl] - (optional) time-to-live (TTL) for cache expiration (in milliseconds).
* @param [options.maxSize] - (optional) maximum cache size (LRU eviction).
* @param [options.key] - (optional) custom function to resolve the cache key.
*
* @returns A new function that memorizes the input function.
*/
export function memo<T extends Fn>(
fn: T,
{ key, maxSize = Infinity, ttl = Infinity }: MemoOptions<T> = {},
): (...args: Parameters<T>) => ReturnType<T> {
const cache = new Map<PropertyKey, CacheEntry<ReturnType<T>>>();
return (...args: Parameters<T>): ReturnType<T> => {
const cacheKey = key ? key(...args) : defaultKey(args);
const now = Date.now();
const cached = cache.get(cacheKey);
if (cached && cached.expiresAt > now) {
cache.delete(cacheKey);
cache.set(cacheKey, cached);
return cached.value;
}
const result = fn(...args);
const entry: CacheEntry<ReturnType<T>> = { expiresAt: now + ttl, value: result as ReturnType<T> };
cache.delete(cacheKey);
cache.set(cacheKey, entry);
if (isPromise(result)) {
// Evict on rejection so subsequent calls retry instead of returning a settled failure
void result.catch(() => cache.delete(cacheKey));
}
while (cache.size > maxSize) {
const oldestKey = cache.keys().next().value;
if (oldestKey === undefined) {
break;
}
cache.delete(oldestKey);
}
return result as ReturnType<T>;
};
}Features
- Isomorphic: Works in both Browser and Node.js.
- Smart Caching: Efficiently stores and retrieves results for pure functions.
- Cache Management: Prevent memory leaks with
maxSizeandttloptions. - LRU Eviction: Automatically removes the oldest entries when the cache limit is reached.
- Type-safe: Properly preserves the original function's signature and return type.
API
Type Definitions
ts
import type { Fn } from '../types';
type MemoOptions<T extends Fn> = {
key?: (...args: Parameters<T>) => PropertyKey;
maxSize?: number;
ttl?: number;
};
type CacheEntry<T> = {
expiresAt: number;
value: T;
};
const UNDEFINED_SENTINEL = '\x00undefined\x00';
const defaultKey = (args: unknown[]): string => {
try {
return JSON.stringify(args, (_, value) => (value === undefined ? UNDEFINED_SENTINEL : value));
} catch (error) {
const reason = error instanceof Error && error.message ? ` Reason: ${error.message}` : '';
throw new TypeError(
`[toolkit/memo] Failed to serialize memo arguments. Provide options.key for non-serializable arguments.${reason}`,
{ cause: error },
);
}
};
const isPromise = (value: unknown): value is Promise<unknown> =>
typeof value === 'object' &&
value !== null &&
'then' in value &&
typeof (value as Promise<unknown>).then === 'function';
/**
* Creates a function that memoizes the result of the provided function.
* Supports sync and async functions, including in-flight deduplication for async calls.
*
* @example
* ```ts
* const add = (x, y) => x + y;
* const memoizedAdd = memo(add, { ttl: 5000, maxSize: 10 });
*
* memoizedAdd(1, 2); // 3 (caches the result)
* memoizedAdd(1, 2); // 3 (from cache)
* ```
*
* @param fn - The function to memorize.
* @param options - Memoization options.
* @param [options.ttl] - (optional) time-to-live (TTL) for cache expiration (in milliseconds).
* @param [options.maxSize] - (optional) maximum cache size (LRU eviction).
* @param [options.key] - (optional) custom function to resolve the cache key.
*
* @returns A new function that memorizes the input function.
*/
export function memo<T extends Fn>(
fn: T,
{ key, maxSize = Infinity, ttl = Infinity }: MemoOptions<T> = {},
): (...args: Parameters<T>) => ReturnType<T> {
const cache = new Map<PropertyKey, CacheEntry<ReturnType<T>>>();
return (...args: Parameters<T>): ReturnType<T> => {
const cacheKey = key ? key(...args) : defaultKey(args);
const now = Date.now();
const cached = cache.get(cacheKey);
if (cached && cached.expiresAt > now) {
cache.delete(cacheKey);
cache.set(cacheKey, cached);
return cached.value;
}
const result = fn(...args);
const entry: CacheEntry<ReturnType<T>> = { expiresAt: now + ttl, value: result as ReturnType<T> };
cache.delete(cacheKey);
cache.set(cacheKey, entry);
if (isPromise(result)) {
// Evict on rejection so subsequent calls retry instead of returning a settled failure
void result.catch(() => cache.delete(cacheKey));
}
while (cache.size > maxSize) {
const oldestKey = cache.keys().next().value;
if (oldestKey === undefined) {
break;
}
cache.delete(oldestKey);
}
return result as ReturnType<T>;
};
}ts
function memo<T extends (...args: any[]) => any>(fn: T, options?: MemoizeOptions<T>): T;Parameters
fn: The function to memoize.options: Optional configuration:ttl: Time in milliseconds before a cached result expires.maxSize: Maximum number of entries to keep in the cache.
Returns
- A new function that caches results.
Examples
Basic Memoization
ts
import { memo } from '@vielzeug/toolkit';
const heavyCalculation = (n: number) => {
console.log('Calculating...');
return n * n;
};
const cachedCalc = memo(heavyCalculation);
cachedCalc(5); // Logs 'Calculating...', returns 25
cachedCalc(5); // Returns 25 immediately (from cache)With Expiration (TTL)
ts
import { memo } from '@vielzeug/toolkit';
// Cache results for only 1 minute
const getStats = memo(fetchStats, { ttl: 60000 });Limiting Memory Usage
ts
import { memo } from '@vielzeug/toolkit';
// Keep only the last 100 results
const formatData = memo(formatter, { maxSize: 100 });Implementation Notes
- Performance-optimized using a standard
Mapfor the cache. - The cache key is generated based on the string representation of all arguments.
- Throws
TypeErroriffnis not a function.