@naturalcycles/js-lib 14.82.0 → 14.84.1
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 -0
- package/dist/decorators/asyncMemo.decorator.js +96 -0
- package/dist/decorators/memo.decorator.d.ts +8 -0
- package/dist/decorators/memo.decorator.js +11 -6
- package/dist/decorators/memo.util.d.ts +27 -8
- package/dist/decorators/memo.util.js +1 -1
- package/dist/error/assert.d.ts +4 -4
- package/dist/error/error.model.d.ts +7 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/promise/pRetry.d.ts +5 -1
- package/dist/promise/pRetry.js +6 -1
- package/dist/promise/pTimeout.d.ts +5 -0
- package/dist/promise/pTimeout.js +5 -1
- package/dist-esm/decorators/asyncMemo.decorator.js +104 -0
- package/dist-esm/decorators/memo.decorator.js +11 -5
- package/dist-esm/decorators/memo.util.js +1 -1
- package/dist-esm/index.js +1 -0
- package/dist-esm/promise/pRetry.js +3 -1
- package/dist-esm/promise/pTimeout.js +2 -1
- package/package.json +1 -1
- package/src/decorators/asyncMemo.decorator.ts +151 -0
- package/src/decorators/memo.decorator.ts +16 -5
- package/src/decorators/memo.util.ts +36 -10
- package/src/error/assert.ts +4 -4
- package/src/error/error.model.ts +9 -0
- package/src/index.ts +3 -1
- package/src/promise/pRetry.ts +12 -2
- package/src/promise/pTimeout.ts +14 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Merge } from '../typeFest';
|
|
2
|
+
import { MemoOptions } from './memo.decorator';
|
|
3
|
+
import { AsyncMemoCache } from './memo.util';
|
|
4
|
+
export declare type AsyncMemoOptions = Merge<MemoOptions, {
|
|
5
|
+
/**
|
|
6
|
+
* Provide a custom implementation of MemoCache.
|
|
7
|
+
* Function that creates an instance of `MemoCache`.
|
|
8
|
+
* e.g LRUMemoCache from `@naturalcycles/nodejs-lib`.
|
|
9
|
+
*
|
|
10
|
+
* It's an ARRAY of Caches, to allow multiple layers of Cache.
|
|
11
|
+
* It will check it one by one, starting from the first.
|
|
12
|
+
* HIT will be returned immediately, MISS will go one level deeper, or returned (if the end of the Cache stack is reached).
|
|
13
|
+
*/
|
|
14
|
+
cacheFactory: () => AsyncMemoCache[];
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Like @_Memo, but allowing async MemoCache implementation.
|
|
18
|
+
*
|
|
19
|
+
* Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
|
|
20
|
+
* Return `null` instead (it'll be cached).
|
|
21
|
+
*/
|
|
22
|
+
export declare const _AsyncMemo: (opt: AsyncMemoOptions) => MethodDecorator;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports._AsyncMemo = void 0;
|
|
4
|
+
const time_util_1 = require("../time/time.util");
|
|
5
|
+
const decorator_util_1 = require("./decorator.util");
|
|
6
|
+
const memo_decorator_1 = require("./memo.decorator");
|
|
7
|
+
const memo_util_1 = require("./memo.util");
|
|
8
|
+
/**
|
|
9
|
+
* Like @_Memo, but allowing async MemoCache implementation.
|
|
10
|
+
*
|
|
11
|
+
* Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
|
|
12
|
+
* Return `null` instead (it'll be cached).
|
|
13
|
+
*/
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
15
|
+
const _AsyncMemo = (opt) => (target, key, descriptor) => {
|
|
16
|
+
if (typeof descriptor.value !== 'function') {
|
|
17
|
+
throw new TypeError('Memoization can be applied only to methods');
|
|
18
|
+
}
|
|
19
|
+
const originalFn = descriptor.value;
|
|
20
|
+
// Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
|
|
21
|
+
const cache = new Map();
|
|
22
|
+
const { logHit = false, logMiss = false, noLogArgs = false, logger = console, cacheFactory, cacheKeyFn = memo_util_1.jsonMemoSerializer, noCacheRejected = false, noCacheResolved = false, } = opt;
|
|
23
|
+
const keyStr = String(key);
|
|
24
|
+
const methodSignature = (0, decorator_util_1._getTargetMethodSignature)(target, keyStr);
|
|
25
|
+
descriptor.value = async function (...args) {
|
|
26
|
+
const ctx = this;
|
|
27
|
+
const cacheKey = cacheKeyFn(args);
|
|
28
|
+
if (!cache.has(ctx)) {
|
|
29
|
+
cache.set(ctx, cacheFactory());
|
|
30
|
+
// here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created
|
|
31
|
+
// UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache)
|
|
32
|
+
}
|
|
33
|
+
if (args.length === 1 && args[0] === memo_decorator_1.CACHE_DROP) {
|
|
34
|
+
// Special event - CACHE_DROP
|
|
35
|
+
// Function will return undefined
|
|
36
|
+
logger.log(`${methodSignature} @_AsyncMemo.dropCache()`);
|
|
37
|
+
try {
|
|
38
|
+
await Promise.all(cache.get(ctx).map(c => c.clear()));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
logger.error(err);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let value;
|
|
46
|
+
try {
|
|
47
|
+
for await (const cacheLayer of cache.get(ctx)) {
|
|
48
|
+
value = await cacheLayer.get(cacheKey);
|
|
49
|
+
if (value !== undefined) {
|
|
50
|
+
// it's a hit!
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// log error, but don't throw, treat it as a "miss"
|
|
57
|
+
logger.error(err);
|
|
58
|
+
}
|
|
59
|
+
if (value !== undefined) {
|
|
60
|
+
// hit!
|
|
61
|
+
if (logHit) {
|
|
62
|
+
logger.log(`${(0, decorator_util_1._getMethodSignature)(ctx, keyStr)}(${(0, decorator_util_1._getArgsSignature)(args, noLogArgs)}) @_AsyncMemo hit`);
|
|
63
|
+
}
|
|
64
|
+
return value instanceof Error ? Promise.reject(value) : Promise.resolve(value);
|
|
65
|
+
}
|
|
66
|
+
// Here we know it's a MISS, let's execute the real method
|
|
67
|
+
const started = Date.now();
|
|
68
|
+
try {
|
|
69
|
+
value = await originalFn.apply(ctx, args);
|
|
70
|
+
if (!noCacheResolved) {
|
|
71
|
+
Promise.all(cache.get(ctx).map(cacheLayer => cacheLayer.set(cacheKey, value))).catch(err => {
|
|
72
|
+
// log and ignore the error
|
|
73
|
+
logger.error(err);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (!noCacheRejected) {
|
|
80
|
+
// We put it to cache as raw Error, not Promise.reject(err)
|
|
81
|
+
Promise.all(cache.get(ctx).map(cacheLayer => cacheLayer.set(cacheKey, err))).catch(err => {
|
|
82
|
+
// log and ignore the error
|
|
83
|
+
logger.error(err);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (logMiss) {
|
|
90
|
+
logger.log(`${(0, decorator_util_1._getMethodSignature)(ctx, keyStr)}(${(0, decorator_util_1._getArgsSignature)(args, noLogArgs)}) @_AsyncMemo miss (${(0, time_util_1._since)(started)})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
return descriptor;
|
|
95
|
+
};
|
|
96
|
+
exports._AsyncMemo = _AsyncMemo;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { CommonLogger } from '../log/commonLogger';
|
|
2
2
|
import { MemoCache } from './memo.util';
|
|
3
|
+
/**
|
|
4
|
+
* Symbol to indicate that the Cache should be dropped.
|
|
5
|
+
*/
|
|
6
|
+
export declare const CACHE_DROP: unique symbol;
|
|
3
7
|
export interface MemoOptions {
|
|
4
8
|
/**
|
|
5
9
|
* Default to false
|
|
@@ -30,11 +34,15 @@ export interface MemoOptions {
|
|
|
30
34
|
/**
|
|
31
35
|
* Don't cache resolved promises.
|
|
32
36
|
* Setting this to `true` will make the decorator to await the result.
|
|
37
|
+
*
|
|
38
|
+
* Default false.
|
|
33
39
|
*/
|
|
34
40
|
noCacheResolved?: boolean;
|
|
35
41
|
/**
|
|
36
42
|
* Don't cache rejected promises.
|
|
37
43
|
* Setting this to `true` will make the decorator to await the result.
|
|
44
|
+
*
|
|
45
|
+
* Default false.
|
|
38
46
|
*/
|
|
39
47
|
noCacheRejected?: boolean;
|
|
40
48
|
}
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// http://inlehmansterms.net/2015/03/01/javascript-memoization/
|
|
6
6
|
// https://community.risingstack.com/the-worlds-fastest-javascript-memoization-library/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports._Memo = void 0;
|
|
8
|
+
exports._Memo = exports.CACHE_DROP = void 0;
|
|
9
9
|
const time_util_1 = require("../time/time.util");
|
|
10
10
|
const decorator_util_1 = require("./decorator.util");
|
|
11
11
|
const memo_util_1 = require("./memo.util");
|
|
12
|
+
/**
|
|
13
|
+
* Symbol to indicate that the Cache should be dropped.
|
|
14
|
+
*/
|
|
15
|
+
exports.CACHE_DROP = Symbol('CACHE_DROP');
|
|
12
16
|
/**
|
|
13
17
|
* Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
|
|
14
18
|
* of the cache is the same. Key, by defaul, is calculated as `JSON.stringify(...args)`.
|
|
@@ -39,6 +43,12 @@ const _Memo = (opt = {}) => (target, key, descriptor) => {
|
|
|
39
43
|
const methodSignature = (0, decorator_util_1._getTargetMethodSignature)(target, keyStr);
|
|
40
44
|
descriptor.value = function (...args) {
|
|
41
45
|
const ctx = this;
|
|
46
|
+
if (args.length === 1 && args[0] === exports.CACHE_DROP) {
|
|
47
|
+
// Special event - CACHE_DROP
|
|
48
|
+
// Function will return undefined
|
|
49
|
+
logger.log(`${methodSignature} @_Memo.CACHE_DROP`);
|
|
50
|
+
return cache.get(ctx)?.clear();
|
|
51
|
+
}
|
|
42
52
|
const cacheKey = cacheKeyFn(args);
|
|
43
53
|
if (!cache.has(ctx)) {
|
|
44
54
|
cache.set(ctx, cacheFactory());
|
|
@@ -91,11 +101,6 @@ const _Memo = (opt = {}) => (target, key, descriptor) => {
|
|
|
91
101
|
return res;
|
|
92
102
|
}
|
|
93
103
|
};
|
|
94
|
-
descriptor.value.dropCache = () => {
|
|
95
|
-
logger.log(`${methodSignature} @_Memo.dropCache()`);
|
|
96
|
-
cache.forEach(memoCache => memoCache.clear());
|
|
97
|
-
cache.clear();
|
|
98
|
-
};
|
|
99
104
|
return descriptor;
|
|
100
105
|
};
|
|
101
106
|
exports._Memo = _Memo;
|
|
@@ -1,15 +1,34 @@
|
|
|
1
|
+
import { Promisable } from '../typeFest';
|
|
1
2
|
export declare type MemoSerializer = (args: any[]) => any;
|
|
2
3
|
export declare const jsonMemoSerializer: MemoSerializer;
|
|
3
|
-
export interface MemoCache {
|
|
4
|
-
has(k:
|
|
5
|
-
get(k:
|
|
6
|
-
set(k:
|
|
4
|
+
export interface MemoCache<KEY = any, VALUE = any> {
|
|
5
|
+
has(k: KEY): boolean;
|
|
6
|
+
get(k: KEY): VALUE | undefined;
|
|
7
|
+
set(k: KEY, v: VALUE): void;
|
|
8
|
+
/**
|
|
9
|
+
* Clear is only called when `.dropCache()` is called.
|
|
10
|
+
* Otherwise the Cache is "persistent" (never cleared).
|
|
11
|
+
*/
|
|
7
12
|
clear(): void;
|
|
8
13
|
}
|
|
9
|
-
export
|
|
14
|
+
export interface AsyncMemoCache<KEY = any, VALUE = any> {
|
|
15
|
+
/**
|
|
16
|
+
* `undefined` value returned indicates the ABSENCE of value in the Cache.
|
|
17
|
+
* This also means that you CANNOT store `undefined` value in the Cache, as it'll be treated as a MISS.
|
|
18
|
+
* You CAN store `null` value instead, it will be treated as a HIT.
|
|
19
|
+
*/
|
|
20
|
+
get(k: KEY): Promisable<VALUE | undefined>;
|
|
21
|
+
set(k: KEY, v: VALUE): Promisable<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Clear is only called when `.dropCache()` is called.
|
|
24
|
+
* Otherwise the Cache is "persistent" (never cleared).
|
|
25
|
+
*/
|
|
26
|
+
clear(): Promisable<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALUE>, AsyncMemoCache<KEY, VALUE> {
|
|
10
29
|
private m;
|
|
11
|
-
has(k:
|
|
12
|
-
get(k:
|
|
13
|
-
set(k:
|
|
30
|
+
has(k: KEY): boolean;
|
|
31
|
+
get(k: KEY): VALUE | undefined;
|
|
32
|
+
set(k: KEY, v: VALUE): void;
|
|
14
33
|
clear(): void;
|
|
15
34
|
}
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MapMemoCache = exports.jsonMemoSerializer = void 0;
|
|
4
4
|
const object_util_1 = require("../object/object.util");
|
|
5
5
|
const jsonMemoSerializer = args => {
|
|
6
|
-
if (
|
|
6
|
+
if (args.length === 0)
|
|
7
7
|
return undefined;
|
|
8
8
|
if (args.length === 1 && (0, object_util_1._isPrimitive)(args[0]))
|
|
9
9
|
return args[0];
|
package/dist/error/assert.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorData
|
|
1
|
+
import { ErrorData } from '..';
|
|
2
2
|
import { AppError } from './app.error';
|
|
3
3
|
/**
|
|
4
4
|
* Evaluates the `condition` (casts it to Boolean).
|
|
@@ -16,21 +16,21 @@ import { AppError } from './app.error';
|
|
|
16
16
|
* 3. Sets `userFriendly` flag to true, cause it's always better to have at least SOME clue, rather than fully generic "Oops" error.
|
|
17
17
|
*/
|
|
18
18
|
export declare function _assert(condition: any, // will be evaluated as Boolean
|
|
19
|
-
message?: string, errorData?:
|
|
19
|
+
message?: string, errorData?: ErrorData): asserts condition;
|
|
20
20
|
/**
|
|
21
21
|
* Like _assert(), but prints more helpful error message.
|
|
22
22
|
* API is similar to Node's assert.equals().
|
|
23
23
|
*
|
|
24
24
|
* Does SHALLOW, but strict equality (===), use _assertDeepEquals() for deep equality.
|
|
25
25
|
*/
|
|
26
|
-
export declare function _assertEquals<T>(actual: any, expected: T, message?: string, errorData?:
|
|
26
|
+
export declare function _assertEquals<T>(actual: any, expected: T, message?: string, errorData?: ErrorData): asserts actual is T;
|
|
27
27
|
/**
|
|
28
28
|
* Like _assert(), but prints more helpful error message.
|
|
29
29
|
* API is similar to Node's assert.deepEquals().
|
|
30
30
|
*
|
|
31
31
|
* Does DEEP equality via _deepEquals()
|
|
32
32
|
*/
|
|
33
|
-
export declare function _assertDeepEquals<T>(actual: any, expected: T, message?: string, errorData?:
|
|
33
|
+
export declare function _assertDeepEquals<T>(actual: any, expected: T, message?: string, errorData?: ErrorData): asserts actual is T;
|
|
34
34
|
export declare function _assertIsError<ERR extends Error = Error>(err: any, message?: string): asserts err is ERR;
|
|
35
35
|
export declare function _assertIsString(v: any, message?: string): asserts v is string;
|
|
36
36
|
export declare function _assertIsNumber(v: any, message?: string): asserts v is number;
|
|
@@ -26,6 +26,13 @@ export interface ErrorData {
|
|
|
26
26
|
* `originalMessage` is used to preserve the original `error.message` as it came from the backend.
|
|
27
27
|
*/
|
|
28
28
|
originalMessage?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Can be used by error-reporting tools (e.g Sentry).
|
|
31
|
+
* If fingerprint is defined - it'll be used INSTEAD of default fingerprint of a tool.
|
|
32
|
+
* Can be used to force-group errors that are NOT needed to be split by endpoint or calling function.
|
|
33
|
+
*/
|
|
34
|
+
fingerprint?: string[];
|
|
35
|
+
httpStatusCode?: number;
|
|
29
36
|
/**
|
|
30
37
|
* Open-ended.
|
|
31
38
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ export * from './decorators/debounce.decorator';
|
|
|
8
8
|
export * from './decorators/decorator.util';
|
|
9
9
|
export * from './decorators/logMethod.decorator';
|
|
10
10
|
export * from './decorators/memo.decorator';
|
|
11
|
-
|
|
11
|
+
export * from './decorators/asyncMemo.decorator';
|
|
12
|
+
import { MemoCache, AsyncMemoCache } from './decorators/memo.util';
|
|
12
13
|
export * from './decorators/memoFn';
|
|
13
14
|
export * from './decorators/retry.decorator';
|
|
14
15
|
export * from './decorators/timeout.decorator';
|
|
@@ -58,5 +59,5 @@ export * from './string/safeJsonStringify';
|
|
|
58
59
|
import { PQueue, PQueueCfg } from './promise/pQueue';
|
|
59
60
|
export * from './seq/seq';
|
|
60
61
|
export * from './math/stack.util';
|
|
61
|
-
export type { AbortableMapper, AbortablePredicate, AbortableAsyncPredicate, AbortableAsyncMapper, PQueueCfg, MemoCache, PromiseDecoratorCfg, PromiseDecoratorResp, ErrorData, ErrorObject, HttpErrorData, HttpErrorResponse, Admin401ErrorData, Admin403ErrorData, StringMap, PromiseMap, AnyObject, AnyFunction, ValuesOf, ValueOf, KeyValueTuple, ObjectMapper, ObjectPredicate, InstanceId, IsoDate, IsoDateTime, Reviver, PMapOptions, Mapper, AsyncMapper, Predicate, AsyncPredicate, BatchResult, DeferredPromise, PRetryOptions, PTimeoutOptions, TryCatchOptions, StringifyAnyOptions, JsonStringifyFunction, Merge, ReadonlyDeep, Promisable, Simplify, ConditionalPick, ConditionalExcept, Class, UnixTimestamp, BaseDBEntity, SavedDBEntity, Saved, Unsaved, CreatedUpdated, CreatedUpdatedId, ObjectWithId, AnyObjectWithId, JsonSchema, JsonSchemaAny, JsonSchemaOneOf, JsonSchemaAllOf, JsonSchemaAnyOf, JsonSchemaNot, JsonSchemaRef, JsonSchemaConst, JsonSchemaEnum, JsonSchemaString, JsonSchemaNumber, JsonSchemaBoolean, JsonSchemaNull, JsonSchemaRootObject, JsonSchemaObject, JsonSchemaArray, JsonSchemaTuple, JsonSchemaBuilder, CommonLogLevel, CommonLogWithLevelFunction, CommonLogFunction, CommonLogger, };
|
|
62
|
+
export type { AbortableMapper, AbortablePredicate, AbortableAsyncPredicate, AbortableAsyncMapper, PQueueCfg, MemoCache, AsyncMemoCache, PromiseDecoratorCfg, PromiseDecoratorResp, ErrorData, ErrorObject, HttpErrorData, HttpErrorResponse, Admin401ErrorData, Admin403ErrorData, StringMap, PromiseMap, AnyObject, AnyFunction, ValuesOf, ValueOf, KeyValueTuple, ObjectMapper, ObjectPredicate, InstanceId, IsoDate, IsoDateTime, Reviver, PMapOptions, Mapper, AsyncMapper, Predicate, AsyncPredicate, BatchResult, DeferredPromise, PRetryOptions, PTimeoutOptions, TryCatchOptions, StringifyAnyOptions, JsonStringifyFunction, Merge, ReadonlyDeep, Promisable, Simplify, ConditionalPick, ConditionalExcept, Class, UnixTimestamp, BaseDBEntity, SavedDBEntity, Saved, Unsaved, CreatedUpdated, CreatedUpdatedId, ObjectWithId, AnyObjectWithId, JsonSchema, JsonSchemaAny, JsonSchemaOneOf, JsonSchemaAllOf, JsonSchemaAnyOf, JsonSchemaNot, JsonSchemaRef, JsonSchemaConst, JsonSchemaEnum, JsonSchemaString, JsonSchemaNumber, JsonSchemaBoolean, JsonSchemaNull, JsonSchemaRootObject, JsonSchemaObject, JsonSchemaArray, JsonSchemaTuple, JsonSchemaBuilder, CommonLogLevel, CommonLogWithLevelFunction, CommonLogFunction, CommonLogger, };
|
|
62
63
|
export { is, _createPromiseDecorator, _stringMapValues, _stringMapEntries, _objectKeys, pMap, _passthroughMapper, _passUndefinedMapper, _passthroughPredicate, _passNothingPredicate, _noop, ErrorMode, pDefer, AggregatedError, pRetry, pRetryFn, pTimeout, pTimeoutFn, _tryCatch, _TryCatch, _stringifyAny, jsonSchema, JsonSchemaAnyBuilder, commonLoggerMinLevel, commonLoggerNoop, commonLogLevelNumber, commonLoggerPipe, commonLoggerPrefix, commonLoggerCreate, PQueue, END, SKIP, };
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ Object.defineProperty(exports, "_createPromiseDecorator", { enumerable: true, ge
|
|
|
13
13
|
(0, tslib_1.__exportStar)(require("./decorators/decorator.util"), exports);
|
|
14
14
|
(0, tslib_1.__exportStar)(require("./decorators/logMethod.decorator"), exports);
|
|
15
15
|
(0, tslib_1.__exportStar)(require("./decorators/memo.decorator"), exports);
|
|
16
|
+
(0, tslib_1.__exportStar)(require("./decorators/asyncMemo.decorator"), exports);
|
|
16
17
|
(0, tslib_1.__exportStar)(require("./decorators/memoFn"), exports);
|
|
17
18
|
(0, tslib_1.__exportStar)(require("./decorators/retry.decorator"), exports);
|
|
18
19
|
(0, tslib_1.__exportStar)(require("./decorators/timeout.decorator"), exports);
|
package/dist/promise/pRetry.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyFunction, CommonLogger } from '..';
|
|
1
|
+
import { AnyFunction, CommonLogger, ErrorData } from '..';
|
|
2
2
|
export interface PRetryOptions {
|
|
3
3
|
/**
|
|
4
4
|
* If set - will be included in the error message.
|
|
@@ -76,6 +76,10 @@ export interface PRetryOptions {
|
|
|
76
76
|
* @experimental
|
|
77
77
|
*/
|
|
78
78
|
keepStackTrace?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Will be merged with `err.data` object.
|
|
81
|
+
*/
|
|
82
|
+
errorData?: ErrorData;
|
|
79
83
|
}
|
|
80
84
|
/**
|
|
81
85
|
* Returns a Function (!), enhanced with retry capabilities.
|
package/dist/promise/pRetry.js
CHANGED
|
@@ -31,7 +31,7 @@ async function pRetry(fn, opt = {}) {
|
|
|
31
31
|
return await new Promise((resolve, reject) => {
|
|
32
32
|
const rejectWithTimeout = () => {
|
|
33
33
|
timedOut = true; // to prevent more tries
|
|
34
|
-
const err = new pTimeout_1.TimeoutError(`"${fname}" timed out after ${timeout} ms
|
|
34
|
+
const err = new pTimeout_1.TimeoutError(`"${fname}" timed out after ${timeout} ms`, opt.errorData);
|
|
35
35
|
if (fakeError) {
|
|
36
36
|
// keep original stack
|
|
37
37
|
err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
|
|
@@ -75,6 +75,11 @@ async function pRetry(fn, opt = {}) {
|
|
|
75
75
|
fakeError.stack.replace('Error: RetryError', ''),
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
;
|
|
79
|
+
err.data = {
|
|
80
|
+
...err.data,
|
|
81
|
+
...opt.errorData,
|
|
82
|
+
};
|
|
78
83
|
reject(err);
|
|
79
84
|
}
|
|
80
85
|
else {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppError } from '../error/app.error';
|
|
2
|
+
import { ErrorData } from '../error/error.model';
|
|
2
3
|
import { AnyFunction } from '../types';
|
|
3
4
|
export declare class TimeoutError extends AppError {
|
|
4
5
|
}
|
|
@@ -25,6 +26,10 @@ export interface PTimeoutOptions {
|
|
|
25
26
|
* @experimental
|
|
26
27
|
*/
|
|
27
28
|
keepStackTrace?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Will be merged with `err.data` object.
|
|
31
|
+
*/
|
|
32
|
+
errorData?: ErrorData;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Decorates a Function with a timeout.
|
package/dist/promise/pTimeout.js
CHANGED
|
@@ -37,11 +37,15 @@ async function pTimeout(promise, opt) {
|
|
|
37
37
|
catch (err) {
|
|
38
38
|
if (fakeError)
|
|
39
39
|
err.stack = fakeError.stack; // keep original stack
|
|
40
|
+
err.data = {
|
|
41
|
+
...err.data,
|
|
42
|
+
...opt.errorData,
|
|
43
|
+
};
|
|
40
44
|
reject(err);
|
|
41
45
|
}
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
|
-
const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms
|
|
48
|
+
const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms`, opt.errorData);
|
|
45
49
|
if (fakeError)
|
|
46
50
|
err.stack = fakeError.stack; // keep original stack
|
|
47
51
|
reject(err);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { __asyncValues } from "tslib";
|
|
2
|
+
import { _since } from '../time/time.util';
|
|
3
|
+
import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util';
|
|
4
|
+
import { CACHE_DROP } from './memo.decorator';
|
|
5
|
+
import { jsonMemoSerializer } from './memo.util';
|
|
6
|
+
/**
|
|
7
|
+
* Like @_Memo, but allowing async MemoCache implementation.
|
|
8
|
+
*
|
|
9
|
+
* Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
|
|
10
|
+
* Return `null` instead (it'll be cached).
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
13
|
+
export const _AsyncMemo = (opt) => (target, key, descriptor) => {
|
|
14
|
+
if (typeof descriptor.value !== 'function') {
|
|
15
|
+
throw new TypeError('Memoization can be applied only to methods');
|
|
16
|
+
}
|
|
17
|
+
const originalFn = descriptor.value;
|
|
18
|
+
// Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
const { logHit = false, logMiss = false, noLogArgs = false, logger = console, cacheFactory, cacheKeyFn = jsonMemoSerializer, noCacheRejected = false, noCacheResolved = false, } = opt;
|
|
21
|
+
const keyStr = String(key);
|
|
22
|
+
const methodSignature = _getTargetMethodSignature(target, keyStr);
|
|
23
|
+
descriptor.value = async function (...args) {
|
|
24
|
+
var e_1, _a;
|
|
25
|
+
const ctx = this;
|
|
26
|
+
const cacheKey = cacheKeyFn(args);
|
|
27
|
+
if (!cache.has(ctx)) {
|
|
28
|
+
cache.set(ctx, cacheFactory());
|
|
29
|
+
// here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created
|
|
30
|
+
// UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache)
|
|
31
|
+
}
|
|
32
|
+
if (args.length === 1 && args[0] === CACHE_DROP) {
|
|
33
|
+
// Special event - CACHE_DROP
|
|
34
|
+
// Function will return undefined
|
|
35
|
+
logger.log(`${methodSignature} @_AsyncMemo.dropCache()`);
|
|
36
|
+
try {
|
|
37
|
+
await Promise.all(cache.get(ctx).map(c => c.clear()));
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
logger.error(err);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let value;
|
|
45
|
+
try {
|
|
46
|
+
try {
|
|
47
|
+
for (var _b = __asyncValues(cache.get(ctx)), _c; _c = await _b.next(), !_c.done;) {
|
|
48
|
+
const cacheLayer = _c.value;
|
|
49
|
+
value = await cacheLayer.get(cacheKey);
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
// it's a hit!
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (_c && !_c.done && (_a = _b.return)) await _a.call(_b);
|
|
60
|
+
}
|
|
61
|
+
finally { if (e_1) throw e_1.error; }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// log error, but don't throw, treat it as a "miss"
|
|
66
|
+
logger.error(err);
|
|
67
|
+
}
|
|
68
|
+
if (value !== undefined) {
|
|
69
|
+
// hit!
|
|
70
|
+
if (logHit) {
|
|
71
|
+
logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, noLogArgs)}) @_AsyncMemo hit`);
|
|
72
|
+
}
|
|
73
|
+
return value instanceof Error ? Promise.reject(value) : Promise.resolve(value);
|
|
74
|
+
}
|
|
75
|
+
// Here we know it's a MISS, let's execute the real method
|
|
76
|
+
const started = Date.now();
|
|
77
|
+
try {
|
|
78
|
+
value = await originalFn.apply(ctx, args);
|
|
79
|
+
if (!noCacheResolved) {
|
|
80
|
+
Promise.all(cache.get(ctx).map(cacheLayer => cacheLayer.set(cacheKey, value))).catch(err => {
|
|
81
|
+
// log and ignore the error
|
|
82
|
+
logger.error(err);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
if (!noCacheRejected) {
|
|
89
|
+
// We put it to cache as raw Error, not Promise.reject(err)
|
|
90
|
+
Promise.all(cache.get(ctx).map(cacheLayer => cacheLayer.set(cacheKey, err))).catch(err => {
|
|
91
|
+
// log and ignore the error
|
|
92
|
+
logger.error(err);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
if (logMiss) {
|
|
99
|
+
logger.log(`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(args, noLogArgs)}) @_AsyncMemo miss (${_since(started)})`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return descriptor;
|
|
104
|
+
};
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { _since } from '../time/time.util';
|
|
7
7
|
import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util';
|
|
8
8
|
import { jsonMemoSerializer, MapMemoCache } from './memo.util';
|
|
9
|
+
/**
|
|
10
|
+
* Symbol to indicate that the Cache should be dropped.
|
|
11
|
+
*/
|
|
12
|
+
export const CACHE_DROP = Symbol('CACHE_DROP');
|
|
9
13
|
/**
|
|
10
14
|
* Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
|
|
11
15
|
* of the cache is the same. Key, by defaul, is calculated as `JSON.stringify(...args)`.
|
|
@@ -35,7 +39,14 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
|
|
|
35
39
|
const keyStr = String(key);
|
|
36
40
|
const methodSignature = _getTargetMethodSignature(target, keyStr);
|
|
37
41
|
descriptor.value = function (...args) {
|
|
42
|
+
var _a;
|
|
38
43
|
const ctx = this;
|
|
44
|
+
if (args.length === 1 && args[0] === CACHE_DROP) {
|
|
45
|
+
// Special event - CACHE_DROP
|
|
46
|
+
// Function will return undefined
|
|
47
|
+
logger.log(`${methodSignature} @_Memo.CACHE_DROP`);
|
|
48
|
+
return (_a = cache.get(ctx)) === null || _a === void 0 ? void 0 : _a.clear();
|
|
49
|
+
}
|
|
39
50
|
const cacheKey = cacheKeyFn(args);
|
|
40
51
|
if (!cache.has(ctx)) {
|
|
41
52
|
cache.set(ctx, cacheFactory());
|
|
@@ -88,10 +99,5 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
|
|
|
88
99
|
return res;
|
|
89
100
|
}
|
|
90
101
|
};
|
|
91
|
-
descriptor.value.dropCache = () => {
|
|
92
|
-
logger.log(`${methodSignature} @_Memo.dropCache()`);
|
|
93
|
-
cache.forEach(memoCache => memoCache.clear());
|
|
94
|
-
cache.clear();
|
|
95
|
-
};
|
|
96
102
|
return descriptor;
|
|
97
103
|
};
|
package/dist-esm/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export * from './decorators/debounce.decorator';
|
|
|
8
8
|
export * from './decorators/decorator.util';
|
|
9
9
|
export * from './decorators/logMethod.decorator';
|
|
10
10
|
export * from './decorators/memo.decorator';
|
|
11
|
+
export * from './decorators/asyncMemo.decorator';
|
|
11
12
|
export * from './decorators/memoFn';
|
|
12
13
|
export * from './decorators/retry.decorator';
|
|
13
14
|
export * from './decorators/timeout.decorator';
|
|
@@ -27,7 +27,7 @@ export async function pRetry(fn, opt = {}) {
|
|
|
27
27
|
return await new Promise((resolve, reject) => {
|
|
28
28
|
const rejectWithTimeout = () => {
|
|
29
29
|
timedOut = true; // to prevent more tries
|
|
30
|
-
const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms
|
|
30
|
+
const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`, opt.errorData);
|
|
31
31
|
if (fakeError) {
|
|
32
32
|
// keep original stack
|
|
33
33
|
err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
|
|
@@ -71,6 +71,8 @@ export async function pRetry(fn, opt = {}) {
|
|
|
71
71
|
fakeError.stack.replace('Error: RetryError', ''),
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
+
;
|
|
75
|
+
err.data = Object.assign(Object.assign({}, err.data), opt.errorData);
|
|
74
76
|
reject(err);
|
|
75
77
|
}
|
|
76
78
|
else {
|
|
@@ -32,11 +32,12 @@ export async function pTimeout(promise, opt) {
|
|
|
32
32
|
catch (err) {
|
|
33
33
|
if (fakeError)
|
|
34
34
|
err.stack = fakeError.stack; // keep original stack
|
|
35
|
+
err.data = Object.assign(Object.assign({}, err.data), opt.errorData);
|
|
35
36
|
reject(err);
|
|
36
37
|
}
|
|
37
38
|
return;
|
|
38
39
|
}
|
|
39
|
-
const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms
|
|
40
|
+
const err = new TimeoutError(`"${name || 'pTimeout function'}" timed out after ${timeout} ms`, opt.errorData);
|
|
40
41
|
if (fakeError)
|
|
41
42
|
err.stack = fakeError.stack; // keep original stack
|
|
42
43
|
reject(err);
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { _since } from '../time/time.util'
|
|
2
|
+
import { Merge } from '../typeFest'
|
|
3
|
+
import { AnyObject } from '../types'
|
|
4
|
+
import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util'
|
|
5
|
+
import { CACHE_DROP, MemoOptions } from './memo.decorator'
|
|
6
|
+
import { AsyncMemoCache, jsonMemoSerializer } from './memo.util'
|
|
7
|
+
|
|
8
|
+
export type AsyncMemoOptions = Merge<
|
|
9
|
+
MemoOptions,
|
|
10
|
+
{
|
|
11
|
+
/**
|
|
12
|
+
* Provide a custom implementation of MemoCache.
|
|
13
|
+
* Function that creates an instance of `MemoCache`.
|
|
14
|
+
* e.g LRUMemoCache from `@naturalcycles/nodejs-lib`.
|
|
15
|
+
*
|
|
16
|
+
* It's an ARRAY of Caches, to allow multiple layers of Cache.
|
|
17
|
+
* It will check it one by one, starting from the first.
|
|
18
|
+
* HIT will be returned immediately, MISS will go one level deeper, or returned (if the end of the Cache stack is reached).
|
|
19
|
+
*/
|
|
20
|
+
cacheFactory: () => AsyncMemoCache[]
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Like @_Memo, but allowing async MemoCache implementation.
|
|
26
|
+
*
|
|
27
|
+
* Method CANNOT return `undefined`, as undefined will always be treated as cache MISS and retried.
|
|
28
|
+
* Return `null` instead (it'll be cached).
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
31
|
+
export const _AsyncMemo =
|
|
32
|
+
(opt: AsyncMemoOptions): MethodDecorator =>
|
|
33
|
+
(target, key, descriptor) => {
|
|
34
|
+
if (typeof descriptor.value !== 'function') {
|
|
35
|
+
throw new TypeError('Memoization can be applied only to methods')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const originalFn = descriptor.value
|
|
39
|
+
|
|
40
|
+
// Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
|
|
41
|
+
const cache = new Map<AnyObject, AsyncMemoCache[]>()
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
logHit = false,
|
|
45
|
+
logMiss = false,
|
|
46
|
+
noLogArgs = false,
|
|
47
|
+
logger = console,
|
|
48
|
+
cacheFactory,
|
|
49
|
+
cacheKeyFn = jsonMemoSerializer,
|
|
50
|
+
noCacheRejected = false,
|
|
51
|
+
noCacheResolved = false,
|
|
52
|
+
} = opt
|
|
53
|
+
|
|
54
|
+
const keyStr = String(key)
|
|
55
|
+
const methodSignature = _getTargetMethodSignature(target, keyStr)
|
|
56
|
+
|
|
57
|
+
descriptor.value = async function (this: typeof target, ...args: any[]): Promise<any> {
|
|
58
|
+
const ctx = this
|
|
59
|
+
|
|
60
|
+
const cacheKey = cacheKeyFn(args)
|
|
61
|
+
|
|
62
|
+
if (!cache.has(ctx)) {
|
|
63
|
+
cache.set(ctx, cacheFactory())
|
|
64
|
+
// here, no need to check the cache. It's definitely a miss, because the cacheLayers is just created
|
|
65
|
+
// UPD: no! AsyncMemo supports "persistent caches" (e.g Database-backed cache)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (args.length === 1 && args[0] === CACHE_DROP) {
|
|
69
|
+
// Special event - CACHE_DROP
|
|
70
|
+
// Function will return undefined
|
|
71
|
+
logger.log(`${methodSignature} @_AsyncMemo.dropCache()`)
|
|
72
|
+
try {
|
|
73
|
+
await Promise.all(cache.get(ctx)!.map(c => c.clear()))
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger.error(err)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let value: any
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
for await (const cacheLayer of cache.get(ctx)!) {
|
|
85
|
+
value = await cacheLayer.get(cacheKey)
|
|
86
|
+
if (value !== undefined) {
|
|
87
|
+
// it's a hit!
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// log error, but don't throw, treat it as a "miss"
|
|
93
|
+
logger.error(err)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value !== undefined) {
|
|
97
|
+
// hit!
|
|
98
|
+
if (logHit) {
|
|
99
|
+
logger.log(
|
|
100
|
+
`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(
|
|
101
|
+
args,
|
|
102
|
+
noLogArgs,
|
|
103
|
+
)}) @_AsyncMemo hit`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return value instanceof Error ? Promise.reject(value) : Promise.resolve(value)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Here we know it's a MISS, let's execute the real method
|
|
111
|
+
const started = Date.now()
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
value = await originalFn.apply(ctx, args)
|
|
115
|
+
|
|
116
|
+
if (!noCacheResolved) {
|
|
117
|
+
Promise.all(cache.get(ctx)!.map(cacheLayer => cacheLayer.set(cacheKey, value))).catch(
|
|
118
|
+
err => {
|
|
119
|
+
// log and ignore the error
|
|
120
|
+
logger.error(err)
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return value
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (!noCacheRejected) {
|
|
128
|
+
// We put it to cache as raw Error, not Promise.reject(err)
|
|
129
|
+
Promise.all(cache.get(ctx)!.map(cacheLayer => cacheLayer.set(cacheKey, err))).catch(
|
|
130
|
+
err => {
|
|
131
|
+
// log and ignore the error
|
|
132
|
+
logger.error(err)
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw err
|
|
138
|
+
} finally {
|
|
139
|
+
if (logMiss) {
|
|
140
|
+
logger.log(
|
|
141
|
+
`${_getMethodSignature(ctx, keyStr)}(${_getArgsSignature(
|
|
142
|
+
args,
|
|
143
|
+
noLogArgs,
|
|
144
|
+
)}) @_AsyncMemo miss (${_since(started)})`,
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} as any
|
|
149
|
+
|
|
150
|
+
return descriptor
|
|
151
|
+
}
|
|
@@ -10,6 +10,11 @@ import { AnyObject } from '../types'
|
|
|
10
10
|
import { _getArgsSignature, _getMethodSignature, _getTargetMethodSignature } from './decorator.util'
|
|
11
11
|
import { jsonMemoSerializer, MapMemoCache, MemoCache } from './memo.util'
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Symbol to indicate that the Cache should be dropped.
|
|
15
|
+
*/
|
|
16
|
+
export const CACHE_DROP = Symbol('CACHE_DROP')
|
|
17
|
+
|
|
13
18
|
export interface MemoOptions {
|
|
14
19
|
/**
|
|
15
20
|
* Default to false
|
|
@@ -45,12 +50,16 @@ export interface MemoOptions {
|
|
|
45
50
|
/**
|
|
46
51
|
* Don't cache resolved promises.
|
|
47
52
|
* Setting this to `true` will make the decorator to await the result.
|
|
53
|
+
*
|
|
54
|
+
* Default false.
|
|
48
55
|
*/
|
|
49
56
|
noCacheResolved?: boolean
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* Don't cache rejected promises.
|
|
53
60
|
* Setting this to `true` will make the decorator to await the result.
|
|
61
|
+
*
|
|
62
|
+
* Default false.
|
|
54
63
|
*/
|
|
55
64
|
noCacheRejected?: boolean
|
|
56
65
|
}
|
|
@@ -102,6 +111,13 @@ export const _Memo =
|
|
|
102
111
|
descriptor.value = function (this: typeof target, ...args: any[]): any {
|
|
103
112
|
const ctx = this
|
|
104
113
|
|
|
114
|
+
if (args.length === 1 && args[0] === CACHE_DROP) {
|
|
115
|
+
// Special event - CACHE_DROP
|
|
116
|
+
// Function will return undefined
|
|
117
|
+
logger.log(`${methodSignature} @_Memo.CACHE_DROP`)
|
|
118
|
+
return cache.get(ctx)?.clear()
|
|
119
|
+
}
|
|
120
|
+
|
|
105
121
|
const cacheKey = cacheKeyFn(args)
|
|
106
122
|
|
|
107
123
|
if (!cache.has(ctx)) {
|
|
@@ -179,11 +195,6 @@ export const _Memo =
|
|
|
179
195
|
return res
|
|
180
196
|
}
|
|
181
197
|
} as any
|
|
182
|
-
;(descriptor.value as any).dropCache = () => {
|
|
183
|
-
logger.log(`${methodSignature} @_Memo.dropCache()`)
|
|
184
|
-
cache.forEach(memoCache => memoCache.clear())
|
|
185
|
-
cache.clear()
|
|
186
|
-
}
|
|
187
198
|
|
|
188
199
|
return descriptor
|
|
189
200
|
}
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { _isPrimitive } from '../object/object.util'
|
|
2
|
+
import { Promisable } from '../typeFest'
|
|
2
3
|
|
|
3
4
|
export type MemoSerializer = (args: any[]) => any
|
|
4
5
|
|
|
5
6
|
export const jsonMemoSerializer: MemoSerializer = args => {
|
|
6
|
-
if (
|
|
7
|
+
if (args.length === 0) return undefined
|
|
7
8
|
if (args.length === 1 && _isPrimitive(args[0])) return args[0]
|
|
8
9
|
return JSON.stringify(args)
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export interface MemoCache {
|
|
12
|
-
has(k:
|
|
13
|
-
get(k:
|
|
14
|
-
set(k:
|
|
12
|
+
export interface MemoCache<KEY = any, VALUE = any> {
|
|
13
|
+
has(k: KEY): boolean
|
|
14
|
+
get(k: KEY): VALUE | undefined
|
|
15
|
+
set(k: KEY, v: VALUE): void
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Clear is only called when `.dropCache()` is called.
|
|
19
|
+
* Otherwise the Cache is "persistent" (never cleared).
|
|
20
|
+
*/
|
|
15
21
|
clear(): void
|
|
16
22
|
}
|
|
17
23
|
|
|
24
|
+
export interface AsyncMemoCache<KEY = any, VALUE = any> {
|
|
25
|
+
// `has` method is removed, because it is assumed that it has a cost and it's best to avoid doing both `has` and then `get`
|
|
26
|
+
// has(k: any): Promise<boolean>
|
|
27
|
+
/**
|
|
28
|
+
* `undefined` value returned indicates the ABSENCE of value in the Cache.
|
|
29
|
+
* This also means that you CANNOT store `undefined` value in the Cache, as it'll be treated as a MISS.
|
|
30
|
+
* You CAN store `null` value instead, it will be treated as a HIT.
|
|
31
|
+
*/
|
|
32
|
+
get(k: KEY): Promisable<VALUE | undefined>
|
|
33
|
+
set(k: KEY, v: VALUE): Promisable<void>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear is only called when `.dropCache()` is called.
|
|
37
|
+
* Otherwise the Cache is "persistent" (never cleared).
|
|
38
|
+
*/
|
|
39
|
+
clear(): Promisable<void>
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
// SingleValueMemoCache and ObjectMemoCache are example-only, not used in production code
|
|
19
43
|
/*
|
|
20
44
|
export class SingleValueMemoCache implements MemoCache {
|
|
@@ -61,18 +85,20 @@ export class ObjectMemoCache implements MemoCache {
|
|
|
61
85
|
}
|
|
62
86
|
*/
|
|
63
87
|
|
|
64
|
-
export class MapMemoCache
|
|
65
|
-
|
|
88
|
+
export class MapMemoCache<KEY = any, VALUE = any>
|
|
89
|
+
implements MemoCache<KEY, VALUE>, AsyncMemoCache<KEY, VALUE>
|
|
90
|
+
{
|
|
91
|
+
private m = new Map<KEY, VALUE>()
|
|
66
92
|
|
|
67
|
-
has(k:
|
|
93
|
+
has(k: KEY): boolean {
|
|
68
94
|
return this.m.has(k)
|
|
69
95
|
}
|
|
70
96
|
|
|
71
|
-
get(k:
|
|
97
|
+
get(k: KEY): VALUE | undefined {
|
|
72
98
|
return this.m.get(k)
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
set(k:
|
|
101
|
+
set(k: KEY, v: VALUE): void {
|
|
76
102
|
this.m.set(k, v)
|
|
77
103
|
}
|
|
78
104
|
|
package/src/error/assert.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ErrorData,
|
|
1
|
+
import { ErrorData, _deepEquals, _stringifyAny } from '..'
|
|
2
2
|
import { AppError } from './app.error'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -19,7 +19,7 @@ import { AppError } from './app.error'
|
|
|
19
19
|
export function _assert(
|
|
20
20
|
condition: any, // will be evaluated as Boolean
|
|
21
21
|
message?: string,
|
|
22
|
-
errorData?:
|
|
22
|
+
errorData?: ErrorData,
|
|
23
23
|
): asserts condition {
|
|
24
24
|
if (!condition) {
|
|
25
25
|
throw new AssertionError(message || 'see stacktrace', {
|
|
@@ -39,7 +39,7 @@ export function _assertEquals<T>(
|
|
|
39
39
|
actual: any,
|
|
40
40
|
expected: T,
|
|
41
41
|
message?: string,
|
|
42
|
-
errorData?:
|
|
42
|
+
errorData?: ErrorData,
|
|
43
43
|
): asserts actual is T {
|
|
44
44
|
if (actual !== expected) {
|
|
45
45
|
const msg = [
|
|
@@ -67,7 +67,7 @@ export function _assertDeepEquals<T>(
|
|
|
67
67
|
actual: any,
|
|
68
68
|
expected: T,
|
|
69
69
|
message?: string,
|
|
70
|
-
errorData?:
|
|
70
|
+
errorData?: ErrorData,
|
|
71
71
|
): asserts actual is T {
|
|
72
72
|
if (!_deepEquals(actual, expected)) {
|
|
73
73
|
const msg = [
|
package/src/error/error.model.ts
CHANGED
|
@@ -31,6 +31,15 @@ export interface ErrorData {
|
|
|
31
31
|
*/
|
|
32
32
|
originalMessage?: string
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Can be used by error-reporting tools (e.g Sentry).
|
|
36
|
+
* If fingerprint is defined - it'll be used INSTEAD of default fingerprint of a tool.
|
|
37
|
+
* Can be used to force-group errors that are NOT needed to be split by endpoint or calling function.
|
|
38
|
+
*/
|
|
39
|
+
fingerprint?: string[]
|
|
40
|
+
|
|
41
|
+
httpStatusCode?: number
|
|
42
|
+
|
|
34
43
|
/**
|
|
35
44
|
* Open-ended.
|
|
36
45
|
*/
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,8 @@ export * from './decorators/debounce.decorator'
|
|
|
12
12
|
export * from './decorators/decorator.util'
|
|
13
13
|
export * from './decorators/logMethod.decorator'
|
|
14
14
|
export * from './decorators/memo.decorator'
|
|
15
|
-
|
|
15
|
+
export * from './decorators/asyncMemo.decorator'
|
|
16
|
+
import { MemoCache, AsyncMemoCache } from './decorators/memo.util'
|
|
16
17
|
export * from './decorators/memoFn'
|
|
17
18
|
export * from './decorators/retry.decorator'
|
|
18
19
|
export * from './decorators/timeout.decorator'
|
|
@@ -160,6 +161,7 @@ export type {
|
|
|
160
161
|
AbortableAsyncMapper,
|
|
161
162
|
PQueueCfg,
|
|
162
163
|
MemoCache,
|
|
164
|
+
AsyncMemoCache,
|
|
163
165
|
PromiseDecoratorCfg,
|
|
164
166
|
PromiseDecoratorResp,
|
|
165
167
|
ErrorData,
|
package/src/promise/pRetry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _since, _stringifyAny, AnyFunction, CommonLogger } from '..'
|
|
1
|
+
import { _since, _stringifyAny, AnyFunction, AppError, CommonLogger, ErrorData } from '..'
|
|
2
2
|
import { TimeoutError } from './pTimeout'
|
|
3
3
|
|
|
4
4
|
export interface PRetryOptions {
|
|
@@ -91,6 +91,11 @@ export interface PRetryOptions {
|
|
|
91
91
|
* @experimental
|
|
92
92
|
*/
|
|
93
93
|
keepStackTrace?: boolean
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Will be merged with `err.data` object.
|
|
97
|
+
*/
|
|
98
|
+
errorData?: ErrorData
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/**
|
|
@@ -139,7 +144,7 @@ export async function pRetry<T>(
|
|
|
139
144
|
return await new Promise((resolve, reject) => {
|
|
140
145
|
const rejectWithTimeout = () => {
|
|
141
146
|
timedOut = true // to prevent more tries
|
|
142
|
-
const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms
|
|
147
|
+
const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`, opt.errorData)
|
|
143
148
|
if (fakeError) {
|
|
144
149
|
// keep original stack
|
|
145
150
|
err.stack = fakeError.stack!.replace('Error: RetryError', 'TimeoutError')
|
|
@@ -199,6 +204,11 @@ export async function pRetry<T>(
|
|
|
199
204
|
})
|
|
200
205
|
}
|
|
201
206
|
|
|
207
|
+
;(err as AppError).data = {
|
|
208
|
+
...(err as AppError).data,
|
|
209
|
+
...opt.errorData,
|
|
210
|
+
}
|
|
211
|
+
|
|
202
212
|
reject(err)
|
|
203
213
|
} else {
|
|
204
214
|
// Retry after delay
|
package/src/promise/pTimeout.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppError } from '../error/app.error'
|
|
2
|
+
import { ErrorData } from '../error/error.model'
|
|
2
3
|
import { AnyFunction } from '../types'
|
|
3
4
|
|
|
4
5
|
export class TimeoutError extends AppError {}
|
|
@@ -29,6 +30,11 @@ export interface PTimeoutOptions {
|
|
|
29
30
|
* @experimental
|
|
30
31
|
*/
|
|
31
32
|
keepStackTrace?: boolean
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Will be merged with `err.data` object.
|
|
36
|
+
*/
|
|
37
|
+
errorData?: ErrorData
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
@@ -63,12 +69,19 @@ export async function pTimeout<T>(promise: Promise<T>, opt: PTimeoutOptions): Pr
|
|
|
63
69
|
resolve(onTimeout())
|
|
64
70
|
} catch (err: any) {
|
|
65
71
|
if (fakeError) err.stack = fakeError.stack // keep original stack
|
|
72
|
+
err.data = {
|
|
73
|
+
...err.data,
|
|
74
|
+
...opt.errorData,
|
|
75
|
+
}
|
|
66
76
|
reject(err)
|
|
67
77
|
}
|
|
68
78
|
return
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
const err = new TimeoutError(
|
|
81
|
+
const err = new TimeoutError(
|
|
82
|
+
`"${name || 'pTimeout function'}" timed out after ${timeout} ms`,
|
|
83
|
+
opt.errorData,
|
|
84
|
+
)
|
|
72
85
|
if (fakeError) err.stack = fakeError.stack // keep original stack
|
|
73
86
|
reject(err)
|
|
74
87
|
}, timeout)
|