p-memoize
Version:
Memoize promise-returning & async functions
167 lines (131 loc) • 8.8 kB
TypeScript
export type AnyAsyncFunction = (...arguments_: readonly any[]) => Promise<unknown | void>;
type AsyncReturnType<FunctionType extends AnyAsyncFunction> = Awaited<ReturnType<FunctionType>>;
export type CacheStorage<KeyType, ValueType> = {
has: (key: KeyType) => Promise<boolean> | boolean;
get: (key: KeyType) => Promise<ValueType | undefined> | ValueType | undefined;
set: (key: KeyType, value: ValueType) => Promise<unknown> | unknown;
/**
Not used by p-memoize. It is allowed so that a cache which has one can still be passed as an object literal.
*/
delete?: (key: KeyType) => unknown;
clear?: () => unknown;
};
/**
Controls whether a fulfilled value should be written to the cache.
*/
export type ShouldCache<KeyType, ValueType, ArgumentsList extends readonly unknown[] = readonly unknown[]> = (value: ValueType, context: {
key: KeyType;
argumentsList: ArgumentsList;
}) => boolean | Promise<boolean>;
export type Options<FunctionToMemoize extends AnyAsyncFunction, CacheKeyType> = {
/**
Determines the cache key for storing the result based on the function arguments. By default, __only the first argument is considered__ and it is compared by strict equality, so two objects only share an entry when they are the same one.
A `cacheKey` function can return any type supported by `Map` (or whatever structure you use in the `cache` option).
You can have it cache **all** the arguments by value with `JSON.stringify`, if they are compatible:
```
import pMemoize from 'p-memoize';
pMemoize(function_, {cacheKey: JSON.stringify});
```
Or you can use a more full-featured serializer like [serialize-javascript](https://github.com/yahoo/serialize-javascript) to add support for `RegExp`, `Date` and so on.
```
import pMemoize from 'p-memoize';
import serializeJavascript from 'serialize-javascript';
pMemoize(function_, {cacheKey: serializeJavascript});
```
- A call with no arguments has an undefined first argument, so it shares an entry with a call passing `undefined` explicitly. Pass a `cacheKey` that also looks at the argument count to tell them apart.
- A `cacheKey` that returns a promise is rejected by the type: a promise is a fresh object on every call, so every key would be unique and nothing would ever be memoized.
- The receiver (`this`) is not part of the key, and `cacheKey` cannot see it. A method memoized on a prototype, or one given a `cache` that is shared between instances, returns whichever instance's result was cached first. Use `pMemoizeDecorator()` without a custom `cache` for methods that depend on the instance.
@default arguments_ => arguments_[0]
@example arguments_ => JSON.stringify(arguments_)
*/
readonly cacheKey?: (arguments_: Parameters<FunctionToMemoize>) => CacheKeyType extends PromiseLike<unknown> ? never : CacheKeyType;
/**
Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, and `.set(key, value)`, and optionally `.clear()`. You could for example use a `WeakMap` instead or [`quick-lru`](https://github.com/sindresorhus/quick-lru) for a LRU cache. To disable caching so that only concurrent executions resolve with the same value, pass `false`.
- `.has()` decides whether there is a hit, and `.get()` is only called on a hit, to supply the value. If the two disagree, for example because the entry is evicted in between, the call resolves to `undefined` without running the function.
- Its key type must be the one `cacheKey` produces, which by default is the first argument's type. TypeScript cannot always check this, so a cache keyed by something else may only fail at runtime, depending on the cache.
- A cache with no `.clear()` cannot be emptied by `pMemoizeClear()`, and a `WeakMap` has none.
@default new Map()
@example new WeakMap()
*/
readonly cache?: CacheStorage<CacheKeyType, AsyncReturnType<FunctionToMemoize>> | false;
/**
Controls whether a fulfilled value should be written to the cache.
It runs after the function fulfills and before `cache.set`.
- Omit to keep current behavior (always write).
- Return `false` to skip writing to the cache (in-flight de-duplication is still cleared).
- Throw or reject to propagate the error and skip caching. The predicate does not run at all when caching is disabled with `cache: false`.
@example
```
import pMemoize from 'p-memoize';
// Only cache defined values
const getMaybe = pMemoize(async key => db.get(key), {
shouldCache: value => value !== undefined,
});
// Only cache non-empty arrays
const search = pMemoize(async query => fetchResults(query), {
shouldCache: value => Array.isArray(value) && value.length > 0,
});
```
Note: This only affects writes; reads from the cache are unchanged.
*/
readonly shouldCache?: ShouldCache<CacheKeyType, AsyncReturnType<FunctionToMemoize>, Parameters<FunctionToMemoize>>;
};
/**
[Memoize](https://en.wikipedia.org/wiki/Memoization) functions - An optimization used to speed up consecutive function calls by caching the result of calls with identical input.
For an overloaded function, the option types come from one of its overloads, because `ReturnType` resolves a type with several call signatures to a single one. Pass both type arguments to `pMemoize` when that matters.
@param fn - Function to be memoized.
@param options - See the `Options` type above.
@example
```
import pMemoize from 'p-memoize';
import got from 'got';
const memoizedGot = pMemoize(got);
await memoizedGot('https://sindresorhus.com');
// This call is cached
await memoizedGot('https://sindresorhus.com');
```
The default cache keeps every value for as long as the memoized function exists. To let entries expire, pass a `cache` that does it, which the readme's "Time-based cache expiration" section covers.
*/
export default function pMemoize<FunctionToMemoize extends AnyAsyncFunction, CacheKeyType = Parameters<FunctionToMemoize>[0]>(fn: FunctionToMemoize, options?: Options<FunctionToMemoize, CacheKeyType>): FunctionToMemoize;
/**
- Only class methods are supported; regular functions are not part of the decorators proposals.
- Uses the new ECMAScript decorators (TypeScript 5.0+). Legacy `experimentalDecorators` are not supported.
- Babel’s legacy decorators are not supported as they implement a different proposal variant.
- Private methods are not supported.
- Put `@pMemoizeDecorator()` first (topmost) when you combine it with other method decorators. Decorators are applied from the bottom up, so a decorator listed above it replaces the method after it, and the memoized copy is then skipped so that it does not hide that replacement. This happens silently.
- A subclass that overrides the method is not memoized for it, and a `super.method()` call from the override reaches the original, unmemoized method.
@returns A decorator to memoize class methods or static class methods.
@example
```
import {pMemoizeDecorator} from 'p-memoize';
class Example {
index = 0
@pMemoizeDecorator()
async counter() {
return ++this.index;
}
}
class ExampleWithOptions {
index = 0
// Only cache positive results.
@pMemoizeDecorator({shouldCache: value => value > 0})
async counter() {
return ++this.index;
}
}
```
*/
export declare function pMemoizeDecorator<FunctionToMemoize extends AnyAsyncFunction = (...arguments_: any[]) => Promise<any>, CacheKeyType = Parameters<FunctionToMemoize>[0]>(options?: Options<NoInfer<FunctionToMemoize>, NoInfer<CacheKeyType>>): <This, Value extends FunctionToMemoize>(value: Value, context: ClassMethodDecoratorContext<This, Value> & {
private: false;
}) => void;
/**
Clear all cached data of a memoized function.
What the clear reaches depends on what it is clearing:
- The cache is emptied for every memoized function sharing it, and a call still running on any of them will not write its result back into it.
- Only the in-flight calls of `fn` itself are dropped, so a later call on it starts fresh rather than joining one that began before the clear. Another memoized function over the same cache keeps sharing its own in-flight calls until they settle.
A `cache.set()` that has already started when you clear is not undone, so that value can still land in the cleared cache.
This function is synchronous: a `clear()` that returns a promise finishes after it returns, and a rejection from it is dropped rather than reported, since there is nothing to return it through. A `clear()` that throws synchronously still propagates to the caller.
@param fn - Memoized function.
*/
export declare function pMemoizeClear(fn: AnyAsyncFunction): void;
export {};