@naturalcycles/js-lib 14.82.0 → 14.83.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.
@@ -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,39 @@
1
1
  export declare type MemoSerializer = (args: any[]) => any;
2
2
  export declare const jsonMemoSerializer: MemoSerializer;
3
- export interface MemoCache {
4
- has(k: any): boolean;
5
- get(k: any): any;
6
- set(k: any, v: any): void;
3
+ export interface MemoCache<KEY = any, VALUE = any> {
4
+ has(k: KEY): boolean;
5
+ get(k: KEY): VALUE | undefined;
6
+ set(k: KEY, v: VALUE): void;
7
+ /**
8
+ * Clear is only called when `.dropCache()` is called.
9
+ * Otherwise the Cache is "persistent" (never cleared).
10
+ */
7
11
  clear(): void;
8
12
  }
9
- export declare class MapMemoCache implements MemoCache {
13
+ export interface AsyncMemoCache<KEY = any, VALUE = any> {
14
+ /**
15
+ * `undefined` value returned indicates the ABSENCE of value in the Cache.
16
+ * This also means that you CANNOT store `undefined` value in the Cache, as it'll be treated as a MISS.
17
+ * You CAN store `null` value instead, it will be treated as a HIT.
18
+ */
19
+ get(k: KEY): Promise<VALUE | undefined>;
20
+ set(k: KEY, v: VALUE): Promise<void>;
21
+ /**
22
+ * Clear is only called when `.dropCache()` is called.
23
+ * Otherwise the Cache is "persistent" (never cleared).
24
+ */
25
+ clear(): Promise<void>;
26
+ }
27
+ export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALUE> {
10
28
  private m;
11
- has(k: any): boolean;
12
- get(k: any): any;
13
- set(k: any, v: any): void;
29
+ has(k: KEY): boolean;
30
+ get(k: KEY): VALUE | undefined;
31
+ set(k: KEY, v: VALUE): void;
14
32
  clear(): void;
15
33
  }
34
+ export declare class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
35
+ private m;
36
+ get(k: KEY): Promise<VALUE | undefined>;
37
+ set(k: KEY, v: VALUE): Promise<void>;
38
+ clear(): Promise<void>;
39
+ }
@@ -1,9 +1,9 @@
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 object_util_1 = require("../object/object.util");
5
5
  const jsonMemoSerializer = args => {
6
- if (!args.length)
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];
@@ -73,3 +73,18 @@ class MapMemoCache {
73
73
  }
74
74
  }
75
75
  exports.MapMemoCache = MapMemoCache;
76
+ class MapAsyncMemoCache {
77
+ constructor() {
78
+ this.m = new Map();
79
+ }
80
+ async get(k) {
81
+ return this.m.get(k);
82
+ }
83
+ async set(k, v) {
84
+ this.m.set(k, v);
85
+ }
86
+ async clear() {
87
+ this.m.clear();
88
+ }
89
+ }
90
+ exports.MapAsyncMemoCache = MapAsyncMemoCache;
@@ -26,6 +26,12 @@ 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[];
29
35
  /**
30
36
  * Open-ended.
31
37
  */
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
- import { MemoCache } from './decorators/memo.util';
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);
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  import { _isPrimitive } from '../object/object.util';
2
2
  export const jsonMemoSerializer = args => {
3
- if (!args.length)
3
+ if (args.length === 0)
4
4
  return undefined;
5
5
  if (args.length === 1 && _isPrimitive(args[0]))
6
6
  return args[0];
@@ -68,3 +68,17 @@ export class MapMemoCache {
68
68
  this.m.clear();
69
69
  }
70
70
  }
71
+ export class MapAsyncMemoCache {
72
+ constructor() {
73
+ this.m = new Map();
74
+ }
75
+ async get(k) {
76
+ return this.m.get(k);
77
+ }
78
+ async set(k, v) {
79
+ this.m.set(k, v);
80
+ }
81
+ async clear() {
82
+ this.m.clear();
83
+ }
84
+ }
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';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.82.0",
3
+ "version": "14.83.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -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
  }
@@ -3,18 +3,41 @@ import { _isPrimitive } from '../object/object.util'
3
3
  export type MemoSerializer = (args: any[]) => any
4
4
 
5
5
  export const jsonMemoSerializer: MemoSerializer = args => {
6
- if (!args.length) return undefined
6
+ if (args.length === 0) return undefined
7
7
  if (args.length === 1 && _isPrimitive(args[0])) return args[0]
8
8
  return JSON.stringify(args)
9
9
  }
10
10
 
11
- export interface MemoCache {
12
- has(k: any): boolean
13
- get(k: any): any
14
- set(k: any, v: any): void
11
+ export interface MemoCache<KEY = any, VALUE = any> {
12
+ has(k: KEY): boolean
13
+ get(k: KEY): VALUE | undefined
14
+ set(k: KEY, v: VALUE): void
15
+
16
+ /**
17
+ * Clear is only called when `.dropCache()` is called.
18
+ * Otherwise the Cache is "persistent" (never cleared).
19
+ */
15
20
  clear(): void
16
21
  }
17
22
 
23
+ export interface AsyncMemoCache<KEY = any, VALUE = any> {
24
+ // `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`
25
+ // has(k: any): Promise<boolean>
26
+ /**
27
+ * `undefined` value returned indicates the ABSENCE of value in the Cache.
28
+ * This also means that you CANNOT store `undefined` value in the Cache, as it'll be treated as a MISS.
29
+ * You CAN store `null` value instead, it will be treated as a HIT.
30
+ */
31
+ get(k: KEY): Promise<VALUE | undefined>
32
+ set(k: KEY, v: VALUE): Promise<void>
33
+
34
+ /**
35
+ * Clear is only called when `.dropCache()` is called.
36
+ * Otherwise the Cache is "persistent" (never cleared).
37
+ */
38
+ clear(): Promise<void>
39
+ }
40
+
18
41
  // SingleValueMemoCache and ObjectMemoCache are example-only, not used in production code
19
42
  /*
20
43
  export class SingleValueMemoCache implements MemoCache {
@@ -61,18 +84,18 @@ export class ObjectMemoCache implements MemoCache {
61
84
  }
62
85
  */
63
86
 
64
- export class MapMemoCache implements MemoCache {
65
- private m = new Map<any, any>()
87
+ export class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALUE> {
88
+ private m = new Map<KEY, VALUE>()
66
89
 
67
- has(k: any): boolean {
90
+ has(k: KEY): boolean {
68
91
  return this.m.has(k)
69
92
  }
70
93
 
71
- get(k: any): any {
94
+ get(k: KEY): VALUE | undefined {
72
95
  return this.m.get(k)
73
96
  }
74
97
 
75
- set(k: any, v: any): void {
98
+ set(k: KEY, v: VALUE): void {
76
99
  this.m.set(k, v)
77
100
  }
78
101
 
@@ -80,3 +103,19 @@ export class MapMemoCache implements MemoCache {
80
103
  this.m.clear()
81
104
  }
82
105
  }
106
+
107
+ export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
108
+ private m = new Map<KEY, VALUE>()
109
+
110
+ async get(k: KEY): Promise<VALUE | undefined> {
111
+ return this.m.get(k)
112
+ }
113
+
114
+ async set(k: KEY, v: VALUE): Promise<void> {
115
+ this.m.set(k, v)
116
+ }
117
+
118
+ async clear(): Promise<void> {
119
+ this.m.clear()
120
+ }
121
+ }
@@ -31,6 +31,13 @@ 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
+
34
41
  /**
35
42
  * Open-ended.
36
43
  */
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
- import { MemoCache } from './decorators/memo.util'
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,