From a4dc1aeccd8ccb9b5f51b070cc3124b1834ca9d2 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 14:18:40 +0100 Subject: [PATCH 01/11] Add more basic cache tests --- packages/alfa-cache/test/cache.spec.ts | 43 ++++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/alfa-cache/test/cache.spec.ts b/packages/alfa-cache/test/cache.spec.ts index 868823c139..d31333c588 100644 --- a/packages/alfa-cache/test/cache.spec.ts +++ b/packages/alfa-cache/test/cache.spec.ts @@ -3,19 +3,44 @@ import { test } from "@siteimprove/alfa-test"; import { None, Some } from "@siteimprove/alfa-option"; import { Cache } from "../dist/cache.js"; -const foo = {}; -const bar = {}; -const baz = {}; +type Foo = { x: number }; +type Bar = { y: string }; +const zero: Foo = { x: 0 }; +const one: Foo = { x: 1 }; +const two: Foo = { x: 2 }; + +const a: Bar = { y: "a" }; +const b: Bar = { y: "bbbbbbbbbb" }; + +// This effectively does a basic test of Cache.merge const cache = Cache.from([ - [foo, 1], - [bar, 2], + [zero, 0], + [one, 1], ]); -test("get() returns some when getting a value that does exist", (t) => { - t.deepEqual(cache.get(foo), Some.of(1)); +test("has()/get() returns some when getting a value that does exist", (t) => { + t(cache.has(zero)); + t.deepEqual(cache.get(zero), Some.of(0)); +}); + +test("has()/get() returns none when getting a value that does not exist", (t) => { + // We do not want to use `two` here to avoid race condition if tests are + // run asynchronously. + t(!cache.has({ x: 2 })); + t.equal(cache.get({ x: 2 }), None); }); -test("get() returns none when getting a value that does not exist", (t) => { - t.equal(cache.get(baz), None); +test("set() adds a value to a cache", (t) => { + cache.set(two, 2); + t(cache.has(two)); + t.deepEqual(cache.get(two), Some.of(2)); +}); + +test("get() adds a value to a cache when ifMissing is provided", (t) => { + const three: Foo = { x: 3 }; + const value = cache.get(three, () => 3); + + t(cache.has(three)); + t.equal(value, 3); }); From 98572d1cd4eec8c553bcada0702fb6cf9b0d0d72 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 14:20:25 +0100 Subject: [PATCH 02/11] Add explicit Cache.Key type --- packages/alfa-cache/src/cache.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/alfa-cache/src/cache.ts b/packages/alfa-cache/src/cache.ts index 0f09037902..65c5568a90 100644 --- a/packages/alfa-cache/src/cache.ts +++ b/packages/alfa-cache/src/cache.ts @@ -5,8 +5,8 @@ import type { Mapper } from "@siteimprove/alfa-mapper"; /** * @public */ -export class Cache { - public static empty(): Cache { +export class Cache { + public static empty(): Cache { return new Cache(); } @@ -65,7 +65,9 @@ export class Cache { * @public */ export namespace Cache { - export function from( + export type Key = object; + + export function from( iterable: Iterable, ): Cache { return Cache.empty().merge(iterable); From 83c6d94f41304c4abd63e4e9a4947160c1983827 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 14:37:35 +0100 Subject: [PATCH 03/11] Add memoize decorator --- packages/alfa-cache/src/cache.ts | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/alfa-cache/src/cache.ts b/packages/alfa-cache/src/cache.ts index 65c5568a90..3a416a6c90 100644 --- a/packages/alfa-cache/src/cache.ts +++ b/packages/alfa-cache/src/cache.ts @@ -72,4 +72,40 @@ export namespace Cache { ): Cache { return Cache.empty().merge(iterable); } + + type ToCache, T> = Args extends [ + infer Head extends object, + ...infer Tail extends Array, + ] + ? Cache> + : T; + + export function memoize, Return>( + target: (this: This, ...args: Args) => Return, + ): (this: This, ...args: Args) => Return { + const cache = Cache.empty() as ToCache; + + return function (this: This, ...args: Args) { + const that = this; + function memoized>( + cache: ToCache, + ...inner: A + ): Return { + if (inner.length === 0) { + return cache as Return; + } + + const [head, ...tail] = inner; + // @ts-ignore + const next = cache.get( + head, + // @ts-ignore + tail.length === 0 ? () => target.bind(that)(...args) : Cache.empty, + ); + return memoized(next, ...tail); + } + + return memoized(cache, ...args); + }; + } } From c2b7761ff197bde79d0bea9eda0cd7abdc3afef0 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 14:37:52 +0100 Subject: [PATCH 04/11] Add memoize tests (as function) --- packages/alfa-cache/test/cache.spec.ts | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/packages/alfa-cache/test/cache.spec.ts b/packages/alfa-cache/test/cache.spec.ts index d31333c588..612203586a 100644 --- a/packages/alfa-cache/test/cache.spec.ts +++ b/packages/alfa-cache/test/cache.spec.ts @@ -44,3 +44,86 @@ test("get() adds a value to a cache when ifMissing is provided", (t) => { t(cache.has(three)); t.equal(value, 3); }); + +test("memoize caches values of a unary function", (t) => { + // We also test the return values of `doStuff` to ensure that we didn't retrieve + // the wrong cache entry. + let called = 0; + + function doStuff(foo: Foo): number { + called++; + return foo.x; + } + + // Not memoized, `called` is incremented each time + t.equal(doStuff(zero), 0); + t.equal(called, 1); + + t.equal(doStuff(zero), 0); + t.equal(called, 2); + + t.equal(doStuff(one), 1); + t.equal(called, 3); + + const memoized = Cache.memoize(doStuff); + + // Memoized, `called` is incremented only in case of cache miss. + t.equal(memoized(zero), 0); // Initial call, miss + t.equal(called, 4); + + t.equal(memoized(zero), 0); // hit + t.equal(called, 4); + + t.equal(memoized(one), 1); // different argument, miss + t.equal(called, 5); + + t.equal(memoized(one), 1); // hit + t.equal(called, 5); + + t.equal(memoized(zero), 0); // still a hit + t.equal(called, 5); +}); + +test("memoize caches values of a binary function", (t) => { + let called = 0; + + function doStuff(foo: Foo, bar: Bar): number { + called++; + + return foo.x + bar.y.length; + } + + // Not memoized, `called` is incremented each time + t.equal(doStuff(zero, a), 1); + t.equal(called, 1); + + t.equal(doStuff(one, a), 2); + t.equal(called, 2); + + t.equal(doStuff(zero, a), 1); + t.equal(called, 3); + + const memoize = Cache.memoize(doStuff); + + // Memoized, `called` is incremented only in case of cache miss. + t.equal(memoize(zero, a), 1); // Initial call, miss + t.equal(called, 4); + + t.equal(memoize(one, a), 2); // different foo, miss + t.equal(called, 5); + + t.equal(memoize(zero, a), 1); // hit (same as 1st) + t.equal(called, 5); + + t.equal(memoize(one, a), 2); // hit (same as 2nd) + t.equal(called, 5); + + t.equal(memoize(zero, b), 10); // different bar, miss + t.equal(called, 6); + + t.equal(memoize(zero, b), 10); // hit (same as 5th) + t.equal(called, 6); + + t.equal(memoize(one, b), 11); // different pair, miss + t.equal(called, 7); +}); From a456962ef69f2bc8ccc53b6dee8d8b1a66d433f5 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 14:45:55 +0100 Subject: [PATCH 05/11] Add memoize tests (as decorator) --- packages/alfa-cache/test/cache.spec.ts | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/packages/alfa-cache/test/cache.spec.ts b/packages/alfa-cache/test/cache.spec.ts index 612203586a..64f87d3bea 100644 --- a/packages/alfa-cache/test/cache.spec.ts +++ b/packages/alfa-cache/test/cache.spec.ts @@ -127,3 +127,63 @@ test("memoize caches values of a binary function", (t) => { t.equal(memoize(one, b), 11); // different pair, miss t.equal(called, 7); }); + +test("@memoize caches values of a binary method", (t) => { + // Here also, we test the return values of `doStuffA` / `doStuffB` to ensure + // that we didn't retrieve the wrong cache entry. + + class MyClass { + public called: number; + + public constructor() { + this.called = 0; + } + + public doStuffA(foo: Foo, bar: Bar): number { + this.called++; + + return foo.x + bar.y.length; + } + + @Cache.memoize + public doStuffB(foo: Foo, bar: Bar): number { + this.called++; + + return foo.x + bar.y.length; + } + } + + const instance = new MyClass(); + + // doStuffA is not cached, `called` is incremented each time + t.equal(instance.doStuffA(zero, a), 1); + t.equal(instance.called, 1); + + t.equal(instance.doStuffA(one, a), 2); + t.equal(instance.called, 2); + + t.equal(instance.doStuffA(zero, a), 1); + t.equal(instance.called, 3); + + // doStuffB is cached, `called` is incremented only in case of cache miss + t.equal(instance.doStuffB(zero, a), 1); // Initial call, miss + t.equal(instance.called, 4); + + t.equal(instance.doStuffB(one, a), 2); // different foo, miss + t.equal(instance.called, 5); + + t.equal(instance.doStuffB(zero, a), 1); // hit (same as 1st) + t.equal(instance.called, 5); + + t.equal(instance.doStuffB(one, a), 2); // hit (same as 2nd) + t.equal(instance.called, 5); + + t.equal(instance.doStuffB(zero, b), 10); // different bar, miss + t.equal(instance.called, 6); + + t.equal(instance.doStuffB(zero, b), 10); // hit (same as 5th) + t.equal(instance.called, 6); + + t.equal(instance.doStuffB(one, b), 11); // different pair, miss + t.equal(instance.called, 7); +}); From a67d52de69cbb53fd31cb3f3b83b2f3a74d40d90 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 15:12:51 +0100 Subject: [PATCH 06/11] Add documentation --- packages/alfa-cache/src/cache.ts | 96 +++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/packages/alfa-cache/src/cache.ts b/packages/alfa-cache/src/cache.ts index 3a416a6c90..80db989bf2 100644 --- a/packages/alfa-cache/src/cache.ts +++ b/packages/alfa-cache/src/cache.ts @@ -3,9 +3,31 @@ import { Option, None } from "@siteimprove/alfa-option"; import type { Mapper } from "@siteimprove/alfa-mapper"; /** + * Caches are wrapper around Javascript's `WeakMap` to store transient values. + * + * @remarks + * Caches are mutable! To preserve referential transparency, the preferred way + * of using caches is to store them as a local variable (never send them as + * parameters); and to use a single `cache.get(key, () => …)` call to retrieve + * values from it. Ideally, use `Cache.memoize()` to create a memoized function. + * + * Since Caches are built on WeakMap, the keys must be objects. + * + * Since Caches are built on WeakMap, they do not prevent the garbage collection + * of keys, and the associated value is then freed too. This avoids memory leaks, + * and ensure a lightweight caching mechanism for objects that stay in memory for + * some time. + * + * Typical use of Caches is to store indirect values related to a DOM tree (e.g., + * the associated ARIA tree, …) Once the audit is done and the DOM tree is + * discarded, the cache is automatically freed. + * * @public */ export class Cache { + /** + * Creates an empty cache. + */ public static empty(): Cache { return new Cache(); } @@ -14,8 +36,15 @@ export class Cache { private constructor() {} + /** + * Returns the value (if it exists) associated with the given key. + */ public get(key: K): Option; + /** + * Returns the value associated with the given key; if it does not exist, + * evaluates `ifMissing`, store the result in the cache and returns it. + */ public get(key: K, ifMissing: Mapper): V; public get( @@ -43,15 +72,27 @@ export class Cache { return value; } + /** + * Tests whether a given key exists in the cache. + */ public has(key: K): boolean { return this._storage.has(key); } + /** + * Adds a key-value pair to a cache. + * + * @remarks + * Avoid using this. Prefer using the `ifMissing` parameter of `get()` instead. + */ public set(key: K, value: V): this { this._storage.set(key, value); return this; } + /** + * Merges a cache with an iterable of key-value pairs. + */ public merge(iterable: Iterable): this { return Iterable.reduce( iterable, @@ -65,14 +106,23 @@ export class Cache { * @public */ export namespace Cache { + /** + * Allowed keys in a Cache. + */ export type Key = object; + /** + * Create a new cache from an iterable of key-value pairs. + */ export function from( iterable: Iterable, ): Cache { return Cache.empty().merge(iterable); } + /** + * Turns `<[A, B, C], T>` into `Cache>>`. + */ type ToCache, T> = Args extends [ infer Head extends object, ...infer Tail extends Array, @@ -80,28 +130,68 @@ export namespace Cache { ? Cache> : T; + /** + * Memoize a function or method. + */ export function memoize, Return>( + // When called on an instance's method `target`, `this` is the instance. target: (this: This, ...args: Args) => Return, ): (this: This, ...args: Args) => Return { + // First, we create the cache. const cache = Cache.empty() as ToCache; + // Next, we create the memoized function. Since the cache is scoped to the + // decorator, it cannot be accessed from outside and won't be tampered with. return function (this: This, ...args: Args) { + // Here, `this` is still the instance on which the (new) method is added. + // We need to save it for later. const that = this; + + // We create a recursive memoized function that will traverse the cache, + // parameter by parameter. It needs to be passed a (partial) cache + // together with the remaining parameters. + // This is OK since the side-effect happens only to the previously defined + // scoped cache. function memoized>( cache: ToCache, - ...inner: A + ...innerArgs: A ): Return { - if (inner.length === 0) { + // From now on, `this` is the `memoized` function itself, hence the need + // for an earlier copy. + + if (innerArgs.length === 0) { + // We have reached the end of the parameters, always hitting the cache, + // thus `ToCache` is `Return`, and `cache` is the actual + // return value that was `.get()` in the previous call. + + // Typescript is completely lost here. It cannot make the connection + // between `innerArgs` being of length 0, and `A` being `[]`; thus is + // unable to correctly infer that `ToCache` is `Return`. return cache as Return; } - const [head, ...tail] = inner; + // There are still parameters to handle, deconstruct them. + const [head, ...tail] = innerArgs; + + // On that bit, TS is so lost that we just disable it… // @ts-ignore + + // Compute the next cache to use, by retrieving the values associated + // with `head`. This will be either the final value (if `head` is the las + // parameter), or a further cache. const next = cache.get( head, // @ts-ignore + // If there are no more parameters, we need to call the original function. + // In case of method, we need to re-bind it to the original instance. + // (we could directly return the result in that case, but since we need + // to test `innerArgs.length === 0` anyway, we let that handle it) + // + // If there are more parameters, we just create an empty cache. tail.length === 0 ? () => target.bind(that)(...args) : Cache.empty, ); + + // Recurse with the next cache and the remaining parameters. return memoized(next, ...tail); } From 839da9748b362e5d4d06033475c90870df519b2a Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 15:13:19 +0100 Subject: [PATCH 07/11] Extract API --- docs/review/api/alfa-cache.api.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/review/api/alfa-cache.api.md b/docs/review/api/alfa-cache.api.md index 258319f149..fbfbe75e5b 100644 --- a/docs/review/api/alfa-cache.api.md +++ b/docs/review/api/alfa-cache.api.md @@ -8,26 +8,21 @@ import { Iterable as Iterable_2 } from '@siteimprove/alfa-iterable'; import type { Mapper } from '@siteimprove/alfa-mapper'; import { Option } from '@siteimprove/alfa-option'; -// @public (undocumented) -export class Cache { - // (undocumented) - static empty(): Cache; - // (undocumented) +// @public +export class Cache { + static empty(): Cache; get(key: K): Option; - // (undocumented) get(key: K, ifMissing: Mapper): V; - // (undocumented) has(key: K): boolean; - // (undocumented) merge(iterable: Iterable_2): this; - // (undocumented) set(key: K, value: V): this; } // @public (undocumented) export namespace Cache { - // (undocumented) - export function from(iterable: Iterable_2): Cache; + export function from(iterable: Iterable_2): Cache; + export type Key = object; + export function memoize, Return>(target: (this: This, ...args: Args) => Return): (this: This, ...args: Args) => Return; } // (No @packageDocumentation comment for this package) From b83811c4cd6617dd46f48c9461ac9078b46c50f0 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 15:18:48 +0100 Subject: [PATCH 08/11] Add changeset --- .changeset/large-moons-nail.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/large-moons-nail.md diff --git a/.changeset/large-moons-nail.md b/.changeset/large-moons-nail.md new file mode 100644 index 0000000000..9e6390c0de --- /dev/null +++ b/.changeset/large-moons-nail.md @@ -0,0 +1,7 @@ +--- +"@siteimprove/alfa-cache": minor +--- + +**Added:** A `Cache.memoize` decorator is now available. + +It can decorate methods, or wrap functions, whose parameters are all objects. It will automatically create a cache with the various parameters and correctly call it. From a9a999f02923d65e36a85c9b4c9ed148b748c142 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 15:41:21 +0100 Subject: [PATCH 09/11] Make one example of using Cache.memoize on a function --- .../predicate/is-programmatically-hidden.ts | 59 ++++++++----------- packages/alfa-cache/src/cache.ts | 10 ++-- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts b/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts index d184d373dc..ad5e5e2eaf 100644 --- a/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts +++ b/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts @@ -7,7 +7,7 @@ import { Context } from "@siteimprove/alfa-selector"; import { Style } from "@siteimprove/alfa-style"; const { hasAttribute, isElement } = Element; -const { or, test, equals } = Predicate; +const { or, equals } = Predicate; const { and } = Refinement; const { hasComputedStyle } = Style; @@ -34,39 +34,32 @@ export function isProgrammaticallyHidden( ); } -const cache = Cache.empty>>(); - -function hasHiddenAncestors( +function _hasHiddenAncestors( device: Device, - context: Context = Context.empty(), + context: Context, ): Predicate { - return (node) => - cache - .get(device, Cache.empty) - .get(context, Cache.empty) - .get(node, () => - test( - or( - // Either it is a programmatically hidden element - and( - isElement, - or( - hasComputedStyle( - "display", - ({ values: [outside] }) => outside.value === "none", - device, - context, - ), - hasAttribute("aria-hidden", equals("true")), - ), - ), - // Or its parent is programmatically hidden - (node: Node) => - node - .parent(Node.fullTree) - .some(hasHiddenAncestors(device, context)), - ), - node, + return or( + // Either it is a programmatically hidden element + and( + isElement, + or( + hasComputedStyle( + "display", + ({ values: [outside] }) => outside.value === "none", + device, + context, ), - ); + hasAttribute("aria-hidden", equals("true")), + ), + ), + // Or its parent is programmatically hidden + (node: Node) => + node.parent(Node.fullTree).some(_hasHiddenAncestors(device, context)), + ); } + +const hasHiddenAncestors = Cache.memoize< + unknown, + [Device, Context], + Predicate +>(_hasHiddenAncestors); diff --git a/packages/alfa-cache/src/cache.ts b/packages/alfa-cache/src/cache.ts index 80db989bf2..2e7ef68450 100644 --- a/packages/alfa-cache/src/cache.ts +++ b/packages/alfa-cache/src/cache.ts @@ -123,9 +123,9 @@ export namespace Cache { /** * Turns `<[A, B, C], T>` into `Cache>>`. */ - type ToCache, T> = Args extends [ - infer Head extends object, - ...infer Tail extends Array, + type ToCache, T> = Args extends [ + infer Head extends Key, + ...infer Tail extends Array, ] ? Cache> : T; @@ -133,7 +133,7 @@ export namespace Cache { /** * Memoize a function or method. */ - export function memoize, Return>( + export function memoize, Return>( // When called on an instance's method `target`, `this` is the instance. target: (this: This, ...args: Args) => Return, ): (this: This, ...args: Args) => Return { @@ -152,7 +152,7 @@ export namespace Cache { // together with the remaining parameters. // This is OK since the side-effect happens only to the previously defined // scoped cache. - function memoized>( + function memoized>( cache: ToCache, ...innerArgs: A ): Return { From 48eff374983b4279d165560315c44778f7080862 Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 15:42:17 +0100 Subject: [PATCH 10/11] Extract documentation --- docs/review/api/alfa-cache.api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/review/api/alfa-cache.api.md b/docs/review/api/alfa-cache.api.md index fbfbe75e5b..1582c79d76 100644 --- a/docs/review/api/alfa-cache.api.md +++ b/docs/review/api/alfa-cache.api.md @@ -22,7 +22,7 @@ export class Cache { export namespace Cache { export function from(iterable: Iterable_2): Cache; export type Key = object; - export function memoize, Return>(target: (this: This, ...args: Args) => Return): (this: This, ...args: Args) => Return; + export function memoize, Return>(target: (this: This, ...args: Args) => Return): (this: This, ...args: Args) => Return; } // (No @packageDocumentation comment for this package) From 83540d1fa0d19fb9389a392e1a60c33b7f32d91d Mon Sep 17 00:00:00 2001 From: Jean-Yves Moyen Date: Mon, 25 Nov 2024 16:01:50 +0100 Subject: [PATCH 11/11] Improve decorators --- docs/review/api/alfa-cache.api.md | 1 + .../dom/predicate/is-programmatically-hidden.ts | 6 +----- packages/alfa-cache/src/cache.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/review/api/alfa-cache.api.md b/docs/review/api/alfa-cache.api.md index 1582c79d76..5891e8e655 100644 --- a/docs/review/api/alfa-cache.api.md +++ b/docs/review/api/alfa-cache.api.md @@ -23,6 +23,7 @@ export namespace Cache { export function from(iterable: Iterable_2): Cache; export type Key = object; export function memoize, Return>(target: (this: This, ...args: Args) => Return): (this: This, ...args: Args) => Return; + export function memoize, Return>(target: (...args: Args) => Return): (...args: Args) => Return; } // (No @packageDocumentation comment for this package) diff --git a/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts b/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts index ad5e5e2eaf..51ccba7b65 100644 --- a/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts +++ b/packages/alfa-aria/src/dom/predicate/is-programmatically-hidden.ts @@ -58,8 +58,4 @@ function _hasHiddenAncestors( ); } -const hasHiddenAncestors = Cache.memoize< - unknown, - [Device, Context], - Predicate ->(_hasHiddenAncestors); +const hasHiddenAncestors = Cache.memoize(_hasHiddenAncestors); diff --git a/packages/alfa-cache/src/cache.ts b/packages/alfa-cache/src/cache.ts index 2e7ef68450..c9204307fc 100644 --- a/packages/alfa-cache/src/cache.ts +++ b/packages/alfa-cache/src/cache.ts @@ -131,8 +131,20 @@ export namespace Cache { : T; /** - * Memoize a function or method. + * Memoize a method. */ + export function memoize, Return>( + // When called on an instance's method `target`, `this` is the instance. + target: (this: This, ...args: Args) => Return, + ): (this: This, ...args: Args) => Return; + + /** + * Memoize a function + */ + export function memoize, Return>( + target: (...args: Args) => Return, + ): (...args: Args) => Return; + export function memoize, Return>( // When called on an instance's method `target`, `this` is the instance. target: (this: This, ...args: Args) => Return,