mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
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:
parent
4ec7dd4be9
commit
0c64f32cc3
72
.github/dependency_graph.svg
vendored
72
.github/dependency_graph.svg
vendored
@ -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-\nstructures -->
|
||||
<g id="node12" class="node">
|
||||
<g id="node13" class="node">
|
||||
<title>data-\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-</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-\nmatter -->
|
||||
<g id="node18" class="node">
|
||||
<g id="node19" class="node">
|
||||
<title>front-\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-</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-\ntypes -->
|
||||
<g id="node25" class="node">
|
||||
<g id="node26" class="node">
|
||||
<title>media-\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-</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 |
@ -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"],
|
||||
|
@ -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
87
cache/_serialize_arg_list.ts
vendored
Normal 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
184
cache/_serialize_arg_list_test.ts
vendored
Normal 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
9
cache/deno.json
vendored
Normal 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
151
cache/lru_cache.ts
vendored
Normal 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
24
cache/lru_cache_test.ts
vendored
Normal 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
144
cache/memoize.ts
vendored
Normal 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
561
cache/memoize_test.ts
vendored
Normal 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() 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<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
26
cache/mod.ts
vendored
Normal 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";
|
Loading…
Reference in New Issue
Block a user