@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.
@@ -369,8 +369,7 @@ export function _shuffle(array, opt = {}) {
369
369
  return a;
370
370
  }
371
371
  export function _firstLast(array) {
372
- if (!array.length)
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
- if (!array.length)
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
- if (!array.length)
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
- if (!array.length)
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
- if (!array.length)
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;
@@ -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
- if (!this.length)
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
- if (!length)
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 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 });
@@ -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';
@@ -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
- if (res.body === null) {
458
- // Error is to be handled upstream
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) {
@@ -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
- if (!this.size)
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.98.1",
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": "0.0.0",
17
+ "@naturalcycles/dev-lib": "20.70.0",
18
18
  "@types/node": "^26",
19
19
  "@types/semver": "^7",
20
20
  "typescript": "^7",
@@ -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
- if (!array.length) throw new Error('_firstLast called on empty array')
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
- if (!array.length) throw new Error('_last called on empty array')
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
- if (!array.length) throw new Error('_first called on empty array')
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
- if (!array.length) throw new Error('_minMax called on empty array')
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
- if (!array.length) throw new Error('_minMaxBy called on empty array')
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
@@ -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
- if (!this.length) throw new Error('Array.first called on empty array')
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
- if (!length) throw new Error('Array.last called on empty array')
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 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
 
@@ -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'
@@ -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
- if (res.body === null) {
564
- // Error is to be handled upstream
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
@@ -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
- if (!this.size) throw new Error('Set.first called on empty set')
27
+ _assert(this.size, 'Set.first called on empty set')
26
28
  return this.firstOrUndefined()!
27
29
  }
28
30