├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── index.ts ├── license ├── package.json ├── readme.md ├── test-d └── index.test-d.ts ├── test.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | dist 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import mimicFn from 'mimic-fn'; 2 | import type {AsyncReturnType} from 'type-fest'; 3 | 4 | // TODO: Use the one in `type-fest` when it's added there. 5 | export type AnyAsyncFunction = (...arguments_: readonly any[]) => Promise; 6 | 7 | const cacheStore = new WeakMap | false>(); 8 | 9 | export type CacheStorage = { 10 | has: (key: KeyType) => Promise | boolean; 11 | get: (key: KeyType) => Promise | ValueType | undefined; 12 | set: (key: KeyType, value: ValueType) => Promise | unknown; 13 | delete: (key: KeyType) => unknown; 14 | clear?: () => unknown; 15 | }; 16 | 17 | export type Options< 18 | FunctionToMemoize extends AnyAsyncFunction, 19 | CacheKeyType, 20 | > = { 21 | /** 22 | Determines the cache key for storing the result based on the function arguments. By default, __only the first argument is considered__ and it only works with [primitives](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). 23 | 24 | A `cacheKey` function can return any type supported by `Map` (or whatever structure you use in the `cache` option). 25 | 26 | You can have it cache **all** the arguments by value with `JSON.stringify`, if they are compatible: 27 | 28 | ``` 29 | import pMemoize from 'p-memoize'; 30 | 31 | pMemoize(function_, {cacheKey: JSON.stringify}); 32 | ``` 33 | 34 | 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. 35 | 36 | ``` 37 | import pMemoize from 'p-memoize'; 38 | import serializeJavascript from 'serialize-javascript'; 39 | 40 | pMemoize(function_, {cacheKey: serializeJavascript}); 41 | ``` 42 | 43 | @default arguments_ => arguments_[0] 44 | @example arguments_ => JSON.stringify(arguments_) 45 | */ 46 | readonly cacheKey?: (arguments_: Parameters) => CacheKeyType; 47 | 48 | /** 49 | Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, 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`. 50 | 51 | @default new Map() 52 | @example new WeakMap() 53 | */ 54 | readonly cache?: CacheStorage> | false; 55 | }; 56 | 57 | /** 58 | [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. 59 | 60 | @param fn - Function to be memoized. 61 | 62 | @example 63 | ``` 64 | import {setTimeout as delay} from 'node:timer/promises'; 65 | import pMemoize from 'p-memoize'; 66 | import got from 'got'; 67 | 68 | const memoizedGot = pMemoize(got); 69 | 70 | await memoizedGot('https://sindresorhus.com'); 71 | 72 | // This call is cached 73 | await memoizedGot('https://sindresorhus.com'); 74 | 75 | await delay(2000); 76 | 77 | // This call is not cached as the cache has expired 78 | await memoizedGot('https://sindresorhus.com'); 79 | ``` 80 | */ 81 | export default function pMemoize< 82 | FunctionToMemoize extends AnyAsyncFunction, 83 | CacheKeyType, 84 | >( 85 | fn: FunctionToMemoize, 86 | { 87 | cacheKey = ([firstArgument]) => firstArgument as CacheKeyType, 88 | cache = new Map>(), 89 | }: Options = {}, 90 | ): FunctionToMemoize { 91 | // Promise objects can't be serialized so we keep track of them internally and only provide their resolved values to `cache` 92 | // `Promise>` is used instead of `ReturnType` because promise properties are not kept 93 | const promiseCache = new Map>>(); 94 | 95 | const memoized = function (this: any, ...arguments_: Parameters): Promise> { // eslint-disable-line @typescript-eslint/promise-function-async 96 | const key = cacheKey(arguments_); 97 | 98 | if (promiseCache.has(key)) { 99 | return promiseCache.get(key)!; 100 | } 101 | 102 | const promise = (async () => { 103 | try { 104 | if (cache && await cache.has(key)) { 105 | return (await cache.get(key))!; 106 | } 107 | 108 | const promise = fn.apply(this, arguments_) as Promise>; 109 | 110 | const result = await promise; 111 | 112 | try { 113 | return result; 114 | } finally { 115 | if (cache) { 116 | await cache.set(key, result); 117 | } 118 | } 119 | } finally { 120 | promiseCache.delete(key); 121 | } 122 | })() as Promise>; 123 | 124 | promiseCache.set(key, promise); 125 | 126 | return promise; 127 | } as FunctionToMemoize; 128 | 129 | mimicFn(memoized, fn, { 130 | ignoreNonConfigurable: true, 131 | }); 132 | 133 | cacheStore.set(memoized, cache); 134 | 135 | return memoized; 136 | } 137 | 138 | /** 139 | - Only class methods and getters/setters can be memoized, not regular functions (they aren't part of the proposal); 140 | - Only [TypeScript’s decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators) are supported, not [Babel’s](https://babeljs.io/docs/en/babel-plugin-proposal-decorators), which use a different version of the proposal; 141 | - Being an experimental feature, they need to be enabled with `--experimentalDecorators`; follow TypeScript’s docs. 142 | 143 | @returns A [decorator](https://github.com/tc39/proposal-decorators) to memoize class methods or static class methods. 144 | 145 | @example 146 | ``` 147 | import {pMemoizeDecorator} from 'p-memoize'; 148 | 149 | class Example { 150 | index = 0 151 | 152 | @pMemoizeDecorator() 153 | async counter() { 154 | return ++this.index; 155 | } 156 | } 157 | 158 | class ExampleWithOptions { 159 | index = 0 160 | 161 | @pMemoizeDecorator() 162 | async counter() { 163 | return ++this.index; 164 | } 165 | } 166 | ``` 167 | */ 168 | export function pMemoizeDecorator< 169 | FunctionToMemoize extends AnyAsyncFunction, 170 | CacheKeyType, 171 | >( 172 | options: Options = {}, 173 | ) { 174 | const instanceMap = new WeakMap(); 175 | 176 | return ( 177 | target: any, 178 | propertyKey: string, 179 | descriptor: PropertyDescriptor, 180 | ): void => { 181 | const input = target[propertyKey]; // eslint-disable-line @typescript-eslint/no-unsafe-assignment 182 | 183 | if (typeof input !== 'function') { 184 | throw new TypeError('The decorated value must be a function'); 185 | } 186 | 187 | delete descriptor.value; 188 | delete descriptor.writable; 189 | 190 | descriptor.get = function () { 191 | if (!instanceMap.has(this)) { 192 | const value = pMemoize(input, options) as FunctionToMemoize; 193 | instanceMap.set(this, value); 194 | return value; 195 | } 196 | 197 | return instanceMap.get(this) as FunctionToMemoize; 198 | }; 199 | }; 200 | } 201 | 202 | /** 203 | Clear all cached data of a memoized function. 204 | 205 | @param fn - Memoized function. 206 | */ 207 | export function pMemoizeClear(fn: AnyAsyncFunction): void { 208 | if (!cacheStore.has(fn)) { 209 | throw new TypeError('Can\'t clear a function that was not memoized!'); 210 | } 211 | 212 | const cache = cacheStore.get(fn); 213 | 214 | if (!cache) { 215 | throw new TypeError('Can\'t clear a function that doesn\'t use a cache!'); 216 | } 217 | 218 | if (typeof cache.clear !== 'function') { 219 | throw new TypeError('The cache Map can\'t be cleared!'); 220 | } 221 | 222 | cache.clear(); 223 | } 224 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p-memoize", 3 | "version": "7.1.1", 4 | "description": "Memoize promise-returning & async functions", 5 | "license": "MIT", 6 | "repository": "sindresorhus/p-memoize", 7 | "funding": "https://github.com/sindresorhus/p-memoize?sponsor=1", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": "./dist/index.js", 15 | "types": "./dist/index.d.ts", 16 | "engines": { 17 | "node": ">=14.16" 18 | }, 19 | "scripts": { 20 | "test": "xo && ava && npm run build && tsd", 21 | "build": "del-cli dist && tsc", 22 | "prepack": "npm run build" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "keywords": [ 28 | "promise", 29 | "memoize", 30 | "mem", 31 | "memoization", 32 | "function", 33 | "cache", 34 | "caching", 35 | "optimize", 36 | "performance", 37 | "ttl", 38 | "expire", 39 | "async", 40 | "await", 41 | "promises", 42 | "time", 43 | "out", 44 | "cancel", 45 | "bluebird" 46 | ], 47 | "dependencies": { 48 | "mimic-fn": "^4.0.0", 49 | "type-fest": "^3.0.0" 50 | }, 51 | "devDependencies": { 52 | "@sindresorhus/tsconfig": "^3.0.1", 53 | "@types/serialize-javascript": "^5.0.2", 54 | "ava": "^4.3.3", 55 | "del-cli": "^5.0.0", 56 | "delay": "^5.0.0", 57 | "p-defer": "^4.0.0", 58 | "p-state": "^1.0.0", 59 | "serialize-javascript": "^6.0.0", 60 | "ts-node": "^10.9.1", 61 | "tsd": "^0.24.1", 62 | "xo": "^0.52.4" 63 | }, 64 | "ava": { 65 | "extensions": { 66 | "ts": "module" 67 | }, 68 | "nodeArguments": [ 69 | "--loader=ts-node/esm" 70 | ] 71 | }, 72 | "xo": { 73 | "rules": { 74 | "@typescript-eslint/no-redundant-type-constituents": "off" 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # p-memoize 2 | 3 | > [Memoize](https://en.wikipedia.org/wiki/Memoization) promise-returning & async functions 4 | 5 | Useful for speeding up consecutive function calls by caching the result of calls with identical input. 6 | 7 | 8 | 9 | By default, **only the memoized function's first argument is considered** via strict equality comparison. If you need to cache multiple arguments or cache `object`s *by value*, have a look at alternative [caching strategies](#caching-strategy) below. 10 | 11 | This package is similar to [mem](https://github.com/sindresorhus/mem) but with async-specific enhancements; in particular, it allows for asynchronous caches and does not cache rejected promises. 12 | 13 | ## Install 14 | 15 | ```sh 16 | npm install p-memoize 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```js 22 | import pMemoize from 'p-memoize'; 23 | import got from 'got'; 24 | 25 | const memoizedGot = pMemoize(got); 26 | 27 | await memoizedGot('https://sindresorhus.com'); 28 | 29 | // This call is cached 30 | await memoizedGot('https://sindresorhus.com'); 31 | ``` 32 | 33 | ### Caching strategy 34 | 35 | Similar to the [caching strategy for `mem`](https://github.com/sindresorhus/mem#options) with the following exceptions: 36 | 37 | - Promises returned from a memoized function are locally cached until resolving, when their value is added to `cache`. Special properties assigned to a returned promise will not be kept after resolution and every promise may need to resolve with a serializable object if caching results in a database. 38 | - `.get()`, `.has()` and `.set()` methods on `cache` can run asynchronously by returning a promise. 39 | - Instead of `.set()` being provided an object with the properties `value` and `maxAge`, it will only be provided `value` as the first argument. If you want to implement time-based expiry, consider [doing so in `cache`](#time-based-cache-expiration). 40 | 41 | ## API 42 | 43 | ### pMemoize(fn, options?) 44 | 45 | Returns a memoized version of the given function. 46 | 47 | #### fn 48 | 49 | Type: `Function` 50 | 51 | Promise-returning or async function to be memoized. 52 | 53 | #### options 54 | 55 | Type: `object` 56 | 57 | ##### cacheKey 58 | 59 | Type: `Function`\ 60 | Default: `arguments_ => arguments_[0]`\ 61 | Example: `arguments_ => JSON.stringify(arguments_)` 62 | 63 | Determines the cache key for storing the result based on the function arguments. By default, **only the first argument is considered**. 64 | 65 | A `cacheKey` function can return any type supported by `Map` (or whatever structure you use in the `cache` option). 66 | 67 | See the [caching strategy](#caching-strategy) section for more information. 68 | 69 | ##### cache 70 | 71 | Type: `object | false`\ 72 | Default: `new Map()` 73 | 74 | Use a different cache storage. Must implement the following methods: `.has(key)`, `.get(key)`, `.set(key, value)`, `.delete(key)`, 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`. 75 | 76 | See the [caching strategy](https://github.com/sindresorhus/mem#caching-strategy) section in the `mem` package for more information. 77 | 78 | ### pMemoizeDecorator(options) 79 | 80 | Returns a [decorator](https://github.com/tc39/proposal-decorators) to memoize class methods or static class methods. 81 | 82 | Notes: 83 | 84 | - Only class methods and getters/setters can be memoized, not regular functions (they aren't part of the proposal); 85 | - Only [TypeScript’s decorators](https://www.typescriptlang.org/docs/handbook/decorators.html#parameter-decorators) are supported, not [Babel’s](https://babeljs.io/docs/en/babel-plugin-proposal-decorators), which use a different version of the proposal; 86 | - Being an experimental feature, they need to be enabled with `--experimentalDecorators`; follow TypeScript’s docs. 87 | 88 | #### options 89 | 90 | Type: `object` 91 | 92 | Same as options for `pMemoize()`. 93 | 94 | ```ts 95 | import {pMemoizeDecorator} from 'p-memoize'; 96 | 97 | class Example { 98 | index = 0 99 | 100 | @pMemoizeDecorator() 101 | async counter() { 102 | return ++this.index; 103 | } 104 | } 105 | 106 | class ExampleWithOptions { 107 | index = 0 108 | 109 | @pMemoizeDecorator() 110 | async counter() { 111 | return ++this.index; 112 | } 113 | } 114 | ``` 115 | 116 | ### pMemoizeClear(memoized) 117 | 118 | Clear all cached data of a memoized function. 119 | 120 | It will throw when given a non-memoized function. 121 | 122 | ## Tips 123 | 124 | ### Time-based cache expiration 125 | 126 | ```js 127 | import pMemoize from 'p-memoize'; 128 | import ExpiryMap from 'expiry-map'; 129 | import got from 'got'; 130 | 131 | const cache = new ExpiryMap(10000); // Cached values expire after 10 seconds 132 | 133 | const memoizedGot = pMemoize(got, {cache}); 134 | ``` 135 | 136 | ### Caching promise rejections 137 | 138 | ```js 139 | import pMemoize from 'p-memoize'; 140 | import pReflect from 'p-reflect'; 141 | 142 | const memoizedGot = pMemoize(async (url, options) => pReflect(got(url, options))); 143 | 144 | await memoizedGot('https://example.com'); 145 | // {isFulfilled: true, isRejected: false, value: '...'} 146 | ``` 147 | 148 | ## Related 149 | 150 | - [p-debounce](https://github.com/sindresorhus/p-debounce) - Debounce promise-returning & async functions 151 | - [p-throttle](https://github.com/sindresorhus/p-throttle) - Throttle promise-returning & async functions 152 | - [More…](https://github.com/sindresorhus/promise-fun) 153 | -------------------------------------------------------------------------------- /test-d/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import pMemoize, {pMemoizeClear} from '../index.js'; 3 | 4 | const fn = async (text: string) => Boolean(text); 5 | 6 | expectType(pMemoize(fn)); 7 | expectType(pMemoize(fn, {cacheKey: ([firstArgument]: [string]) => firstArgument})); 8 | expectType( 9 | pMemoize(fn, { 10 | // The cacheKey returns an array. This isn't deduplicated by a regular Map, but it's valid. The correct solution would be to use ManyKeysMap to deduplicate it correctly 11 | cacheKey: (arguments_: [string]) => arguments_, 12 | cache: new Map<[string], boolean>(), 13 | }), 14 | ); 15 | expectType( 16 | // The `firstArgument` of `fn` is of type `string`, so it's used 17 | pMemoize(fn, {cache: new Map()}), 18 | ); 19 | expectType( 20 | pMemoize(fn, {cache: false}), 21 | ); 22 | 23 | /* Overloaded function tests */ 24 | async function overloadedFn(parameter: false): Promise; 25 | async function overloadedFn(parameter: true): Promise; 26 | async function overloadedFn(parameter: boolean): Promise { 27 | return parameter; 28 | } 29 | 30 | expectType(pMemoize(overloadedFn)); 31 | expectType(await pMemoize(overloadedFn)(true)); 32 | expectType(await pMemoize(overloadedFn)(false)); 33 | 34 | pMemoizeClear(fn); 35 | 36 | // `cacheKey` tests. 37 | // The argument should match the memoized function’s parameters 38 | pMemoize(async (text: string) => Boolean(text), { 39 | cacheKey(arguments_) { 40 | expectType<[string]>(arguments_); 41 | }, 42 | }); 43 | 44 | pMemoize(async () => 1, { 45 | cacheKey(arguments_) { 46 | expectType<[]>(arguments_); // eslint-disable-line @typescript-eslint/ban-types 47 | }, 48 | }); 49 | 50 | // Ensures that the various cache functions infer their arguments type from the return type of `cacheKey` 51 | pMemoize(async (_arguments: {key: string}) => 1, { 52 | cacheKey(arguments_: [{key: string}]) { 53 | expectType<[{key: string}]>(arguments_); 54 | return new Date(); 55 | }, 56 | cache: { 57 | async get(key) { 58 | expectType(key); 59 | return 5; 60 | }, 61 | set(key, data) { 62 | expectType(key); 63 | expectType(data); 64 | }, 65 | async has(key) { 66 | expectType(key); 67 | return true; 68 | }, 69 | delete(key) { 70 | expectType(key); 71 | }, 72 | clear: () => undefined, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import serializeJavascript from 'serialize-javascript'; 3 | import pDefer from 'p-defer'; 4 | import {promiseStateAsync as promiseState} from 'p-state'; 5 | import pMemoize, {pMemoizeDecorator, pMemoizeClear} from './index.js'; 6 | 7 | test('memoize', async t => { 8 | let index = 0; 9 | const fixture = async () => index++; 10 | const memoized = pMemoize(fixture); 11 | t.is(await memoized(), 0); 12 | t.is(await memoized(), 0); 13 | t.is(await memoized(), 0); 14 | // @ts-expect-error Argument type does not match 15 | t.is(await memoized(undefined), 0); 16 | // @ts-expect-error Argument type does not match 17 | t.is(await memoized(undefined), 0); 18 | // @ts-expect-error Argument type does not match 19 | t.is(await memoized('foo'), 1); 20 | // @ts-expect-error Argument type does not match 21 | t.is(await memoized('foo'), 1); 22 | // @ts-expect-error Argument type does not match 23 | t.is(await memoized('foo'), 1); 24 | // @ts-expect-error Argument type does not match 25 | t.is(await memoized('foo', 'bar'), 1); 26 | // @ts-expect-error Argument type does not match 27 | t.is(await memoized('foo', 'bar'), 1); 28 | // @ts-expect-error Argument type does not match 29 | t.is(await memoized('foo', 'bar'), 1); 30 | // @ts-expect-error Argument type does not match 31 | t.is(await memoized(1), 2); 32 | // @ts-expect-error Argument type does not match 33 | t.is(await memoized(1), 2); 34 | // @ts-expect-error Argument type does not match 35 | t.is(await memoized(null), 3); 36 | // @ts-expect-error Argument type does not match 37 | t.is(await memoized(null), 3); 38 | // @ts-expect-error Argument type does not match 39 | t.is(await memoized(fixture), 4); 40 | // @ts-expect-error Argument type does not match 41 | t.is(await memoized(fixture), 4); 42 | // @ts-expect-error Argument type does not match 43 | t.is(await memoized(true), 5); 44 | // @ts-expect-error Argument type does not match 45 | t.is(await memoized(true), 5); 46 | 47 | // Ensure that functions are stored by reference and not by "value" (e.g. their `.toString()` representation) 48 | // @ts-expect-error Argument type does not match 49 | t.is(await memoized(() => index++), 6); 50 | // @ts-expect-error Argument type does not match 51 | t.is(await memoized(() => index++), 7); 52 | }); 53 | 54 | test('pending promises are cached', async t => { 55 | const {promise, resolve} = pDefer(); 56 | let invocationsCount = 0; 57 | const cache = new Map(); 58 | 59 | const memoized = pMemoize(async () => { 60 | invocationsCount++; 61 | return promise; 62 | }, {cache}); 63 | 64 | const promise1 = memoized(); 65 | t.is(await promiseState(promise1), 'pending'); 66 | 67 | const promise2 = memoized(); 68 | t.is(await promiseState(promise2), 'pending'); 69 | 70 | t.is(invocationsCount, 1, 'pending promises are cached'); 71 | 72 | resolve(true); 73 | 74 | t.true(await promise1, 'promise resolution is propagated'); 75 | t.true(await promise2, 'promise resolution is propagated'); 76 | t.true(await memoized(), 'cache is hit'); 77 | t.true(cache.get(undefined), 'result is cached'); 78 | }); 79 | 80 | test('pending promises are cached synchronously', async t => { 81 | const {promise, resolve} = pDefer(); 82 | let invocationsCount = 0; 83 | const cache = new Map(); 84 | 85 | const memoized = pMemoize(async () => { 86 | invocationsCount++; 87 | return promise; 88 | }, {cache}); 89 | 90 | const promise1 = memoized(); 91 | const promise2 = memoized(); 92 | t.is(promise1, promise2); 93 | 94 | resolve(true); 95 | 96 | t.true(await promise1, 'promise is executed'); 97 | t.true(await promise2, 'promise resolution is propagated'); 98 | 99 | t.is(invocationsCount, 1, 'pending promises are cached'); 100 | 101 | t.true(await memoized(), 'cache is hit'); 102 | t.true(cache.get(undefined), 'result is cached'); 103 | }); 104 | 105 | test('cacheKey option', async t => { 106 | let index = 0; 107 | const fixture = async (..._arguments: any) => index++; 108 | const memoized = pMemoize(fixture, {cacheKey: ([firstArgument]) => String(firstArgument)}); 109 | t.is(await memoized(1), 0); 110 | t.is(await memoized(1), 0); 111 | t.is(await memoized('1'), 0); 112 | t.is(await memoized('2'), 1); 113 | t.is(await memoized(2), 1); 114 | }); 115 | 116 | test('memoize with multiple non-primitive arguments', async t => { 117 | let index = 0; 118 | const memoized = pMemoize(async () => index++, {cacheKey: JSON.stringify}); 119 | t.is(await memoized(), 0); 120 | t.is(await memoized(), 0); 121 | // @ts-expect-error Argument type does not match 122 | t.is(await memoized({foo: true}, {bar: false}), 1); 123 | // @ts-expect-error Argument type does not match 124 | t.is(await memoized({foo: true}, {bar: false}), 1); 125 | // @ts-expect-error Argument type does not match 126 | t.is(await memoized({foo: true}, {bar: false}, {baz: true}), 2); 127 | // @ts-expect-error Argument type does not match 128 | t.is(await memoized({foo: true}, {bar: false}, {baz: true}), 2); 129 | }); 130 | 131 | test('memoize with regexp arguments', async t => { 132 | let index = 0; 133 | const memoized = pMemoize(async () => index++, {cacheKey: serializeJavascript}); 134 | t.is(await memoized(), 0); 135 | t.is(await memoized(), 0); 136 | // @ts-expect-error Argument type does not match 137 | t.is(await memoized(/Sindre Sorhus/), 1); 138 | // @ts-expect-error Argument type does not match 139 | t.is(await memoized(/Sindre Sorhus/), 1); 140 | // @ts-expect-error Argument type does not match 141 | t.is(await memoized(/Elvin Peng/), 2); 142 | // @ts-expect-error Argument type does not match 143 | t.is(await memoized(/Elvin Peng/), 2); 144 | }); 145 | 146 | test('memoize with Symbol arguments', async t => { 147 | let index = 0; 148 | const argument1 = Symbol('fixture1'); 149 | const argument2 = Symbol('fixture2'); 150 | const memoized = pMemoize(async () => index++); 151 | t.is(await memoized(), 0); 152 | t.is(await memoized(), 0); 153 | // @ts-expect-error Argument type does not match 154 | t.is(await memoized(argument1), 1); 155 | // @ts-expect-error Argument type does not match 156 | t.is(await memoized(argument1), 1); 157 | // @ts-expect-error Argument type does not match 158 | t.is(await memoized(argument2), 2); 159 | // @ts-expect-error Argument type does not match 160 | t.is(await memoized(argument2), 2); 161 | }); 162 | 163 | test('cache option', async t => { 164 | let index = 0; 165 | const fixture = async (..._arguments: any) => index++; 166 | const memoized = pMemoize(fixture, { 167 | cache: new WeakMap(), 168 | cacheKey: ([firstArgument]: [ReturnValue]): ReturnValue => firstArgument, 169 | }); 170 | const foo = {}; 171 | const bar = {}; 172 | t.is(await memoized(foo), 0); 173 | t.is(await memoized(foo), 0); 174 | t.is(await memoized(bar), 1); 175 | t.is(await memoized(bar), 1); 176 | }); 177 | 178 | test('internal promise cache is only used if a value already exists in cache', async t => { 179 | let index = 0; 180 | const fixture = async (..._arguments: any) => index++; 181 | const cache = new Map(); 182 | const memoized = pMemoize(fixture, { 183 | cache, 184 | cacheKey: ([firstArgument]: [ReturnValue]): ReturnValue => firstArgument, 185 | }); 186 | const foo = {}; 187 | t.is(await memoized(foo), 0); 188 | t.is(await memoized(foo), 0); 189 | cache.delete(foo); 190 | t.is(await memoized(foo), 1); 191 | t.is(await memoized(foo), 1); 192 | }); 193 | 194 | test('preserves the original function name', t => { 195 | t.is(pMemoize(async function foo() {}).name, 'foo'); // eslint-disable-line func-names, @typescript-eslint/no-empty-function 196 | }); 197 | 198 | test('disables caching', async t => { 199 | let index = 0; 200 | 201 | const memoized = pMemoize(async () => index++, {cache: false}); 202 | 203 | t.is(await memoized(), 0); 204 | t.is(await memoized(), 1); 205 | t.is(await memoized(), 2); 206 | t.deepEqual(await Promise.all([memoized(), memoized()]), [3, 3]); 207 | }); 208 | 209 | test('.pMemoizeClear()', async t => { 210 | let index = 0; 211 | const fixture = async () => index++; 212 | const memoized = pMemoize(fixture); 213 | t.is(await memoized(), 0); 214 | t.is(await memoized(), 0); 215 | pMemoizeClear(memoized); 216 | t.is(await memoized(), 1); 217 | t.is(await memoized(), 1); 218 | }); 219 | 220 | test('prototype support', async t => { 221 | class Unicorn { 222 | index = 0; 223 | async foo() { 224 | return this.index++; 225 | } 226 | } 227 | 228 | Unicorn.prototype.foo = pMemoize(Unicorn.prototype.foo); 229 | 230 | const unicorn = new Unicorn(); 231 | 232 | t.is(await unicorn.foo(), 0); 233 | t.is(await unicorn.foo(), 0); 234 | t.is(await unicorn.foo(), 0); 235 | }); 236 | 237 | test('.pMemoizeDecorator()', async t => { 238 | let returnValue = 1; 239 | const returnValue2 = 101; 240 | 241 | class TestClass { 242 | @pMemoizeDecorator() 243 | async counter() { 244 | return returnValue++; 245 | } 246 | 247 | @pMemoizeDecorator() 248 | async counter2() { 249 | return returnValue2; 250 | } 251 | } 252 | 253 | const alpha = new TestClass(); 254 | t.is(await alpha.counter(), 1); 255 | t.is(await alpha.counter(), 1, 'The method should be memoized'); 256 | t.is(await alpha.counter2(), 101, 'The method should be memoized separately from the other one'); 257 | 258 | const beta = new TestClass(); 259 | t.is(await beta.counter(), 2, 'The method should not be memoized across instances'); 260 | }); 261 | 262 | test('.pMemoizeClear() throws when called with a plain function', t => { 263 | t.throws(() => { 264 | pMemoizeClear(async () => {}); // eslint-disable-line @typescript-eslint/no-empty-function 265 | }, { 266 | message: 'Can\'t clear a function that was not memoized!', 267 | instanceOf: TypeError, 268 | }); 269 | }); 270 | 271 | test('.pMemoizeClear() throws when called on an unclearable cache', t => { 272 | const fixture = async () => 1; 273 | const memoized = pMemoize(fixture, { 274 | cache: new WeakMap(), 275 | }); 276 | 277 | t.throws(() => { 278 | pMemoizeClear(memoized); 279 | }, { 280 | message: 'The cache Map can\'t be cleared!', 281 | instanceOf: TypeError, 282 | }); 283 | }); 284 | 285 | test('.pMemoizeClear() throws when called on a disabled cache', t => { 286 | const fixture = async () => 1; 287 | const memoized = pMemoize(fixture, { 288 | cache: false, 289 | }); 290 | 291 | t.throws(() => { 292 | pMemoizeClear(memoized); 293 | }, { 294 | message: 'Can\'t clear a function that doesn\'t use a cache!', 295 | instanceOf: TypeError, 296 | }); 297 | }); 298 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sindresorhus/tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "experimentalDecorators": true 6 | }, 7 | "files": [ 8 | "index.ts" 9 | ], 10 | "ts-node": { 11 | "transpileOnly": true, 12 | "files": true, 13 | "experimentalResolver": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------