@toa.io/extensions.fetch 1.0.0-alpha.284 → 1.0.0-alpha.287

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/CHANGELOG.md CHANGED
@@ -3,108 +3,6 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- # [1.0.0-alpha.284](https://github.com/toa-io/toa/compare/v1.0.0-alpha.283...v1.0.0-alpha.284) (2026-09-05)
7
-
8
- **Note:** Version bump only for package @toa.io/extensions.fetch
9
-
10
-
11
-
12
-
13
-
14
- # [1.0.0-alpha.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
15
-
16
- ### Features
17
-
18
- * **extensions:** an extension declares what it puts on a component's context ([133bc2a](https://github.com/toa-io/toa/commit/133bc2aad1e45a2d4aa68a08a5e44ca48d3b7414))
19
-
20
-
21
- # [1.0.0-alpha.282](https://github.com/toa-io/toa/compare/v1.0.0-alpha.281...v1.0.0-alpha.282) (2026-09-03)
22
-
23
- **Note:** Version bump only for package @toa.io/extensions.fetch
24
-
25
-
26
-
27
-
28
-
29
- # [1.0.0-alpha.278](https://github.com/toa-io/toa/compare/v1.0.0-alpha.277...v1.0.0-alpha.278) (2026-09-03)
30
-
31
- **Note:** Version bump only for package @toa.io/extensions.fetch
32
-
33
-
34
-
35
-
36
-
37
- # [1.0.0-alpha.277](https://github.com/toa-io/toa/compare/v1.0.0-alpha.276...v1.0.0-alpha.277) (2026-09-03)
38
-
39
- ### Bug Fixes
40
-
41
- * drop the build output that landed beside the sources ([a724f69](https://github.com/toa-io/toa/commit/a724f6921190e5c9d945e33c6035bb12d832e5c8))
42
-
43
-
44
- # [1.0.0-alpha.273](https://github.com/toa-io/toa/compare/v1.0.0-alpha.272...v1.0.0-alpha.273) (2026-09-02)
45
-
46
- **Note:** Version bump only for package @toa.io/extensions.fetch
47
-
48
-
49
-
50
-
51
-
52
- # [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
53
-
54
- **Note:** Version bump only for package @toa.io/extensions.fetch
55
-
56
-
57
-
58
-
59
-
60
- # [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
61
-
62
- **Note:** Version bump only for package @toa.io/extensions.fetch
63
-
64
-
65
-
66
-
67
-
68
- # [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
69
-
70
- **Note:** Version bump only for package @toa.io/extensions.fetch
71
-
72
-
73
-
74
-
75
-
76
- # [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
77
-
78
- **Note:** Version bump only for package @toa.io/extensions.fetch
79
-
80
-
81
-
82
-
83
-
84
- # [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
85
-
86
- **Note:** Version bump only for package @toa.io/extensions.fetch
87
-
88
-
89
-
90
-
91
-
92
- # [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
93
-
94
- **Note:** Version bump only for package @toa.io/extensions.fetch
95
-
96
-
97
-
98
-
99
-
100
- # [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
101
-
102
- **Note:** Version bump only for package @toa.io/extensions.fetch
103
-
104
-
105
-
106
-
107
-
108
- # [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
6
+ # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
109
7
 
110
8
  **Note:** Version bump only for package @toa.io/extensions.fetch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.fetch",
3
- "version": "1.0.0-alpha.284",
3
+ "version": "1.0.0-alpha.287",
4
4
  "type": "module",
5
5
  "description": "Toa Fetch extension",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -21,8 +21,8 @@
21
21
  "transpile": "tsc"
22
22
  },
23
23
  "dependencies": {
24
- "@toa.io/core": "1.0.0-alpha.284",
25
- "openspan": "1.0.0-alpha.277"
24
+ "@toa.io/core": "1.0.0-alpha.287",
25
+ "openspan": "1.0.0-alpha.286"
26
26
  },
27
- "gitHead": "006755556879e91e8d68fa86753aa3b475bdf4bf"
27
+ "gitHead": "5c7944a70c16065ab6372072385cc1f02f5db77c"
28
28
  }
@@ -58,8 +58,14 @@ it('does not retry by default', async () => {
58
58
  })
59
59
 
60
60
  it('retries unexpected responses and returns an expected response', async () => {
61
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 503 }), fetchMock.mock.callCount())
62
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 201 }), fetchMock.mock.callCount() + 1)
61
+ fetchMock.mock.mockImplementationOnce(
62
+ async () => new Response(null, { status: 503 }),
63
+ fetchMock.mock.callCount()
64
+ )
65
+ fetchMock.mock.mockImplementationOnce(
66
+ async () => new Response(null, { status: 201 }),
67
+ fetchMock.mock.callCount() + 1
68
+ )
63
69
 
64
70
  const response = await aspect.invoke('create', 'https://example.com', {
65
71
  retry: { attempts: 3, expected: [201], delay: 0 }
@@ -73,8 +79,14 @@ it('cancels an unexpected response body before retrying', async () => {
73
79
  const cancel = mock.fn()
74
80
  const body = new ReadableStream({ cancel })
75
81
 
76
- fetchMock.mock.mockImplementationOnce(async () => new Response(body, { status: 503 }), fetchMock.mock.callCount())
77
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 200 }), fetchMock.mock.callCount() + 1)
82
+ fetchMock.mock.mockImplementationOnce(
83
+ async () => new Response(body, { status: 503 }),
84
+ fetchMock.mock.callCount()
85
+ )
86
+ fetchMock.mock.mockImplementationOnce(
87
+ async () => new Response(null, { status: 200 }),
88
+ fetchMock.mock.callCount() + 1
89
+ )
78
90
 
79
91
  await aspect.invoke('get', 'https://example.com', {
80
92
  retry: { attempts: 2, delay: 0 }
@@ -90,8 +102,14 @@ it('continues retrying when response body cancellation fails', async () => {
90
102
  }
91
103
  })
92
104
 
93
- fetchMock.mock.mockImplementationOnce(async () => new Response(body, { status: 503 }), fetchMock.mock.callCount())
94
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 200 }), fetchMock.mock.callCount() + 1)
105
+ fetchMock.mock.mockImplementationOnce(
106
+ async () => new Response(body, { status: 503 }),
107
+ fetchMock.mock.callCount()
108
+ )
109
+ fetchMock.mock.mockImplementationOnce(
110
+ async () => new Response(null, { status: 200 }),
111
+ fetchMock.mock.callCount() + 1
112
+ )
95
113
 
96
114
  const response = await aspect.invoke('get', 'https://example.com', {
97
115
  retry: { attempts: 2, delay: 0 }
@@ -101,8 +119,14 @@ it('continues retrying when response body cancellation fails', async () => {
101
119
  })
102
120
 
103
121
  it('replays a regular request body', async () => {
104
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 503 }), fetchMock.mock.callCount())
105
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 200 }), fetchMock.mock.callCount() + 1)
122
+ fetchMock.mock.mockImplementationOnce(
123
+ async () => new Response(null, { status: 503 }),
124
+ fetchMock.mock.callCount()
125
+ )
126
+ fetchMock.mock.mockImplementationOnce(
127
+ async () => new Response(null, { status: 200 }),
128
+ fetchMock.mock.callCount() + 1
129
+ )
106
130
 
107
131
  await aspect.invoke('create', 'https://example.com', {
108
132
  method: 'POST',
@@ -118,11 +142,18 @@ it('replays a regular request body', async () => {
118
142
  })
119
143
 
120
144
  it('honors Retry-After instead of the configured delay', async () => {
121
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, {
122
- status: 503,
123
- headers: { 'retry-after': '0' }
124
- }), fetchMock.mock.callCount())
125
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 200 }), fetchMock.mock.callCount() + 1)
145
+ fetchMock.mock.mockImplementationOnce(
146
+ async () =>
147
+ new Response(null, {
148
+ status: 503,
149
+ headers: { 'retry-after': '0' }
150
+ }),
151
+ fetchMock.mock.callCount()
152
+ )
153
+ fetchMock.mock.mockImplementationOnce(
154
+ async () => new Response(null, { status: 200 }),
155
+ fetchMock.mock.callCount() + 1
156
+ )
126
157
 
127
158
  const response = await aspect.invoke('get', 'https://example.com', {
128
159
  retry: { attempts: 2, delay: 60_000 }
@@ -133,8 +164,14 @@ it('honors Retry-After instead of the configured delay', async () => {
133
164
  })
134
165
 
135
166
  it('returns the last unexpected response', async () => {
136
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 500 }), fetchMock.mock.callCount())
137
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 502 }), fetchMock.mock.callCount() + 1)
167
+ fetchMock.mock.mockImplementationOnce(
168
+ async () => new Response(null, { status: 500 }),
169
+ fetchMock.mock.callCount()
170
+ )
171
+ fetchMock.mock.mockImplementationOnce(
172
+ async () => new Response(null, { status: 502 }),
173
+ fetchMock.mock.callCount() + 1
174
+ )
138
175
 
139
176
  const response = await aspect.invoke('get', 'https://example.com', {
140
177
  retry: { attempts: 2, delay: 0 }
@@ -147,12 +184,22 @@ it('retries network errors and throws the final error', async () => {
147
184
  const first = new Error('first')
148
185
  const last = new Error('last')
149
186
 
150
- fetchMock.mock.mockImplementationOnce(async () => { throw first }, fetchMock.mock.callCount())
151
- fetchMock.mock.mockImplementationOnce(async () => { throw last }, fetchMock.mock.callCount() + 1)
152
-
153
- await assert.rejects(aspect.invoke('get', 'https://example.com', {
154
- retry: { attempts: 2, delay: 0 }
155
- }), (error: any) => { assert.strictEqual(error, last); return true })
187
+ fetchMock.mock.mockImplementationOnce(async () => {
188
+ throw first
189
+ }, fetchMock.mock.callCount())
190
+ fetchMock.mock.mockImplementationOnce(async () => {
191
+ throw last
192
+ }, fetchMock.mock.callCount() + 1)
193
+
194
+ await assert.rejects(
195
+ aspect.invoke('get', 'https://example.com', {
196
+ retry: { attempts: 2, delay: 0 }
197
+ }),
198
+ (error: any) => {
199
+ assert.strictEqual(error, last)
200
+ return true
201
+ }
202
+ )
156
203
 
157
204
  assert.strictEqual(fetchMock.mock.callCount(), 2)
158
205
  })
@@ -175,26 +222,35 @@ it('aborts while waiting for another attempt', async () => {
175
222
  })
176
223
 
177
224
  it('rejects invalid retry options', async () => {
178
- await assert.rejects(aspect.invoke('get', 'https://example.com', {
179
- retry: { attempts: 0 }
180
- }), (error: any) => /retry\.attempts/.test(error.message))
181
-
182
- await assert.rejects(aspect.invoke('get', 'https://example.com', {
183
- retry: { attempts: 2, expected: [] }
184
- }), (error: any) => /retry\.expected/.test(error.message))
225
+ await assert.rejects(
226
+ aspect.invoke('get', 'https://example.com', {
227
+ retry: { attempts: 0 }
228
+ }),
229
+ (error: any) => /retry\.attempts/.test(error.message)
230
+ )
231
+
232
+ await assert.rejects(
233
+ aspect.invoke('get', 'https://example.com', {
234
+ retry: { attempts: 2, expected: [] }
235
+ }),
236
+ (error: any) => /retry\.expected/.test(error.message)
237
+ )
185
238
  })
186
239
 
187
240
  it('rejects an explicit streaming body before sending it', async () => {
188
241
  const body = new ReadableStream()
189
242
 
190
- await assert.rejects(aspect.invoke('create', 'https://example.com', {
191
- method: 'POST',
192
- body,
193
- // Required by Node.js for a streaming request body.
194
- // @ts-expect-error -- duplex is implemented by Node but absent from lib.dom RequestInit.
195
- duplex: 'half',
196
- retry: { attempts: 2 }
197
- }), (error: any) => /non-replayable/.test(error.message))
243
+ await assert.rejects(
244
+ aspect.invoke('create', 'https://example.com', {
245
+ method: 'POST',
246
+ body,
247
+ // Required by Node.js for a streaming request body.
248
+ // @ts-expect-error -- duplex is implemented by Node but absent from lib.dom RequestInit.
249
+ duplex: 'half',
250
+ retry: { attempts: 2 }
251
+ }),
252
+ (error: any) => /non-replayable/.test(error.message)
253
+ )
198
254
 
199
255
  assert.strictEqual(fetchMock.mock.callCount(), 0)
200
256
  })
@@ -202,16 +258,25 @@ it('rejects an explicit streaming body before sending it', async () => {
202
258
  it('rejects a Request input with a body before retrying', async () => {
203
259
  const request = new Request('https://example.com', { method: 'POST', body: 'hello' })
204
260
 
205
- await assert.rejects(aspect.invoke('create', request, {
206
- retry: { attempts: 2 }
207
- }), (error: any) => /non-replayable/.test(error.message))
261
+ await assert.rejects(
262
+ aspect.invoke('create', request, {
263
+ retry: { attempts: 2 }
264
+ }),
265
+ (error: any) => /non-replayable/.test(error.message)
266
+ )
208
267
 
209
268
  assert.strictEqual(fetchMock.mock.callCount(), 0)
210
269
  })
211
270
 
212
271
  it('creates a scoped parent span with a client span for each attempt', async () => {
213
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 503 }), fetchMock.mock.callCount())
214
- fetchMock.mock.mockImplementationOnce(async () => new Response(null, { status: 204 }), fetchMock.mock.callCount() + 1)
272
+ fetchMock.mock.mockImplementationOnce(
273
+ async () => new Response(null, { status: 503 }),
274
+ fetchMock.mock.callCount()
275
+ )
276
+ fetchMock.mock.mockImplementationOnce(
277
+ async () => new Response(null, { status: 204 }),
278
+ fetchMock.mock.callCount() + 1
279
+ )
215
280
 
216
281
  await aspect.invoke('update', 'https://example.com/items?token=secret', {
217
282
  method: 'PUT',
@@ -257,14 +322,21 @@ it('creates a scoped parent span with a client span for each attempt', async ()
257
322
  }
258
323
  }
259
324
  ])
260
- assert.ok(!(JSON.stringify(spans).includes('token=secret')))
325
+ assert.ok(!JSON.stringify(spans).includes('token=secret'))
261
326
  })
262
327
 
263
328
  it('marks the span as failed after a final network error', async () => {
264
- fetchMock.mock.mockImplementation(async () => { throw new Error('unavailable') })
329
+ fetchMock.mock.mockImplementation(async () => {
330
+ throw new Error('unavailable')
331
+ })
265
332
 
266
- await assert.rejects(aspect.invoke('get', 'https://example.com'), (error: any) => /unavailable/.test(error.message))
333
+ await assert.rejects(aspect.invoke('get', 'https://example.com'), (error: any) =>
334
+ /unavailable/.test(error.message)
335
+ )
267
336
 
268
337
  assert.strictEqual(spans.length, 2)
269
- assert.strictEqual(spans.every((span) => span.status === 'error'), true)
338
+ assert.strictEqual(
339
+ spans.every((span) => span.status === 'error'),
340
+ true
341
+ )
270
342
  })
package/source/Aspect.ts CHANGED
@@ -1,19 +1,24 @@
1
1
  import { Connector } from '@toa.io/core'
2
2
  import { console, type Console, type SpanOptions } from 'openspan'
3
- import type { Locator, extensions } from '@toa.io/core'
3
+ import type { Locator } from '@toa.io/core'
4
+ import type { extensions } from '@toa.io/core/types'
4
5
 
5
6
  export class Aspect extends Connector implements extensions.Aspect {
6
7
  public readonly name = 'fetch'
7
8
  private readonly locator: Locator
8
9
  private readonly consoles: Record<string, Console> = {}
9
10
 
10
- public constructor (locator: Locator) {
11
+ public constructor(locator: Locator) {
11
12
  super()
12
13
 
13
14
  this.locator = locator
14
15
  }
15
16
 
16
- public async invoke (operation: string, input: RequestInfo | URL, init?: FetchInit): Promise<Response> {
17
+ public async invoke(
18
+ operation: string,
19
+ input: RequestInfo | URL,
20
+ init?: FetchInit
21
+ ): Promise<Response> {
17
22
  const { retry, ...nativeInit } = init ?? {}
18
23
  const body = nativeInit.body ?? (input instanceof Request ? input.body : null)
19
24
 
@@ -41,7 +46,10 @@ export class Aspect extends Connector implements extensions.Aspect {
41
46
  const output = this.output(operation)
42
47
 
43
48
  return await output.span(options, async () => {
44
- const response = await invoke(request, retry, { output, parentAttributes: attributes })
49
+ const response = await invoke(request, retry, {
50
+ output,
51
+ parentAttributes: attributes
52
+ })
45
53
 
46
54
  attributes['http.response.status_code'] = response.status
47
55
 
@@ -49,7 +57,7 @@ export class Aspect extends Connector implements extensions.Aspect {
49
57
  })
50
58
  }
51
59
 
52
- private output (operation: string): Console {
60
+ private output(operation: string): Console {
53
61
  this.consoles[operation] ??= console.fork({
54
62
  namespace: this.locator.namespace,
55
63
  component: this.locator.name,
@@ -60,8 +68,11 @@ export class Aspect extends Connector implements extensions.Aspect {
60
68
  }
61
69
  }
62
70
 
63
- async function invoke (request: Request, options: RetryOptions | undefined,
64
- telemetry: InvocationTelemetry): Promise<Response> {
71
+ async function invoke(
72
+ request: Request,
73
+ options: RetryOptions | undefined,
74
+ telemetry: InvocationTelemetry
75
+ ): Promise<Response> {
65
76
  const retry = normalize(options)
66
77
 
67
78
  let response: Response | undefined
@@ -79,7 +90,8 @@ async function invoke (request: Request, options: RetryOptions | undefined,
79
90
  if (expected(response.status, retry.expected) || attempt === retry.attempts)
80
91
  return response
81
92
 
82
- const interval = retryAfter(response.headers.get('retry-after')) ?? delay(retry, attempt)
93
+ const interval =
94
+ retryAfter(response.headers.get('retry-after')) ?? delay(retry, attempt)
83
95
 
84
96
  await discard(response)
85
97
  await wait(interval, request.signal)
@@ -88,7 +100,11 @@ async function invoke (request: Request, options: RetryOptions | undefined,
88
100
  return response!
89
101
  }
90
102
 
91
- async function send (request: Request, attempt: number, telemetry: InvocationTelemetry): Promise<Response> {
103
+ async function send(
104
+ request: Request,
105
+ attempt: number,
106
+ telemetry: InvocationTelemetry
107
+ ): Promise<Response> {
92
108
  const attributes: Record<string, unknown> = {
93
109
  ...telemetry.parentAttributes,
94
110
  'retry.attempt': attempt
@@ -96,20 +112,26 @@ async function send (request: Request, attempt: number, telemetry: InvocationTel
96
112
 
97
113
  delete attributes['retry.attempts']
98
114
 
99
- return await telemetry.output.span({
100
- name: `attempt ${attempt}`,
101
- kind: 'client',
102
- attributes
103
- }, async () => {
104
- const response = await globalThis.fetch(request.clone())
115
+ return await telemetry.output.span(
116
+ {
117
+ name: `attempt ${attempt}`,
118
+ kind: 'client',
119
+ attributes
120
+ },
121
+ async () => {
122
+ const response = await globalThis.fetch(request.clone())
105
123
 
106
- attributes['http.response.status_code'] = response.status
124
+ attributes['http.response.status_code'] = response.status
107
125
 
108
- return response
109
- })
126
+ return response
127
+ }
128
+ )
110
129
  }
111
130
 
112
- async function retryError (error: unknown, parameters: RetryErrorParameters): Promise<void> {
131
+ async function retryError(
132
+ error: unknown,
133
+ parameters: RetryErrorParameters
134
+ ): Promise<void> {
113
135
  const { attempt, options, request } = parameters
114
136
 
115
137
  if (attempt === options.attempts || request.signal.aborted) throw error
@@ -117,7 +139,7 @@ async function retryError (error: unknown, parameters: RetryErrorParameters): Pr
117
139
  await wait(delay(options, attempt), request.signal)
118
140
  }
119
141
 
120
- async function discard (response: Response): Promise<void> {
142
+ async function discard(response: Response): Promise<void> {
121
143
  try {
122
144
  await response.body?.cancel()
123
145
  } catch {
@@ -125,22 +147,37 @@ async function discard (response: Response): Promise<void> {
125
147
  }
126
148
  }
127
149
 
128
- function normalize (options?: RetryOptions): NormalizedRetryOptions {
150
+ function normalize(options?: RetryOptions): NormalizedRetryOptions {
129
151
  if (options === undefined)
130
- return { attempts: 1, expected: undefined, delay: DEFAULT_DELAY, factor: DEFAULT_FACTOR }
152
+ return {
153
+ attempts: 1,
154
+ expected: undefined,
155
+ delay: DEFAULT_DELAY,
156
+ factor: DEFAULT_FACTOR
157
+ }
131
158
 
132
159
  if (!Number.isInteger(options.attempts) || options.attempts < 1)
133
160
  throw new TypeError('retry.attempts must be an integer greater than or equal to 1')
134
161
 
135
- if (options.expected !== undefined &&
136
- (options.expected.length === 0 || options.expected.some((status) =>
137
- !Number.isInteger(status) || status < 100 || status > 599)))
162
+ if (
163
+ options.expected !== undefined &&
164
+ (options.expected.length === 0 ||
165
+ options.expected.some(
166
+ (status) => !Number.isInteger(status) || status < 100 || status > 599
167
+ ))
168
+ )
138
169
  throw new TypeError('retry.expected must contain HTTP status codes')
139
170
 
140
- if (options.delay !== undefined && (!Number.isFinite(options.delay) || options.delay < 0))
171
+ if (
172
+ options.delay !== undefined &&
173
+ (!Number.isFinite(options.delay) || options.delay < 0)
174
+ )
141
175
  throw new TypeError('retry.delay must be a non-negative number')
142
176
 
143
- if (options.factor !== undefined && (!Number.isFinite(options.factor) || options.factor < 0))
177
+ if (
178
+ options.factor !== undefined &&
179
+ (!Number.isFinite(options.factor) || options.factor < 0)
180
+ )
144
181
  throw new TypeError('retry.factor must be a non-negative number')
145
182
 
146
183
  return {
@@ -151,15 +188,17 @@ function normalize (options?: RetryOptions): NormalizedRetryOptions {
151
188
  }
152
189
  }
153
190
 
154
- function expected (status: number, statuses?: number[]): boolean {
155
- return statuses === undefined ? status >= 200 && status < 300 : statuses.includes(status)
191
+ function expected(status: number, statuses?: number[]): boolean {
192
+ return statuses === undefined
193
+ ? status >= 200 && status < 300
194
+ : statuses.includes(status)
156
195
  }
157
196
 
158
- function delay (options: NormalizedRetryOptions, attempt: number): number {
197
+ function delay(options: NormalizedRetryOptions, attempt: number): number {
159
198
  return options.delay * Math.pow(options.factor, attempt - 1)
160
199
  }
161
200
 
162
- function retryAfter (value: string | null): number | undefined {
201
+ function retryAfter(value: string | null): number | undefined {
163
202
  if (value === null) return undefined
164
203
 
165
204
  const seconds = Number(value)
@@ -173,7 +212,7 @@ function retryAfter (value: string | null): number | undefined {
173
212
  return Math.max(0, date - Date.now())
174
213
  }
175
214
 
176
- async function wait (milliseconds: number, signal: AbortSignal): Promise<void> {
215
+ async function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
177
216
  if (signal.aborted) throw signal.reason
178
217
 
179
218
  await new Promise<void>((resolve, reject) => {
@@ -181,19 +220,19 @@ async function wait (milliseconds: number, signal: AbortSignal): Promise<void> {
181
220
 
182
221
  signal.addEventListener('abort', aborted, { once: true })
183
222
 
184
- function done (): void {
223
+ function done(): void {
185
224
  signal.removeEventListener('abort', aborted)
186
225
  resolve()
187
226
  }
188
227
 
189
- function aborted (): void {
228
+ function aborted(): void {
190
229
  clearTimeout(timeout)
191
230
  reject(signal.reason)
192
231
  }
193
232
  })
194
233
  }
195
234
 
196
- function requestURL (request: Request): URL {
235
+ function requestURL(request: Request): URL {
197
236
  return new URL(request.url)
198
237
  }
199
238
 
package/source/Factory.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Aspect } from './Aspect.js'
2
- import type { Locator, extensions } from '@toa.io/core'
2
+ import type { Locator } from '@toa.io/core'
3
+ import type { extensions } from '@toa.io/core/types'
3
4
 
4
5
  export class Factory implements extensions.Factory {
5
- public aspect (locator: Locator): extensions.Aspect {
6
+ public aspect(locator: Locator): extensions.Aspect {
6
7
  return new Aspect(locator)
7
8
  }
8
9
  }
package/source/context.ts CHANGED
@@ -1,7 +1,7 @@
1
- import type { Contribution } from '@toa.io/core'
1
+ import type { Contribution } from '@toa.io/core/types'
2
2
 
3
3
  /** What this extension puts on the context of every component, being declared for all. */
4
- export function context (): Contribution {
4
+ export function context(): Contribution {
5
5
  return {
6
6
  name: 'fetch',
7
7
  type: '(input: string | URL | Request, init?: FetchInit) => Promise<Response>',
@@ -1,5 +1,6 @@
1
1
  import { Connector } from '@toa.io/core';
2
- import type { Locator, extensions } from '@toa.io/core';
2
+ import type { Locator } from '@toa.io/core';
3
+ import type { extensions } from '@toa.io/core/types';
3
4
  export declare class Aspect extends Connector implements extensions.Aspect {
4
5
  readonly name = "fetch";
5
6
  private readonly locator;