LRU: add cachedMethod as alternative to @cached

Summary:
Getting `cached` working consistently across `tsc`, `eslint`, `esbuild`
(vite), `babel` (jest) seems annoying. For example, `eslint` thinks the
`cached` import is unused. Let's add a non-decorator version for the same
feature.

Reviewed By: evangrayk

Differential Revision: D54007743

fbshipit-source-id: 0d8594fec974bc485ba2d6dddca889f69a50a162
This commit is contained in:
Jun Wu 2024-02-21 10:40:32 -08:00 committed by Facebook GitHub Bot
parent 09c53005d4
commit 365e083f2d
2 changed files with 79 additions and 1 deletions

View File

@ -289,6 +289,25 @@ function cacheDecorator<T>(opts?: CacheOpts<T>) {
};
}
/** Cache an "instance" function like `this.foo`. */
export function cachedMethod<T, F extends AnyFunction<T>>(originalFunc: F, opts?: CacheOpts<T>): F {
const getExtraKeys =
opts?.getExtraKeys ??
function (this: T): unknown[] {
// Use `this` as extra key if it's a value object (hash + eq).
if (isValueObject(this)) {
return [this];
}
// Scan through cachable properties.
if (this != null && typeof this === 'object') {
return Object.values(this).filter(isCachable);
}
// Give up - do not add extra cache keys.
return [];
};
return cachedFunction(originalFunc, {...opts, getExtraKeys});
}
const cachableTypeNames = new Set([
'number',
'string',

View File

@ -7,7 +7,7 @@
import type {LRUWithStats} from '../LRU';
import {cached, LRU, clearTrackedCache} from '../LRU';
import {cached, LRU, clearTrackedCache, cachedMethod} from '../LRU';
import {SelfUpdate} from '../immutableExt';
import {List, Record} from 'immutable';
@ -388,3 +388,62 @@ describe('cached()', () => {
});
});
});
describe('cachedMethod', () => {
it('is an alternative to `@cached()` decorator', () => {
let calledTimes = 0;
class Fib {
fib = cachedMethod(this.fibImpl);
fibImpl(n: number): number {
calledTimes += 1;
return n < 2 ? n : this.fib(n - 1) + this.fib(n - 2);
}
}
const f = new Fib();
expect(f.fib(20)).toBe(6765);
expect(calledTimes).toBe(21);
// Note new Fib() instance does not share the cache like the decorator version.
const f2 = new Fib();
expect(f2.fib(20)).toBe(6765);
expect(calledTimes).toBe(42);
});
it('supports shared cache among instances', () => {
let calledTimes = 0;
const cache: LRUWithStats = new LRU(10);
class Fib {
fib = cachedMethod(this.fibImpl, {cache});
fibImpl(n: number): number {
calledTimes += 1;
return n < 2 ? n : this.fib(n - 1) + this.fib(n - 2);
}
}
const f1 = new Fib();
const f2 = new Fib();
expect(f1.fib(20)).toBe(6765);
expect(calledTimes).toBe(21);
expect(f2.fib(20)).toBe(6765);
expect(calledTimes).toBe(21); // f2 reuses f1's cache.
});
it('considers state of `this`', () => {
let calledTimes = 0;
class Add {
constructor(public n: number) {}
add: (n: number) => number = cachedMethod(this.addImpl);
addImpl(n: number): number {
calledTimes += 1;
return this.n + n;
}
}
const a = new Add(10);
expect(a.add(5)).toBe(15);
expect(calledTimes).toBe(1);
a.n = 20;
expect(a.add(5)).toBe(25); // `this` changed
expect(calledTimes).toBe(2);
a.n = 10;
expect(a.add(5)).toBe(15);
expect(calledTimes).toBe(2); // reused cache
});
});