feat(cache/unstable): add memoize() and LruCache (#4725)

Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
lionel-rowe 2024-08-09 00:12:24 +08:00 committed by GitHub
parent 4ec7dd4be9
commit 0c64f32cc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1229 additions and 33 deletions

View File

@ -63,32 +63,38 @@
<ellipse fill="lightgreen" stroke="black" cx="929" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="929" y="-139.8" font-family="Times,serif" font-size="14.00">async</text>
</g>
<!-- cli -->
<!-- cache -->
<g id="node7" class="node">
<title>cache</title>
<ellipse fill="none" stroke="black" cx="787" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="787" y="-355.8" font-family="Times,serif" font-size="14.00">cache</text>
</g>
<!-- cli -->
<g id="node8" class="node">
<title>cli</title>
<ellipse fill="lightgreen" stroke="black" cx="569" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="569" y="-139.8" font-family="Times,serif" font-size="14.00">cli</text>
</g>
<!-- collections -->
<g id="node8" class="node">
<g id="node9" class="node">
<title>collections</title>
<ellipse fill="lightgreen" stroke="black" cx="1493" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="1493" y="-139.8" font-family="Times,serif" font-size="14.00">collections</text>
</g>
<!-- crypto -->
<g id="node9" class="node">
<g id="node10" class="node">
<title>crypto</title>
<ellipse fill="lightgreen" stroke="black" cx="1147" cy="-36" rx="36" ry="36"/>
<text text-anchor="middle" x="1147" y="-31.8" font-family="Times,serif" font-size="14.00">crypto</text>
</g>
<!-- csv -->
<g id="node10" class="node">
<g id="node11" class="node">
<title>csv</title>
<ellipse fill="lightgreen" stroke="black" cx="36" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="36" y="-247.8" font-family="Times,serif" font-size="14.00">csv</text>
</g>
<!-- streams -->
<g id="node11" class="node">
<g id="node12" class="node">
<title>streams</title>
<ellipse fill="lightgreen" stroke="black" cx="81" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="81" y="-139.8" font-family="Times,serif" font-size="14.00">streams</text>
@ -106,32 +112,32 @@
<polygon fill="black" stroke="black" points="414.31,-43.72 423.97,-39.36 413.7,-36.74 414.31,-43.72"/>
</g>
<!-- data&#45;\nstructures -->
<g id="node12" class="node">
<g id="node13" class="node">
<title>data&#45;\nstructures</title>
<ellipse fill="lightgreen" stroke="black" cx="1275" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="1275" y="-148.2" font-family="Times,serif" font-size="14.00">data&#45;</text>
<text text-anchor="middle" x="1275" y="-131.4" font-family="Times,serif" font-size="14.00">structures</text>
</g>
<!-- datetime -->
<g id="node13" class="node">
<g id="node14" class="node">
<title>datetime</title>
<ellipse fill="none" stroke="black" cx="1628" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1628" y="-355.8" font-family="Times,serif" font-size="14.00">datetime</text>
</g>
<!-- dotenv -->
<g id="node14" class="node">
<g id="node15" class="node">
<title>dotenv</title>
<ellipse fill="none" stroke="black" cx="1718" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1718" y="-355.8" font-family="Times,serif" font-size="14.00">dotenv</text>
</g>
<!-- encoding -->
<g id="node15" class="node">
<g id="node16" class="node">
<title>encoding</title>
<ellipse fill="lightgreen" stroke="black" cx="261" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="261" y="-139.8" font-family="Times,serif" font-size="14.00">encoding</text>
</g>
<!-- expect -->
<g id="node16" class="node">
<g id="node17" class="node">
<title>expect</title>
<ellipse fill="lightgreen" stroke="black" cx="1384" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="1384" y="-247.8" font-family="Times,serif" font-size="14.00">expect</text>
@ -149,20 +155,20 @@
<polygon fill="black" stroke="black" points="1395.87,-70.54 1387.37,-64.22 1390.08,-74.47 1395.87,-70.54"/>
</g>
<!-- fmt -->
<g id="node17" class="node">
<g id="node18" class="node">
<title>fmt</title>
<ellipse fill="lightgreen" stroke="black" cx="659" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="659" y="-139.8" font-family="Times,serif" font-size="14.00">fmt</text>
</g>
<!-- front&#45;\nmatter -->
<g id="node18" class="node">
<g id="node19" class="node">
<title>front&#45;\nmatter</title>
<ellipse fill="lightgreen" stroke="black" cx="1538" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1538" y="-364.2" font-family="Times,serif" font-size="14.00">front&#45;</text>
<text text-anchor="middle" x="1538" y="-347.4" font-family="Times,serif" font-size="14.00">matter</text>
</g>
<!-- toml -->
<g id="node19" class="node">
<g id="node20" class="node">
<title>toml</title>
<ellipse fill="lightgreen" stroke="black" cx="1493" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="1493" y="-247.8" font-family="Times,serif" font-size="14.00">toml</text>
@ -174,7 +180,7 @@
<polygon fill="black" stroke="black" points="1513.95,-293.38 1506.82,-285.55 1507.5,-296.12 1513.95,-293.38"/>
</g>
<!-- yaml -->
<g id="node20" class="node">
<g id="node21" class="node">
<title>yaml</title>
<ellipse fill="lightgreen" stroke="black" cx="1583" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="1583" y="-247.8" font-family="Times,serif" font-size="14.00">yaml</text>
@ -192,13 +198,13 @@
<polygon fill="black" stroke="black" points="1496.5,-190.33 1493,-180.33 1489.5,-190.33 1496.5,-190.33"/>
</g>
<!-- fs -->
<g id="node21" class="node">
<g id="node22" class="node">
<title>fs</title>
<ellipse fill="lightgreen" stroke="black" cx="839" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="839" y="-139.8" font-family="Times,serif" font-size="14.00">fs</text>
</g>
<!-- path -->
<g id="node22" class="node">
<g id="node23" class="node">
<title>path</title>
<ellipse fill="lightgreen" stroke="black" cx="929" cy="-36" rx="36" ry="36"/>
<text text-anchor="middle" x="929" y="-31.8" font-family="Times,serif" font-size="14.00">path</text>
@ -210,13 +216,13 @@
<polygon fill="black" stroke="black" points="902.29,-73.86 906.1,-63.97 896.96,-69.33 902.29,-73.86"/>
</g>
<!-- html -->
<g id="node23" class="node">
<g id="node24" class="node">
<title>html</title>
<ellipse fill="lightgreen" stroke="black" cx="1808" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1808" y="-355.8" font-family="Times,serif" font-size="14.00">html</text>
</g>
<!-- http -->
<g id="node24" class="node">
<g id="node25" class="node">
<title>http</title>
<ellipse fill="lightgreen" stroke="black" cx="389" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="389" y="-247.8" font-family="Times,serif" font-size="14.00">http</text>
@ -252,7 +258,7 @@
<polygon fill="black" stroke="black" points="883.17,-44.42 892.83,-40.07 882.56,-37.44 883.17,-44.42"/>
</g>
<!-- media&#45;\ntypes -->
<g id="node25" class="node">
<g id="node26" class="node">
<title>media&#45;\ntypes</title>
<ellipse fill="lightgreen" stroke="black" cx="389" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="389" y="-148.2" font-family="Times,serif" font-size="14.00">media&#45;</text>
@ -265,7 +271,7 @@
<polygon fill="black" stroke="black" points="392.5,-190.33 389,-180.33 385.5,-190.33 392.5,-190.33"/>
</g>
<!-- net -->
<g id="node26" class="node">
<g id="node27" class="node">
<title>net</title>
<ellipse fill="lightgreen" stroke="black" cx="479" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="479" y="-139.8" font-family="Times,serif" font-size="14.00">net</text>
@ -277,13 +283,13 @@
<polygon fill="black" stroke="black" points="452.29,-181.86 456.1,-171.97 446.96,-177.33 452.29,-181.86"/>
</g>
<!-- ini -->
<g id="node27" class="node">
<g id="node28" class="node">
<title>ini</title>
<ellipse fill="lightgreen" stroke="black" cx="1898" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1898" y="-355.8" font-family="Times,serif" font-size="14.00">ini</text>
</g>
<!-- json -->
<g id="node28" class="node">
<g id="node29" class="node">
<title>json</title>
<ellipse fill="lightgreen" stroke="black" cx="126" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="126" y="-247.8" font-family="Times,serif" font-size="14.00">json</text>
@ -295,7 +301,7 @@
<polygon fill="black" stroke="black" points="101.95,-185.38 94.82,-177.55 95.5,-188.12 101.95,-185.38"/>
</g>
<!-- jsonc -->
<g id="node29" class="node">
<g id="node30" class="node">
<title>jsonc</title>
<ellipse fill="lightgreen" stroke="black" cx="126" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="126" y="-355.8" font-family="Times,serif" font-size="14.00">jsonc</text>
@ -307,7 +313,7 @@
<polygon fill="black" stroke="black" points="129.5,-298.33 126,-288.33 122.5,-298.33 129.5,-298.33"/>
</g>
<!-- log -->
<g id="node30" class="node">
<g id="node31" class="node">
<title>log</title>
<ellipse fill="none" stroke="black" cx="794" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="794" y="-247.8" font-family="Times,serif" font-size="14.00">log</text>
@ -331,7 +337,7 @@
<polygon fill="black" stroke="black" points="824.5,-188.12 825.18,-177.55 818.05,-185.38 824.5,-188.12"/>
</g>
<!-- msgpack -->
<g id="node31" class="node">
<g id="node32" class="node">
<title>msgpack</title>
<ellipse fill="lightgreen" stroke="black" cx="171" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="171" y="-139.8" font-family="Times,serif" font-size="14.00">msgpack</text>
@ -343,19 +349,19 @@
<polygon fill="black" stroke="black" points="415.34,-48.29 424.5,-42.97 414.02,-41.42 415.34,-48.29"/>
</g>
<!-- regexp -->
<g id="node32" class="node">
<g id="node33" class="node">
<title>regexp</title>
<ellipse fill="lightgreen" stroke="black" cx="1988" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="1988" y="-355.8" font-family="Times,serif" font-size="14.00">regexp</text>
</g>
<!-- semver -->
<g id="node33" class="node">
<g id="node34" class="node">
<title>semver</title>
<ellipse fill="lightgreen" stroke="black" cx="2078" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="2078" y="-355.8" font-family="Times,serif" font-size="14.00">semver</text>
</g>
<!-- testing -->
<g id="node34" class="node">
<g id="node35" class="node">
<title>testing</title>
<ellipse fill="lightgreen" stroke="black" cx="1147" cy="-252" rx="36" ry="36"/>
<text text-anchor="middle" x="1147" y="-247.8" font-family="Times,serif" font-size="14.00">testing</text>
@ -397,19 +403,19 @@
<polygon fill="black" stroke="black" points="973.64,-48.73 963.05,-48.33 971.08,-55.24 973.64,-48.73"/>
</g>
<!-- text -->
<g id="node35" class="node">
<g id="node36" class="node">
<title>text</title>
<ellipse fill="lightgreen" stroke="black" cx="2168" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="2168" y="-355.8" font-family="Times,serif" font-size="14.00">text</text>
</g>
<!-- ulid -->
<g id="node36" class="node">
<g id="node37" class="node">
<title>ulid</title>
<ellipse fill="lightgreen" stroke="black" cx="2258" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="2258" y="-355.8" font-family="Times,serif" font-size="14.00">ulid</text>
</g>
<!-- url -->
<g id="node37" class="node">
<g id="node38" class="node">
<title>url</title>
<ellipse fill="none" stroke="black" cx="1019" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="1019" y="-139.8" font-family="Times,serif" font-size="14.00">url</text>
@ -421,7 +427,7 @@
<polygon fill="black" stroke="black" points="961.04,-69.33 951.9,-63.97 955.71,-73.86 961.04,-69.33"/>
</g>
<!-- uuid -->
<g id="node38" class="node">
<g id="node39" class="node">
<title>uuid</title>
<ellipse fill="lightgreen" stroke="black" cx="1147" cy="-144" rx="36" ry="36"/>
<text text-anchor="middle" x="1147" y="-139.8" font-family="Times,serif" font-size="14.00">uuid</text>
@ -439,7 +445,7 @@
<polygon fill="black" stroke="black" points="1150.5,-82.33 1147,-72.33 1143.5,-82.33 1150.5,-82.33"/>
</g>
<!-- webgpu -->
<g id="node39" class="node">
<g id="node40" class="node">
<title>webgpu</title>
<ellipse fill="none" stroke="black" cx="2348" cy="-360" rx="36" ry="36"/>
<text text-anchor="middle" x="2348" y="-355.8" font-family="Times,serif" font-size="14.00">webgpu</text>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -39,6 +39,7 @@ type Mod =
| "assert"
| "async"
| "bytes"
| "cache"
| "cli"
| "collections"
| "crypto"
@ -80,6 +81,7 @@ const ENTRYPOINTS: Record<Mod, string[]> = {
assert: ["mod.ts"],
async: ["mod.ts"],
bytes: ["mod.ts"],
cache: ["mod.ts"],
cli: ["mod.ts"],
collections: ["mod.ts"],
crypto: ["mod.ts"],

View File

@ -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",

87
cache/_serialize_arg_list.ts vendored Normal file
View File

@ -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<Return>(
cache: MemoizationCache<unknown, Return>,
): (this: unknown, ...args: unknown[]) => string {
const weakKeyToKeySegmentCache = new WeakMap<WeakKey, string>();
const weakKeySegmentToKeyCache = new Map<string, string[]>();
let i = 0;
const registry = new FinalizationRegistry<string>((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);
}

184
cache/_serialize_arg_list_test.ts vendored Normal file
View File

@ -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<T> extends FinalizationRegistry<T> {
#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;
}
});

9
cache/deno.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"name": "@std/cache",
"version": "0.1.0",
"exports": {
".": "./mod.ts",
"./lru-cache": "./lru_cache.ts",
"./memoize": "./memoize.ts"
}
}

151
cache/lru_cache.ts vendored Normal file
View File

@ -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<string, number>(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<K, V> extends Map<K, V>
implements MemoizationCache<K, V> {
/**
* 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<string, number>(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<string, number>(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<string, number>(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<string, number>(100);
* cache.set("a", 1);
* ```
*/
override set(key: K, value: V): this {
this.#setMostRecentlyUsed(key, value);
this.#pruneToMaxSize();
return this;
}
}

24
cache/lru_cache_test.ts vendored Normal file
View File

@ -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);
});

144
cache/memoize.ts vendored Normal file
View File

@ -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<K, V> = {
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<Key, 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 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<Fn>, ...args: Parameters<Fn>) => 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<Key, ReturnType<Fn>> = Map<
Key,
ReturnType<Fn>
>,
>(
fn: Fn,
options?: MemoizeOptions<Fn, Key, Cache>,
): Fn {
const cache = options?.cache ?? new Map();
const getKey = options?.getKey ??
_serializeArgList(
cache as MemoizationCache<unknown, unknown>,
) as unknown as (
(this: ThisParameterType<Fn>, ...args: Parameters<Fn>) => Key
);
const memoized = function (
this: ThisParameterType<Fn>,
...args: Parameters<Fn>
): ReturnType<Fn> {
const key = getKey.apply(this, args) as Key;
if (cache.has(key)) {
return cache.get(key)!;
}
let val = fn.apply(this, args) as ReturnType<Fn>;
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 },
},
);
}

561
cache/memoize_test.ts vendored Normal file
View File

@ -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() doesnt 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<string, number>(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<string, number>(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<typeof memoized, "cache" | "getKey"> = fn;
const _t1: ThisParameterType<typeof fn> = 1;
// @ts-expect-error Type 'string' is not assignable to type 'number'.
const _t2: ThisParameterType<typeof fn> = "1";
const _a1: Parameters<typeof fn>[0] = 1;
// @ts-expect-error Type 'string' is not assignable to type 'number'.
const _a2: Parameters<typeof fn>[0] = "1";
// @ts-expect-error Tuple type '[x: number]' of length '1' has no element at index '1'.
const _a3: Parameters<typeof fn>[1] = {} as never;
const _r1: ReturnType<typeof fn> = 1;
// @ts-expect-error Type 'string' is not assignable to type 'number'.
const _r2: ReturnType<typeof fn> = "1";
});
});
await t.step("memoize() correctly preserves generic types", () => {
// no need to run, only for type checking
void (() => {
const fn = <T>(x: T): T => x;
const memoized = memoize(fn);
const _fn2: typeof fn = memoized;
const _fn3: Omit<typeof memoized, "cache" | "getKey"> = 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<number> = (n: number) => n;
// @ts-expect-error Type 'string' is not assignable to type 'number'.
const _fn5: typeof fn<string> = (n: number) => n;
});
});
});

26
cache/mod.ts vendored Normal file
View File

@ -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<unknown, bigint>(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";

View File

@ -97,6 +97,7 @@
"./assert",
"./async",
"./bytes",
"./cache",
"./cli",
"./collections",
"./crypto",