@toa.io/extensions.fetch 1.0.0-alpha.251
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/package.json +30 -0
- package/readme.md +61 -0
- package/source/Aspect.test.ts +272 -0
- package/source/Aspect.ts +230 -0
- package/source/Factory.ts +8 -0
- package/source/index.ts +2 -0
- package/transpiled/Aspect.d.ts +19 -0
- package/transpiled/Aspect.js +157 -0
- package/transpiled/Aspect.js.map +1 -0
- package/transpiled/Aspect.test.d.ts +1 -0
- package/transpiled/Aspect.test.js +215 -0
- package/transpiled/Aspect.test.js.map +1 -0
- package/transpiled/Factory.d.ts +4 -0
- package/transpiled/Factory.js +11 -0
- package/transpiled/Factory.js.map +1 -0
- package/transpiled/index.d.ts +2 -0
- package/transpiled/index.js +6 -0
- package/transpiled/index.js.map +1 -0
- package/tsconfig.json +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@toa.io/extensions.fetch",
|
|
3
|
+
"version": "1.0.0-alpha.251",
|
|
4
|
+
"description": "Toa Fetch extension",
|
|
5
|
+
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
|
+
"homepage": "https://github.com/toa-io/toa#readme",
|
|
7
|
+
"main": "transpiled/index.js",
|
|
8
|
+
"types": "transpiled/index.d.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/toa-io/toa.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/toa-io/toa/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"transpile": "tsc"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@toa.io/core": "1.0.0-alpha.251",
|
|
24
|
+
"openspan": "1.0.0-alpha.251"
|
|
25
|
+
},
|
|
26
|
+
"jest": {
|
|
27
|
+
"preset": "ts-jest",
|
|
28
|
+
"testEnvironment": "node"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Fetch
|
|
2
|
+
|
|
3
|
+
The Fetch extension provides every Node component with a WHATWG-compatible `context.fetch()`.
|
|
4
|
+
It is built in and requires no manifest declaration.
|
|
5
|
+
|
|
6
|
+
```js
|
|
7
|
+
const response = await context.fetch('https://example.com/items')
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Retry
|
|
11
|
+
|
|
12
|
+
Retries are disabled unless `retry` is provided in the second argument.
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
const response = await context.fetch('https://example.com/items', {
|
|
16
|
+
retry: {
|
|
17
|
+
attempts: 3,
|
|
18
|
+
expected: [200],
|
|
19
|
+
delay: 100,
|
|
20
|
+
factor: 2
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `attempts` is required and includes the initial request.
|
|
26
|
+
- `expected` defaults to all statuses from `200` through `299`.
|
|
27
|
+
- `delay` defaults to `100` milliseconds.
|
|
28
|
+
- `factor` defaults to `2`.
|
|
29
|
+
|
|
30
|
+
`Retry-After` takes precedence over the calculated delay. Aborting the request also aborts a retry
|
|
31
|
+
delay. After the final unexpected HTTP response, that `Response` is returned. After the final
|
|
32
|
+
network failure, the error is thrown.
|
|
33
|
+
|
|
34
|
+
When a response status is not expected and another attempt remains, the extension calls
|
|
35
|
+
`response.body.cancel()` before waiting and retrying. The unexpected body is not exposed to the
|
|
36
|
+
caller and is not buffered or drained. Cancellation is best-effort: a cancellation error does not
|
|
37
|
+
prevent the next attempt. If no attempts remain, the final unexpected `Response` is returned with
|
|
38
|
+
its body untouched, so the caller can consume it normally.
|
|
39
|
+
|
|
40
|
+
An explicit `ReadableStream` body and a `Request` input that already contains a body cannot be
|
|
41
|
+
retried because their contents cannot be recreated safely. They remain supported when retries are
|
|
42
|
+
disabled or `attempts` is `1`.
|
|
43
|
+
|
|
44
|
+
## Telemetry
|
|
45
|
+
|
|
46
|
+
Each call creates one logical parent span whose name has the form `POST https://example.com`.
|
|
47
|
+
Every actual network attempt creates a nested client span:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
POST https://example.com
|
|
51
|
+
├── attempt 1
|
|
52
|
+
├── attempt 2
|
|
53
|
+
└── attempt 3
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The parent span records the final status and total number of attempts. Each attempt span records its
|
|
57
|
+
one-based attempt number and response status. A network failure marks that attempt span as failed;
|
|
58
|
+
the parent is marked as failed only when the complete fetch call fails. The full URL, query string,
|
|
59
|
+
headers, and bodies are not recorded.
|
|
60
|
+
|
|
61
|
+
The extension does not cache responses.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Locator } from '@toa.io/core'
|
|
2
|
+
import { consoleExporter, exporting } from 'openspan'
|
|
3
|
+
import { Aspect } from './Aspect'
|
|
4
|
+
import type { Span } from 'openspan'
|
|
5
|
+
|
|
6
|
+
const nativeFetch = globalThis.fetch
|
|
7
|
+
|
|
8
|
+
let aspect: Aspect
|
|
9
|
+
let fetchMock: jest.MockedFunction<typeof fetch>
|
|
10
|
+
let spans: Span[]
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
aspect = new Aspect(new Locator('users', 'identity'))
|
|
14
|
+
fetchMock = jest.fn()
|
|
15
|
+
globalThis.fetch = fetchMock
|
|
16
|
+
spans = []
|
|
17
|
+
exporting([{ export: (span) => spans.push(span) }])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
globalThis.fetch = nativeFetch
|
|
22
|
+
exporting([consoleExporter])
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('delegates to native fetch and returns its Response', async () => {
|
|
26
|
+
const response = new Response('ok', { status: 201, headers: { 'x-test': 'yes' } })
|
|
27
|
+
|
|
28
|
+
fetchMock.mockResolvedValue(response)
|
|
29
|
+
|
|
30
|
+
const result = await aspect.invoke('create', 'https://example.com/items?secret=yes', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: '{}'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(result).toBe(response)
|
|
37
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
38
|
+
|
|
39
|
+
const request = fetchMock.mock.calls[0][0] as Request
|
|
40
|
+
|
|
41
|
+
expect(request).toBeInstanceOf(Request)
|
|
42
|
+
expect(request.method).toBe('POST')
|
|
43
|
+
expect(request.url).toBe('https://example.com/items?secret=yes')
|
|
44
|
+
await expect(request.text()).resolves.toBe('{}')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not retry by default', async () => {
|
|
48
|
+
fetchMock.mockResolvedValue(new Response(null, { status: 503 }))
|
|
49
|
+
|
|
50
|
+
const response = await aspect.invoke('get', 'https://example.com')
|
|
51
|
+
|
|
52
|
+
expect(response.status).toBe(503)
|
|
53
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('retries unexpected responses and returns an expected response', async () => {
|
|
57
|
+
fetchMock
|
|
58
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
59
|
+
.mockResolvedValueOnce(new Response(null, { status: 201 }))
|
|
60
|
+
|
|
61
|
+
const response = await aspect.invoke('create', 'https://example.com', {
|
|
62
|
+
retry: { attempts: 3, expected: [201], delay: 0 }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(response.status).toBe(201)
|
|
66
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('cancels an unexpected response body before retrying', async () => {
|
|
70
|
+
const cancel = jest.fn()
|
|
71
|
+
const body = new ReadableStream({ cancel })
|
|
72
|
+
|
|
73
|
+
fetchMock
|
|
74
|
+
.mockResolvedValueOnce(new Response(body, { status: 503 }))
|
|
75
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
|
76
|
+
|
|
77
|
+
await aspect.invoke('get', 'https://example.com', {
|
|
78
|
+
retry: { attempts: 2, delay: 0 }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(cancel).toHaveBeenCalledTimes(1)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('continues retrying when response body cancellation fails', async () => {
|
|
85
|
+
const body = new ReadableStream({
|
|
86
|
+
cancel: () => {
|
|
87
|
+
throw new Error('cancel failed')
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
fetchMock
|
|
92
|
+
.mockResolvedValueOnce(new Response(body, { status: 503 }))
|
|
93
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
|
94
|
+
|
|
95
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
96
|
+
retry: { attempts: 2, delay: 0 }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(response.status).toBe(200)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('replays a regular request body', async () => {
|
|
103
|
+
fetchMock
|
|
104
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
105
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
|
106
|
+
|
|
107
|
+
await aspect.invoke('create', 'https://example.com', {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
body: 'hello',
|
|
110
|
+
retry: { attempts: 2, delay: 0 }
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const first = fetchMock.mock.calls[0][0] as Request
|
|
114
|
+
const second = fetchMock.mock.calls[1][0] as Request
|
|
115
|
+
|
|
116
|
+
await expect(first.text()).resolves.toBe('hello')
|
|
117
|
+
await expect(second.text()).resolves.toBe('hello')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('honors Retry-After instead of the configured delay', async () => {
|
|
121
|
+
fetchMock
|
|
122
|
+
.mockResolvedValueOnce(new Response(null, {
|
|
123
|
+
status: 503,
|
|
124
|
+
headers: { 'retry-after': '0' }
|
|
125
|
+
}))
|
|
126
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }))
|
|
127
|
+
|
|
128
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
129
|
+
retry: { attempts: 2, delay: 60_000 }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(response.status).toBe(200)
|
|
133
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('returns the last unexpected response', async () => {
|
|
137
|
+
fetchMock
|
|
138
|
+
.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
|
139
|
+
.mockResolvedValueOnce(new Response(null, { status: 502 }))
|
|
140
|
+
|
|
141
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
142
|
+
retry: { attempts: 2, delay: 0 }
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(response.status).toBe(502)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('retries network errors and throws the final error', async () => {
|
|
149
|
+
const first = new Error('first')
|
|
150
|
+
const last = new Error('last')
|
|
151
|
+
|
|
152
|
+
fetchMock.mockRejectedValueOnce(first).mockRejectedValueOnce(last)
|
|
153
|
+
|
|
154
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
155
|
+
retry: { attempts: 2, delay: 0 }
|
|
156
|
+
})).rejects.toBe(last)
|
|
157
|
+
|
|
158
|
+
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('aborts while waiting for another attempt', async () => {
|
|
162
|
+
const controller = new AbortController()
|
|
163
|
+
|
|
164
|
+
fetchMock.mockResolvedValue(new Response(null, { status: 503 }))
|
|
165
|
+
|
|
166
|
+
const promise = aspect.invoke('get', 'https://example.com', {
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
retry: { attempts: 2, delay: 60_000 }
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
172
|
+
controller.abort(new Error('cancelled'))
|
|
173
|
+
|
|
174
|
+
await expect(promise).rejects.toThrow('cancelled')
|
|
175
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('rejects invalid retry options', async () => {
|
|
179
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
180
|
+
retry: { attempts: 0 }
|
|
181
|
+
})).rejects.toThrow('retry.attempts')
|
|
182
|
+
|
|
183
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
184
|
+
retry: { attempts: 2, expected: [] }
|
|
185
|
+
})).rejects.toThrow('retry.expected')
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('rejects an explicit streaming body before sending it', async () => {
|
|
189
|
+
const body = new ReadableStream()
|
|
190
|
+
|
|
191
|
+
await expect(aspect.invoke('create', 'https://example.com', {
|
|
192
|
+
method: 'POST',
|
|
193
|
+
body,
|
|
194
|
+
// Required by Node.js for a streaming request body.
|
|
195
|
+
// @ts-expect-error -- duplex is implemented by Node but absent from lib.dom RequestInit.
|
|
196
|
+
duplex: 'half',
|
|
197
|
+
retry: { attempts: 2 }
|
|
198
|
+
})).rejects.toThrow('non-replayable')
|
|
199
|
+
|
|
200
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('rejects a Request input with a body before retrying', async () => {
|
|
204
|
+
const request = new Request('https://example.com', { method: 'POST', body: 'hello' })
|
|
205
|
+
|
|
206
|
+
await expect(aspect.invoke('create', request, {
|
|
207
|
+
retry: { attempts: 2 }
|
|
208
|
+
})).rejects.toThrow('non-replayable')
|
|
209
|
+
|
|
210
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('creates a scoped parent span with a client span for each attempt', async () => {
|
|
214
|
+
fetchMock
|
|
215
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
216
|
+
.mockResolvedValueOnce(new Response(null, { status: 204 }))
|
|
217
|
+
|
|
218
|
+
await aspect.invoke('update', 'https://example.com/items?token=secret', {
|
|
219
|
+
method: 'PUT',
|
|
220
|
+
retry: { attempts: 2, delay: 0 }
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
expect(spans).toHaveLength(3)
|
|
224
|
+
|
|
225
|
+
const parent = spans.find((span) => span.name === 'PUT https://example.com')!
|
|
226
|
+
const attempts = spans.filter((span) => span.parentId === parent.spanId)
|
|
227
|
+
|
|
228
|
+
expect(parent).toMatchObject({
|
|
229
|
+
name: 'PUT https://example.com',
|
|
230
|
+
kind: 'internal',
|
|
231
|
+
scope: {
|
|
232
|
+
namespace: 'identity',
|
|
233
|
+
component: 'users',
|
|
234
|
+
operation: 'update'
|
|
235
|
+
},
|
|
236
|
+
attributes: {
|
|
237
|
+
'http.request.method': 'PUT',
|
|
238
|
+
'http.response.status_code': 204,
|
|
239
|
+
'url.scheme': 'https',
|
|
240
|
+
'server.address': 'example.com',
|
|
241
|
+
'retry.attempts': 2
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
expect(attempts).toMatchObject([
|
|
245
|
+
{
|
|
246
|
+
name: 'attempt 1',
|
|
247
|
+
kind: 'client',
|
|
248
|
+
attributes: {
|
|
249
|
+
'retry.attempt': 1,
|
|
250
|
+
'http.response.status_code': 503
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'attempt 2',
|
|
255
|
+
kind: 'client',
|
|
256
|
+
attributes: {
|
|
257
|
+
'retry.attempt': 2,
|
|
258
|
+
'http.response.status_code': 204
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
])
|
|
262
|
+
expect(JSON.stringify(spans)).not.toContain('token=secret')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('marks the span as failed after a final network error', async () => {
|
|
266
|
+
fetchMock.mockRejectedValue(new Error('unavailable'))
|
|
267
|
+
|
|
268
|
+
await expect(aspect.invoke('get', 'https://example.com')).rejects.toThrow('unavailable')
|
|
269
|
+
|
|
270
|
+
expect(spans).toHaveLength(2)
|
|
271
|
+
expect(spans.every((span) => span.status === 'error')).toBe(true)
|
|
272
|
+
})
|
package/source/Aspect.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core'
|
|
2
|
+
import { console, type Console, type SpanOptions } from 'openspan'
|
|
3
|
+
import type { Locator, extensions } from '@toa.io/core'
|
|
4
|
+
|
|
5
|
+
export class Aspect extends Connector implements extensions.Aspect {
|
|
6
|
+
public readonly name = 'fetch'
|
|
7
|
+
private readonly locator: Locator
|
|
8
|
+
private readonly consoles: Record<string, Console> = {}
|
|
9
|
+
|
|
10
|
+
public constructor (locator: Locator) {
|
|
11
|
+
super()
|
|
12
|
+
|
|
13
|
+
this.locator = locator
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async invoke (operation: string, input: RequestInfo | URL, init?: FetchInit): Promise<Response> {
|
|
17
|
+
const { retry, ...nativeInit } = init ?? {}
|
|
18
|
+
const body = nativeInit.body ?? (input instanceof Request ? input.body : null)
|
|
19
|
+
|
|
20
|
+
if (retry !== undefined && retry.attempts > 1 && body instanceof ReadableStream)
|
|
21
|
+
throw new TypeError('Cannot retry a request with a non-replayable body')
|
|
22
|
+
|
|
23
|
+
const request = new Request(input, nativeInit)
|
|
24
|
+
|
|
25
|
+
const attributes: Record<string, unknown> = {
|
|
26
|
+
'http.request.method': request.method,
|
|
27
|
+
'url.scheme': requestURL(request).protocol.slice(0, -1),
|
|
28
|
+
'server.address': requestURL(request).hostname
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const url = requestURL(request)
|
|
32
|
+
|
|
33
|
+
if (url.port !== '') attributes['server.port'] = Number(url.port)
|
|
34
|
+
|
|
35
|
+
const options: SpanOptions = {
|
|
36
|
+
name: `${request.method} ${url.origin}`,
|
|
37
|
+
kind: 'internal',
|
|
38
|
+
attributes
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const output = this.output(operation)
|
|
42
|
+
|
|
43
|
+
return await output.span(options, async () => {
|
|
44
|
+
const response = await invoke(request, retry, { output, parentAttributes: attributes })
|
|
45
|
+
|
|
46
|
+
attributes['http.response.status_code'] = response.status
|
|
47
|
+
|
|
48
|
+
return response
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private output (operation: string): Console {
|
|
53
|
+
this.consoles[operation] ??= console.fork({
|
|
54
|
+
namespace: this.locator.namespace,
|
|
55
|
+
component: this.locator.name,
|
|
56
|
+
operation
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return this.consoles[operation]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function invoke (request: Request, options: RetryOptions | undefined,
|
|
64
|
+
telemetry: InvocationTelemetry): Promise<Response> {
|
|
65
|
+
const retry = normalize(options)
|
|
66
|
+
|
|
67
|
+
let response: Response | undefined
|
|
68
|
+
|
|
69
|
+
for (let attempt = 1; attempt <= retry.attempts; attempt++) {
|
|
70
|
+
telemetry.parentAttributes['retry.attempts'] = attempt
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
response = await send(request, attempt, telemetry)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await retryError(error, { attempt, options: retry, request })
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (expected(response.status, retry.expected) || attempt === retry.attempts)
|
|
80
|
+
return response
|
|
81
|
+
|
|
82
|
+
const interval = retryAfter(response.headers.get('retry-after')) ?? delay(retry, attempt)
|
|
83
|
+
|
|
84
|
+
await discard(response)
|
|
85
|
+
await wait(interval, request.signal)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return response!
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function send (request: Request, attempt: number, telemetry: InvocationTelemetry): Promise<Response> {
|
|
92
|
+
const attributes: Record<string, unknown> = {
|
|
93
|
+
...telemetry.parentAttributes,
|
|
94
|
+
'retry.attempt': attempt
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
delete attributes['retry.attempts']
|
|
98
|
+
|
|
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())
|
|
105
|
+
|
|
106
|
+
attributes['http.response.status_code'] = response.status
|
|
107
|
+
|
|
108
|
+
return response
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function retryError (error: unknown, parameters: RetryErrorParameters): Promise<void> {
|
|
113
|
+
const { attempt, options, request } = parameters
|
|
114
|
+
|
|
115
|
+
if (attempt === options.attempts || request.signal.aborted) throw error
|
|
116
|
+
|
|
117
|
+
await wait(delay(options, attempt), request.signal)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function discard (response: Response): Promise<void> {
|
|
121
|
+
try {
|
|
122
|
+
await response.body?.cancel()
|
|
123
|
+
} catch {
|
|
124
|
+
// The response is discarded regardless of whether its stream closes cleanly.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalize (options?: RetryOptions): NormalizedRetryOptions {
|
|
129
|
+
if (options === undefined)
|
|
130
|
+
return { attempts: 1, expected: undefined, delay: DEFAULT_DELAY, factor: DEFAULT_FACTOR }
|
|
131
|
+
|
|
132
|
+
if (!Number.isInteger(options.attempts) || options.attempts < 1)
|
|
133
|
+
throw new TypeError('retry.attempts must be an integer greater than or equal to 1')
|
|
134
|
+
|
|
135
|
+
if (options.expected !== undefined &&
|
|
136
|
+
(options.expected.length === 0 || options.expected.some((status) =>
|
|
137
|
+
!Number.isInteger(status) || status < 100 || status > 599)))
|
|
138
|
+
throw new TypeError('retry.expected must contain HTTP status codes')
|
|
139
|
+
|
|
140
|
+
if (options.delay !== undefined && (!Number.isFinite(options.delay) || options.delay < 0))
|
|
141
|
+
throw new TypeError('retry.delay must be a non-negative number')
|
|
142
|
+
|
|
143
|
+
if (options.factor !== undefined && (!Number.isFinite(options.factor) || options.factor < 0))
|
|
144
|
+
throw new TypeError('retry.factor must be a non-negative number')
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
attempts: options.attempts,
|
|
148
|
+
expected: options.expected,
|
|
149
|
+
delay: options.delay ?? DEFAULT_DELAY,
|
|
150
|
+
factor: options.factor ?? DEFAULT_FACTOR
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function expected (status: number, statuses?: number[]): boolean {
|
|
155
|
+
return statuses === undefined ? status >= 200 && status < 300 : statuses.includes(status)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function delay (options: NormalizedRetryOptions, attempt: number): number {
|
|
159
|
+
return options.delay * Math.pow(options.factor, attempt - 1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function retryAfter (value: string | null): number | undefined {
|
|
163
|
+
if (value === null) return undefined
|
|
164
|
+
|
|
165
|
+
const seconds = Number(value)
|
|
166
|
+
|
|
167
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000
|
|
168
|
+
|
|
169
|
+
const date = Date.parse(value)
|
|
170
|
+
|
|
171
|
+
if (Number.isNaN(date)) return undefined
|
|
172
|
+
|
|
173
|
+
return Math.max(0, date - Date.now())
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function wait (milliseconds: number, signal: AbortSignal): Promise<void> {
|
|
177
|
+
if (signal.aborted) throw signal.reason
|
|
178
|
+
|
|
179
|
+
await new Promise<void>((resolve, reject) => {
|
|
180
|
+
const timeout = setTimeout(done, milliseconds)
|
|
181
|
+
|
|
182
|
+
signal.addEventListener('abort', aborted, { once: true })
|
|
183
|
+
|
|
184
|
+
function done (): void {
|
|
185
|
+
signal.removeEventListener('abort', aborted)
|
|
186
|
+
resolve()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function aborted (): void {
|
|
190
|
+
clearTimeout(timeout)
|
|
191
|
+
reject(signal.reason)
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function requestURL (request: Request): URL {
|
|
197
|
+
return new URL(request.url)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface FetchInit extends RequestInit {
|
|
201
|
+
retry?: RetryOptions
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface RetryOptions {
|
|
205
|
+
attempts: number
|
|
206
|
+
expected?: number[]
|
|
207
|
+
delay?: number
|
|
208
|
+
factor?: number
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface NormalizedRetryOptions {
|
|
212
|
+
attempts: number
|
|
213
|
+
expected?: number[]
|
|
214
|
+
delay: number
|
|
215
|
+
factor: number
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface RetryErrorParameters {
|
|
219
|
+
attempt: number
|
|
220
|
+
options: NormalizedRetryOptions
|
|
221
|
+
request: Request
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface InvocationTelemetry {
|
|
225
|
+
output: Console
|
|
226
|
+
parentAttributes: Record<string, unknown>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const DEFAULT_DELAY = 100
|
|
230
|
+
const DEFAULT_FACTOR = 2
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Connector } from '@toa.io/core';
|
|
2
|
+
import type { Locator, extensions } from '@toa.io/core';
|
|
3
|
+
export declare class Aspect extends Connector implements extensions.Aspect {
|
|
4
|
+
readonly name = "fetch";
|
|
5
|
+
private readonly locator;
|
|
6
|
+
private readonly consoles;
|
|
7
|
+
constructor(locator: Locator);
|
|
8
|
+
invoke(operation: string, input: RequestInfo | URL, init?: FetchInit): Promise<Response>;
|
|
9
|
+
private output;
|
|
10
|
+
}
|
|
11
|
+
export interface FetchInit extends RequestInit {
|
|
12
|
+
retry?: RetryOptions;
|
|
13
|
+
}
|
|
14
|
+
export interface RetryOptions {
|
|
15
|
+
attempts: number;
|
|
16
|
+
expected?: number[];
|
|
17
|
+
delay?: number;
|
|
18
|
+
factor?: number;
|
|
19
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Aspect = void 0;
|
|
4
|
+
const core_1 = require("@toa.io/core");
|
|
5
|
+
const openspan_1 = require("openspan");
|
|
6
|
+
class Aspect extends core_1.Connector {
|
|
7
|
+
name = 'fetch';
|
|
8
|
+
locator;
|
|
9
|
+
consoles = {};
|
|
10
|
+
constructor(locator) {
|
|
11
|
+
super();
|
|
12
|
+
this.locator = locator;
|
|
13
|
+
}
|
|
14
|
+
async invoke(operation, input, init) {
|
|
15
|
+
const { retry, ...nativeInit } = init ?? {};
|
|
16
|
+
const body = nativeInit.body ?? (input instanceof Request ? input.body : null);
|
|
17
|
+
if (retry !== undefined && retry.attempts > 1 && body instanceof ReadableStream)
|
|
18
|
+
throw new TypeError('Cannot retry a request with a non-replayable body');
|
|
19
|
+
const request = new Request(input, nativeInit);
|
|
20
|
+
const attributes = {
|
|
21
|
+
'http.request.method': request.method,
|
|
22
|
+
'url.scheme': requestURL(request).protocol.slice(0, -1),
|
|
23
|
+
'server.address': requestURL(request).hostname
|
|
24
|
+
};
|
|
25
|
+
const url = requestURL(request);
|
|
26
|
+
if (url.port !== '')
|
|
27
|
+
attributes['server.port'] = Number(url.port);
|
|
28
|
+
const options = {
|
|
29
|
+
name: `${request.method} ${url.origin}`,
|
|
30
|
+
kind: 'internal',
|
|
31
|
+
attributes
|
|
32
|
+
};
|
|
33
|
+
const output = this.output(operation);
|
|
34
|
+
return await output.span(options, async () => {
|
|
35
|
+
const response = await invoke(request, retry, { output, parentAttributes: attributes });
|
|
36
|
+
attributes['http.response.status_code'] = response.status;
|
|
37
|
+
return response;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
output(operation) {
|
|
41
|
+
this.consoles[operation] ??= openspan_1.console.fork({
|
|
42
|
+
namespace: this.locator.namespace,
|
|
43
|
+
component: this.locator.name,
|
|
44
|
+
operation
|
|
45
|
+
});
|
|
46
|
+
return this.consoles[operation];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.Aspect = Aspect;
|
|
50
|
+
async function invoke(request, options, telemetry) {
|
|
51
|
+
const retry = normalize(options);
|
|
52
|
+
let response;
|
|
53
|
+
for (let attempt = 1; attempt <= retry.attempts; attempt++) {
|
|
54
|
+
telemetry.parentAttributes['retry.attempts'] = attempt;
|
|
55
|
+
try {
|
|
56
|
+
response = await send(request, attempt, telemetry);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await retryError(error, { attempt, options: retry, request });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (expected(response.status, retry.expected) || attempt === retry.attempts)
|
|
63
|
+
return response;
|
|
64
|
+
const interval = retryAfter(response.headers.get('retry-after')) ?? delay(retry, attempt);
|
|
65
|
+
await discard(response);
|
|
66
|
+
await wait(interval, request.signal);
|
|
67
|
+
}
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
async function send(request, attempt, telemetry) {
|
|
71
|
+
const attributes = {
|
|
72
|
+
...telemetry.parentAttributes,
|
|
73
|
+
'retry.attempt': attempt
|
|
74
|
+
};
|
|
75
|
+
delete attributes['retry.attempts'];
|
|
76
|
+
return await telemetry.output.span({
|
|
77
|
+
name: `attempt ${attempt}`,
|
|
78
|
+
kind: 'client',
|
|
79
|
+
attributes
|
|
80
|
+
}, async () => {
|
|
81
|
+
const response = await globalThis.fetch(request.clone());
|
|
82
|
+
attributes['http.response.status_code'] = response.status;
|
|
83
|
+
return response;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function retryError(error, parameters) {
|
|
87
|
+
const { attempt, options, request } = parameters;
|
|
88
|
+
if (attempt === options.attempts || request.signal.aborted)
|
|
89
|
+
throw error;
|
|
90
|
+
await wait(delay(options, attempt), request.signal);
|
|
91
|
+
}
|
|
92
|
+
async function discard(response) {
|
|
93
|
+
try {
|
|
94
|
+
await response.body?.cancel();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// The response is discarded regardless of whether its stream closes cleanly.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function normalize(options) {
|
|
101
|
+
if (options === undefined)
|
|
102
|
+
return { attempts: 1, expected: undefined, delay: DEFAULT_DELAY, factor: DEFAULT_FACTOR };
|
|
103
|
+
if (!Number.isInteger(options.attempts) || options.attempts < 1)
|
|
104
|
+
throw new TypeError('retry.attempts must be an integer greater than or equal to 1');
|
|
105
|
+
if (options.expected !== undefined &&
|
|
106
|
+
(options.expected.length === 0 || options.expected.some((status) => !Number.isInteger(status) || status < 100 || status > 599)))
|
|
107
|
+
throw new TypeError('retry.expected must contain HTTP status codes');
|
|
108
|
+
if (options.delay !== undefined && (!Number.isFinite(options.delay) || options.delay < 0))
|
|
109
|
+
throw new TypeError('retry.delay must be a non-negative number');
|
|
110
|
+
if (options.factor !== undefined && (!Number.isFinite(options.factor) || options.factor < 0))
|
|
111
|
+
throw new TypeError('retry.factor must be a non-negative number');
|
|
112
|
+
return {
|
|
113
|
+
attempts: options.attempts,
|
|
114
|
+
expected: options.expected,
|
|
115
|
+
delay: options.delay ?? DEFAULT_DELAY,
|
|
116
|
+
factor: options.factor ?? DEFAULT_FACTOR
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function expected(status, statuses) {
|
|
120
|
+
return statuses === undefined ? status >= 200 && status < 300 : statuses.includes(status);
|
|
121
|
+
}
|
|
122
|
+
function delay(options, attempt) {
|
|
123
|
+
return options.delay * Math.pow(options.factor, attempt - 1);
|
|
124
|
+
}
|
|
125
|
+
function retryAfter(value) {
|
|
126
|
+
if (value === null)
|
|
127
|
+
return undefined;
|
|
128
|
+
const seconds = Number(value);
|
|
129
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
130
|
+
return seconds * 1000;
|
|
131
|
+
const date = Date.parse(value);
|
|
132
|
+
if (Number.isNaN(date))
|
|
133
|
+
return undefined;
|
|
134
|
+
return Math.max(0, date - Date.now());
|
|
135
|
+
}
|
|
136
|
+
async function wait(milliseconds, signal) {
|
|
137
|
+
if (signal.aborted)
|
|
138
|
+
throw signal.reason;
|
|
139
|
+
await new Promise((resolve, reject) => {
|
|
140
|
+
const timeout = setTimeout(done, milliseconds);
|
|
141
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
142
|
+
function done() {
|
|
143
|
+
signal.removeEventListener('abort', aborted);
|
|
144
|
+
resolve();
|
|
145
|
+
}
|
|
146
|
+
function aborted() {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
reject(signal.reason);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function requestURL(request) {
|
|
153
|
+
return new URL(request.url);
|
|
154
|
+
}
|
|
155
|
+
const DEFAULT_DELAY = 100;
|
|
156
|
+
const DEFAULT_FACTOR = 2;
|
|
157
|
+
//# sourceMappingURL=Aspect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Aspect.js","sourceRoot":"","sources":["../source/Aspect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@toa.io/core");
|
|
4
|
+
const openspan_1 = require("openspan");
|
|
5
|
+
const Aspect_1 = require("./Aspect");
|
|
6
|
+
const nativeFetch = globalThis.fetch;
|
|
7
|
+
let aspect;
|
|
8
|
+
let fetchMock;
|
|
9
|
+
let spans;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
aspect = new Aspect_1.Aspect(new core_1.Locator('users', 'identity'));
|
|
12
|
+
fetchMock = jest.fn();
|
|
13
|
+
globalThis.fetch = fetchMock;
|
|
14
|
+
spans = [];
|
|
15
|
+
(0, openspan_1.exporting)([{ export: (span) => spans.push(span) }]);
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
globalThis.fetch = nativeFetch;
|
|
19
|
+
(0, openspan_1.exporting)([openspan_1.consoleExporter]);
|
|
20
|
+
});
|
|
21
|
+
it('delegates to native fetch and returns its Response', async () => {
|
|
22
|
+
const response = new Response('ok', { status: 201, headers: { 'x-test': 'yes' } });
|
|
23
|
+
fetchMock.mockResolvedValue(response);
|
|
24
|
+
const result = await aspect.invoke('create', 'https://example.com/items?secret=yes', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'content-type': 'application/json' },
|
|
27
|
+
body: '{}'
|
|
28
|
+
});
|
|
29
|
+
expect(result).toBe(response);
|
|
30
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
31
|
+
const request = fetchMock.mock.calls[0][0];
|
|
32
|
+
expect(request).toBeInstanceOf(Request);
|
|
33
|
+
expect(request.method).toBe('POST');
|
|
34
|
+
expect(request.url).toBe('https://example.com/items?secret=yes');
|
|
35
|
+
await expect(request.text()).resolves.toBe('{}');
|
|
36
|
+
});
|
|
37
|
+
it('does not retry by default', async () => {
|
|
38
|
+
fetchMock.mockResolvedValue(new Response(null, { status: 503 }));
|
|
39
|
+
const response = await aspect.invoke('get', 'https://example.com');
|
|
40
|
+
expect(response.status).toBe(503);
|
|
41
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
it('retries unexpected responses and returns an expected response', async () => {
|
|
44
|
+
fetchMock
|
|
45
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
46
|
+
.mockResolvedValueOnce(new Response(null, { status: 201 }));
|
|
47
|
+
const response = await aspect.invoke('create', 'https://example.com', {
|
|
48
|
+
retry: { attempts: 3, expected: [201], delay: 0 }
|
|
49
|
+
});
|
|
50
|
+
expect(response.status).toBe(201);
|
|
51
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
52
|
+
});
|
|
53
|
+
it('cancels an unexpected response body before retrying', async () => {
|
|
54
|
+
const cancel = jest.fn();
|
|
55
|
+
const body = new ReadableStream({ cancel });
|
|
56
|
+
fetchMock
|
|
57
|
+
.mockResolvedValueOnce(new Response(body, { status: 503 }))
|
|
58
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
59
|
+
await aspect.invoke('get', 'https://example.com', {
|
|
60
|
+
retry: { attempts: 2, delay: 0 }
|
|
61
|
+
});
|
|
62
|
+
expect(cancel).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
it('continues retrying when response body cancellation fails', async () => {
|
|
65
|
+
const body = new ReadableStream({
|
|
66
|
+
cancel: () => {
|
|
67
|
+
throw new Error('cancel failed');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
fetchMock
|
|
71
|
+
.mockResolvedValueOnce(new Response(body, { status: 503 }))
|
|
72
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
73
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
74
|
+
retry: { attempts: 2, delay: 0 }
|
|
75
|
+
});
|
|
76
|
+
expect(response.status).toBe(200);
|
|
77
|
+
});
|
|
78
|
+
it('replays a regular request body', async () => {
|
|
79
|
+
fetchMock
|
|
80
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
81
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
82
|
+
await aspect.invoke('create', 'https://example.com', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body: 'hello',
|
|
85
|
+
retry: { attempts: 2, delay: 0 }
|
|
86
|
+
});
|
|
87
|
+
const first = fetchMock.mock.calls[0][0];
|
|
88
|
+
const second = fetchMock.mock.calls[1][0];
|
|
89
|
+
await expect(first.text()).resolves.toBe('hello');
|
|
90
|
+
await expect(second.text()).resolves.toBe('hello');
|
|
91
|
+
});
|
|
92
|
+
it('honors Retry-After instead of the configured delay', async () => {
|
|
93
|
+
fetchMock
|
|
94
|
+
.mockResolvedValueOnce(new Response(null, {
|
|
95
|
+
status: 503,
|
|
96
|
+
headers: { 'retry-after': '0' }
|
|
97
|
+
}))
|
|
98
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
99
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
100
|
+
retry: { attempts: 2, delay: 60_000 }
|
|
101
|
+
});
|
|
102
|
+
expect(response.status).toBe(200);
|
|
103
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
104
|
+
});
|
|
105
|
+
it('returns the last unexpected response', async () => {
|
|
106
|
+
fetchMock
|
|
107
|
+
.mockResolvedValueOnce(new Response(null, { status: 500 }))
|
|
108
|
+
.mockResolvedValueOnce(new Response(null, { status: 502 }));
|
|
109
|
+
const response = await aspect.invoke('get', 'https://example.com', {
|
|
110
|
+
retry: { attempts: 2, delay: 0 }
|
|
111
|
+
});
|
|
112
|
+
expect(response.status).toBe(502);
|
|
113
|
+
});
|
|
114
|
+
it('retries network errors and throws the final error', async () => {
|
|
115
|
+
const first = new Error('first');
|
|
116
|
+
const last = new Error('last');
|
|
117
|
+
fetchMock.mockRejectedValueOnce(first).mockRejectedValueOnce(last);
|
|
118
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
119
|
+
retry: { attempts: 2, delay: 0 }
|
|
120
|
+
})).rejects.toBe(last);
|
|
121
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
122
|
+
});
|
|
123
|
+
it('aborts while waiting for another attempt', async () => {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
fetchMock.mockResolvedValue(new Response(null, { status: 503 }));
|
|
126
|
+
const promise = aspect.invoke('get', 'https://example.com', {
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
retry: { attempts: 2, delay: 60_000 }
|
|
129
|
+
});
|
|
130
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
131
|
+
controller.abort(new Error('cancelled'));
|
|
132
|
+
await expect(promise).rejects.toThrow('cancelled');
|
|
133
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
134
|
+
});
|
|
135
|
+
it('rejects invalid retry options', async () => {
|
|
136
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
137
|
+
retry: { attempts: 0 }
|
|
138
|
+
})).rejects.toThrow('retry.attempts');
|
|
139
|
+
await expect(aspect.invoke('get', 'https://example.com', {
|
|
140
|
+
retry: { attempts: 2, expected: [] }
|
|
141
|
+
})).rejects.toThrow('retry.expected');
|
|
142
|
+
});
|
|
143
|
+
it('rejects an explicit streaming body before sending it', async () => {
|
|
144
|
+
const body = new ReadableStream();
|
|
145
|
+
await expect(aspect.invoke('create', 'https://example.com', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
body,
|
|
148
|
+
// Required by Node.js for a streaming request body.
|
|
149
|
+
// @ts-expect-error -- duplex is implemented by Node but absent from lib.dom RequestInit.
|
|
150
|
+
duplex: 'half',
|
|
151
|
+
retry: { attempts: 2 }
|
|
152
|
+
})).rejects.toThrow('non-replayable');
|
|
153
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
it('rejects a Request input with a body before retrying', async () => {
|
|
156
|
+
const request = new Request('https://example.com', { method: 'POST', body: 'hello' });
|
|
157
|
+
await expect(aspect.invoke('create', request, {
|
|
158
|
+
retry: { attempts: 2 }
|
|
159
|
+
})).rejects.toThrow('non-replayable');
|
|
160
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
it('creates a scoped parent span with a client span for each attempt', async () => {
|
|
163
|
+
fetchMock
|
|
164
|
+
.mockResolvedValueOnce(new Response(null, { status: 503 }))
|
|
165
|
+
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
166
|
+
await aspect.invoke('update', 'https://example.com/items?token=secret', {
|
|
167
|
+
method: 'PUT',
|
|
168
|
+
retry: { attempts: 2, delay: 0 }
|
|
169
|
+
});
|
|
170
|
+
expect(spans).toHaveLength(3);
|
|
171
|
+
const parent = spans.find((span) => span.name === 'PUT https://example.com');
|
|
172
|
+
const attempts = spans.filter((span) => span.parentId === parent.spanId);
|
|
173
|
+
expect(parent).toMatchObject({
|
|
174
|
+
name: 'PUT https://example.com',
|
|
175
|
+
kind: 'internal',
|
|
176
|
+
scope: {
|
|
177
|
+
namespace: 'identity',
|
|
178
|
+
component: 'users',
|
|
179
|
+
operation: 'update'
|
|
180
|
+
},
|
|
181
|
+
attributes: {
|
|
182
|
+
'http.request.method': 'PUT',
|
|
183
|
+
'http.response.status_code': 204,
|
|
184
|
+
'url.scheme': 'https',
|
|
185
|
+
'server.address': 'example.com',
|
|
186
|
+
'retry.attempts': 2
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
expect(attempts).toMatchObject([
|
|
190
|
+
{
|
|
191
|
+
name: 'attempt 1',
|
|
192
|
+
kind: 'client',
|
|
193
|
+
attributes: {
|
|
194
|
+
'retry.attempt': 1,
|
|
195
|
+
'http.response.status_code': 503
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'attempt 2',
|
|
200
|
+
kind: 'client',
|
|
201
|
+
attributes: {
|
|
202
|
+
'retry.attempt': 2,
|
|
203
|
+
'http.response.status_code': 204
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
]);
|
|
207
|
+
expect(JSON.stringify(spans)).not.toContain('token=secret');
|
|
208
|
+
});
|
|
209
|
+
it('marks the span as failed after a final network error', async () => {
|
|
210
|
+
fetchMock.mockRejectedValue(new Error('unavailable'));
|
|
211
|
+
await expect(aspect.invoke('get', 'https://example.com')).rejects.toThrow('unavailable');
|
|
212
|
+
expect(spans).toHaveLength(2);
|
|
213
|
+
expect(spans.every((span) => span.status === 'error')).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
//# sourceMappingURL=Aspect.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Aspect.test.js","sourceRoot":"","sources":["../source/Aspect.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Factory = void 0;
|
|
4
|
+
const Aspect_1 = require("./Aspect");
|
|
5
|
+
class Factory {
|
|
6
|
+
aspect(locator) {
|
|
7
|
+
return new Aspect_1.Aspect(locator);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.Factory = Factory;
|
|
11
|
+
//# sourceMappingURL=Factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Factory.js","sourceRoot":"","sources":["../source/Factory.ts"],"names":[],"mappings":";;;AAAA,qCAAiC;AAGjC,MAAa,OAAO;IACX,MAAM,CAAE,OAAgB;QAC7B,OAAO,IAAI,eAAM,CAAC,OAAO,CAAC,CAAA;IAC5B,CAAC;CACF;AAJD,0BAIC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Factory = void 0;
|
|
4
|
+
var Factory_1 = require("./Factory");
|
|
5
|
+
Object.defineProperty(exports, "Factory", { enumerable: true, get: function () { return Factory_1.Factory; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";;;AAAA,qCAAmC;AAA1B,kGAAA,OAAO,OAAA"}
|