diff --git a/.github/dependency_graph.svg b/.github/dependency_graph.svg
index 53ab4c7f7861..e3fed595ba5f 100644
--- a/.github/dependency_graph.svg
+++ b/.github/dependency_graph.svg
@@ -63,32 +63,38 @@
async
-
+
+cache
+
+cache
+
+
+
cli
cli
-
+
collections
collections
-
+
crypto
crypto
-
+
csv
csv
-
+
streams
streams
@@ -106,32 +112,32 @@
-
+
data-\nstructures
data-
structures
-
+
datetime
datetime
-
+
dotenv
dotenv
-
+
encoding
encoding
-
+
expect
expect
@@ -149,20 +155,20 @@
-
+
fmt
fmt
-
+
front-\nmatter
front-
matter
-
+
toml
toml
@@ -174,7 +180,7 @@
-
+
yaml
yaml
@@ -192,13 +198,13 @@
-
+
fs
fs
-
+
path
path
@@ -210,13 +216,13 @@
-
+
html
html
-
+
http
http
@@ -252,7 +258,7 @@
-
+
media-\ntypes
media-
@@ -265,7 +271,7 @@
-
+
net
net
@@ -277,13 +283,13 @@
-
+
ini
ini
-
+
json
json
@@ -295,7 +301,7 @@
-
+
jsonc
jsonc
@@ -307,7 +313,7 @@
-
+
log
log
@@ -331,7 +337,7 @@
-
+
msgpack
msgpack
@@ -343,19 +349,19 @@
-
+
regexp
regexp
-
+
semver
semver
-
+
testing
testing
@@ -397,19 +403,19 @@
-
+
text
text
-
+
ulid
ulid
-
+
url
url
@@ -421,7 +427,7 @@
-
+
uuid
uuid
@@ -439,7 +445,7 @@
-
+
webgpu
webgpu
diff --git a/_tools/check_circular_package_dependencies.ts b/_tools/check_circular_package_dependencies.ts
index 79bb26cf6fad..9c7203ca7e36 100644
--- a/_tools/check_circular_package_dependencies.ts
+++ b/_tools/check_circular_package_dependencies.ts
@@ -39,6 +39,7 @@ type Mod =
| "assert"
| "async"
| "bytes"
+ | "cache"
| "cli"
| "collections"
| "crypto"
@@ -80,6 +81,7 @@ const ENTRYPOINTS: Record = {
assert: ["mod.ts"],
async: ["mod.ts"],
bytes: ["mod.ts"],
+ cache: ["mod.ts"],
cli: ["mod.ts"],
collections: ["mod.ts"],
crypto: ["mod.ts"],
diff --git a/_tools/check_docs.ts b/_tools/check_docs.ts
index 95ee6875101f..e374a3620aa1 100644
--- a/_tools/check_docs.ts
+++ b/_tools/check_docs.ts
@@ -31,6 +31,7 @@ const ENTRY_POINTS = [
"../assert/mod.ts",
"../async/mod.ts",
"../bytes/mod.ts",
+ "../cache/mod.ts",
"../cli/mod.ts",
"../crypto/mod.ts",
"../collections/mod.ts",
diff --git a/cache/_serialize_arg_list.ts b/cache/_serialize_arg_list.ts
new file mode 100644
index 000000000000..43e0395bfd5d
--- /dev/null
+++ b/cache/_serialize_arg_list.ts
@@ -0,0 +1,87 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import type { MemoizationCache } from "./memoize.ts";
+
+/**
+ * Default serialization of arguments list for use as cache keys. Equivalence
+ * follows [`SameValueZero`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero)
+ * reference equality, such that `getKey(x, y) === getKey(x, y)` for all values
+ * of `x` and `y`, but `getKey({}) !== getKey({})`.
+ *
+ * @param cache The cache for which the keys will be used.
+ * @returns `getKey`, the function for getting cache keys.
+ */
+
+export function _serializeArgList(
+ cache: MemoizationCache,
+): (this: unknown, ...args: unknown[]) => string {
+ const weakKeyToKeySegmentCache = new WeakMap();
+ const weakKeySegmentToKeyCache = new Map();
+ let i = 0;
+
+ const registry = new FinalizationRegistry((keySegment) => {
+ for (const key of weakKeySegmentToKeyCache.get(keySegment) ?? []) {
+ cache.delete(key);
+ }
+ weakKeySegmentToKeyCache.delete(keySegment);
+ });
+
+ return function getKey(...args) {
+ const weakKeySegments: string[] = [];
+ const keySegments = [this, ...args].map((arg) => {
+ if (typeof arg === "undefined") return "undefined";
+ if (typeof arg === "bigint") return `${arg}n`;
+
+ if (typeof arg === "number") {
+ return String(arg);
+ }
+
+ if (
+ arg === null ||
+ typeof arg === "string" ||
+ typeof arg === "boolean"
+ ) {
+ // This branch will need to be updated if further types are added to
+ // the language that support value equality,
+ // e.g. https://github.com/tc39/proposal-record-tuple
+ return JSON.stringify(arg);
+ }
+
+ try {
+ assertWeakKey(arg);
+ } catch {
+ if (typeof arg === "symbol") {
+ return `Symbol.for(${JSON.stringify(arg.description)})`;
+ }
+ // Non-weak keys other than `Symbol.for(...)` are handled by the branches above.
+ throw new Error(
+ "Should be unreachable. Please open an issue at https://github.com/denoland/std/issues/new",
+ );
+ }
+
+ if (!weakKeyToKeySegmentCache.has(arg)) {
+ const keySegment = `{${i++}}`;
+ weakKeySegments.push(keySegment);
+ registry.register(arg, keySegment);
+ weakKeyToKeySegmentCache.set(arg, keySegment);
+ }
+
+ const keySegment = weakKeyToKeySegmentCache.get(arg)!;
+ weakKeySegments.push(keySegment);
+ return keySegment;
+ });
+
+ const key = keySegments.join(",");
+
+ for (const keySegment of weakKeySegments) {
+ const keys = weakKeySegmentToKeyCache.get(keySegment) ?? [];
+ keys.push(key);
+ weakKeySegmentToKeyCache.set(keySegment, keys);
+ }
+
+ return key;
+ };
+}
+
+function assertWeakKey(arg: unknown): asserts arg is WeakKey {
+ new WeakRef(arg as WeakKey);
+}
diff --git a/cache/_serialize_arg_list_test.ts b/cache/_serialize_arg_list_test.ts
new file mode 100644
index 000000000000..f44f16b81e0f
--- /dev/null
+++ b/cache/_serialize_arg_list_test.ts
@@ -0,0 +1,184 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assertEquals } from "@std/assert";
+import { _serializeArgList } from "./_serialize_arg_list.ts";
+import { delay } from "@std/async";
+
+Deno.test("_serializeArgList() serializes simple numbers", () => {
+ const getKey = _serializeArgList(new Map());
+ assertEquals(getKey(1), "undefined,1");
+ assertEquals(getKey(1, 2), "undefined,1,2");
+ assertEquals(getKey(1, 2, 3), "undefined,1,2,3");
+});
+
+Deno.test("_serializeArgList() serializes reference types", () => {
+ const getKey = _serializeArgList(new Map());
+ const obj = {};
+ const arr: [] = [];
+ const sym = Symbol("xyz");
+
+ assertEquals(getKey(obj), "undefined,{0}");
+ assertEquals(getKey(obj, obj), "undefined,{0},{0}");
+
+ assertEquals(getKey(arr), "undefined,{1}");
+ assertEquals(getKey(sym), "undefined,{2}");
+ assertEquals(
+ getKey(obj, arr, sym),
+ "undefined,{0},{1},{2}",
+ );
+});
+
+Deno.test("_serializeArgList() gives same results as SameValueZero algorithm", async (t) => {
+ /**
+ * [`SameValueZero`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-samevaluezero),
+ * used by [`Set`](https://tc39.es/ecma262/multipage/keyed-collections.html#sec-set-objects):
+ *
+ * > Distinct values are discriminated using the SameValueZero comparison algorithm.
+ */
+ const sameValueZero = (x: unknown, y: unknown) => new Set([x, y]).size === 1;
+
+ const getKey = _serializeArgList(new Map());
+
+ const values = [
+ 1,
+ "1",
+ '"1"',
+ 1n,
+ 0,
+ -0,
+ 0n,
+ true,
+ "true",
+ null,
+ undefined,
+ Infinity,
+ -Infinity,
+ NaN,
+ {},
+ {},
+ Symbol("x"),
+ Symbol.for("x"),
+ ];
+
+ await t.step("Serialization of values", () => {
+ assertEquals(
+ getKey(...values),
+ 'undefined,1,"1","\\"1\\"",1n,0,0,0n,true,"true",null,undefined,Infinity,-Infinity,NaN,{0},{1},{2},Symbol.for("x")',
+ );
+ });
+
+ await t.step("Gives consistent serialization for each value", () => {
+ for (const x of values) {
+ assertEquals(getKey(x), getKey(x));
+ }
+ });
+
+ await t.step("Gives same equivalence for each pair of values", () => {
+ for (const x of values) {
+ for (const y of values) {
+ const expectedEquivalence = sameValueZero(x, y);
+ const actualEquivalence = getKey(x) === getKey(y);
+ assertEquals(actualEquivalence, expectedEquivalence);
+ }
+ }
+ });
+});
+
+Deno.test("_serializeArgList() discriminates on `this` arg", () => {
+ const getKey = _serializeArgList(new Map());
+ const obj1 = {};
+ const obj2 = {};
+
+ assertEquals(getKey(), "undefined");
+ assertEquals(getKey.call(obj1), "{0}");
+ assertEquals(getKey.call(obj2), "{1}");
+ assertEquals(getKey.call(obj1, obj2), "{0},{1}");
+});
+
+Deno.test("_serializeArgList() allows garbage collection for weak keys", async () => {
+ // @ts-expect-error - Triggering true garbage collection is only available
+ // with `--v8-flags="--expose-gc"`, so we mock `FinalizationRegistry` with
+ // `using` and some `Symbol.dispose` trickery if it's not available. Run this
+ // test with `deno test --v8-flags="--expose-gc"` to test actual gc behavior
+ // (however, even calling `globalThis.gc` doesn't _guarantee_ garbage
+ // collection, so this may be flaky between v8 versions etc.)
+ const gc = globalThis.gc as undefined | (() => void);
+
+ class MockFinalizationRegistry extends FinalizationRegistry {
+ #cleanupCallback: (heldValue: T) => void;
+
+ constructor(cleanupCallback: (heldValue: T) => void) {
+ super(cleanupCallback);
+ this.#cleanupCallback = cleanupCallback;
+ }
+
+ override register(target: WeakKey, heldValue: T) {
+ Object.assign(target, {
+ onCleanup: () => {
+ this.#cleanupCallback(heldValue);
+ },
+ });
+ }
+ }
+
+ function makeRegisterableObject() {
+ const onCleanup = null as (() => void) | null;
+ return {
+ onCleanup,
+ [Symbol.dispose]() {
+ this.onCleanup?.();
+ },
+ };
+ }
+
+ const OriginalFinalizationRegistry = FinalizationRegistry;
+
+ try {
+ if (!gc) {
+ globalThis.FinalizationRegistry = MockFinalizationRegistry;
+ }
+
+ const cache = new Map();
+ const getKey = _serializeArgList(cache);
+
+ using outerScopeObj = makeRegisterableObject();
+
+ const k1 = getKey(outerScopeObj);
+ const k2 = getKey(globalThis);
+ const k3 = getKey("primitive");
+ const k4 = getKey(globalThis, "primitive");
+ const k5 = getKey(globalThis, "primitive", outerScopeObj);
+
+ const persistentKeys = new Set([k1, k2, k3, k4, k5]);
+
+ await (async () => {
+ using obj1 = makeRegisterableObject();
+ using obj2 = makeRegisterableObject();
+
+ const k6 = getKey(obj1);
+ const k7 = getKey(obj2);
+ const k8 = getKey(obj1, obj2);
+ const k9 = getKey(obj1, globalThis);
+ const k10 = getKey(obj1, "primitive");
+ const k11 = getKey(obj1, outerScopeObj);
+
+ const ephemeralKeys = new Set([k6, k7, k8, k9, k10, k11]);
+
+ const keys = new Set([...ephemeralKeys, ...persistentKeys]);
+ for (const [idx, key] of [...keys].entries()) {
+ cache.set(key, idx + 1);
+ }
+
+ gc?.();
+ // wait for gc to run
+ await delay(0);
+ assertEquals(cache.size, keys.size);
+ })();
+
+ gc?.();
+ // wait for gc to run
+ await delay(0);
+ assertEquals(cache.size, persistentKeys.size);
+ } finally {
+ globalThis.FinalizationRegistry = OriginalFinalizationRegistry;
+ }
+});
diff --git a/cache/deno.json b/cache/deno.json
new file mode 100644
index 000000000000..1685e5bfd0ed
--- /dev/null
+++ b/cache/deno.json
@@ -0,0 +1,9 @@
+{
+ "name": "@std/cache",
+ "version": "0.1.0",
+ "exports": {
+ ".": "./mod.ts",
+ "./lru-cache": "./lru_cache.ts",
+ "./memoize": "./memoize.ts"
+ }
+}
diff --git a/cache/lru_cache.ts b/cache/lru_cache.ts
new file mode 100644
index 000000000000..d5e2791aa06e
--- /dev/null
+++ b/cache/lru_cache.ts
@@ -0,0 +1,151 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import type { MemoizationCache } from "./memoize.ts";
+export type { MemoizationCache };
+
+/**
+ * [Least-recently-used](
+ * https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
+ * ) cache.
+ *
+ * Automatically removes entries above the max size based on when they were
+ * last accessed with `get`, `set`, or `has`.
+ *
+ * @typeParam K The type of the cache keys.
+ * @typeParam V The type of the cache values.
+ *
+ * @example Basic usage
+ * ```ts
+ * import { LruCache } from "@std/cache";
+ * import { assert, assertEquals } from "@std/assert";
+ *
+ * const MAX_SIZE = 3;
+ * const cache = new LruCache(MAX_SIZE);
+ *
+ * cache.set("a", 1);
+ * cache.set("b", 2);
+ * cache.set("c", 3);
+ * cache.set("d", 4);
+ *
+ * // most recent values are stored up to `MAX_SIZE`
+ * assertEquals(cache.get("b"), 2);
+ * assertEquals(cache.get("c"), 3);
+ * assertEquals(cache.get("d"), 4);
+ *
+ * // less recent values are removed
+ * assert(!cache.has("a"));
+ * ```
+ */
+export class LruCache extends Map
+ implements MemoizationCache {
+ /**
+ * The maximum number of entries to store in the cache.
+ *
+ * @example Max size
+ * ```ts no-assert
+ * import { LruCache } from "@std/cache";
+ * import { assertEquals } from "@std/assert";
+ *
+ * const cache = new LruCache(100);
+ * assertEquals(cache.maxSize, 100);
+ * ```
+ */
+ maxSize: number;
+
+ /**
+ * Constructs a new `LruCache`.
+ *
+ * @param maxSize The maximum number of entries to store in the cache.
+ */
+ constructor(maxSize: number) {
+ super();
+ this.maxSize = maxSize;
+ }
+
+ #setMostRecentlyUsed(key: K, value: V): void {
+ // delete then re-add to ensure most recently accessed elements are last
+ super.delete(key);
+ super.set(key, value);
+ }
+
+ #pruneToMaxSize(): void {
+ if (this.size > this.maxSize) {
+ this.delete(this.keys().next().value);
+ }
+ }
+
+ /**
+ * Checks whether an element with the specified key exists or not.
+ *
+ * @param key The key to check.
+ * @returns `true` if the cache contains the specified key, otherwise `false`.
+ *
+ * @example Checking for the existence of a key
+ * ```ts
+ * import { LruCache } from "@std/cache";
+ * import { assert } from "@std/assert";
+ *
+ * const cache = new LruCache(100);
+ *
+ * cache.set("a", 1);
+ * assert(cache.has("a"));
+ * ```
+ */
+ override has(key: K): boolean {
+ const exists = super.has(key);
+
+ if (exists) {
+ this.#setMostRecentlyUsed(key, super.get(key)!);
+ }
+
+ return exists;
+ }
+
+ /**
+ * Gets the element with the specified key.
+ *
+ * @param key The key to get the value for.
+ * @returns The value associated with the specified key, or `undefined` if the key is not present in the cache.
+ *
+ * @example Getting a value from the cache
+ * ```ts
+ * import { LruCache } from "@std/cache";
+ * import { assertEquals } from "@std/assert";
+ *
+ * const cache = new LruCache(100);
+ *
+ * cache.set("a", 1);
+ * assertEquals(cache.get("a"), 1);
+ * ```
+ */
+ override get(key: K): V | undefined {
+ if (super.has(key)) {
+ const value = super.get(key)!;
+ this.#setMostRecentlyUsed(key, value);
+ return value;
+ }
+
+ return undefined;
+ }
+
+ /**
+ * Sets the specified key to the specified value.
+ *
+ * @param key The key to set the value for.
+ * @param value The value to set.
+ * @returns `this` for chaining.
+ *
+ * @example Setting a value in the cache
+ * ```ts no-assert
+ * import { LruCache } from "@std/cache";
+ *
+ * const cache = new LruCache(100);
+ * cache.set("a", 1);
+ * ```
+ */
+ override set(key: K, value: V): this {
+ this.#setMostRecentlyUsed(key, value);
+ this.#pruneToMaxSize();
+
+ return this;
+ }
+}
diff --git a/cache/lru_cache_test.ts b/cache/lru_cache_test.ts
new file mode 100644
index 000000000000..ea5908e3efd5
--- /dev/null
+++ b/cache/lru_cache_test.ts
@@ -0,0 +1,24 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { assert, assertEquals } from "@std/assert";
+import { LruCache } from "./lru_cache.ts";
+
+Deno.test("LruCache deletes least-recently-used", () => {
+ const cache = new LruCache(3);
+
+ cache.set(1, "!");
+ cache.set(2, "!");
+ cache.set(1, "updated");
+ cache.set(3, "!");
+ cache.set(4, "!");
+
+ assertEquals(cache.size, 3);
+ assert(!cache.has(2));
+ assertEquals(cache.get(2), undefined);
+ assertEquals([...cache.keys()], [1, 3, 4]);
+ assertEquals(cache.get(3), "!");
+ assertEquals(cache.get(1), "updated");
+
+ cache.delete(3);
+ assertEquals(cache.size, 2);
+ assertEquals(cache.get(3), undefined);
+});
diff --git a/cache/memoize.ts b/cache/memoize.ts
new file mode 100644
index 000000000000..29ca4593ed7e
--- /dev/null
+++ b/cache/memoize.ts
@@ -0,0 +1,144 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore no-unused-vars
+import type { LruCache } from "./lru_cache.ts";
+import { _serializeArgList } from "./_serialize_arg_list.ts";
+
+/**
+ * A cache suitable for use with {@linkcode memoize}.
+ */
+export type MemoizationCache = {
+ has: (key: K) => boolean;
+ get: (key: K) => V | undefined;
+ set: (key: K, val: V) => unknown;
+ delete: (key: K) => unknown;
+};
+
+/**
+ * Options for {@linkcode memoize}.
+ *
+ * @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>,
+> = {
+ /**
+ * 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 a {@linkcode 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, ...args: Parameters) => Key;
+};
+
+/**
+ * Cache the results of a function based on its arguments.
+ *
+ * @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.
+ *
+ * @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))`.
+ * > * Memoization will not cache thrown errors and will eject promises from
+ * > the cache upon rejection. If you want to retain errors or rejected
+ * > promises in the cache, you will need to catch and return them.
+ */
+export function memoize<
+ Fn extends (...args: never[]) => unknown,
+ Key = string,
+ Cache extends MemoizationCache> = Map<
+ Key,
+ ReturnType
+ >,
+>(
+ fn: Fn,
+ options?: MemoizeOptions,
+): Fn {
+ const cache = options?.cache ?? new Map();
+ const getKey = options?.getKey ??
+ _serializeArgList(
+ cache as MemoizationCache,
+ ) as unknown as (
+ (this: ThisParameterType, ...args: Parameters) => Key
+ );
+ const memoized = function (
+ this: ThisParameterType,
+ ...args: Parameters
+ ): ReturnType {
+ const key = getKey.apply(this, args) as Key;
+
+ if (cache.has(key)) {
+ return cache.get(key)!;
+ }
+
+ let val = fn.apply(this, args) as ReturnType;
+
+ if (val instanceof Promise) {
+ val = val.catch((reason) => {
+ cache.delete(key);
+ throw reason;
+ }) as typeof val;
+ }
+
+ cache.set(key, val);
+
+ return val;
+ } as Fn;
+
+ return Object.defineProperties(
+ memoized,
+ {
+ length: { value: fn.length },
+ name: { value: fn.name },
+ },
+ );
+}
diff --git a/cache/memoize_test.ts b/cache/memoize_test.ts
new file mode 100644
index 000000000000..7e162b9c1f90
--- /dev/null
+++ b/cache/memoize_test.ts
@@ -0,0 +1,561 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import {
+ assert,
+ assertAlmostEquals,
+ assertEquals,
+ assertRejects,
+} from "@std/assert";
+import { delay } from "@std/async";
+import { memoize } from "./memoize.ts";
+import { LruCache } from "./lru_cache.ts";
+
+Deno.test(
+ "memoize() memoizes nullary function (lazy/singleton)",
+ async (t) => {
+ await t.step("async function", async () => {
+ let numTimesCalled = 0;
+
+ const db = {
+ connect() {
+ ++numTimesCalled;
+ return Promise.resolve({});
+ },
+ };
+
+ const getConn = memoize(async () => await db.connect());
+ const conn = await getConn();
+ assertEquals(numTimesCalled, 1);
+ const conn2 = await getConn();
+ // equal by reference
+ assert(conn2 === conn);
+ assertEquals(numTimesCalled, 1);
+ });
+
+ await t.step("sync function", async () => {
+ const firstHitDate = memoize(() => new Date());
+
+ const date = firstHitDate();
+
+ await delay(10);
+
+ const date2 = firstHitDate();
+
+ assertEquals(date, date2);
+ });
+ },
+);
+
+Deno.test("memoize() allows simple memoization with primitive arg", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ });
+
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(888), -888);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() is performant for expensive fibonacci function", () => {
+ const fib = memoize((n: bigint): bigint =>
+ n <= 2n ? 1n : fib(n - 1n) + fib(n - 2n)
+ );
+
+ const startTime = Date.now();
+ assertEquals(fib(100n), 354224848179261915075n);
+
+ assertAlmostEquals(Date.now(), startTime, 10);
+});
+
+Deno.test("memoize() allows multiple primitive args", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((a: number, b: number) => {
+ ++numTimesCalled;
+ return a + b;
+ });
+
+ assertEquals(fn(7, 8), 15);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(7, 8), 15);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(7, 9), 16);
+ assertEquals(numTimesCalled, 2);
+ assertEquals(fn(8, 7), 15);
+ assertEquals(numTimesCalled, 3);
+});
+
+Deno.test("memoize() allows ...spread primitive args", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((...ns: number[]) => {
+ ++numTimesCalled;
+ return ns.reduce((total, val) => total + val, 0);
+ });
+
+ assertEquals(fn(), 0);
+ assertEquals(fn(), 0);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(7), 7);
+ assertEquals(fn(7), 7);
+ assertEquals(numTimesCalled, 2);
+ assertEquals(fn(7, 8), 15);
+ assertEquals(fn(7, 8), 15);
+ assertEquals(numTimesCalled, 3);
+ assertEquals(fn(7, 8, 9), 24);
+ assertEquals(fn(7, 8, 9), 24);
+ assertEquals(numTimesCalled, 4);
+});
+
+Deno.test(
+ "memoize() caches unary function by all passed args by default (implicit extra args as array callback)",
+ () => {
+ let numTimesCalled = 0;
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ });
+
+ assertEquals([1, 1, 2, 2].map(fn), [-1, -1, -2, -2]);
+ assertEquals(numTimesCalled, 4);
+ },
+);
+
+Deno.test("memoize() preserves `this` binding`", () => {
+ class X {
+ readonly key = "CONSTANT";
+ timesCalled = 0;
+
+ #method() {
+ return 1;
+ }
+
+ method() {
+ ++this.timesCalled;
+ return this.#method();
+ }
+ }
+
+ const x = new X();
+
+ const method = x.method.bind(x);
+
+ const fn = memoize(method);
+ assertEquals(fn(), 1);
+
+ const fn2 = memoize(x.method).bind(x);
+ assertEquals(fn2(), 1);
+});
+
+// based on https://github.com/lodash/lodash/blob/4.17.15/test/test.js#L14704-L14716
+Deno.test("memoize() uses `this` binding of function for `getKey`", () => {
+ type Obj = { b: number; c: number; memoized: (a: number) => number };
+
+ let numTimesCalled = 0;
+
+ const fn = function (this: Obj, a: number) {
+ ++numTimesCalled;
+ return a + this.b + this.c;
+ };
+ const getKey = function (this: Obj, a: number) {
+ return JSON.stringify([a, this.b, this.c]);
+ };
+
+ const memoized = memoize(fn, { getKey });
+
+ const obj: Obj = { memoized, "b": 2, "c": 3 };
+ assertEquals(obj.memoized(1), 6);
+ assertEquals(numTimesCalled, 1);
+
+ assertEquals(obj.memoized(1), 6);
+ assertEquals(numTimesCalled, 1);
+
+ obj.b = 3;
+ obj.c = 5;
+ assertEquals(obj.memoized(1), 9);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() allows reference arg with default caching", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((sym: symbol) => {
+ ++numTimesCalled;
+ return sym;
+ });
+ const sym1 = Symbol();
+ const sym2 = Symbol();
+
+ fn(sym1);
+ assertEquals(numTimesCalled, 1);
+ fn(sym1);
+ assertEquals(numTimesCalled, 1);
+ fn(sym2);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() allows multiple reference args with default caching", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((obj1: unknown, obj2: unknown) => {
+ ++numTimesCalled;
+ return { obj1, obj2 };
+ });
+ const obj1 = {};
+ const obj2 = {};
+
+ fn(obj1, obj1);
+ assertEquals(numTimesCalled, 1);
+ fn(obj1, obj1);
+ assertEquals(numTimesCalled, 1);
+ fn(obj1, obj2);
+ assertEquals(numTimesCalled, 2);
+ fn(obj2, obj2);
+ assertEquals(numTimesCalled, 3);
+ fn(obj2, obj1);
+ assertEquals(numTimesCalled, 4);
+});
+
+Deno.test("memoize() allows non-primitive arg with `getKey`", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((d: Date) => {
+ ++numTimesCalled;
+ return new Date(0 - d.valueOf());
+ }, { getKey: (n) => n.valueOf() });
+ const date1 = new Date(42);
+ const date2 = new Date(888);
+
+ assertEquals(fn(date1), new Date(-42));
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(date1), new Date(-42));
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(date2), new Date(-888));
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() allows non-primitive arg with `getKey`", () => {
+ 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);
+});
+
+Deno.test(
+ "memoize() allows multiple non-primitive args with `getKey` returning primitive",
+ () => {
+ let numTimesCalled = 0;
+
+ const fn = memoize((...args: { val: number }[]) => {
+ ++numTimesCalled;
+ return args.reduce((total, { val }) => total + val, 0);
+ }, { getKey: (...args) => JSON.stringify(args) });
+
+ assertEquals(fn({ val: 1 }, { val: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ val: 1 }, { val: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ val: 2 }, { val: 1 }), 3);
+ assertEquals(numTimesCalled, 2);
+ },
+);
+
+Deno.test(
+ "memoize() allows multiple non-primitive args with `getKey` returning stringified array of primitives",
+ () => {
+ let numTimesCalled = 0;
+
+ const fn = memoize((...args: { val: number }[]) => {
+ ++numTimesCalled;
+ return args.reduce((total, { val }) => total + val, 0);
+ }, { getKey: (...args) => JSON.stringify(args.map((arg) => arg.val)) });
+
+ assertEquals(fn({ val: 1 }, { val: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ val: 1 }, { val: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ val: 2 }, { val: 1 }), 3);
+ assertEquals(numTimesCalled, 2);
+ },
+);
+
+Deno.test(
+ "memoize() allows multiple non-primitive args of different types, `getKey` returning custom string from props",
+ () => {
+ let numTimesCalled = 0;
+
+ const fn = memoize((one: { one: number }, two: { two: number }) => {
+ ++numTimesCalled;
+ return one.one + two.two;
+ }, { getKey: (one, two) => `${one.one},${two.two}` });
+
+ assertEquals(fn({ one: 1 }, { two: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ one: 1 }, { two: 2 }), 3);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn({ one: 2 }, { two: 1 }), 3);
+ assertEquals(numTimesCalled, 2);
+ },
+);
+
+Deno.test("memoize() allows primitive arg with `getKey`", () => {
+ let numTimesCalled = 0;
+ const fn = memoize((arg: string | number | boolean) => {
+ ++numTimesCalled;
+
+ try {
+ return JSON.parse(String(arg)) as string | number | boolean;
+ } catch {
+ return arg;
+ }
+ }, { getKey: (arg) => String(arg) });
+
+ assertEquals(fn("true"), true);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(true), true);
+ assertEquals(numTimesCalled, 1);
+
+ assertEquals(fn("42"), 42);
+ assertEquals(numTimesCalled, 2);
+ assertEquals(fn(42), 42);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() works with async functions", async () => {
+ // wait time per call of the original (un-memoized) function
+ const DELAY_MS = 100;
+ // max amount of execution time per call of the memoized function
+ const TOLERANCE_MS = 5;
+
+ const startTime = Date.now();
+ const fn = memoize(async (n: number) => {
+ await delay(DELAY_MS);
+ return 0 - n;
+ });
+
+ const nums = [42, 888, 42, 42, 42, 42, 888, 888, 888, 888];
+ const expected = [-42, -888, -42, -42, -42, -42, -888, -888, -888, -888];
+ const results: number[] = [];
+
+ // call in serial to test time elapsed
+ for (const num of nums) {
+ results.push(await fn(num));
+ }
+
+ assertEquals(results, expected);
+
+ const numUnique = new Set(nums).size;
+
+ assertAlmostEquals(
+ Date.now() - startTime,
+ numUnique * DELAY_MS,
+ nums.length * TOLERANCE_MS,
+ );
+});
+
+Deno.test(
+ "memoize() doesn’t cache rejected promises for future function calls",
+ async () => {
+ let rejectNext = true;
+ const fn = memoize(async (n: number) => {
+ await Promise.resolve();
+ const thisCallWillReject = rejectNext;
+ rejectNext = !rejectNext;
+ if (thisCallWillReject) {
+ throw new Error();
+ }
+ return 0 - n;
+ });
+
+ // first call rejects
+ await assertRejects(() => fn(42));
+ // second call succeeds (rejected response is discarded)
+ assertEquals(await fn(42), -42);
+ // subsequent calls also succeed (successful response from cache is used)
+ assertEquals(await fn(42), -42);
+ },
+);
+
+Deno.test(
+ "memoize() causes async functions called in parallel to return the same promise (even if rejected)",
+ async () => {
+ let rejectNext = true;
+ const fn = memoize(async (n: number) => {
+ await Promise.resolve();
+ if (rejectNext) {
+ rejectNext = false;
+ throw new Error(`Rejected ${n}`);
+ }
+ return 0 - n;
+ });
+
+ const promises = [42, 42, 888, 888].map((x) => fn(x));
+
+ const results = await Promise.allSettled(promises);
+
+ assert(promises[1] === promises[0]);
+ assert(results[1]!.status === "rejected");
+ assert(results[1]!.reason.message === "Rejected 42");
+
+ assert(promises[3] === promises[2]);
+ assert(results[3]!.status === "fulfilled");
+ assert(results[3]!.value === -888);
+ },
+);
+
+Deno.test("memoize() allows passing a `Map` as a cache", () => {
+ let numTimesCalled = 0;
+ const cache = new Map();
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ }, { cache });
+
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 1);
+});
+
+Deno.test("memoize() allows passing a custom cache object", () => {
+ let numTimesCalled = 0;
+
+ const uselessCache = {
+ has: () => false,
+ get: () => {
+ throw new Error("`has` is always false, so `get` is never called");
+ },
+ set: () => {},
+ delete: () => {},
+ keys: () => [],
+ };
+
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ }, { cache: uselessCache });
+
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 1);
+ assertEquals(fn(42), -42);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() deletes stale entries of passed `LruCache`", () => {
+ let numTimesCalled = 0;
+
+ const MAX_SIZE = 5;
+
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ }, { cache: new LruCache(MAX_SIZE) });
+
+ assertEquals(fn(0), 0);
+ assertEquals(fn(0), 0);
+ assertEquals(numTimesCalled, 1);
+
+ for (let i = 1; i < MAX_SIZE; ++i) {
+ assertEquals(fn(i), 0 - i);
+ assertEquals(fn(i), 0 - i);
+ assertEquals(numTimesCalled, i + 1);
+ }
+
+ assertEquals(fn(MAX_SIZE), 0 - MAX_SIZE);
+ assertEquals(fn(MAX_SIZE), 0 - MAX_SIZE);
+ assertEquals(numTimesCalled, MAX_SIZE + 1);
+
+ assertEquals(fn(0), 0);
+ assertEquals(fn(0), 0);
+ assertEquals(numTimesCalled, MAX_SIZE + 2);
+});
+
+Deno.test("memoize() only caches single latest result with a `LruCache` of maxSize=1", () => {
+ let numTimesCalled = 0;
+
+ const fn = memoize((n: number) => {
+ ++numTimesCalled;
+ return 0 - n;
+ }, { cache: new LruCache(1) });
+
+ assertEquals(fn(0), 0);
+ assertEquals(fn(0), 0);
+ assertEquals(numTimesCalled, 1);
+
+ assertEquals(fn(1), -1);
+ assertEquals(numTimesCalled, 2);
+});
+
+Deno.test("memoize() preserves function length", () => {
+ assertEquals(memoize.length, 2);
+
+ assertEquals(memoize(() => {}).length, 0);
+ assertEquals(memoize((_arg) => {}).length, 1);
+ assertEquals(memoize((_1, _2) => {}).length, 2);
+ assertEquals(memoize((..._args) => {}).length, 0);
+ assertEquals(memoize((_1, ..._args) => {}).length, 1);
+});
+
+Deno.test("memoize() preserves function name", () => {
+ assertEquals(memoize.name, "memoize");
+
+ const fn1 = () => {};
+ function fn2() {}
+ const obj = { ["!"]: () => {} };
+
+ assertEquals(memoize(() => {}).name, "");
+ assertEquals(memoize(fn1).name, "fn1");
+ assertEquals(memoize(fn1.bind({})).name, "bound fn1");
+ assertEquals(memoize(fn2).name, "fn2");
+ assertEquals(memoize(function fn3() {}).name, "fn3");
+ assertEquals(memoize(obj["!"]).name, "!");
+});
+
+Deno.test("memoize() has correct TS types", async (t) => {
+ await t.step("simple types", () => {
+ // no need to run, only for type checking
+ void (() => {
+ const fn: (this: number, x: number) => number = (_) => 1;
+ const memoized = memoize(fn);
+
+ const _fn2: typeof fn = memoized;
+ const _fn3: Omit = fn;
+
+ const _t1: ThisParameterType = 1;
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ const _t2: ThisParameterType = "1";
+
+ const _a1: Parameters[0] = 1;
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ const _a2: Parameters[0] = "1";
+ // @ts-expect-error Tuple type '[x: number]' of length '1' has no element at index '1'.
+ const _a3: Parameters[1] = {} as never;
+
+ const _r1: ReturnType = 1;
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ const _r2: ReturnType = "1";
+ });
+ });
+
+ await t.step("memoize() correctly preserves generic types", () => {
+ // no need to run, only for type checking
+ void (() => {
+ const fn = (x: T): T => x;
+ const memoized = memoize(fn);
+
+ const _fn2: typeof fn = memoized;
+ const _fn3: Omit = fn;
+
+ const _r1: number = fn(1);
+ const _r2: string = fn("1");
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ const _r3: number = fn("1");
+
+ const _fn4: typeof fn = (n: number) => n;
+ // @ts-expect-error Type 'string' is not assignable to type 'number'.
+ const _fn5: typeof fn = (n: number) => n;
+ });
+ });
+});
diff --git a/cache/mod.ts b/cache/mod.ts
new file mode 100644
index 000000000000..96384df880a4
--- /dev/null
+++ b/cache/mod.ts
@@ -0,0 +1,26 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+// This module is browser compatible.
+
+/**
+ * In-memory cache utilities, such as memoization and caches with different
+ * expiration policies.
+ *
+ * ```ts
+ * import { memoize, LruCache } from "@std/cache";
+ * import { assertEquals } from "@std/assert";
+ *
+ * const cache = new LruCache(1000);
+ *
+ * // 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);
+ * }, { cache });
+ *
+ * assertEquals(fib(100n), 354224848179261915075n);
+ * ```
+ *
+ * @module
+ */
+
+export * from "./memoize.ts";
+export * from "./lru_cache.ts";
diff --git a/deno.json b/deno.json
index bc23b1426272..eab3adc3d114 100644
--- a/deno.json
+++ b/deno.json
@@ -97,6 +97,7 @@
"./assert",
"./async",
"./bytes",
+ "./cache",
"./cli",
"./collections",
"./crypto",