@naturalcycles/js-lib 14.214.0 → 14.216.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/dist/decorators/asyncMemo.decorator.d.ts +22 -20
- package/dist/decorators/asyncMemo.decorator.js +81 -58
- package/dist/decorators/memo.decorator.d.ts +20 -13
- package/dist/decorators/memo.decorator.js +43 -41
- package/dist/decorators/memo.util.d.ts +33 -14
- package/dist/decorators/memo.util.js +27 -1
- package/dist/decorators/memoFn.d.ts +2 -0
- package/dist/decorators/memoFn.js +8 -24
- package/dist/decorators/memoFnAsync.d.ts +2 -3
- package/dist/decorators/memoFnAsync.js +14 -32
- package/dist/object/deepEquals.d.ts +58 -6
- package/dist/object/deepEquals.js +146 -52
- package/dist/types.d.ts +5 -0
- package/dist/types.js +6 -1
- package/dist-esm/decorators/asyncMemo.decorator.js +80 -58
- package/dist-esm/decorators/memo.decorator.js +41 -40
- package/dist-esm/decorators/memo.util.js +26 -1
- package/dist-esm/decorators/memoFn.js +8 -24
- package/dist-esm/decorators/memoFnAsync.js +14 -32
- package/dist-esm/object/deepEquals.js +142 -49
- package/dist-esm/types.js +5 -0
- package/package.json +1 -4
- package/src/decorators/asyncMemo.decorator.ts +106 -80
- package/src/decorators/memo.decorator.ts +55 -51
- package/src/decorators/memo.util.ts +49 -19
- package/src/decorators/memoFn.ts +9 -27
- package/src/decorators/memoFnAsync.ts +12 -31
- package/src/object/deepEquals.ts +136 -45
- package/src/types.ts +6 -0
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger';
|
|
2
|
-
import
|
|
2
|
+
import { AnyAsyncFunction, AnyFunction, AnyObject } from '../types';
|
|
3
|
+
import { AsyncMemoCache } from './memo.util';
|
|
3
4
|
export interface AsyncMemoOptions {
|
|
4
5
|
/**
|
|
5
|
-
* Provide a custom implementation of
|
|
6
|
-
* Function that creates an instance of `
|
|
7
|
-
* e.g LRUMemoCache from `@naturalcycles/nodejs-lib`.
|
|
6
|
+
* Provide a custom implementation of AsyncMemoCache.
|
|
7
|
+
* Function that creates an instance of `AsyncMemoCache`.
|
|
8
8
|
*/
|
|
9
|
-
cacheFactory
|
|
9
|
+
cacheFactory: () => AsyncMemoCache;
|
|
10
10
|
/**
|
|
11
11
|
* Provide a custom implementation of CacheKey function.
|
|
12
12
|
*/
|
|
13
13
|
cacheKeyFn?: (args: any[]) => any;
|
|
14
|
-
/**
|
|
15
|
-
* Default true.
|
|
16
|
-
*
|
|
17
|
-
* Set to `false` to skip caching rejected promises (errors).
|
|
18
|
-
*
|
|
19
|
-
* True will ensure "max 1 execution", but will "remember" rejection.
|
|
20
|
-
* False will allow >1 execution in case of errors.
|
|
21
|
-
*/
|
|
22
|
-
cacheRejections?: boolean;
|
|
23
14
|
/**
|
|
24
15
|
* Default to `console`
|
|
25
16
|
*/
|
|
26
17
|
logger?: CommonLogger;
|
|
27
18
|
}
|
|
19
|
+
export interface AsyncMemoInstance {
|
|
20
|
+
/**
|
|
21
|
+
* Clears the cache.
|
|
22
|
+
*/
|
|
23
|
+
clear: () => Promise<void>;
|
|
24
|
+
getInstanceCache: () => Map<AnyObject, AsyncMemoCache>;
|
|
25
|
+
getCache: (instance: AnyAsyncFunction) => AsyncMemoCache | undefined;
|
|
26
|
+
}
|
|
28
27
|
/**
|
|
29
28
|
* Like @_Memo, but allowing async MemoCache implementation.
|
|
30
29
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* todo: test for "swarm requests", it should return "the same promise" and not cause a swarm origin hit
|
|
30
|
+
* Implementation is more complex than @_Memo, because it needs to handle "in-flight" Promises
|
|
31
|
+
* while waiting for cache to resolve, to prevent "async swarm" issue.
|
|
34
32
|
*
|
|
35
|
-
*
|
|
36
|
-
|
|
33
|
+
* @experimental consider normal @_Memo for most of the cases, it's stable and predictable
|
|
34
|
+
*/
|
|
35
|
+
export declare const _AsyncMemo: (opt: AsyncMemoOptions) => MethodDecorator;
|
|
36
|
+
/**
|
|
37
|
+
Call it on a method that is decorated with `@_AsyncMemo` to get access to additional functions,
|
|
38
|
+
e.g `clear` to clear the cache, or get its underlying data.
|
|
37
39
|
*/
|
|
38
|
-
export declare
|
|
40
|
+
export declare function _getAsyncMemo(method: AnyFunction): AsyncMemoInstance;
|
|
@@ -1,95 +1,118 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports._AsyncMemo = void 0;
|
|
3
|
+
exports._getAsyncMemo = exports._AsyncMemo = void 0;
|
|
4
|
+
const assert_1 = require("../error/assert");
|
|
5
|
+
const types_1 = require("../types");
|
|
4
6
|
const decorator_util_1 = require("./decorator.util");
|
|
5
7
|
const memo_util_1 = require("./memo.util");
|
|
6
8
|
/**
|
|
7
9
|
* Like @_Memo, but allowing async MemoCache implementation.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* Implementation is more complex than @_Memo, because it needs to handle "in-flight" Promises
|
|
12
|
+
* while waiting for cache to resolve, to prevent "async swarm" issue.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
|
|
14
|
-
* Return `null` instead (it'll be cached).
|
|
14
|
+
* @experimental consider normal @_Memo for most of the cases, it's stable and predictable
|
|
15
15
|
*/
|
|
16
16
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
17
|
-
const _AsyncMemo = (opt
|
|
17
|
+
const _AsyncMemo = (opt) => (target, key, descriptor) => {
|
|
18
18
|
if (typeof descriptor.value !== 'function') {
|
|
19
19
|
throw new TypeError('Memoization can be applied only to methods');
|
|
20
20
|
}
|
|
21
21
|
const originalFn = descriptor.value;
|
|
22
22
|
// Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const instanceCache = new Map();
|
|
24
|
+
// Cache from Instance to Map<key, Promise>
|
|
25
|
+
// This cache is temporary, with only one purpose - to prevent "async swarm"
|
|
26
|
+
// It only holds values that are "in-flight", until Promise is resolved
|
|
27
|
+
// After it's resolved - it's evicted from the cache and moved to the "proper" `instanceCache`
|
|
28
|
+
const instancePromiseCache = new Map();
|
|
29
|
+
const { logger = console, cacheFactory, cacheKeyFn = memo_util_1.jsonMemoSerializer } = opt;
|
|
25
30
|
const keyStr = String(key);
|
|
26
31
|
const methodSignature = (0, decorator_util_1._getTargetMethodSignature)(target, keyStr);
|
|
27
|
-
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
33
|
+
descriptor.value = function (...args) {
|
|
28
34
|
const ctx = this;
|
|
29
35
|
const cacheKey = cacheKeyFn(args);
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
let cache = instanceCache.get(ctx);
|
|
37
|
+
let promiseCache = instancePromiseCache.get(ctx);
|
|
38
|
+
if (!cache) {
|
|
39
|
+
cache = cacheFactory();
|
|
40
|
+
instanceCache.set(ctx, cache);
|
|
32
41
|
// here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created
|
|
33
42
|
// UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache)
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
if (!promiseCache) {
|
|
45
|
+
promiseCache = new Map();
|
|
46
|
+
instancePromiseCache.set(ctx, promiseCache);
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
let promise = promiseCache.get(cacheKey);
|
|
49
|
+
// If there's already "in-flight" cache request - return that, to avoid "async swarm"
|
|
50
|
+
if (promise) {
|
|
51
|
+
// console.log('return promise', promiseCache.size)
|
|
52
|
+
return promise;
|
|
42
53
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
promise = cache.get(cacheKey).then(async (value) => {
|
|
55
|
+
if (value !== types_1.MISS) {
|
|
56
|
+
// console.log('hit', promiseCache.size)
|
|
57
|
+
promiseCache.delete(cacheKey);
|
|
58
|
+
return value;
|
|
47
59
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
if (cacheRejections) {
|
|
68
|
-
// We put it to cache as raw Error, not Promise.reject(err)
|
|
69
|
-
// This is to support both sync and async functions
|
|
60
|
+
// Miss
|
|
61
|
+
// console.log('miss', promiseCache.size)
|
|
62
|
+
return await onMiss();
|
|
63
|
+
}, async (err) => {
|
|
64
|
+
// Log the cache error and proceed "as cache Miss"
|
|
65
|
+
logger.error(err);
|
|
66
|
+
return await onMiss();
|
|
67
|
+
});
|
|
68
|
+
promiseCache.set(cacheKey, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
//
|
|
71
|
+
async function onMiss() {
|
|
72
|
+
try {
|
|
73
|
+
const value = await originalFn.apply(ctx, args);
|
|
74
|
+
// Save the value in the Cache, in parallel,
|
|
75
|
+
// not to slow down the main function execution
|
|
76
|
+
// and not to fail on possible cache issues
|
|
70
77
|
void (async () => {
|
|
71
78
|
try {
|
|
72
|
-
await cache.
|
|
79
|
+
await cache.set(cacheKey, value);
|
|
73
80
|
}
|
|
74
81
|
catch (err) {
|
|
75
|
-
// log and ignore the error
|
|
76
|
-
|
|
82
|
+
logger.error(err); // log and ignore the error
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
// Clear the "in-flight" promise cache entry, as we now have a "permanent" cache entry
|
|
86
|
+
promiseCache.delete(cacheKey);
|
|
87
|
+
// console.log('cache set and cleared', promiseCache!.size)
|
|
77
88
|
}
|
|
78
89
|
})();
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
promiseCache.delete(cacheKey);
|
|
94
|
+
throw err;
|
|
79
95
|
}
|
|
80
|
-
throw err;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
descriptor.value.dropCache = async () => {
|
|
84
|
-
logger.log(`${methodSignature} @_AsyncMemo.dropCache()`);
|
|
85
|
-
try {
|
|
86
|
-
await Promise.all([...cache.values()].map(c => c.clear()));
|
|
87
|
-
cache.clear();
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
logger.error(err);
|
|
91
96
|
}
|
|
92
97
|
};
|
|
98
|
+
(0, types_1._objectAssign)(descriptor.value, {
|
|
99
|
+
clear: async () => {
|
|
100
|
+
logger.log(`${methodSignature} @_AsyncMemo.clear()`);
|
|
101
|
+
await Promise.all([...instanceCache.values()].map(c => c.clear()));
|
|
102
|
+
instanceCache.clear();
|
|
103
|
+
},
|
|
104
|
+
getInstanceCache: () => instanceCache,
|
|
105
|
+
getCache: instance => instanceCache.get(instance),
|
|
106
|
+
});
|
|
93
107
|
return descriptor;
|
|
94
108
|
};
|
|
95
109
|
exports._AsyncMemo = _AsyncMemo;
|
|
110
|
+
/**
|
|
111
|
+
Call it on a method that is decorated with `@_AsyncMemo` to get access to additional functions,
|
|
112
|
+
e.g `clear` to clear the cache, or get its underlying data.
|
|
113
|
+
*/
|
|
114
|
+
function _getAsyncMemo(method) {
|
|
115
|
+
(0, assert_1._assert)(typeof method?.getInstanceCache === 'function', 'method is not an AsyncMemo instance');
|
|
116
|
+
return method;
|
|
117
|
+
}
|
|
118
|
+
exports._getAsyncMemo = _getAsyncMemo;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger';
|
|
2
|
+
import { AnyFunction, AnyObject } from '../types';
|
|
2
3
|
import type { MemoCache } from './memo.util';
|
|
3
4
|
export interface MemoOptions {
|
|
4
5
|
/**
|
|
@@ -11,31 +12,32 @@ export interface MemoOptions {
|
|
|
11
12
|
* Provide a custom implementation of CacheKey function.
|
|
12
13
|
*/
|
|
13
14
|
cacheKeyFn?: (args: any[]) => any;
|
|
14
|
-
/**
|
|
15
|
-
* Defaults to true.
|
|
16
|
-
* Set to false to skip caching errors.
|
|
17
|
-
*
|
|
18
|
-
* True will ensure "max 1 execution", but will "remember" errors.
|
|
19
|
-
* False will allow >1 execution in case of errors.
|
|
20
|
-
*/
|
|
21
|
-
cacheErrors?: boolean;
|
|
22
15
|
/**
|
|
23
16
|
* Default to `console`
|
|
24
17
|
*/
|
|
25
18
|
logger?: CommonLogger;
|
|
26
19
|
}
|
|
20
|
+
export interface MemoInstance {
|
|
21
|
+
/**
|
|
22
|
+
* Clears the cache.
|
|
23
|
+
*/
|
|
24
|
+
clear: () => void;
|
|
25
|
+
getInstanceCache: () => Map<AnyObject, MemoCache>;
|
|
26
|
+
getCache: (instance: AnyFunction) => MemoCache | undefined;
|
|
27
|
+
}
|
|
27
28
|
/**
|
|
28
29
|
* Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
|
|
29
30
|
* of the cache is the same. Key, by defaul, is calculated as `JSON.stringify(...args)`.
|
|
30
|
-
* Cache is stored indefinitely in internal Map.
|
|
31
|
+
* Cache is stored indefinitely in the internal Map.
|
|
32
|
+
*
|
|
33
|
+
* If origin function throws an Error - it is NOT cached.
|
|
34
|
+
* So, error-throwing functions will be called multiple times.
|
|
35
|
+
* Therefor, if the origin function can possibly throw - it should try to be idempotent.
|
|
31
36
|
*
|
|
32
37
|
* Cache is stored **per instance** - separate cache for separate instances of the class.
|
|
33
38
|
* If you don't want it that way - you can use a static method, then there will be only one "instance".
|
|
34
39
|
*
|
|
35
|
-
* Supports dropping it's cache by calling .
|
|
36
|
-
*
|
|
37
|
-
* Doesn't support Async functions, use @_AsyncMemo instead!
|
|
38
|
-
* (or, it will simply return the [unresolved] Promise further, without awaiting it)
|
|
40
|
+
* Supports dropping it's cache by calling .clear() method of decorated function (useful in unit testing).
|
|
39
41
|
*
|
|
40
42
|
* Based on:
|
|
41
43
|
* https://github.com/mgechev/memo-decorator/blob/master/index.ts
|
|
@@ -44,3 +46,8 @@ export interface MemoOptions {
|
|
|
44
46
|
* https://community.risingstack.com/the-worlds-fastest-javascript-memoization-library/
|
|
45
47
|
*/
|
|
46
48
|
export declare const _Memo: (opt?: MemoOptions) => MethodDecorator;
|
|
49
|
+
/**
|
|
50
|
+
Call it on a method that is decorated with `@_Memo` to get access to additional functions,
|
|
51
|
+
e.g `clear` to clear the cache, or get its underlying data.
|
|
52
|
+
*/
|
|
53
|
+
export declare function _getMemo(method: AnyFunction): MemoInstance;
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports._Memo = void 0;
|
|
3
|
+
exports._getMemo = exports._Memo = void 0;
|
|
4
|
+
const assert_1 = require("../error/assert");
|
|
5
|
+
const types_1 = require("../types");
|
|
4
6
|
const decorator_util_1 = require("./decorator.util");
|
|
5
7
|
const memo_util_1 = require("./memo.util");
|
|
6
8
|
/**
|
|
7
9
|
* Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
|
|
8
10
|
* of the cache is the same. Key, by defaul, is calculated as `JSON.stringify(...args)`.
|
|
9
|
-
* Cache is stored indefinitely in internal Map.
|
|
11
|
+
* Cache is stored indefinitely in the internal Map.
|
|
12
|
+
*
|
|
13
|
+
* If origin function throws an Error - it is NOT cached.
|
|
14
|
+
* So, error-throwing functions will be called multiple times.
|
|
15
|
+
* Therefor, if the origin function can possibly throw - it should try to be idempotent.
|
|
10
16
|
*
|
|
11
17
|
* Cache is stored **per instance** - separate cache for separate instances of the class.
|
|
12
18
|
* If you don't want it that way - you can use a static method, then there will be only one "instance".
|
|
13
19
|
*
|
|
14
|
-
* Supports dropping it's cache by calling .
|
|
15
|
-
*
|
|
16
|
-
* Doesn't support Async functions, use @_AsyncMemo instead!
|
|
17
|
-
* (or, it will simply return the [unresolved] Promise further, without awaiting it)
|
|
20
|
+
* Supports dropping it's cache by calling .clear() method of decorated function (useful in unit testing).
|
|
18
21
|
*
|
|
19
22
|
* Based on:
|
|
20
23
|
* https://github.com/mgechev/memo-decorator/blob/master/index.ts
|
|
@@ -31,55 +34,54 @@ const _Memo = (opt = {}) => (target, key, descriptor) => {
|
|
|
31
34
|
// Map<ctx => MemoCache<cacheKey, result>>
|
|
32
35
|
//
|
|
33
36
|
// Internal map is from cacheKey to result
|
|
34
|
-
// External map is from ctx (instance of class) to Internal map
|
|
37
|
+
// External map (instanceCache) is from ctx (instance of class) to Internal map
|
|
35
38
|
// External map is Weak to not cause memory leaks, to allow ctx objects to be garbage collected
|
|
36
39
|
// UPD: tests show that normal Map also doesn't leak (to be tested further)
|
|
37
|
-
// Normal Map is needed to allow .
|
|
38
|
-
const
|
|
39
|
-
const { logger = console, cacheFactory = () => new memo_util_1.MapMemoCache(), cacheKeyFn = memo_util_1.jsonMemoSerializer,
|
|
40
|
+
// Normal Map is needed to allow .clear()
|
|
41
|
+
const instanceCache = new Map();
|
|
42
|
+
const { logger = console, cacheFactory = () => new memo_util_1.MapMemoCache(), cacheKeyFn = memo_util_1.jsonMemoSerializer, } = opt;
|
|
40
43
|
const keyStr = String(key);
|
|
41
44
|
const methodSignature = (0, decorator_util_1._getTargetMethodSignature)(target, keyStr);
|
|
42
45
|
descriptor.value = function (...args) {
|
|
43
46
|
const ctx = this;
|
|
44
47
|
const cacheKey = cacheKeyFn(args);
|
|
45
|
-
let
|
|
46
|
-
if (!cache
|
|
47
|
-
cache
|
|
48
|
+
let cache = instanceCache.get(ctx);
|
|
49
|
+
if (!cache) {
|
|
50
|
+
cache = cacheFactory();
|
|
51
|
+
instanceCache.set(ctx, cache);
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
throw value;
|
|
53
|
-
}
|
|
54
|
-
return value;
|
|
53
|
+
if (cache.has(cacheKey)) {
|
|
54
|
+
// Hit
|
|
55
|
+
return cache.get(cacheKey);
|
|
55
56
|
}
|
|
57
|
+
// Miss
|
|
58
|
+
const value = originalFn.apply(ctx, args);
|
|
56
59
|
try {
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
cache.get(ctx).set(cacheKey, value);
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
logger.error(err);
|
|
63
|
-
}
|
|
64
|
-
return value;
|
|
60
|
+
cache.set(cacheKey, value);
|
|
65
61
|
}
|
|
66
62
|
catch (err) {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
cache.get(ctx).set(cacheKey, err);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
logger.error(err);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
throw err;
|
|
63
|
+
logger.error(err);
|
|
76
64
|
}
|
|
65
|
+
return value;
|
|
77
66
|
};
|
|
78
|
-
descriptor.value
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
(0, types_1._objectAssign)(descriptor.value, {
|
|
68
|
+
clear: () => {
|
|
69
|
+
logger.log(`${methodSignature} @_Memo.clear()`);
|
|
70
|
+
instanceCache.forEach(memoCache => memoCache.clear());
|
|
71
|
+
instanceCache.clear();
|
|
72
|
+
},
|
|
73
|
+
getInstanceCache: () => instanceCache,
|
|
74
|
+
getCache: instance => instanceCache.get(instance),
|
|
75
|
+
});
|
|
83
76
|
return descriptor;
|
|
84
77
|
};
|
|
85
78
|
exports._Memo = _Memo;
|
|
79
|
+
/**
|
|
80
|
+
Call it on a method that is decorated with `@_Memo` to get access to additional functions,
|
|
81
|
+
e.g `clear` to clear the cache, or get its underlying data.
|
|
82
|
+
*/
|
|
83
|
+
function _getMemo(method) {
|
|
84
|
+
(0, assert_1._assert)(typeof method?.getInstanceCache === 'function', 'method is not a Memo instance');
|
|
85
|
+
return method;
|
|
86
|
+
}
|
|
87
|
+
exports._getMemo = _getMemo;
|
|
@@ -1,34 +1,53 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { MISS } from '..';
|
|
2
2
|
export type MemoSerializer = (args: any[]) => any;
|
|
3
3
|
export declare const jsonMemoSerializer: MemoSerializer;
|
|
4
4
|
export interface MemoCache<KEY = any, VALUE = any> {
|
|
5
5
|
has: (k: KEY) => boolean;
|
|
6
|
-
get: (k: KEY) => VALUE | Error | undefined;
|
|
7
|
-
set: (k: KEY, v: VALUE | Error) => void;
|
|
8
6
|
/**
|
|
9
|
-
*
|
|
7
|
+
* `get` return signature doesn't contain `undefined`,
|
|
8
|
+
* because `undefined` is a valid VALUE to store in the Cache.
|
|
9
|
+
* `undefined` does NOT mean cache miss.
|
|
10
|
+
* Cache misses are checked by calling `has` method instead.
|
|
11
|
+
*/
|
|
12
|
+
get: (k: KEY) => VALUE;
|
|
13
|
+
set: (k: KEY, v: VALUE) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Clear is only called when `_getMemoCache().clear()` is called.
|
|
10
16
|
* Otherwise the Cache is "persistent" (never cleared).
|
|
11
17
|
*/
|
|
12
18
|
clear: () => void;
|
|
13
19
|
}
|
|
14
20
|
export interface AsyncMemoCache<KEY = any, VALUE = any> {
|
|
15
21
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* MISS symbol indicates the ABSENCE of value in the Cache.
|
|
23
|
+
* You can safely store `undefined` or `null` values in the Cache,
|
|
24
|
+
* they will not be interpreted as a cache miss, because there is a special MISS symbol for that.
|
|
19
25
|
*/
|
|
20
|
-
get: (k: KEY) =>
|
|
21
|
-
set: (k: KEY, v: VALUE
|
|
26
|
+
get: (k: KEY) => Promise<VALUE | typeof MISS>;
|
|
27
|
+
set: (k: KEY, v: VALUE) => Promise<void>;
|
|
22
28
|
/**
|
|
23
|
-
* Clear is only called when
|
|
29
|
+
* Clear is only called when `_getAsyncMemo().clear()` is called.
|
|
24
30
|
* Otherwise the Cache is "persistent" (never cleared).
|
|
25
31
|
*/
|
|
26
|
-
clear: () =>
|
|
32
|
+
clear: () => Promise<void>;
|
|
27
33
|
}
|
|
28
|
-
export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALUE
|
|
34
|
+
export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALUE> {
|
|
29
35
|
private m;
|
|
30
36
|
has(k: KEY): boolean;
|
|
31
|
-
get(k: KEY): VALUE
|
|
32
|
-
set(k: KEY, v: VALUE
|
|
37
|
+
get(k: KEY): VALUE;
|
|
38
|
+
set(k: KEY, v: VALUE): void;
|
|
33
39
|
clear(): void;
|
|
34
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Implementation of AsyncMemoCache backed by a synchronous Map.
|
|
43
|
+
* Doesn't have a practical use except testing,
|
|
44
|
+
* because the point of AsyncMemoCache is to have an **async** backed cache.
|
|
45
|
+
*/
|
|
46
|
+
export declare class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
|
|
47
|
+
private delay;
|
|
48
|
+
constructor(delay?: number);
|
|
49
|
+
private m;
|
|
50
|
+
get(k: KEY): Promise<VALUE | typeof MISS>;
|
|
51
|
+
set(k: KEY, v: VALUE): Promise<void>;
|
|
52
|
+
clear(): Promise<void>;
|
|
53
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MapMemoCache = exports.jsonMemoSerializer = void 0;
|
|
3
|
+
exports.MapAsyncMemoCache = exports.MapMemoCache = exports.jsonMemoSerializer = void 0;
|
|
4
4
|
const __1 = require("..");
|
|
5
5
|
const jsonMemoSerializer = args => {
|
|
6
6
|
if (args.length === 0)
|
|
@@ -73,3 +73,29 @@ class MapMemoCache {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
exports.MapMemoCache = MapMemoCache;
|
|
76
|
+
/**
|
|
77
|
+
* Implementation of AsyncMemoCache backed by a synchronous Map.
|
|
78
|
+
* Doesn't have a practical use except testing,
|
|
79
|
+
* because the point of AsyncMemoCache is to have an **async** backed cache.
|
|
80
|
+
*/
|
|
81
|
+
class MapAsyncMemoCache {
|
|
82
|
+
constructor(delay = 0) {
|
|
83
|
+
this.delay = delay;
|
|
84
|
+
this.m = new Map();
|
|
85
|
+
}
|
|
86
|
+
async get(k) {
|
|
87
|
+
await (0, __1.pDelay)(this.delay);
|
|
88
|
+
if (!this.m.has(k))
|
|
89
|
+
return __1.MISS;
|
|
90
|
+
return this.m.get(k);
|
|
91
|
+
}
|
|
92
|
+
async set(k, v) {
|
|
93
|
+
await (0, __1.pDelay)(this.delay);
|
|
94
|
+
this.m.set(k, v);
|
|
95
|
+
}
|
|
96
|
+
async clear() {
|
|
97
|
+
await (0, __1.pDelay)(this.delay);
|
|
98
|
+
this.m.clear();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.MapAsyncMemoCache = MapAsyncMemoCache;
|
|
@@ -7,5 +7,7 @@ export interface MemoizedFunction {
|
|
|
7
7
|
* Only supports Sync functions.
|
|
8
8
|
* To support Async functions - use _memoFnAsync.
|
|
9
9
|
* Technically, you can use it with Async functions, but it'll return the Promise without awaiting it.
|
|
10
|
+
*
|
|
11
|
+
* @experimental
|
|
10
12
|
*/
|
|
11
13
|
export declare function _memoFn<T extends (...args: any[]) => any>(fn: T, opt?: MemoOptions): T & MemoizedFunction;
|
|
@@ -6,42 +6,26 @@ const memo_util_1 = require("./memo.util");
|
|
|
6
6
|
* Only supports Sync functions.
|
|
7
7
|
* To support Async functions - use _memoFnAsync.
|
|
8
8
|
* Technically, you can use it with Async functions, but it'll return the Promise without awaiting it.
|
|
9
|
+
*
|
|
10
|
+
* @experimental
|
|
9
11
|
*/
|
|
10
12
|
function _memoFn(fn, opt = {}) {
|
|
11
|
-
const { logger = console,
|
|
13
|
+
const { logger = console, cacheFactory = () => new memo_util_1.MapMemoCache(), cacheKeyFn = memo_util_1.jsonMemoSerializer, } = opt;
|
|
12
14
|
const cache = cacheFactory();
|
|
13
15
|
const memoizedFn = function (...args) {
|
|
14
16
|
const ctx = this;
|
|
15
17
|
const cacheKey = cacheKeyFn(args);
|
|
16
|
-
let value;
|
|
17
18
|
if (cache.has(cacheKey)) {
|
|
18
|
-
|
|
19
|
-
if (value instanceof Error) {
|
|
20
|
-
throw value;
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
19
|
+
return cache.get(cacheKey);
|
|
23
20
|
}
|
|
21
|
+
const value = fn.apply(ctx, args);
|
|
24
22
|
try {
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
cache.set(cacheKey, value);
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
logger.error(err);
|
|
31
|
-
}
|
|
32
|
-
return value;
|
|
23
|
+
cache.set(cacheKey, value);
|
|
33
24
|
}
|
|
34
25
|
catch (err) {
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
cache.set(cacheKey, err);
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
logger.error(err);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
throw err;
|
|
26
|
+
logger.error(err);
|
|
44
27
|
}
|
|
28
|
+
return value;
|
|
45
29
|
};
|
|
46
30
|
Object.assign(memoizedFn, { cache });
|
|
47
31
|
return memoizedFn;
|
|
@@ -4,7 +4,6 @@ export interface MemoizedAsyncFunction {
|
|
|
4
4
|
cache: AsyncMemoCache;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* To support Async functions - use _memoFnAsync
|
|
7
|
+
* @experimental
|
|
9
8
|
*/
|
|
10
|
-
export declare function _memoFnAsync<T extends (...args: any[]) => Promise<any>>(fn: T, opt
|
|
9
|
+
export declare function _memoFnAsync<T extends (...args: any[]) => Promise<any>>(fn: T, opt: AsyncMemoOptions): T & MemoizedAsyncFunction;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports._memoFnAsync = void 0;
|
|
4
|
+
const types_1 = require("../types");
|
|
4
5
|
const memo_util_1 = require("./memo.util");
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
* To support Async functions - use _memoFnAsync
|
|
7
|
+
* @experimental
|
|
8
8
|
*/
|
|
9
|
-
function _memoFnAsync(fn, opt
|
|
10
|
-
const { logger = console,
|
|
9
|
+
function _memoFnAsync(fn, opt) {
|
|
10
|
+
const { logger = console, cacheFactory = () => new memo_util_1.MapMemoCache(), cacheKeyFn = memo_util_1.jsonMemoSerializer, } = opt;
|
|
11
11
|
const cache = cacheFactory();
|
|
12
12
|
const memoizedFn = async function (...args) {
|
|
13
13
|
const ctx = this;
|
|
@@ -19,37 +19,19 @@ function _memoFnAsync(fn, opt = {}) {
|
|
|
19
19
|
catch (err) {
|
|
20
20
|
logger.error(err);
|
|
21
21
|
}
|
|
22
|
-
if (value !==
|
|
23
|
-
if (value instanceof Error) {
|
|
24
|
-
throw value;
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
value = await fn.apply(ctx, args);
|
|
30
|
-
void (async () => {
|
|
31
|
-
try {
|
|
32
|
-
await cache.set(cacheKey, value);
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
logger.error(err);
|
|
36
|
-
}
|
|
37
|
-
})();
|
|
22
|
+
if (value !== types_1.MISS) {
|
|
38
23
|
return value;
|
|
39
24
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
await cache.set(cacheKey, err);
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
logger.error(err);
|
|
48
|
-
}
|
|
49
|
-
})();
|
|
25
|
+
value = await fn.apply(ctx, args);
|
|
26
|
+
void (async () => {
|
|
27
|
+
try {
|
|
28
|
+
await cache.set(cacheKey, value);
|
|
50
29
|
}
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
catch (err) {
|
|
31
|
+
logger.error(err);
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return value;
|
|
53
35
|
};
|
|
54
36
|
Object.assign(memoizedFn, { cache });
|
|
55
37
|
return memoizedFn;
|