@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 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
+ })
@@ -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
@@ -0,0 +1,8 @@
1
+ import { Aspect } from './Aspect'
2
+ import type { Locator, extensions } from '@toa.io/core'
3
+
4
+ export class Factory implements extensions.Factory {
5
+ public aspect (locator: Locator): extensions.Aspect {
6
+ return new Aspect(locator)
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ export { Factory } from './Factory'
2
+ export type { FetchInit, RetryOptions } from './Aspect'
@@ -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,4 @@
1
+ import type { Locator, extensions } from '@toa.io/core';
2
+ export declare class Factory implements extensions.Factory {
3
+ aspect(locator: Locator): extensions.Aspect;
4
+ }
@@ -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,2 @@
1
+ export { Factory } from './Factory';
2
+ export type { FetchInit, RetryOptions } from './Aspect';
@@ -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"}
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./transpiled"
5
+ },
6
+ "include": [
7
+ "source"
8
+ ]
9
+ }