@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 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 };
@@ -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 };
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["vitest/importMeta"]
4
+ }
5
+ }