@naturalcycles/js-lib 15.98.0 → 15.98.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.
@@ -1,13 +1,16 @@
1
1
  import type { CommonLogger } from '../log/commonLogger.js';
2
- import type { AnyAsyncFunction, AnyFunction, AnyObject, MaybeParameters } from '../types.js';
2
+ import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js';
3
3
  import type { MethodDecorator } from './decorator.util.js';
4
4
  import type { AsyncMemoCache } from './memo.util.js';
5
5
  export interface AsyncMemoOptions<FN> {
6
6
  /**
7
7
  * Provide a custom implementation of AsyncMemoCache.
8
8
  * Function that creates an instance of `AsyncMemoCache`.
9
+ * Defaults to in-memory `MapAsyncMemoCache`, which never expires.
10
+ * e.g `LRUAsyncMemoCache` (ttl, max) from `@naturalcycles/nodejs-lib`,
11
+ * or `CommonKeyValueDaoMemoCache` (persisted in a DB) from `@naturalcycles/db-lib`.
9
12
  */
10
- cacheFactory: () => AsyncMemoCache;
13
+ cacheFactory?: () => AsyncMemoCache;
11
14
  /**
12
15
  * Provide a custom implementation of CacheKey function.
13
16
  */
@@ -23,17 +26,23 @@ export interface AsyncMemoInstance {
23
26
  */
24
27
  clear: () => Promise<void>;
25
28
  getInstanceCache: () => Map<AnyObject, AsyncMemoCache>;
26
- getCache: (instance: AnyAsyncFunction) => AsyncMemoCache | undefined;
29
+ getCache: (instance: AnyObject) => AsyncMemoCache | undefined;
27
30
  }
28
31
  /**
29
- * Like `@_Memo`, but allowing async MemoCache implementation.
32
+ * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
33
+ *
34
+ * - A rejection is NOT cached, so the next call retries the method.
35
+ * `@_Memo` caches the Promise as-is, so it serves a rejection to every caller until the key is evicted.
36
+ * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
37
+ * and survive a process restart.
38
+ * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
39
+ *
40
+ * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
30
41
  *
31
42
  * Implementation is more complex than `@_Memo`, because it needs to handle "in-flight" Promises
32
43
  * while waiting for cache to resolve, to prevent "async swarm" issue.
33
- *
34
- * @experimental consider normal `@_Memo` for most of the cases, it's stable and predictable
35
44
  */
36
- export declare const _AsyncMemo: <FN>(opt: AsyncMemoOptions<FN>) => MethodDecorator<FN>;
45
+ export declare const _AsyncMemo: <FN>(opt?: AsyncMemoOptions<FN>) => MethodDecorator<FN>;
37
46
  /**
38
47
  Call it on a method that is decorated with `@_AsyncMemo` to get access to additional functions,
39
48
  e.g `clear` to clear the cache, or get its underlying data.
@@ -2,16 +2,22 @@ import { _assert, _assertTypeOf } from '../error/assert.js';
2
2
  import { consoleLogger } from '../log/commonLogger.js';
3
3
  import { _objectAssign, MISS } from '../types.js';
4
4
  import { _getTargetMethodSignature } from './decorator.util.js';
5
- import { jsonMemoSerializer } from './memo.util.js';
5
+ import { jsonMemoSerializer, MapAsyncMemoCache } from './memo.util.js';
6
6
  /**
7
- * Like `@_Memo`, but allowing async MemoCache implementation.
7
+ * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
8
+ *
9
+ * - A rejection is NOT cached, so the next call retries the method.
10
+ * `@_Memo` caches the Promise as-is, so it serves a rejection to every caller until the key is evicted.
11
+ * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
12
+ * and survive a process restart.
13
+ * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
14
+ *
15
+ * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
8
16
  *
9
17
  * Implementation is more complex than `@_Memo`, because it needs to handle "in-flight" Promises
10
18
  * while waiting for cache to resolve, to prevent "async swarm" issue.
11
- *
12
- * @experimental consider normal `@_Memo` for most of the cases, it's stable and predictable
13
19
  */
14
- export const _AsyncMemo = (opt) => (target, key, descriptor) => {
20
+ export const _AsyncMemo = (opt = {}) => (target, key, descriptor) => {
15
21
  _assertTypeOf(descriptor.value, 'function', 'Memoization can be applied only to methods');
16
22
  const originalFn = descriptor.value;
17
23
  // Map from "instance" of the Class where @_AsyncMemo is applied to AsyncMemoCache instance.
@@ -21,7 +27,7 @@ export const _AsyncMemo = (opt) => (target, key, descriptor) => {
21
27
  // It only holds values that are "in-flight", until Promise is resolved
22
28
  // After it's resolved - it's evicted from the cache and moved to the "proper" `instanceCache`
23
29
  const instancePromiseCache = new Map();
24
- const { logger = consoleLogger, cacheFactory, cacheKeyFn = jsonMemoSerializer } = opt;
30
+ const { logger = consoleLogger, cacheFactory = () => new MapAsyncMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
25
31
  const keyStr = String(key);
26
32
  const methodSignature = _getTargetMethodSignature(target, keyStr);
27
33
  // oxlint-disable-next-line @typescript-eslint/promise-function-async
@@ -24,7 +24,7 @@ export interface MemoInstance {
24
24
  */
25
25
  clear: () => void;
26
26
  getInstanceCache: () => Map<AnyObject, MemoCache>;
27
- getCache: (instance: AnyFunction) => MemoCache | undefined;
27
+ getCache: (instance: AnyObject) => MemoCache | undefined;
28
28
  }
29
29
  /**
30
30
  * Memoizes the method of the class, so it caches the output and returns the cached version if the "key"
@@ -35,6 +35,10 @@ export interface MemoInstance {
35
35
  * So, error-throwing functions will be called multiple times.
36
36
  * Therefor, if the origin function can possibly throw - it should try to be idempotent.
37
37
  *
38
+ * For an async method it's the returned **Promise** that is cached, so a rejected Promise IS cached:
39
+ * every caller gets the same rejection until the key is evicted from the cache.
40
+ * Use `@_AsyncMemo` to cache only the resolved value and retry the method on rejection.
41
+ *
38
42
  * Cache is stored **per instance** - separate cache for separate instances of the class.
39
43
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
40
44
  *
@@ -12,6 +12,10 @@ import { jsonMemoSerializer, MapMemoCache } from './memo.util.js';
12
12
  * So, error-throwing functions will be called multiple times.
13
13
  * Therefor, if the origin function can possibly throw - it should try to be idempotent.
14
14
  *
15
+ * For an async method it's the returned **Promise** that is cached, so a rejected Promise IS cached:
16
+ * every caller gets the same rejection until the key is evicted from the cache.
17
+ * Use `@_AsyncMemo` to cache only the resolved value and retry the method on rejection.
18
+ *
15
19
  * Cache is stored **per instance** - separate cache for separate instances of the class.
16
20
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
17
21
  *
@@ -48,12 +48,11 @@ export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<K
48
48
  }
49
49
  /**
50
50
  * Implementation of AsyncMemoCache backed by a synchronous Map.
51
- * Doesn't have a practical use except testing,
52
- * because the point of AsyncMemoCache is to have an **async** backed cache.
51
+ * The default cache of `@_AsyncMemo`: in-memory, never expires.
52
+ * Use it over `@_Memo` when a rejection should not be cached (e.g an `init()` that may fail and
53
+ * should be retried). For an **async** backed cache (Redis, Datastore) use another implementation.
53
54
  */
54
55
  export declare class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
55
- private delay;
56
- constructor(delay?: number);
57
56
  private m;
58
57
  get(k: KEY): Promise<VALUE | typeof MISS>;
59
58
  set(k: KEY, v: VALUE): Promise<void>;
@@ -1,5 +1,4 @@
1
1
  import { _isPrimitive } from '../is.util.js';
2
- import { pDelay } from '../promise/pDelay.js';
3
2
  import { MISS } from '../types.js';
4
3
  export const jsonMemoSerializer = args => {
5
4
  if (args.length === 0)
@@ -70,27 +69,23 @@ export class MapMemoCache {
70
69
  }
71
70
  /**
72
71
  * Implementation of AsyncMemoCache backed by a synchronous Map.
73
- * Doesn't have a practical use except testing,
74
- * because the point of AsyncMemoCache is to have an **async** backed cache.
72
+ * The default cache of `@_AsyncMemo`: in-memory, never expires.
73
+ * Use it over `@_Memo` when a rejection should not be cached (e.g an `init()` that may fail and
74
+ * should be retried). For an **async** backed cache (Redis, Datastore) use another implementation.
75
75
  */
76
76
  export class MapAsyncMemoCache {
77
- delay;
78
- constructor(delay = 0) {
79
- this.delay = delay;
80
- }
81
77
  m = new Map();
82
78
  async get(k) {
83
- await pDelay(this.delay);
84
- if (!this.m.has(k))
79
+ const v = this.m.get(k);
80
+ // `undefined` is a valid VALUE, only then `has` tells it apart from a miss
81
+ if (v === undefined && !this.m.has(k))
85
82
  return MISS;
86
- return this.m.get(k);
83
+ return v;
87
84
  }
88
85
  async set(k, v) {
89
- await pDelay(this.delay);
90
86
  this.m.set(k, v);
91
87
  }
92
88
  async clear() {
93
- await pDelay(this.delay);
94
89
  this.m.clear();
95
90
  }
96
91
  }
@@ -4,4 +4,10 @@ import type { AsyncMemoCache } from './memo.util.js';
4
4
  export interface MemoizedAsyncFunction {
5
5
  cache: AsyncMemoCache;
6
6
  }
7
- export declare function _memoizedAsync<FN extends AnyAsyncFunction>(fn: FN, opt: AsyncMemoOptions<FN>): FN & MemoizedAsyncFunction;
7
+ /**
8
+ * Function version of `@_AsyncMemo`: awaits `fn` and caches the resolved value, not the Promise,
9
+ * so a rejection is not cached. Cache errors are logged and treated as a miss.
10
+ *
11
+ * Unlike the decorator, it does NOT dedupe parallel calls with the same key.
12
+ */
13
+ export declare function _memoizedAsync<FN extends AnyAsyncFunction>(fn: FN, opt?: AsyncMemoOptions<FN>): FN & MemoizedAsyncFunction;
@@ -1,13 +1,20 @@
1
1
  import { consoleLogger } from '../log/commonLogger.js';
2
2
  import { MISS } from '../types.js';
3
- import { jsonMemoSerializer } from './memo.util.js';
4
- export function _memoizedAsync(fn, opt) {
5
- const { logger = consoleLogger, cacheFactory, cacheKeyFn = jsonMemoSerializer } = opt;
3
+ import { jsonMemoSerializer, MapAsyncMemoCache } from './memo.util.js';
4
+ /**
5
+ * Function version of `@_AsyncMemo`: awaits `fn` and caches the resolved value, not the Promise,
6
+ * so a rejection is not cached. Cache errors are logged and treated as a miss.
7
+ *
8
+ * Unlike the decorator, it does NOT dedupe parallel calls with the same key.
9
+ */
10
+ export function _memoizedAsync(fn, opt = {}) {
11
+ const { logger = consoleLogger, cacheFactory = () => new MapAsyncMemoCache(), cacheKeyFn = jsonMemoSerializer, } = opt;
6
12
  const cache = cacheFactory();
7
13
  const memoizedFn = async function (...args) {
8
14
  const ctx = this;
9
15
  const cacheKey = cacheKeyFn(args);
10
- let value;
16
+ // MISS, so that a failed cache `get` is a miss too (and not a cached `undefined`)
17
+ let value = MISS;
11
18
  try {
12
19
  value = await cache.get(cacheKey);
13
20
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.98.0",
4
+ "version": "15.98.1",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -1,19 +1,22 @@
1
1
  import { _assert, _assertTypeOf } from '../error/assert.js'
2
2
  import { consoleLogger } from '../log/commonLogger.js'
3
3
  import type { CommonLogger } from '../log/commonLogger.js'
4
- import type { AnyAsyncFunction, AnyFunction, AnyObject, MaybeParameters } from '../types.js'
4
+ import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js'
5
5
  import { _objectAssign, MISS } from '../types.js'
6
6
  import type { MethodDecorator } from './decorator.util.js'
7
7
  import { _getTargetMethodSignature } from './decorator.util.js'
8
8
  import type { AsyncMemoCache } from './memo.util.js'
9
- import { jsonMemoSerializer } from './memo.util.js'
9
+ import { jsonMemoSerializer, MapAsyncMemoCache } from './memo.util.js'
10
10
 
11
11
  export interface AsyncMemoOptions<FN> {
12
12
  /**
13
13
  * Provide a custom implementation of AsyncMemoCache.
14
14
  * Function that creates an instance of `AsyncMemoCache`.
15
+ * Defaults to in-memory `MapAsyncMemoCache`, which never expires.
16
+ * e.g `LRUAsyncMemoCache` (ttl, max) from `@naturalcycles/nodejs-lib`,
17
+ * or `CommonKeyValueDaoMemoCache` (persisted in a DB) from `@naturalcycles/db-lib`.
15
18
  */
16
- cacheFactory: () => AsyncMemoCache
19
+ cacheFactory?: () => AsyncMemoCache
17
20
 
18
21
  /**
19
22
  * Provide a custom implementation of CacheKey function.
@@ -34,19 +37,25 @@ export interface AsyncMemoInstance {
34
37
 
35
38
  getInstanceCache: () => Map<AnyObject, AsyncMemoCache>
36
39
 
37
- getCache: (instance: AnyAsyncFunction) => AsyncMemoCache | undefined
40
+ getCache: (instance: AnyObject) => AsyncMemoCache | undefined
38
41
  }
39
42
 
40
43
  /**
41
- * Like `@_Memo`, but allowing async MemoCache implementation.
44
+ * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
45
+ *
46
+ * - A rejection is NOT cached, so the next call retries the method.
47
+ * `@_Memo` caches the Promise as-is, so it serves a rejection to every caller until the key is evicted.
48
+ * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
49
+ * and survive a process restart.
50
+ * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
51
+ *
52
+ * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
42
53
  *
43
54
  * Implementation is more complex than `@_Memo`, because it needs to handle "in-flight" Promises
44
55
  * while waiting for cache to resolve, to prevent "async swarm" issue.
45
- *
46
- * @experimental consider normal `@_Memo` for most of the cases, it's stable and predictable
47
56
  */
48
57
  export const _AsyncMemo =
49
- <FN>(opt: AsyncMemoOptions<FN>): MethodDecorator<FN> =>
58
+ <FN>(opt: AsyncMemoOptions<FN> = {}): MethodDecorator<FN> =>
50
59
  (target, key, descriptor) => {
51
60
  _assertTypeOf<AnyFunction>(
52
61
  descriptor.value,
@@ -65,7 +74,11 @@ export const _AsyncMemo =
65
74
  // After it's resolved - it's evicted from the cache and moved to the "proper" `instanceCache`
66
75
  const instancePromiseCache = new Map<AnyObject, Map<any, Promise<any>>>()
67
76
 
68
- const { logger = consoleLogger, cacheFactory, cacheKeyFn = jsonMemoSerializer } = opt
77
+ const {
78
+ logger = consoleLogger,
79
+ cacheFactory = () => new MapAsyncMemoCache(),
80
+ cacheKeyFn = jsonMemoSerializer,
81
+ } = opt
69
82
 
70
83
  const keyStr = String(key)
71
84
  const methodSignature = _getTargetMethodSignature(target, keyStr)
@@ -35,7 +35,7 @@ export interface MemoInstance {
35
35
 
36
36
  getInstanceCache: () => Map<AnyObject, MemoCache>
37
37
 
38
- getCache: (instance: AnyFunction) => MemoCache | undefined
38
+ getCache: (instance: AnyObject) => MemoCache | undefined
39
39
  }
40
40
 
41
41
  /**
@@ -47,6 +47,10 @@ export interface MemoInstance {
47
47
  * So, error-throwing functions will be called multiple times.
48
48
  * Therefor, if the origin function can possibly throw - it should try to be idempotent.
49
49
  *
50
+ * For an async method it's the returned **Promise** that is cached, so a rejected Promise IS cached:
51
+ * every caller gets the same rejection until the key is evicted from the cache.
52
+ * Use `@_AsyncMemo` to cache only the resolved value and retry the method on rejection.
53
+ *
50
54
  * Cache is stored **per instance** - separate cache for separate instances of the class.
51
55
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
52
56
  *
@@ -1,5 +1,4 @@
1
1
  import { _isPrimitive } from '../is.util.js'
2
- import { pDelay } from '../promise/pDelay.js'
3
2
  import type { UnixTimestamp } from '../types.js'
4
3
  import { MISS } from '../types.js'
5
4
 
@@ -123,27 +122,25 @@ export class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALU
123
122
 
124
123
  /**
125
124
  * Implementation of AsyncMemoCache backed by a synchronous Map.
126
- * Doesn't have a practical use except testing,
127
- * because the point of AsyncMemoCache is to have an **async** backed cache.
125
+ * The default cache of `@_AsyncMemo`: in-memory, never expires.
126
+ * Use it over `@_Memo` when a rejection should not be cached (e.g an `init()` that may fail and
127
+ * should be retried). For an **async** backed cache (Redis, Datastore) use another implementation.
128
128
  */
129
129
  export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
130
- constructor(private delay = 0) {}
131
-
132
130
  private m = new Map<KEY, VALUE>()
133
131
 
134
132
  async get(k: KEY): Promise<VALUE | typeof MISS> {
135
- await pDelay(this.delay)
136
- if (!this.m.has(k)) return MISS
137
- return this.m.get(k)!
133
+ const v = this.m.get(k)
134
+ // `undefined` is a valid VALUE, only then `has` tells it apart from a miss
135
+ if (v === undefined && !this.m.has(k)) return MISS
136
+ return v!
138
137
  }
139
138
 
140
139
  async set(k: KEY, v: VALUE): Promise<void> {
141
- await pDelay(this.delay)
142
140
  this.m.set(k, v)
143
141
  }
144
142
 
145
143
  async clear(): Promise<void> {
146
- await pDelay(this.delay)
147
144
  this.m.clear()
148
145
  }
149
146
  }
@@ -3,24 +3,35 @@ import type { AnyAsyncFunction, MaybeParameters } from '../types.js'
3
3
  import { MISS } from '../types.js'
4
4
  import type { AsyncMemoOptions } from './asyncMemo.decorator.js'
5
5
  import type { AsyncMemoCache } from './memo.util.js'
6
- import { jsonMemoSerializer } from './memo.util.js'
6
+ import { jsonMemoSerializer, MapAsyncMemoCache } from './memo.util.js'
7
7
 
8
8
  export interface MemoizedAsyncFunction {
9
9
  cache: AsyncMemoCache
10
10
  }
11
11
 
12
+ /**
13
+ * Function version of `@_AsyncMemo`: awaits `fn` and caches the resolved value, not the Promise,
14
+ * so a rejection is not cached. Cache errors are logged and treated as a miss.
15
+ *
16
+ * Unlike the decorator, it does NOT dedupe parallel calls with the same key.
17
+ */
12
18
  export function _memoizedAsync<FN extends AnyAsyncFunction>(
13
19
  fn: FN,
14
- opt: AsyncMemoOptions<FN>,
20
+ opt: AsyncMemoOptions<FN> = {},
15
21
  ): FN & MemoizedAsyncFunction {
16
- const { logger = consoleLogger, cacheFactory, cacheKeyFn = jsonMemoSerializer } = opt
22
+ const {
23
+ logger = consoleLogger,
24
+ cacheFactory = () => new MapAsyncMemoCache(),
25
+ cacheKeyFn = jsonMemoSerializer,
26
+ } = opt
17
27
 
18
28
  const cache = cacheFactory()
19
29
 
20
30
  const memoizedFn = async function (this: any, ...args: MaybeParameters<FN>): Promise<any> {
21
31
  const ctx = this
22
32
  const cacheKey = cacheKeyFn(args)
23
- let value: any
33
+ // MISS, so that a failed cache `get` is a miss too (and not a cached `undefined`)
34
+ let value: any = MISS
24
35
 
25
36
  try {
26
37
  value = await cache.get(cacheKey)