@naturalcycles/js-lib 15.98.1 → 15.99.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.
@@ -31,10 +31,9 @@ export interface AsyncMemoInstance {
31
31
  /**
32
32
  * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
33
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
34
  * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
37
- * and survive a process restart.
35
+ * and survive a process restart. That's the reason to use it over `@_Memo`.
36
+ * - A rejection is NOT cached, so the next call retries the method (same as `@_Memo`).
38
37
  * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
39
38
  *
40
39
  * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
@@ -6,10 +6,9 @@ import { jsonMemoSerializer, MapAsyncMemoCache } from './memo.util.js';
6
6
  /**
7
7
  * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
8
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
9
  * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
12
- * and survive a process restart.
10
+ * and survive a process restart. That's the reason to use it over `@_Memo`.
11
+ * - A rejection is NOT cached, so the next call retries the method (same as `@_Memo`).
13
12
  * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
14
13
  *
15
14
  * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
@@ -35,9 +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.
38
+ * For an async method it's the returned **Promise** that is cached, so parallel calls with the same key
39
+ * share one in-flight Promise. A rejected Promise is evicted from the cache, so the next call retries
40
+ * the method, same as for a thrown Error.
41
+ * Use `@_AsyncMemo` for an async-backed cache (e.g Redis), which stores the resolved value instead.
41
42
  *
42
43
  * Cache is stored **per instance** - separate cache for separate instances of the class.
43
44
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
@@ -12,9 +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.
15
+ * For an async method it's the returned **Promise** that is cached, so parallel calls with the same key
16
+ * share one in-flight Promise. A rejected Promise is evicted from the cache, so the next call retries
17
+ * the method, same as for a thrown Error.
18
+ * Use `@_AsyncMemo` for an async-backed cache (e.g Redis), which stores the resolved value instead.
18
19
  *
19
20
  * Cache is stored **per instance** - separate cache for separate instances of the class.
20
21
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
@@ -61,6 +62,20 @@ export const _Memo = (opt = {}) => (target, key, descriptor) => {
61
62
  catch (err) {
62
63
  logger.error({ err });
63
64
  }
65
+ if (value instanceof Promise) {
66
+ // Don't cache a rejection: evict it, so the next call retries the method.
67
+ // The derived Promise is handled, so it adds no unhandled rejection.
68
+ value.catch(() => {
69
+ try {
70
+ // Only if the key still holds this very Promise (and not a newer one, cached after an eviction)
71
+ if (cache.get(cacheKey) === value)
72
+ cache.delete(cacheKey);
73
+ }
74
+ catch (err) {
75
+ logger.error({ err });
76
+ }
77
+ });
78
+ }
64
79
  return value;
65
80
  };
66
81
  _objectAssign(descriptor.value, {
@@ -19,6 +19,7 @@ export interface MemoCache<KEY = any, VALUE = any> {
19
19
  */
20
20
  get: (k: KEY) => VALUE;
21
21
  set: (k: KEY, v: VALUE, opt?: MemoCacheOptions) => void;
22
+ delete: (k: KEY) => void;
22
23
  /**
23
24
  * Clear is only called when `_getMemoCache().clear()` is called.
24
25
  * Otherwise the Cache is "persistent" (never cleared).
@@ -33,6 +34,7 @@ export interface AsyncMemoCache<KEY = any, VALUE = any> {
33
34
  */
34
35
  get: (k: KEY) => Promise<VALUE | typeof MISS>;
35
36
  set: (k: KEY, v: VALUE, opt?: MemoCacheOptions) => Promise<void>;
37
+ delete: (k: KEY) => Promise<void>;
36
38
  /**
37
39
  * Clear is only called when `_getAsyncMemo().clear()` is called.
38
40
  * Otherwise the Cache is "persistent" (never cleared).
@@ -44,17 +46,18 @@ export declare class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<K
44
46
  has(k: KEY): boolean;
45
47
  get(k: KEY): VALUE;
46
48
  set(k: KEY, v: VALUE): void;
49
+ delete(k: KEY): void;
47
50
  clear(): void;
48
51
  }
49
52
  /**
50
53
  * Implementation of AsyncMemoCache backed by a synchronous Map.
51
54
  * 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.
55
+ * For an **async** backed cache (Redis, Datastore) use another implementation.
54
56
  */
55
57
  export declare class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
56
58
  private m;
57
59
  get(k: KEY): Promise<VALUE | typeof MISS>;
58
60
  set(k: KEY, v: VALUE): Promise<void>;
61
+ delete(k: KEY): Promise<void>;
59
62
  clear(): Promise<void>;
60
63
  }
@@ -63,6 +63,9 @@ export class MapMemoCache {
63
63
  set(k, v) {
64
64
  this.m.set(k, v);
65
65
  }
66
+ delete(k) {
67
+ this.m.delete(k);
68
+ }
66
69
  clear() {
67
70
  this.m.clear();
68
71
  }
@@ -70,8 +73,7 @@ export class MapMemoCache {
70
73
  /**
71
74
  * Implementation of AsyncMemoCache backed by a synchronous Map.
72
75
  * 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.
76
+ * For an **async** backed cache (Redis, Datastore) use another implementation.
75
77
  */
76
78
  export class MapAsyncMemoCache {
77
79
  m = new Map();
@@ -85,6 +87,9 @@ export class MapAsyncMemoCache {
85
87
  async set(k, v) {
86
88
  this.m.set(k, v);
87
89
  }
90
+ async delete(k) {
91
+ this.m.delete(k);
92
+ }
88
93
  async clear() {
89
94
  this.m.clear();
90
95
  }
@@ -21,6 +21,20 @@ export function _memoized(fn, opt = {}) {
21
21
  catch (err) {
22
22
  logger.error({ err });
23
23
  }
24
+ if (value instanceof Promise) {
25
+ // Don't cache a rejection: evict it, so the next call retries the function.
26
+ // The derived Promise is handled, so it adds no unhandled rejection.
27
+ value.catch(() => {
28
+ try {
29
+ // Only if the key still holds this very Promise (and not a newer one, cached after an eviction)
30
+ if (cache.get(cacheKey) === value)
31
+ cache.delete(cacheKey);
32
+ }
33
+ catch (err) {
34
+ logger.error({ err });
35
+ }
36
+ });
37
+ }
24
38
  return value;
25
39
  };
26
40
  Object.assign(memoizedFn, { cache });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.98.1",
4
+ "version": "15.99.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -43,10 +43,9 @@ export interface AsyncMemoInstance {
43
43
  /**
44
44
  * Like `@_Memo`, but awaits the method and caches the resolved **value**, not the Promise:
45
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
46
  * - The cache is async (`AsyncMemoCache`), so it can be backed by e.g Redis or Datastore
49
- * and survive a process restart.
47
+ * and survive a process restart. That's the reason to use it over `@_Memo`.
48
+ * - A rejection is NOT cached, so the next call retries the method (same as `@_Memo`).
50
49
  * - Parallel calls with the same key share one in-flight Promise, also while the cache lookup is pending.
51
50
  *
52
51
  * Cache errors (`get`/`set`) are logged and treated as a cache miss, they never fail the method.
@@ -47,9 +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.
50
+ * For an async method it's the returned **Promise** that is cached, so parallel calls with the same key
51
+ * share one in-flight Promise. A rejected Promise is evicted from the cache, so the next call retries
52
+ * the method, same as for a thrown Error.
53
+ * Use `@_AsyncMemo` for an async-backed cache (e.g Redis), which stores the resolved value instead.
53
54
  *
54
55
  * Cache is stored **per instance** - separate cache for separate instances of the class.
55
56
  * If you don't want it that way - you can use a static method, then there will be only one "instance".
@@ -115,6 +116,19 @@ export const _Memo =
115
116
  logger.error({ err })
116
117
  }
117
118
 
119
+ if (value instanceof Promise) {
120
+ // Don't cache a rejection: evict it, so the next call retries the method.
121
+ // The derived Promise is handled, so it adds no unhandled rejection.
122
+ value.catch(() => {
123
+ try {
124
+ // Only if the key still holds this very Promise (and not a newer one, cached after an eviction)
125
+ if (cache.get(cacheKey) === value) cache.delete(cacheKey)
126
+ } catch (err) {
127
+ logger.error({ err })
128
+ }
129
+ })
130
+ }
131
+
118
132
  return value
119
133
  } as any
120
134
 
@@ -28,7 +28,7 @@ export interface MemoCache<KEY = any, VALUE = any> {
28
28
  */
29
29
  get: (k: KEY) => VALUE
30
30
  set: (k: KEY, v: VALUE, opt?: MemoCacheOptions) => void
31
-
31
+ delete: (k: KEY) => void
32
32
  /**
33
33
  * Clear is only called when `_getMemoCache().clear()` is called.
34
34
  * Otherwise the Cache is "persistent" (never cleared).
@@ -46,6 +46,7 @@ export interface AsyncMemoCache<KEY = any, VALUE = any> {
46
46
  */
47
47
  get: (k: KEY) => Promise<VALUE | typeof MISS>
48
48
  set: (k: KEY, v: VALUE, opt?: MemoCacheOptions) => Promise<void>
49
+ delete: (k: KEY) => Promise<void>
49
50
 
50
51
  /**
51
52
  * Clear is only called when `_getAsyncMemo().clear()` is called.
@@ -115,6 +116,10 @@ export class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALU
115
116
  this.m.set(k, v)
116
117
  }
117
118
 
119
+ delete(k: KEY): void {
120
+ this.m.delete(k)
121
+ }
122
+
118
123
  clear(): void {
119
124
  this.m.clear()
120
125
  }
@@ -123,8 +128,7 @@ export class MapMemoCache<KEY = any, VALUE = any> implements MemoCache<KEY, VALU
123
128
  /**
124
129
  * Implementation of AsyncMemoCache backed by a synchronous Map.
125
130
  * 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.
131
+ * For an **async** backed cache (Redis, Datastore) use another implementation.
128
132
  */
129
133
  export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache<KEY, VALUE> {
130
134
  private m = new Map<KEY, VALUE>()
@@ -140,6 +144,10 @@ export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache
140
144
  this.m.set(k, v)
141
145
  }
142
146
 
147
+ async delete(k: KEY): Promise<void> {
148
+ this.m.delete(k)
149
+ }
150
+
143
151
  async clear(): Promise<void> {
144
152
  this.m.clear()
145
153
  }
@@ -41,6 +41,19 @@ export function _memoized<FN extends AnyFunction>(
41
41
  logger.error({ err })
42
42
  }
43
43
 
44
+ if (value instanceof Promise) {
45
+ // Don't cache a rejection: evict it, so the next call retries the function.
46
+ // The derived Promise is handled, so it adds no unhandled rejection.
47
+ value.catch(() => {
48
+ try {
49
+ // Only if the key still holds this very Promise (and not a newer one, cached after an eviction)
50
+ if (cache.get(cacheKey) === value) cache.delete(cacheKey)
51
+ } catch (err) {
52
+ logger.error({ err })
53
+ }
54
+ })
55
+ }
56
+
44
57
  return value
45
58
  }
46
59