1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 |
x15
x15
x15
x15
x37
x37
x37
x37
x37
x37
x37
x669
x669
x669
x315
x315
x315
x314
x315
x1
x315
x315
x354
x354
x669
x10
x5
x4
x4
x5
x10
x10
x10
x10
x669
x339
x339
x349
x669
x5
x1
x1
x5
x5
x37
x37
x37
x37
x37
x37
x37
x37
x37 |
|
// Copyright 2018-2026 the Deno authors. MIT license.
// This module is browser compatible.
import { _serializeArgList } from "./_serialize_arg_list.ts";
/**
* A cache suitable for use with {@linkcode memoize}.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export interface MemoizationCache<K, V> {
/** Checks whether a value for the given key exists in the cache. */
has(key: K): boolean;
/** Returns the cached value associated with the given key, if present. */
get(key: K): V | undefined;
/** Stores a value in the cache under the given key. */
set(key: K, val: V): unknown;
/** Removes the value associated with the given key from the cache. */
delete(key: K): unknown;
}
/**
* The result of a memoized function, as stored in its cache.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*/
export type MemoizationCacheResult<T> =
| { kind: "ok"; value: T }
| { kind: "error"; error: unknown }
| (T extends Promise<unknown> ? { kind: "promise"; value: T } : never);
/**
* Options for {@linkcode memoize}.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam Fn The type of the function to memoize.
* @typeParam Key The type of the cache key.
* @typeParam Cache The type of the cache.
*/
export type MemoizeOptions<
Fn extends (...args: never[]) => unknown,
Key,
Cache extends MemoizationCache<Key, MemoizationCacheResult<ReturnType<Fn>>>,
> = {
/**
* Provide a custom cache for getting previous results. By default, a new
* {@linkcode Map} object is instantiated upon memoization and used as a cache, with no
* limit on the number of results to be cached.
*
* Alternatively, you can supply an
* {@link https://jsr.io/@std/cache/doc/lru-cache/~/LruCache | LruCache}
* with a specified max size to limit memory usage.
*/
cache?: Cache;
/**
* Function to get a unique cache key from the function's arguments. By
* default, a composite key is created from all the arguments plus the `this`
* value, using reference equality to check for equivalence.
*
* @example
* ```ts
* import { memoize } from "@std/cache";
* import { assertEquals } from "@std/assert";
*
* const fn = memoize(({ value }: { cacheKey: number; value: number }) => {
* return value;
* }, { getKey: ({ cacheKey }) => cacheKey });
*
* assertEquals(fn({ cacheKey: 1, value: 2 }), 2);
* assertEquals(fn({ cacheKey: 1, value: 99 }), 2);
* assertEquals(fn({ cacheKey: 2, value: 99 }), 99);
* ```
*/
getKey?: (this: ThisParameterType<Fn>, ...args: Parameters<Fn>) => Key;
/**
* Callback to determine if an error or other thrown value is cacheable.
*
* @default {() => false}
*
* @param err The thrown error or other value.
* @returns `true` if the error is cacheable, `false` otherwise.
*/
errorIsCacheable?: (err: unknown) => boolean;
};
/**
* Cache the results of a function based on its arguments.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @typeParam Fn The type of the function to memoize.
* @typeParam Key The type of the cache key.
* @typeParam Cache The type of the cache.
* @param fn The function to memoize
* @param options Options for memoization
*
* @returns The memoized function, with a `cache` property exposing the
* underlying cache for inspection or manual invalidation.
*
* @example Basic usage
* ```ts
* import { memoize } from "@std/cache";
* import { assertEquals } from "@std/assert";
*
* // fibonacci function, which is very slow for n > ~30 if not memoized
* const fib = memoize((n: bigint): bigint => {
* return n <= 2n ? 1n : fib(n - 1n) + fib(n - 2n);
* });
*
* assertEquals(fib(100n), 354224848179261915075n);
* ```
*
* > [!NOTE]
* > * By default, memoization is on the basis of all arguments passed to the
* > function, with equality determined by reference. This means that, for
* > example, passing a memoized function as `arr.map(func)` will not use the
* > cached results, as the index is implicitly passed as an argument. To
* > avoid this, you can pass a custom `getKey` option or use the memoized
* > function inside an anonymous callback like `arr.map((x) => func(x))`.
*/
export function memoize<
Fn extends (...args: never[]) => unknown,
Key = string,
Cache extends MemoizationCache<Key, MemoizationCacheResult<ReturnType<Fn>>> =
Map<
Key,
MemoizationCacheResult<ReturnType<Fn>>
>,
>(
fn: Fn,
options?: MemoizeOptions<Fn, Key, Cache>,
): Fn & { cache: Cache } {
const cache = (options?.cache ?? new Map()) as Cache;
const getKey = options?.getKey ??
_serializeArgList(
cache as MemoizationCache<unknown, unknown>,
) as unknown as (
(this: ThisParameterType<Fn>, ...args: Parameters<Fn>) => Key
);
const errorIsCacheable = options?.errorIsCacheable ?? (() => false);
const memoized = function (
this: ThisParameterType<Fn>,
...args: Parameters<Fn>
): ReturnType<Fn> {
const key = getKey.apply(this, args) as Key;
const cached = cache.get(key);
// `MemoizationCacheResult` is always a truthy object, so `undefined`
// reliably indicates a cache miss here.
if (cached !== undefined) {
switch (cached.kind) {
case "ok":
case "promise":
return cached.value as ReturnType<Fn>;
case "error":
throw cached.error;
}
}
try {
let value = fn.apply(this, args) as ReturnType<Fn>;
if (value instanceof Promise) {
value = value.catch((reason) => {
if (!errorIsCacheable(reason)) {
cache.delete(key);
}
throw reason;
}) as typeof value;
cache.set(
key,
{ kind: "promise", value } as MemoizationCacheResult<ReturnType<Fn>>,
);
} else {
cache.set(key, { kind: "ok", value });
}
return value;
} catch (e) {
if (errorIsCacheable(e)) {
cache.set(key, { kind: "error", error: e });
}
throw e;
}
} as Fn & { cache: Cache };
return Object.defineProperties(
memoized,
{
length: { value: fn.length },
name: { value: fn.name },
cache: { value: cache },
},
);
}
|