@chriscdn/memoize 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/__tests__/memoize.test.ts +43 -0
- package/lib/index.d.ts +29 -0
- package/lib/memoize.cjs +2 -0
- package/lib/memoize.cjs.map +1 -0
- package/lib/memoize.modern.js +2 -0
- package/lib/memoize.modern.js.map +1 -0
- package/lib/memoize.module.js +2 -0
- package/lib/memoize.module.js.map +1 -0
- package/lib/memoize.umd.js +2 -0
- package/lib/memoize.umd.js.map +1 -0
- package/package.json +32 -0
- package/src/index.ts +101 -0
- package/tsconfig.json +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Christopher Meyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @chriscdn/memoize
|
|
2
|
+
|
|
3
|
+
Memoize a synchronous or asynchronous function.
|
|
4
|
+
|
|
5
|
+
## Installing
|
|
6
|
+
|
|
7
|
+
Using npm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @chriscdn/memoize
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Using yarn:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add @chriscdn/memoize
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
The package comes with two functions: `Memoize` and `MemoizeAsync`.
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { Memoize, MemoizeAsync } from "@chriscdn/memoize";
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `Memoize` function can be used to memoize a _synchronous_ function. The `MemoizeAsync` function can be used to memoize an _asynchronous_ function.
|
|
28
|
+
|
|
29
|
+
The `MemoizeAsync` function prevents duplicate evaluations by ensuring that multiple calls with identical parameters are only processed once.
|
|
30
|
+
|
|
31
|
+
**Example (synchronous)**
|
|
32
|
+
|
|
33
|
+
To memoize a function:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const _add = (x: number, y: number) => x + y;
|
|
37
|
+
const add = Memoize(_add);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The `add` function has the same interface as `_add`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const result = add(5, 7);
|
|
44
|
+
// 12
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Example (asynchronous)**
|
|
48
|
+
|
|
49
|
+
The asynchronous case is similar:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const _add = async (x: number, y: number) => x + y;
|
|
53
|
+
const add = MemoizeAsync(_add);
|
|
54
|
+
|
|
55
|
+
const result = await add(5, 7);
|
|
56
|
+
// 12
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
The `Memoize` and `MemoizeAsync` functions accept an `Options` parameter to control the behaviour of the cache. Each option is optional. See the section below for the defaults:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const options = {
|
|
65
|
+
// maximum number of items in the cache
|
|
66
|
+
maxSize: 1000,
|
|
67
|
+
|
|
68
|
+
// maximum number of milliseconds an item is to remain in the cache, undefined implies Infinity
|
|
69
|
+
maxAge: undefined,
|
|
70
|
+
|
|
71
|
+
// a function for generating the cache key (must return a String)
|
|
72
|
+
resolver: (...args) => JSON.stringify(args),
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tests
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
yarn test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { Memoize, MemoizeAsync } from "../src/index";
|
|
3
|
+
|
|
4
|
+
let addSyncCount = 0;
|
|
5
|
+
let addAsyncCount = 0;
|
|
6
|
+
|
|
7
|
+
const add = (x: number, y: number) => {
|
|
8
|
+
addSyncCount += 1;
|
|
9
|
+
return x + y;
|
|
10
|
+
};
|
|
11
|
+
const addCached = Memoize(add);
|
|
12
|
+
|
|
13
|
+
const addAsync = async (x: number, y: number) => {
|
|
14
|
+
addAsyncCount += 1;
|
|
15
|
+
return x + y;
|
|
16
|
+
};
|
|
17
|
+
const addCachedAsync = MemoizeAsync(addAsync);
|
|
18
|
+
|
|
19
|
+
test("sync", async () => {
|
|
20
|
+
expect(addCached(1, 2)).toBe(3);
|
|
21
|
+
expect(addCached(1, 2)).toBe(3);
|
|
22
|
+
expect(addCached(1, 2)).toBe(3);
|
|
23
|
+
expect(addCached(1, 2)).toBe(3);
|
|
24
|
+
expect(addCached(1, 2)).toBe(3);
|
|
25
|
+
|
|
26
|
+
// different key here
|
|
27
|
+
expect(addCached(2, 1)).toBe(3);
|
|
28
|
+
expect(addSyncCount).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("async", async () => {
|
|
32
|
+
await Promise.all([
|
|
33
|
+
addCachedAsync(1, 2).then((value) => expect(value).toBe(3)),
|
|
34
|
+
addCachedAsync(1, 2).then((value) => expect(value).toBe(3)),
|
|
35
|
+
addCachedAsync(1, 2).then((value) => expect(value).toBe(3)),
|
|
36
|
+
addCachedAsync(1, 2).then((value) => expect(value).toBe(3)),
|
|
37
|
+
|
|
38
|
+
// different key here
|
|
39
|
+
addCachedAsync(2, 1).then((value) => expect(value).toBe(3)),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
expect(addAsyncCount).toBe(2);
|
|
43
|
+
});
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type Options<T extends any[]> = {
|
|
2
|
+
maxSize: number;
|
|
3
|
+
maxAge?: number;
|
|
4
|
+
resolver: (...args: T) => string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Memoize a synchronous function.
|
|
8
|
+
*
|
|
9
|
+
* @template {any[]} Args
|
|
10
|
+
* @template {{}} Return
|
|
11
|
+
* @param {(...args: Args) => Return} cb
|
|
12
|
+
* @param {Partial<Options<Args>>} [options={}]
|
|
13
|
+
* @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}
|
|
14
|
+
*/
|
|
15
|
+
declare const Memoize: <Args extends any[], Return extends {}>(cb: (...args: Args) => Return, options?: Partial<Options<Args>>) => (...args: Args) => Return;
|
|
16
|
+
/**
|
|
17
|
+
* Memoize an asynchronous function.
|
|
18
|
+
*
|
|
19
|
+
* This differs from the sychronous case by ensuring multiple calls with the
|
|
20
|
+
* same arguments is only evaluated once. This is controlled by using a
|
|
21
|
+
* semaphore, which forces redundant calls to wait until the first call
|
|
22
|
+
* completes.
|
|
23
|
+
*
|
|
24
|
+
* @param cb
|
|
25
|
+
* @param options
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
declare const MemoizeAsync: <Args extends any[], Return extends {}>(cb: (...args: Args) => Promise<Return>, options?: Partial<Options<Args>>) => (...args: Args) => Promise<Return>;
|
|
29
|
+
export { Memoize, MemoizeAsync };
|
package/lib/memoize.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=require("@chriscdn/promise-semaphore"),r=require("quick-lru");function n(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=/*#__PURE__*/n(e),i=/*#__PURE__*/n(r);exports.Memoize=function(e,r){var n,t;void 0===r&&(r={});var u=null!=(n=r.maxSize)?n:1e3,l=null!=(t=r.resolver)?t:function(){return JSON.stringify([].slice.call(arguments))},a=new i.default({maxAge:r.maxAge,maxSize:u});return function(){var r=[].slice.call(arguments),n=l.apply(void 0,r);if(a.has(n))return a.get(n);var t=e.apply(void 0,r);return a.set(n,t),t}},exports.MemoizeAsync=function(e,r){var n,u;void 0===r&&(r={});var l=r.maxAge,a=null!=(n=r.maxSize)?n:1e3,o=null!=(u=r.resolver)?u:function(){return JSON.stringify([].slice.call(arguments))},c=new t.default,s=new i.default({maxAge:l,maxSize:a});return function(){try{var r=[].slice.call(arguments),n=o.apply(void 0,r);return Promise.resolve(s.has(n)?s.get(n):function(t,i){try{var u=Promise.resolve(c.acquire(n)).then(function(){return s.has(n)?s.get(n):Promise.resolve(e.apply(void 0,r)).then(function(e){return s.set(n,e),e})})}catch(e){return i(!0,e)}return u&&u.then?u.then(i.bind(null,!1),i.bind(null,!0)):i(!1,u)}(0,function(e,r){if(c.release(n),e)throw r;return r}))}catch(e){return Promise.reject(e)}}};
|
|
2
|
+
//# sourceMappingURL=memoize.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoize.cjs","sources":["../src/index.ts"],"sourcesContent":["import Semaphore from \"@chriscdn/promise-semaphore\";\nimport QuickLRU from \"quick-lru\";\n\nconst kDefaultMaxSize = 1000;\n\ntype Options<T extends any[]> = {\n maxSize: number;\n maxAge?: number;\n resolver: (...args: T) => string;\n};\n\n/**\n * Memoize a synchronous function.\n *\n * @template {any[]} Args\n * @template {{}} Return\n * @param {(...args: Args) => Return} cb\n * @param {Partial<Options<Args>>} [options={}]\n * @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}\n */\nconst Memoize = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Return,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return (...args: Args): Return => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n };\n};\n\n/**\n * Memoize an asynchronous function.\n *\n * This differs from the sychronous case by ensuring multiple calls with the\n * same arguments is only evaluated once. This is controlled by using a\n * semaphore, which forces redundant calls to wait until the first call\n * completes.\n *\n * @param cb\n * @param options\n * @returns\n */\nconst MemoizeAsync = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Promise<Return>,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const semaphore = new Semaphore();\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return async (...args: Args): Promise<Return> => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n try {\n await semaphore.acquire(key);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = await cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n } finally {\n semaphore.release(key);\n }\n }\n };\n};\n\nexport { Memoize, MemoizeAsync };\n"],"names":["cb","options","_options$maxSize","_options$resolver","maxSize","resolver","JSON","stringify","slice","call","arguments","cache","QuickLRU","maxAge","args","key","apply","has","get","returnValue","set","_options$maxSize2","_options$resolver2","semaphore","Semaphore","_arguments","Promise","resolve","acquire","then","_finallyRethrows","_wasThrown","_result","release","e","reject"],"mappings":"wMAoBgB,SACZA,EACAC,GACA,IAAAC,EAAAC,OADkC,IAAlCF,IAAAA,EAAkC,CAAA,GAElC,IACMG,EAAyB,OAAlBF,EAAGD,EAAQG,SAAOF,EAtBX,IAwBdG,EAA2B,OAAnBF,EAAGF,EAAQI,UAAQF,EAC5B,WAAmB,OAAAG,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCC,EAAQ,IAAIC,EAAQ,QAAiB,CACvCC,OAP+BZ,EAAQY,OAQvCT,QAAAA,IAGJ,kBAAW,IAAAU,EAAU,GAAAN,MAAAC,KAAAC,WACXK,EAAMV,EAAQW,WAAA,EAAIF,GAExB,GAAIH,EAAMM,IAAIF,GACV,OAAOJ,EAAMO,IAAIH,GAEjB,IAAMI,EAAcnB,EAAEgB,WAAIF,EAAAA,GAE1B,OADAH,EAAMS,IAAIL,EAAKI,GACRA,CAEf,CACJ,uBAcqB,SACjBnB,EACAC,GACA,IAAAoB,EAAAC,OADkC,IAAlCrB,IAAAA,EAAkC,CAAE,GAEpC,IAAMY,EAA6BZ,EAAQY,OACrCT,EAAyB,OAAlBiB,EAAGpB,EAAQG,SAAOiB,EA9DX,IAgEdhB,EAA2B,OAAnBiB,EAAGrB,EAAQI,UAAQiB,EAC5B,WAAA,OAAmBhB,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCa,EAAY,IAAIC,EAAW,QAE3Bb,EAAQ,IAAIC,EAAQ,QAAiB,CACvCC,OAAAA,EACAT,QAAAA,IAGJ,OAAA,WAAA,IAAgDqB,IAA/BX,EAAU,GAAAN,MAAAC,KAAqBC,WACtCK,EAAMV,EAAQW,WAAIF,EAAAA,GAAM,OAAAY,QAAAC,QAE1BhB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,2BAEbW,QAAAC,QACMJ,EAAUK,QAAQb,IAAIc,KAAA,WAAA,OAExBlB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,GAAMW,QAAAC,QAEG3B,EAAEgB,WAAIF,EAAAA,IAAKe,KAAA,SAA/BV,GAEN,OADAR,EAAMS,IAAIL,EAAKI,GACRA,CAAY,EAE1B,4FAZsBW,CAEnB,EAUHC,SAAAA,EAAAC,GAC0B,GAAvBT,EAAUU,QAAQlB,GAAKgB,EAAAC,MAAAA,EAAAA,OAAAA,CAAA,GAGnC,CAAC,MAAAE,GAAAR,OAAAA,QAAAS,OAAAD,EACL,CAAA,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"@chriscdn/promise-semaphore";import r from"quick-lru";const t=(e,t={})=>{var n,a;const s=null!=(n=t.maxSize)?n:1e3,i=null!=(a=t.resolver)?a:(...e)=>JSON.stringify(e),o=new r({maxAge:t.maxAge,maxSize:s});return(...r)=>{const t=i(...r);if(o.has(t))return o.get(t);{const n=e(...r);return o.set(t,n),n}}},n=(t,n={})=>{var a,s;const i=n.maxAge,o=null!=(a=n.maxSize)?a:1e3,l=null!=(s=n.resolver)?s:(...e)=>JSON.stringify(e),m=new e,u=new r({maxAge:i,maxSize:o});return async(...e)=>{const r=l(...e);if(u.has(r))return u.get(r);try{if(await m.acquire(r),u.has(r))return u.get(r);{const n=await t(...e);return u.set(r,n),n}}finally{m.release(r)}}};export{t as Memoize,n as MemoizeAsync};
|
|
2
|
+
//# sourceMappingURL=memoize.modern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoize.modern.js","sources":["../src/index.ts"],"sourcesContent":["import Semaphore from \"@chriscdn/promise-semaphore\";\nimport QuickLRU from \"quick-lru\";\n\nconst kDefaultMaxSize = 1000;\n\ntype Options<T extends any[]> = {\n maxSize: number;\n maxAge?: number;\n resolver: (...args: T) => string;\n};\n\n/**\n * Memoize a synchronous function.\n *\n * @template {any[]} Args\n * @template {{}} Return\n * @param {(...args: Args) => Return} cb\n * @param {Partial<Options<Args>>} [options={}]\n * @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}\n */\nconst Memoize = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Return,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return (...args: Args): Return => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n };\n};\n\n/**\n * Memoize an asynchronous function.\n *\n * This differs from the sychronous case by ensuring multiple calls with the\n * same arguments is only evaluated once. This is controlled by using a\n * semaphore, which forces redundant calls to wait until the first call\n * completes.\n *\n * @param cb\n * @param options\n * @returns\n */\nconst MemoizeAsync = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Promise<Return>,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const semaphore = new Semaphore();\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return async (...args: Args): Promise<Return> => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n try {\n await semaphore.acquire(key);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = await cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n } finally {\n semaphore.release(key);\n }\n }\n };\n};\n\nexport { Memoize, MemoizeAsync };\n"],"names":["Memoize","cb","options","_options$maxSize","_options$resolver","maxSize","resolver","args","JSON","stringify","cache","QuickLRU","maxAge","key","has","get","returnValue","set","MemoizeAsync","_options$maxSize2","_options$resolver2","semaphore","Semaphore","async","acquire","release"],"mappings":"oEAGA,MAiBMA,EAAUA,CACZC,EACAC,EAAkC,CAAA,KAClCC,IAAAA,EAAAC,EACA,MACMC,EAAyBF,OAAlBA,EAAGD,EAAQG,SAAOF,EAtBX,IAwBdG,EAA2B,OAAnBF,EAAGF,EAAQI,UAAQF,EAC5B,IAAIG,IAAeC,KAAKC,UAAUF,GAEjCG,EAAQ,IAAIC,EAAyB,CACvCC,OAP+BV,EAAQU,OAQvCP,YAGJ,MAAO,IAAIE,KACP,MAAMM,EAAMP,KAAYC,GAExB,GAAIG,EAAMI,IAAID,GACV,OAAOH,EAAMK,IAAIF,GACd,CACH,MAAMG,EAAcf,KAAMM,GAE1B,OADAG,EAAMO,IAAIJ,EAAKG,GACRA,CACV,EACL,EAeEE,EAAeA,CACjBjB,EACAC,EAAkC,CAAE,KACpC,IAAAiB,EAAAC,EACA,MAAMR,EAA6BV,EAAQU,OACrCP,EAAyB,OAAlBc,EAAGjB,EAAQG,SAAOc,EA9DX,IAgEdb,EAA2B,OAAnBc,EAAGlB,EAAQI,UAAQc,EAC5B,IAAIb,IAAeC,KAAKC,UAAUF,GAEjCc,EAAY,IAAIC,EAEhBZ,EAAQ,IAAIC,EAAyB,CACvCC,SACAP,YAGJ,OAAOkB,SAAUhB,KACb,MAAMM,EAAMP,KAAYC,GAExB,GAAIG,EAAMI,IAAID,GACV,OAAOH,EAAMK,IAAIF,GAEjB,IAGI,SAFMQ,EAAUG,QAAQX,GAEpBH,EAAMI,IAAID,GACV,OAAOH,EAAMK,IAAIF,GACd,CACH,MAAMG,QAAoBf,KAAMM,GAEhC,OADAG,EAAMO,IAAIJ,EAAKG,GACRA,CACV,CACJ,CAAA,QACGK,EAAUI,QAAQZ,EACrB,CACJ,CACL"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"@chriscdn/promise-semaphore";import r from"quick-lru";var n=function(e,n){var t,i;void 0===n&&(n={});var l=null!=(t=n.maxSize)?t:1e3,a=null!=(i=n.resolver)?i:function(){return JSON.stringify([].slice.call(arguments))},o=new r({maxAge:n.maxAge,maxSize:l});return function(){var r=[].slice.call(arguments),n=a.apply(void 0,r);if(o.has(n))return o.get(n);var t=e.apply(void 0,r);return o.set(n,t),t}},t=function(n,t){var i,l;void 0===t&&(t={});var a=t.maxAge,o=null!=(i=t.maxSize)?i:1e3,u=null!=(l=t.resolver)?l:function(){return JSON.stringify([].slice.call(arguments))},c=new e,s=new r({maxAge:a,maxSize:o});return function(){try{var e=[].slice.call(arguments),r=u.apply(void 0,e);return Promise.resolve(s.has(r)?s.get(r):function(t,i){try{var l=Promise.resolve(c.acquire(r)).then(function(){return s.has(r)?s.get(r):Promise.resolve(n.apply(void 0,e)).then(function(e){return s.set(r,e),e})})}catch(e){return i(!0,e)}return l&&l.then?l.then(i.bind(null,!1),i.bind(null,!0)):i(!1,l)}(0,function(e,n){if(c.release(r),e)throw n;return n}))}catch(e){return Promise.reject(e)}}};export{n as Memoize,t as MemoizeAsync};
|
|
2
|
+
//# sourceMappingURL=memoize.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoize.module.js","sources":["../src/index.ts"],"sourcesContent":["import Semaphore from \"@chriscdn/promise-semaphore\";\nimport QuickLRU from \"quick-lru\";\n\nconst kDefaultMaxSize = 1000;\n\ntype Options<T extends any[]> = {\n maxSize: number;\n maxAge?: number;\n resolver: (...args: T) => string;\n};\n\n/**\n * Memoize a synchronous function.\n *\n * @template {any[]} Args\n * @template {{}} Return\n * @param {(...args: Args) => Return} cb\n * @param {Partial<Options<Args>>} [options={}]\n * @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}\n */\nconst Memoize = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Return,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return (...args: Args): Return => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n };\n};\n\n/**\n * Memoize an asynchronous function.\n *\n * This differs from the sychronous case by ensuring multiple calls with the\n * same arguments is only evaluated once. This is controlled by using a\n * semaphore, which forces redundant calls to wait until the first call\n * completes.\n *\n * @param cb\n * @param options\n * @returns\n */\nconst MemoizeAsync = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Promise<Return>,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const semaphore = new Semaphore();\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return async (...args: Args): Promise<Return> => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n try {\n await semaphore.acquire(key);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = await cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n } finally {\n semaphore.release(key);\n }\n }\n };\n};\n\nexport { Memoize, MemoizeAsync };\n"],"names":["Memoize","cb","options","_options$maxSize","_options$resolver","maxSize","resolver","JSON","stringify","slice","call","arguments","cache","QuickLRU","maxAge","args","key","apply","has","get","returnValue","set","MemoizeAsync","_options$maxSize2","_options$resolver2","semaphore","Semaphore","_arguments","Promise","resolve","acquire","then","_finallyRethrows","_wasThrown","_result","release","e","reject"],"mappings":"oEAGA,IAiBMA,EAAU,SACZC,EACAC,GACA,IAAAC,EAAAC,OADkC,IAAlCF,IAAAA,EAAkC,CAAA,GAElC,IACMG,EAAyB,OAAlBF,EAAGD,EAAQG,SAAOF,EAtBX,IAwBdG,EAA2B,OAAnBF,EAAGF,EAAQI,UAAQF,EAC5B,WAAmB,OAAAG,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCC,EAAQ,IAAIC,EAAyB,CACvCC,OAP+BZ,EAAQY,OAQvCT,QAAAA,IAGJ,kBAAW,IAAAU,EAAU,GAAAN,MAAAC,KAAAC,WACXK,EAAMV,EAAQW,WAAA,EAAIF,GAExB,GAAIH,EAAMM,IAAIF,GACV,OAAOJ,EAAMO,IAAIH,GAEjB,IAAMI,EAAcnB,EAAEgB,WAAIF,EAAAA,GAE1B,OADAH,EAAMS,IAAIL,EAAKI,GACRA,CAEf,CACJ,EAcME,EAAe,SACjBrB,EACAC,GACA,IAAAqB,EAAAC,OADkC,IAAlCtB,IAAAA,EAAkC,CAAE,GAEpC,IAAMY,EAA6BZ,EAAQY,OACrCT,EAAyB,OAAlBkB,EAAGrB,EAAQG,SAAOkB,EA9DX,IAgEdjB,EAA2B,OAAnBkB,EAAGtB,EAAQI,UAAQkB,EAC5B,WAAA,OAAmBjB,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCc,EAAY,IAAIC,EAEhBd,EAAQ,IAAIC,EAAyB,CACvCC,OAAAA,EACAT,QAAAA,IAGJ,OAAA,WAAA,IAAgDsB,IAA/BZ,EAAU,GAAAN,MAAAC,KAAqBC,WACtCK,EAAMV,EAAQW,WAAIF,EAAAA,GAAM,OAAAa,QAAAC,QAE1BjB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,2BAEbY,QAAAC,QACMJ,EAAUK,QAAQd,IAAIe,KAAA,WAAA,OAExBnB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,GAAMY,QAAAC,QAEG5B,EAAEgB,WAAIF,EAAAA,IAAKgB,KAAA,SAA/BX,GAEN,OADAR,EAAMS,IAAIL,EAAKI,GACRA,CAAY,EAE1B,4FAZsBY,CAEnB,EAUHC,SAAAA,EAAAC,GAC0B,GAAvBT,EAAUU,QAAQnB,GAAKiB,EAAAC,MAAAA,EAAAA,OAAAA,CAAA,GAGnC,CAAC,MAAAE,GAAAR,OAAAA,QAAAS,OAAAD,EACL,CAAA,CAAA"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@chriscdn/promise-semaphore"),require("quick-lru")):"function"==typeof define&&define.amd?define(["exports","@chriscdn/promise-semaphore","quick-lru"],r):r((e||self).memoize={},e.Semaphore,e.quickLru)}(this,function(e,r,n){function i(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=/*#__PURE__*/i(r),o=/*#__PURE__*/i(n);e.Memoize=function(e,r){var n,i;void 0===r&&(r={});var t=null!=(n=r.maxSize)?n:1e3,u=null!=(i=r.resolver)?i:function(){return JSON.stringify([].slice.call(arguments))},l=new o.default({maxAge:r.maxAge,maxSize:t});return function(){var r=[].slice.call(arguments),n=u.apply(void 0,r);if(l.has(n))return l.get(n);var i=e.apply(void 0,r);return l.set(n,i),i}},e.MemoizeAsync=function(e,r){var n,i;void 0===r&&(r={});var u=r.maxAge,l=null!=(n=r.maxSize)?n:1e3,a=null!=(i=r.resolver)?i:function(){return JSON.stringify([].slice.call(arguments))},c=new t.default,s=new o.default({maxAge:u,maxSize:l});return function(){try{var r=[].slice.call(arguments),n=a.apply(void 0,r);return Promise.resolve(s.has(n)?s.get(n):function(i,t){try{var o=Promise.resolve(c.acquire(n)).then(function(){return s.has(n)?s.get(n):Promise.resolve(e.apply(void 0,r)).then(function(e){return s.set(n,e),e})})}catch(e){return t(!0,e)}return o&&o.then?o.then(t.bind(null,!1),t.bind(null,!0)):t(!1,o)}(0,function(e,r){if(c.release(n),e)throw r;return r}))}catch(e){return Promise.reject(e)}}}});
|
|
2
|
+
//# sourceMappingURL=memoize.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memoize.umd.js","sources":["../src/index.ts"],"sourcesContent":["import Semaphore from \"@chriscdn/promise-semaphore\";\nimport QuickLRU from \"quick-lru\";\n\nconst kDefaultMaxSize = 1000;\n\ntype Options<T extends any[]> = {\n maxSize: number;\n maxAge?: number;\n resolver: (...args: T) => string;\n};\n\n/**\n * Memoize a synchronous function.\n *\n * @template {any[]} Args\n * @template {{}} Return\n * @param {(...args: Args) => Return} cb\n * @param {Partial<Options<Args>>} [options={}]\n * @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}\n */\nconst Memoize = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Return,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return (...args: Args): Return => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n };\n};\n\n/**\n * Memoize an asynchronous function.\n *\n * This differs from the sychronous case by ensuring multiple calls with the\n * same arguments is only evaluated once. This is controlled by using a\n * semaphore, which forces redundant calls to wait until the first call\n * completes.\n *\n * @param cb\n * @param options\n * @returns\n */\nconst MemoizeAsync = <Args extends any[], Return extends {}>(\n cb: (...args: Args) => Promise<Return>,\n options: Partial<Options<Args>> = {},\n) => {\n const maxAge: number | undefined = options.maxAge;\n const maxSize = options.maxSize ?? kDefaultMaxSize;\n\n const resolver = options.resolver ??\n ((...args: Args) => JSON.stringify(args));\n\n const semaphore = new Semaphore();\n\n const cache = new QuickLRU<string, Return>({\n maxAge,\n maxSize,\n });\n\n return async (...args: Args): Promise<Return> => {\n const key = resolver(...args);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n try {\n await semaphore.acquire(key);\n\n if (cache.has(key)) {\n return cache.get(key)!;\n } else {\n const returnValue = await cb(...args);\n cache.set(key, returnValue);\n return returnValue;\n }\n } finally {\n semaphore.release(key);\n }\n }\n };\n};\n\nexport { Memoize, MemoizeAsync };\n"],"names":["cb","options","_options$maxSize","_options$resolver","maxSize","resolver","JSON","stringify","slice","call","arguments","cache","QuickLRU","maxAge","args","key","apply","has","get","returnValue","set","_options$maxSize2","_options$resolver2","semaphore","Semaphore","_arguments","Promise","resolve","acquire","then","_finallyRethrows","_wasThrown","_result","release","e","reject"],"mappings":"geAoBgB,SACZA,EACAC,GACA,IAAAC,EAAAC,OADkC,IAAlCF,IAAAA,EAAkC,CAAA,GAElC,IACMG,EAAyB,OAAlBF,EAAGD,EAAQG,SAAOF,EAtBX,IAwBdG,EAA2B,OAAnBF,EAAGF,EAAQI,UAAQF,EAC5B,WAAmB,OAAAG,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCC,EAAQ,IAAIC,EAAQ,QAAiB,CACvCC,OAP+BZ,EAAQY,OAQvCT,QAAAA,IAGJ,kBAAW,IAAAU,EAAU,GAAAN,MAAAC,KAAAC,WACXK,EAAMV,EAAQW,WAAA,EAAIF,GAExB,GAAIH,EAAMM,IAAIF,GACV,OAAOJ,EAAMO,IAAIH,GAEjB,IAAMI,EAAcnB,EAAEgB,WAAIF,EAAAA,GAE1B,OADAH,EAAMS,IAAIL,EAAKI,GACRA,CAEf,CACJ,iBAcqB,SACjBnB,EACAC,GACA,IAAAoB,EAAAC,OADkC,IAAlCrB,IAAAA,EAAkC,CAAE,GAEpC,IAAMY,EAA6BZ,EAAQY,OACrCT,EAAyB,OAAlBiB,EAAGpB,EAAQG,SAAOiB,EA9DX,IAgEdhB,EAA2B,OAAnBiB,EAAGrB,EAAQI,UAAQiB,EAC5B,WAAA,OAAmBhB,KAAKC,UAASC,GAAAA,MAAAC,KAAAC,WAAM,EAEtCa,EAAY,IAAIC,EAAW,QAE3Bb,EAAQ,IAAIC,EAAQ,QAAiB,CACvCC,OAAAA,EACAT,QAAAA,IAGJ,OAAA,WAAA,IAAgDqB,IAA/BX,EAAU,GAAAN,MAAAC,KAAqBC,WACtCK,EAAMV,EAAQW,WAAIF,EAAAA,GAAM,OAAAY,QAAAC,QAE1BhB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,2BAEbW,QAAAC,QACMJ,EAAUK,QAAQb,IAAIc,KAAA,WAAA,OAExBlB,EAAMM,IAAIF,GACHJ,EAAMO,IAAIH,GAAMW,QAAAC,QAEG3B,EAAEgB,WAAIF,EAAAA,IAAKe,KAAA,SAA/BV,GAEN,OADAR,EAAMS,IAAIL,EAAKI,GACRA,CAAY,EAE1B,4FAZsBW,CAEnB,EAUHC,SAAAA,EAAAC,GAC0B,GAAvBT,EAAUU,QAAQlB,GAAKgB,EAAAC,MAAAA,EAAAA,OAAAA,CAAA,GAGnC,CAAC,MAAAE,GAAAR,OAAAA,QAAAS,OAAAD,EACL,CAAA,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chriscdn/memoize",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Memoize a synchronous or asynchronous function.",
|
|
5
|
+
"repository": "https://github.com/chriscdn/memoize",
|
|
6
|
+
"author": "Christopher Meyer <chris@schwiiz.org>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"require": "./lib/memoize.cjs",
|
|
13
|
+
"default": "./lib/memoize.modern.js"
|
|
14
|
+
},
|
|
15
|
+
"main": "./lib/memoize.cjs",
|
|
16
|
+
"module": "./lib/memoize.module.js",
|
|
17
|
+
"unpkg": "./lib/memoize.umd.js",
|
|
18
|
+
"types": "./lib/index.d.ts",
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "rm -rf ./lib/ && microbundle",
|
|
21
|
+
"dev": "microbundle watch",
|
|
22
|
+
"test": "vitest"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@chriscdn/promise-semaphore": "^2.0.9",
|
|
26
|
+
"quick-lru": "^7.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"microbundle": "^0.15.1",
|
|
30
|
+
"vitest": "^2.1.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Semaphore from "@chriscdn/promise-semaphore";
|
|
2
|
+
import QuickLRU from "quick-lru";
|
|
3
|
+
|
|
4
|
+
const kDefaultMaxSize = 1000;
|
|
5
|
+
|
|
6
|
+
type Options<T extends any[]> = {
|
|
7
|
+
maxSize: number;
|
|
8
|
+
maxAge?: number;
|
|
9
|
+
resolver: (...args: T) => string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Memoize a synchronous function.
|
|
14
|
+
*
|
|
15
|
+
* @template {any[]} Args
|
|
16
|
+
* @template {{}} Return
|
|
17
|
+
* @param {(...args: Args) => Return} cb
|
|
18
|
+
* @param {Partial<Options<Args>>} [options={}]
|
|
19
|
+
* @returns {Return, options?: Partial<Options<Args>>) => (...args: Args) => Return}
|
|
20
|
+
*/
|
|
21
|
+
const Memoize = <Args extends any[], Return extends {}>(
|
|
22
|
+
cb: (...args: Args) => Return,
|
|
23
|
+
options: Partial<Options<Args>> = {},
|
|
24
|
+
) => {
|
|
25
|
+
const maxAge: number | undefined = options.maxAge;
|
|
26
|
+
const maxSize = options.maxSize ?? kDefaultMaxSize;
|
|
27
|
+
|
|
28
|
+
const resolver = options.resolver ??
|
|
29
|
+
((...args: Args) => JSON.stringify(args));
|
|
30
|
+
|
|
31
|
+
const cache = new QuickLRU<string, Return>({
|
|
32
|
+
maxAge,
|
|
33
|
+
maxSize,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (...args: Args): Return => {
|
|
37
|
+
const key = resolver(...args);
|
|
38
|
+
|
|
39
|
+
if (cache.has(key)) {
|
|
40
|
+
return cache.get(key)!;
|
|
41
|
+
} else {
|
|
42
|
+
const returnValue = cb(...args);
|
|
43
|
+
cache.set(key, returnValue);
|
|
44
|
+
return returnValue;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Memoize an asynchronous function.
|
|
51
|
+
*
|
|
52
|
+
* This differs from the sychronous case by ensuring multiple calls with the
|
|
53
|
+
* same arguments is only evaluated once. This is controlled by using a
|
|
54
|
+
* semaphore, which forces redundant calls to wait until the first call
|
|
55
|
+
* completes.
|
|
56
|
+
*
|
|
57
|
+
* @param cb
|
|
58
|
+
* @param options
|
|
59
|
+
* @returns
|
|
60
|
+
*/
|
|
61
|
+
const MemoizeAsync = <Args extends any[], Return extends {}>(
|
|
62
|
+
cb: (...args: Args) => Promise<Return>,
|
|
63
|
+
options: Partial<Options<Args>> = {},
|
|
64
|
+
) => {
|
|
65
|
+
const maxAge: number | undefined = options.maxAge;
|
|
66
|
+
const maxSize = options.maxSize ?? kDefaultMaxSize;
|
|
67
|
+
|
|
68
|
+
const resolver = options.resolver ??
|
|
69
|
+
((...args: Args) => JSON.stringify(args));
|
|
70
|
+
|
|
71
|
+
const semaphore = new Semaphore();
|
|
72
|
+
|
|
73
|
+
const cache = new QuickLRU<string, Return>({
|
|
74
|
+
maxAge,
|
|
75
|
+
maxSize,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return async (...args: Args): Promise<Return> => {
|
|
79
|
+
const key = resolver(...args);
|
|
80
|
+
|
|
81
|
+
if (cache.has(key)) {
|
|
82
|
+
return cache.get(key)!;
|
|
83
|
+
} else {
|
|
84
|
+
try {
|
|
85
|
+
await semaphore.acquire(key);
|
|
86
|
+
|
|
87
|
+
if (cache.has(key)) {
|
|
88
|
+
return cache.get(key)!;
|
|
89
|
+
} else {
|
|
90
|
+
const returnValue = await cb(...args);
|
|
91
|
+
cache.set(key, returnValue);
|
|
92
|
+
return returnValue;
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
semaphore.release(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { Memoize, MemoizeAsync };
|