@naturalcycles/js-lib 15.98.1 → 15.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/array/array.util.js +5 -10
- package/dist/array/array2.js +3 -4
- package/dist/decorators/asyncMemo.decorator.d.ts +2 -3
- package/dist/decorators/asyncMemo.decorator.js +2 -3
- package/dist/decorators/memo.decorator.d.ts +4 -3
- package/dist/decorators/memo.decorator.js +18 -3
- package/dist/decorators/memo.util.d.ts +5 -2
- package/dist/decorators/memo.util.js +7 -2
- package/dist/decorators/memoized.js +14 -0
- package/dist/error/assert.js +2 -0
- package/dist/http/fetcher.js +3 -5
- package/dist/object/set2.js +2 -2
- package/package.json +2 -2
- package/src/array/array.util.ts +5 -5
- package/src/array/array2.ts +3 -2
- package/src/decorators/asyncMemo.decorator.ts +2 -3
- package/src/decorators/memo.decorator.ts +17 -3
- package/src/decorators/memo.util.ts +11 -3
- package/src/decorators/memoized.ts +13 -0
- package/src/error/assert.ts +2 -0
- package/src/http/fetcher.ts +3 -5
- package/src/object/set2.ts +3 -1
package/dist/array/array.util.js
CHANGED
|
@@ -369,8 +369,7 @@ export function _shuffle(array, opt = {}) {
|
|
|
369
369
|
return a;
|
|
370
370
|
}
|
|
371
371
|
export function _firstLast(array) {
|
|
372
|
-
|
|
373
|
-
throw new Error('_firstLast called on empty array');
|
|
372
|
+
_assert(array.length, '_firstLast called on empty array');
|
|
374
373
|
return [array[0], array[array.length - 1]];
|
|
375
374
|
}
|
|
376
375
|
export function _firstLastOrUndefined(array) {
|
|
@@ -383,8 +382,7 @@ export function _firstLastOrUndefined(array) {
|
|
|
383
382
|
* Throws if array is empty.
|
|
384
383
|
*/
|
|
385
384
|
export function _last(array) {
|
|
386
|
-
|
|
387
|
-
throw new Error('_last called on empty array');
|
|
385
|
+
_assert(array.length, '_last called on empty array');
|
|
388
386
|
return array[array.length - 1];
|
|
389
387
|
}
|
|
390
388
|
/**
|
|
@@ -398,8 +396,7 @@ export function _lastOrUndefined(array) {
|
|
|
398
396
|
* Throws if array is empty.
|
|
399
397
|
*/
|
|
400
398
|
export function _first(array) {
|
|
401
|
-
|
|
402
|
-
throw new Error('_first called on empty array');
|
|
399
|
+
_assert(array.length, '_first called on empty array');
|
|
403
400
|
return array[0];
|
|
404
401
|
}
|
|
405
402
|
/**
|
|
@@ -482,8 +479,7 @@ export function _minBy(array, mapper) {
|
|
|
482
479
|
return min;
|
|
483
480
|
}
|
|
484
481
|
export function _minMax(array) {
|
|
485
|
-
|
|
486
|
-
throw new Error('_minMax called on empty array');
|
|
482
|
+
_assert(array.length, '_minMax called on empty array');
|
|
487
483
|
const result = _minMaxOrUndefined(array);
|
|
488
484
|
_assert(result !== undefined, '_minBy returned undefined');
|
|
489
485
|
return result;
|
|
@@ -510,8 +506,7 @@ export function _minMaxOrUndefined(array) {
|
|
|
510
506
|
return [min, max];
|
|
511
507
|
}
|
|
512
508
|
export function _minMaxBy(array, mapper) {
|
|
513
|
-
|
|
514
|
-
throw new Error('_minMaxBy called on empty array');
|
|
509
|
+
_assert(array.length, '_minMaxBy called on empty array');
|
|
515
510
|
const result = _minMaxByOrUndefined(array, mapper);
|
|
516
511
|
_assert(result !== undefined, '_minMaxBy returned undefined');
|
|
517
512
|
return result;
|
package/dist/array/array2.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _assert } from '../error/assert.js';
|
|
1
2
|
import { _shuffle } from './array.util.js';
|
|
2
3
|
/**
|
|
3
4
|
* Better Array.
|
|
@@ -16,8 +17,7 @@ export class Array2 extends Array {
|
|
|
16
17
|
return this[0];
|
|
17
18
|
}
|
|
18
19
|
first() {
|
|
19
|
-
|
|
20
|
-
throw new Error('Array.first called on empty array');
|
|
20
|
+
_assert(this.length, 'Array.first called on empty array');
|
|
21
21
|
return this[0];
|
|
22
22
|
}
|
|
23
23
|
lastOrUndefined() {
|
|
@@ -25,8 +25,7 @@ export class Array2 extends Array {
|
|
|
25
25
|
}
|
|
26
26
|
last() {
|
|
27
27
|
const { length } = this;
|
|
28
|
-
|
|
29
|
-
throw new Error('Array.last called on empty array');
|
|
28
|
+
_assert(length, 'Array.last called on empty array');
|
|
30
29
|
return this[length - 1];
|
|
31
30
|
}
|
|
32
31
|
uniq() {
|
|
@@ -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
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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/dist/error/assert.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// `_assert` and friends are implemented by throwing in an if
|
|
2
|
+
// oxlint-disable nc/prefer-assert
|
|
1
3
|
import { _deepEquals } from '../object/deepEquals.js';
|
|
2
4
|
import { _stringify } from '../string/stringify.js';
|
|
3
5
|
import { _isBackendErrorResponseObject, _isErrorObject, AssertionError } from './error.util.js';
|
package/dist/http/fetcher.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { abortSignalAnyOrUndefined, abortSignalTimeoutOrUndefined } from '../abort.js';
|
|
5
5
|
import { _ms, _since } from '../datetime/time.util.js';
|
|
6
6
|
import { isServerSide } from '../env.js';
|
|
7
|
-
import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js';
|
|
7
|
+
import { _assert, _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js';
|
|
8
8
|
import { _anyToError, _anyToErrorObject, _errorDataAppend, _errorLikeToErrorObject, HttpRequestError, UnexpectedPassError, } from '../error/error.util.js';
|
|
9
9
|
import { consoleLogger } from '../log/commonLogger.js';
|
|
10
10
|
import { _clamp } from '../number/number.util.js';
|
|
@@ -454,10 +454,8 @@ export class Fetcher {
|
|
|
454
454
|
}
|
|
455
455
|
else if (responseType === 'readableStream') {
|
|
456
456
|
res.body = res.fetchResponse.body;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
throw new Error('fetchResponse.body is null');
|
|
460
|
-
}
|
|
457
|
+
// Error is to be handled upstream
|
|
458
|
+
_assert(res.body !== null, 'fetchResponse.body is null');
|
|
461
459
|
}
|
|
462
460
|
res.retryStatus.retryStopped = true;
|
|
463
461
|
if (req.logResponse) {
|
package/dist/object/set2.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _assert } from '../error/assert.js';
|
|
1
2
|
/**
|
|
2
3
|
* Like Set, but serializes to JSON as an array.
|
|
3
4
|
*
|
|
@@ -20,8 +21,7 @@ export class Set2 extends Set {
|
|
|
20
21
|
return this;
|
|
21
22
|
}
|
|
22
23
|
first() {
|
|
23
|
-
|
|
24
|
-
throw new Error('Set.first called on empty set');
|
|
24
|
+
_assert(this.size, 'Set.first called on empty set');
|
|
25
25
|
return this.firstOrUndefined();
|
|
26
26
|
}
|
|
27
27
|
firstOrUndefined() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/js-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.100.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"tslib": "^2"
|
|
7
7
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@naturalcycles/dev-lib": "
|
|
17
|
+
"@naturalcycles/dev-lib": "20.70.0",
|
|
18
18
|
"@types/node": "^26",
|
|
19
19
|
"@types/semver": "^7",
|
|
20
20
|
"typescript": "^7",
|
package/src/array/array.util.ts
CHANGED
|
@@ -435,7 +435,7 @@ export function _shuffle<T>(array: T[], opt: MutateOptions = {}): T[] {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
export function _firstLast<T>(array: readonly T[]): [first: T, last: T] {
|
|
438
|
-
|
|
438
|
+
_assert(array.length, '_firstLast called on empty array')
|
|
439
439
|
return [array[0]!, array[array.length - 1]!]
|
|
440
440
|
}
|
|
441
441
|
|
|
@@ -449,7 +449,7 @@ export function _firstLastOrUndefined<T>(array: readonly T[]): [first: T, last:
|
|
|
449
449
|
* Throws if array is empty.
|
|
450
450
|
*/
|
|
451
451
|
export function _last<T>(array: readonly T[]): T {
|
|
452
|
-
|
|
452
|
+
_assert(array.length, '_last called on empty array')
|
|
453
453
|
return array[array.length - 1]!
|
|
454
454
|
}
|
|
455
455
|
|
|
@@ -465,7 +465,7 @@ export function _lastOrUndefined<T>(array: readonly T[]): T | undefined {
|
|
|
465
465
|
* Throws if array is empty.
|
|
466
466
|
*/
|
|
467
467
|
export function _first<T>(array: readonly T[]): T {
|
|
468
|
-
|
|
468
|
+
_assert(array.length, '_first called on empty array')
|
|
469
469
|
return array[0]!
|
|
470
470
|
}
|
|
471
471
|
|
|
@@ -561,7 +561,7 @@ export function _minBy<T>(array: readonly T[], mapper: Mapper<T, number | string
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
export function _minMax<T>(array: readonly T[]): [min: NonNullable<T>, max: NonNullable<T>] {
|
|
564
|
-
|
|
564
|
+
_assert(array.length, '_minMax called on empty array')
|
|
565
565
|
const result = _minMaxOrUndefined(array)
|
|
566
566
|
_assert(result !== undefined, '_minBy returned undefined')
|
|
567
567
|
return result
|
|
@@ -592,7 +592,7 @@ export function _minMaxBy<T>(
|
|
|
592
592
|
array: readonly T[],
|
|
593
593
|
mapper: Mapper<T, number | string | undefined>,
|
|
594
594
|
): [min: NonNullable<T>, max: NonNullable<T>] {
|
|
595
|
-
|
|
595
|
+
_assert(array.length, '_minMaxBy called on empty array')
|
|
596
596
|
const result = _minMaxByOrUndefined(array, mapper)
|
|
597
597
|
_assert(result !== undefined, '_minMaxBy returned undefined')
|
|
598
598
|
return result
|
package/src/array/array2.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _assert } from '../error/assert.js'
|
|
1
2
|
import { _shuffle } from './array.util.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -20,7 +21,7 @@ export class Array2<T> extends Array<T> {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
first(): T {
|
|
23
|
-
|
|
24
|
+
_assert(this.length, 'Array.first called on empty array')
|
|
24
25
|
return this[0]!
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -30,7 +31,7 @@ export class Array2<T> extends Array<T> {
|
|
|
30
31
|
|
|
31
32
|
last(): T {
|
|
32
33
|
const { length } = this
|
|
33
|
-
|
|
34
|
+
_assert(length, 'Array.last called on empty array')
|
|
34
35
|
return this[length - 1]!
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
*
|
|
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
|
|
package/src/error/assert.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// `_assert` and friends are implemented by throwing in an if
|
|
2
|
+
// oxlint-disable nc/prefer-assert
|
|
1
3
|
import { _deepEquals } from '../object/deepEquals.js'
|
|
2
4
|
import { _stringify } from '../string/stringify.js'
|
|
3
5
|
import type { Class, NonEmptyArray, UnixTimestamp } from '../types.js'
|
package/src/http/fetcher.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ReadableStream as WebReadableStream } from 'node:stream/web'
|
|
|
6
6
|
import { abortSignalAnyOrUndefined, abortSignalTimeoutOrUndefined } from '../abort.js'
|
|
7
7
|
import { _ms, _since } from '../datetime/time.util.js'
|
|
8
8
|
import { isServerSide } from '../env.js'
|
|
9
|
-
import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js'
|
|
9
|
+
import { _assert, _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js'
|
|
10
10
|
import type { ErrorLike, ErrorObject } from '../error/error.model.js'
|
|
11
11
|
import {
|
|
12
12
|
_anyToError,
|
|
@@ -560,10 +560,8 @@ export class Fetcher {
|
|
|
560
560
|
} else if (responseType === 'readableStream') {
|
|
561
561
|
res.body = res.fetchResponse.body
|
|
562
562
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
throw new Error('fetchResponse.body is null')
|
|
566
|
-
}
|
|
563
|
+
// Error is to be handled upstream
|
|
564
|
+
_assert(res.body !== null, 'fetchResponse.body is null')
|
|
567
565
|
}
|
|
568
566
|
|
|
569
567
|
res.retryStatus.retryStopped = true
|
package/src/object/set2.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { _assert } from '../error/assert.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Like Set, but serializes to JSON as an array.
|
|
3
5
|
*
|
|
@@ -22,7 +24,7 @@ export class Set2<T = any> extends Set<T> {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
first(): T {
|
|
25
|
-
|
|
27
|
+
_assert(this.size, 'Set.first called on empty set')
|
|
26
28
|
return this.firstOrUndefined()!
|
|
27
29
|
}
|
|
28
30
|
|