@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 +1 -103
- package/package.json +4 -4
- package/source/Aspect.test.ts +117 -45
- package/source/Aspect.ts +74 -35
- package/source/Factory.ts +3 -2
- package/source/context.ts +2 -2
- package/transpiled/Aspect.d.ts +2 -1
- package/transpiled/Aspect.js +19 -6
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Aspect.test.js +14 -5
- package/transpiled/Aspect.test.js.map +1 -1
- package/transpiled/Factory.d.ts +2 -1
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/context.d.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
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.
|
|
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.
|
|
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.
|
|
25
|
-
"openspan": "1.0.0-alpha.
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.287",
|
|
25
|
+
"openspan": "1.0.0-alpha.286"
|
|
26
26
|
},
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "5c7944a70c16065ab6372072385cc1f02f5db77c"
|
|
28
28
|
}
|
package/source/Aspect.test.ts
CHANGED
|
@@ -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(
|
|
62
|
-
|
|
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(
|
|
77
|
-
|
|
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(
|
|
94
|
-
|
|
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(
|
|
105
|
-
|
|
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(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
137
|
-
|
|
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 () => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
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(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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(
|
|
206
|
-
|
|
207
|
-
|
|
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(
|
|
214
|
-
|
|
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(!
|
|
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 () => {
|
|
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) =>
|
|
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(
|
|
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
|
|
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
|
|
11
|
+
public constructor(locator: Locator) {
|
|
11
12
|
super()
|
|
12
13
|
|
|
13
14
|
this.locator = locator
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
public async invoke
|
|
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, {
|
|
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
|
|
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
|
|
64
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
124
|
+
attributes['http.response.status_code'] = response.status
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
return response
|
|
127
|
+
}
|
|
128
|
+
)
|
|
110
129
|
}
|
|
111
130
|
|
|
112
|
-
async function retryError
|
|
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
|
|
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
|
|
150
|
+
function normalize(options?: RetryOptions): NormalizedRetryOptions {
|
|
129
151
|
if (options === undefined)
|
|
130
|
-
return {
|
|
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 (
|
|
136
|
-
|
|
137
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
155
|
-
return statuses === undefined
|
|
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
|
|
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
|
|
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
|
|
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
|
|
223
|
+
function done(): void {
|
|
185
224
|
signal.removeEventListener('abort', aborted)
|
|
186
225
|
resolve()
|
|
187
226
|
}
|
|
188
227
|
|
|
189
|
-
function aborted
|
|
228
|
+
function aborted(): void {
|
|
190
229
|
clearTimeout(timeout)
|
|
191
230
|
reject(signal.reason)
|
|
192
231
|
}
|
|
193
232
|
})
|
|
194
233
|
}
|
|
195
234
|
|
|
196
|
-
function requestURL
|
|
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
|
|
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
|
|
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
|
|
4
|
+
export function context(): Contribution {
|
|
5
5
|
return {
|
|
6
6
|
name: 'fetch',
|
|
7
7
|
type: '(input: string | URL | Request, init?: FetchInit) => Promise<Response>',
|
package/transpiled/Aspect.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Connector } from '@toa.io/core';
|
|
2
|
-
import type { Locator
|
|
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;
|