snapreq 0.0.1
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/README.md +144 -0
- package/package.json +39 -0
- package/src/capabilities.js +31 -0
- package/src/errors.js +69 -0
- package/src/headers.js +92 -0
- package/src/index.js +22 -0
- package/src/request.js +104 -0
- package/src/response.js +156 -0
- package/src/retry.js +105 -0
- package/src/snap-req.js +248 -0
- package/src/transports/fetch-transport.js +128 -0
- package/src/transports/node-transport.js +323 -0
- package/src/transports/select.js +73 -0
- package/src/transports/xhr-transport.js +112 -0
- package/src/websocket/websocket-channel.js +176 -0
- package/src/websocket/websocket-client.js +1029 -0
- package/src/websocket/websocket-connection.js +154 -0
package/src/retry.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {SnapReqHttpError} from "./errors.js"
|
|
4
|
+
|
|
5
|
+
const RETRYABLE_ERROR_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "EHOSTUNREACH", "ENOENT", "ETIMEDOUT", "EPIPE"])
|
|
6
|
+
const DEFAULT_RETRYABLE_STATUSES = [502, 503, 504]
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} RetryOptions
|
|
10
|
+
* @property {number} [tries] - Maximum number of attempts. Defaults to 3.
|
|
11
|
+
* @property {number} [waitMs] - Delay between attempts in milliseconds. Defaults to 500.
|
|
12
|
+
* @property {number[]} [retryableStatuses] - HTTP status codes that should be retried. Defaults to 502/503/504.
|
|
13
|
+
* @property {(error: unknown, attempt: number) => boolean} [shouldRetry] - Override the retryable-error classifier.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} NormalizedRetryOptions
|
|
18
|
+
* @property {number} tries - Maximum number of attempts.
|
|
19
|
+
* @property {number} waitMs - Delay between attempts in milliseconds.
|
|
20
|
+
* @property {number[]} retryableStatuses - HTTP status codes that should be retried.
|
|
21
|
+
* @property {(error: unknown, attempt: number) => boolean} shouldRetry - Retryable-error classifier.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The default network-error classifier. Exposed so callers can compose extra
|
|
26
|
+
* rules on top of it (for example matching server-specific 500 messages).
|
|
27
|
+
* @param {unknown} error - Error thrown by a transport.
|
|
28
|
+
* @returns {boolean} - Whether the error is a transient network failure.
|
|
29
|
+
*/
|
|
30
|
+
export function defaultRetryableError(error) {
|
|
31
|
+
if (!error || typeof error !== "object") return false
|
|
32
|
+
|
|
33
|
+
if ("code" in error && typeof error.code === "string" && RETRYABLE_ERROR_CODES.has(error.code)) {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return error instanceof Error && error.message === "socket hang up"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalizes the `retry` option into a complete set of retry settings, or
|
|
42
|
+
* `null` when retries are disabled.
|
|
43
|
+
* @param {boolean | RetryOptions | undefined} retry - Retry configuration.
|
|
44
|
+
* @returns {NormalizedRetryOptions | null} - Normalized retry settings.
|
|
45
|
+
*/
|
|
46
|
+
export function normalizeRetryOptions(retry) {
|
|
47
|
+
if (!retry) return null
|
|
48
|
+
|
|
49
|
+
const options = retry === true ? {} : retry
|
|
50
|
+
const retryableStatuses = options.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES
|
|
51
|
+
const shouldRetry = options.shouldRetry ?? ((/** @type {unknown} */ error) => defaultRetryableError(error))
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
tries: options.tries ?? 3,
|
|
55
|
+
waitMs: options.waitMs ?? 500,
|
|
56
|
+
retryableStatuses,
|
|
57
|
+
shouldRetry
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {number} waitMs - Delay in milliseconds.
|
|
63
|
+
* @returns {Promise<void>} - Resolves after the delay.
|
|
64
|
+
*/
|
|
65
|
+
function wait(waitMs) {
|
|
66
|
+
return new Promise((resolve) => setTimeout(resolve, waitMs))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs a request attempt, retrying transient network errors and retryable HTTP
|
|
71
|
+
* statuses. Retries are only used for buffered requests — the caller must not
|
|
72
|
+
* apply this to streamed responses.
|
|
73
|
+
* @param {() => Promise<import("./response.js").default>} attempt - Performs one request attempt.
|
|
74
|
+
* @param {NormalizedRetryOptions} retry - Normalized retry settings.
|
|
75
|
+
* @returns {Promise<import("./response.js").default>} - The successful (or final) response.
|
|
76
|
+
*/
|
|
77
|
+
export async function runWithRetry(attempt, retry) {
|
|
78
|
+
for (let tryNumber = 1; tryNumber <= retry.tries; tryNumber += 1) {
|
|
79
|
+
/** @type {import("./response.js").default} */
|
|
80
|
+
let response
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
response = await attempt()
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (tryNumber >= retry.tries || !retry.shouldRetry(error, tryNumber)) throw error
|
|
86
|
+
|
|
87
|
+
await wait(retry.waitMs)
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (tryNumber < retry.tries && retry.retryableStatuses.includes(response.status)) {
|
|
92
|
+
await wait(retry.waitMs)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return response
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new SnapReqHttpError({
|
|
100
|
+
message: "Retry loop exited without a response.",
|
|
101
|
+
method: "",
|
|
102
|
+
url: "",
|
|
103
|
+
status: 0
|
|
104
|
+
})
|
|
105
|
+
}
|
package/src/snap-req.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {SnapReqHttpError, SnapReqUnsupportedFeatureError} from "./errors.js"
|
|
4
|
+
import SnapReqHeaders from "./headers.js"
|
|
5
|
+
import {buildUrl, normalizeBody} from "./request.js"
|
|
6
|
+
import {normalizeRetryOptions, runWithRetry} from "./retry.js"
|
|
7
|
+
import {selectTransport} from "./transports/select.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {import("./request.js").CompressionEncoding} CompressionEncoding
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {object} NormalizedRequest
|
|
15
|
+
* @property {string} method - Upper-cased HTTP method.
|
|
16
|
+
* @property {string} url - Fully resolved request URL.
|
|
17
|
+
* @property {SnapReqHeaders} headers - Request headers.
|
|
18
|
+
* @property {import("./request.js").NormalizedBody} body - Normalized request body.
|
|
19
|
+
* @property {CompressionEncoding} bodyCompression - Request body compression.
|
|
20
|
+
* @property {AbortSignal} [signal] - Abort signal.
|
|
21
|
+
* @property {string} [credentials] - Fetch credentials mode ("omit" | "same-origin" | "include").
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {object} RequestOptions
|
|
26
|
+
* @property {string} [method] - HTTP method. Defaults to GET.
|
|
27
|
+
* @property {string} [path] - Request path (joined with the client `baseUrl`) or absolute URL.
|
|
28
|
+
* @property {string} [url] - Alias for `path`.
|
|
29
|
+
* @property {Record<string, string | number | boolean | null | undefined>} [query] - Query parameters.
|
|
30
|
+
* @property {Record<string, string | number> | SnapReqHeaders} [headers] - Per-request headers.
|
|
31
|
+
* @property {any} [body] - Request body: string, object (JSON), Uint8Array/ArrayBuffer, or a stream/async-iterable.
|
|
32
|
+
* @property {CompressionEncoding} [bodyCompression] - Compress the request body (Node transport only).
|
|
33
|
+
* @property {AbortSignal} [signal] - Abort signal for the request.
|
|
34
|
+
* @property {string} [credentials] - Fetch credentials mode.
|
|
35
|
+
* @property {boolean | import("./retry.js").RetryOptions} [retry] - Retry transient failures.
|
|
36
|
+
* @property {boolean} [throwOnError] - Throw `SnapReqHttpError` on non-2xx responses.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A cross-platform HTTP client with one API across Node, web, Expo and React
|
|
41
|
+
* Native. The right transport is chosen at runtime; features a platform cannot
|
|
42
|
+
* provide raise `SnapReqUnsupportedFeatureError` rather than silently changing
|
|
43
|
+
* behaviour.
|
|
44
|
+
*/
|
|
45
|
+
export default class SnapReq {
|
|
46
|
+
/**
|
|
47
|
+
* @param {object} [config] - Client configuration.
|
|
48
|
+
* @param {string} [config.baseUrl] - Origin (and optional base path) prepended to relative paths.
|
|
49
|
+
* @param {string} [config.socketPath] - Unix domain socket path (Node transport only).
|
|
50
|
+
* @param {{ca?: string | Buffer, cert?: string | Buffer, key?: string | Buffer, rejectUnauthorized?: boolean}} [config.tls] - TLS material (Node transport only).
|
|
51
|
+
* @param {boolean} [config.keepAlive] - Reuse connections across requests (Node transport only). Defaults to true.
|
|
52
|
+
* @param {Record<string, string | number> | (() => Record<string, string | number>)} [config.headers] - Default headers (object or factory).
|
|
53
|
+
* @param {boolean | import("./retry.js").RetryOptions} [config.retry] - Default retry policy.
|
|
54
|
+
* @param {boolean} [config.throwOnError] - Throw `SnapReqHttpError` on non-2xx responses by default. Defaults to false.
|
|
55
|
+
* @param {string} [config.credentials] - Default fetch credentials mode.
|
|
56
|
+
* @param {import("./transports/select.js").TransportName | import("./transports/select.js").Transport} [config.transport] - Transport preference or instance. Defaults to "auto".
|
|
57
|
+
*/
|
|
58
|
+
constructor({baseUrl, socketPath, tls, keepAlive = true, headers, retry, throwOnError = false, credentials, transport = "auto"} = {}) {
|
|
59
|
+
this.baseUrl = baseUrl
|
|
60
|
+
this.defaultHeaders = headers
|
|
61
|
+
this.defaultRetry = retry
|
|
62
|
+
this.throwOnError = throwOnError
|
|
63
|
+
this.credentials = credentials
|
|
64
|
+
this._transportPreference = transport
|
|
65
|
+
this._nodeConfig = {socketPath, tls, keepAlive}
|
|
66
|
+
/** @type {Promise<import("./transports/select.js").Transport> | null} */
|
|
67
|
+
this._transportPromise = null
|
|
68
|
+
/** @type {import("./transports/select.js").Transport | null} */
|
|
69
|
+
this._transport = null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** @returns {Promise<import("./transports/select.js").Transport>} - The resolved transport. */
|
|
73
|
+
async _resolveTransport() {
|
|
74
|
+
this._transportPromise ||= selectTransport(this._transportPreference, this._nodeConfig)
|
|
75
|
+
this._transport = await this._transportPromise
|
|
76
|
+
|
|
77
|
+
return this._transport
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @returns {Promise<import("./capabilities.js").TransportCapabilities>} - The active transport's capabilities. */
|
|
81
|
+
async capabilities() {
|
|
82
|
+
return (await this._resolveTransport()).capabilities
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @returns {Promise<string>} - The active transport's name. */
|
|
86
|
+
async transportName() {
|
|
87
|
+
const transport = await this._resolveTransport()
|
|
88
|
+
|
|
89
|
+
return /** @type {any} */ (transport.constructor)?.transportName || "custom"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {RequestOptions} options - Request options.
|
|
94
|
+
* @returns {NormalizedRequest} - The normalized request.
|
|
95
|
+
*/
|
|
96
|
+
_normalize(options) {
|
|
97
|
+
const headers = new SnapReqHeaders()
|
|
98
|
+
const defaults = typeof this.defaultHeaders === "function" ? this.defaultHeaders() : this.defaultHeaders
|
|
99
|
+
|
|
100
|
+
if (defaults) for (const [name, value] of new SnapReqHeaders(defaults).entries()) headers.set(name, value)
|
|
101
|
+
if (options.headers) for (const [name, value] of new SnapReqHeaders(options.headers).entries()) headers.set(name, value)
|
|
102
|
+
|
|
103
|
+
const url = buildUrl(this.baseUrl, options.path ?? options.url ?? "", options.query)
|
|
104
|
+
const body = normalizeBody(options.body, headers)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
method: (options.method || "GET").toUpperCase(),
|
|
108
|
+
url,
|
|
109
|
+
headers,
|
|
110
|
+
body,
|
|
111
|
+
bodyCompression: options.bodyCompression || "identity",
|
|
112
|
+
signal: options.signal,
|
|
113
|
+
credentials: options.credentials ?? this.credentials
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Performs a request and buffers nothing eagerly — read the body via the
|
|
119
|
+
* returned response (`json()`, `text()`, `bytes()`). Retries transient
|
|
120
|
+
* failures when a retry policy is configured (never for streamed bodies).
|
|
121
|
+
* @param {RequestOptions} options - Request options.
|
|
122
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
123
|
+
*/
|
|
124
|
+
async request(options) {
|
|
125
|
+
const normalized = this._normalize(options)
|
|
126
|
+
const transport = await this._resolveTransport()
|
|
127
|
+
const throwOnError = options.throwOnError ?? this.throwOnError
|
|
128
|
+
const retry = normalizeRetryOptions(options.retry ?? this.defaultRetry)
|
|
129
|
+
const canRetry = retry && normalized.body.kind !== "stream"
|
|
130
|
+
|
|
131
|
+
const attempt = () => transport.performRequest(normalized)
|
|
132
|
+
const response = canRetry ? await runWithRetry(attempt, /** @type {any} */ (retry)) : await attempt()
|
|
133
|
+
|
|
134
|
+
if (throwOnError && !response.ok) throw await this._httpError(response, normalized)
|
|
135
|
+
|
|
136
|
+
return response
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Performs a request and returns the response with its body available as a
|
|
141
|
+
* stream (`response.stream()`). Requires a transport that supports response
|
|
142
|
+
* streaming; never retries.
|
|
143
|
+
* @param {RequestOptions} options - Request options.
|
|
144
|
+
* @returns {Promise<import("./response.js").default>} - The streaming response.
|
|
145
|
+
*/
|
|
146
|
+
async requestStream(options) {
|
|
147
|
+
const normalized = this._normalize(options)
|
|
148
|
+
const transport = await this._resolveTransport()
|
|
149
|
+
|
|
150
|
+
if (!transport.capabilities.responseStreaming) {
|
|
151
|
+
throw new SnapReqUnsupportedFeatureError({
|
|
152
|
+
feature: "response streaming",
|
|
153
|
+
transport: /** @type {any} */ (transport.constructor)?.transportName || "custom"
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const response = await transport.performRequest(normalized)
|
|
158
|
+
|
|
159
|
+
if ((options.throwOnError ?? this.throwOnError) && !response.ok) {
|
|
160
|
+
throw await this._httpError(response, normalized)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return response
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {string} path - Request path or absolute URL.
|
|
168
|
+
* @param {RequestOptions} [options] - Request options.
|
|
169
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
170
|
+
*/
|
|
171
|
+
get(path, options = {}) {
|
|
172
|
+
return this.request({...options, method: "GET", path})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} path - Request path or absolute URL.
|
|
177
|
+
* @param {any} [body] - Request body.
|
|
178
|
+
* @param {RequestOptions} [options] - Request options.
|
|
179
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
180
|
+
*/
|
|
181
|
+
post(path, body, options = {}) {
|
|
182
|
+
return this.request({...options, method: "POST", path, body})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} path - Request path or absolute URL.
|
|
187
|
+
* @param {any} [body] - Request body.
|
|
188
|
+
* @param {RequestOptions} [options] - Request options.
|
|
189
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
190
|
+
*/
|
|
191
|
+
put(path, body, options = {}) {
|
|
192
|
+
return this.request({...options, method: "PUT", path, body})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {string} path - Request path or absolute URL.
|
|
197
|
+
* @param {any} [body] - Request body.
|
|
198
|
+
* @param {RequestOptions} [options] - Request options.
|
|
199
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
200
|
+
*/
|
|
201
|
+
patch(path, body, options = {}) {
|
|
202
|
+
return this.request({...options, method: "PATCH", path, body})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} path - Request path or absolute URL.
|
|
207
|
+
* @param {RequestOptions} [options] - Request options.
|
|
208
|
+
* @returns {Promise<import("./response.js").default>} - The response.
|
|
209
|
+
*/
|
|
210
|
+
delete(path, options = {}) {
|
|
211
|
+
return this.request({...options, method: "DELETE", path})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {import("./response.js").default} response - The failed response.
|
|
216
|
+
* @param {NormalizedRequest} request - The request that produced it.
|
|
217
|
+
* @returns {Promise<SnapReqHttpError>} - An error describing the failure.
|
|
218
|
+
*/
|
|
219
|
+
async _httpError(response, request) {
|
|
220
|
+
let responseText = ""
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
responseText = await response.text()
|
|
224
|
+
} catch {
|
|
225
|
+
// Body unavailable (already streamed or read error) — fall back to status text.
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const detail = responseText || response.statusText || ""
|
|
229
|
+
|
|
230
|
+
return new SnapReqHttpError({
|
|
231
|
+
message: `HTTP ${response.status} ${request.method} ${request.url}${detail ? `: ${detail}` : ""}`,
|
|
232
|
+
method: request.method,
|
|
233
|
+
url: request.url,
|
|
234
|
+
status: response.status,
|
|
235
|
+
statusText: response.statusText,
|
|
236
|
+
responseText,
|
|
237
|
+
response
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Releases transport resources (for example Node keep-alive sockets).
|
|
243
|
+
* @returns {void}
|
|
244
|
+
*/
|
|
245
|
+
close() {
|
|
246
|
+
this._transport?.close?.()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {buildCapabilities} from "../capabilities.js"
|
|
4
|
+
import {SnapReqAbortError, SnapReqUnsupportedFeatureError} from "../errors.js"
|
|
5
|
+
import SnapReqHeaders from "../headers.js"
|
|
6
|
+
import SnapReqResponse from "../response.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Transport backed by the `fetch` global. Works on web, Expo / React Native and
|
|
10
|
+
* Node 18+. It cannot open Unix sockets, present client certificates or
|
|
11
|
+
* compress request bodies — those raise `SnapReqUnsupportedFeatureError`.
|
|
12
|
+
* Response streaming uses `response.body` where available and otherwise buffers
|
|
13
|
+
* the body once, keeping the same stream interface everywhere.
|
|
14
|
+
*/
|
|
15
|
+
export default class FetchTransport {
|
|
16
|
+
/** @returns {string} - Transport name. */
|
|
17
|
+
static get transportName() {
|
|
18
|
+
return "fetch"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @returns {boolean} - Whether this transport can run in the current environment. */
|
|
22
|
+
static isAvailable() {
|
|
23
|
+
return typeof fetch === "function"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @returns {import("../capabilities.js").TransportCapabilities} - Supported capabilities. */
|
|
27
|
+
get capabilities() {
|
|
28
|
+
return buildCapabilities({
|
|
29
|
+
responseStreaming: true,
|
|
30
|
+
abort: true
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {import("../snap-req.js").NormalizedRequest} request - Normalized request.
|
|
36
|
+
* @returns {Promise<SnapReqResponse>} - The response.
|
|
37
|
+
*/
|
|
38
|
+
async performRequest(request) {
|
|
39
|
+
if (request.bodyCompression && request.bodyCompression !== "identity") {
|
|
40
|
+
throw new SnapReqUnsupportedFeatureError({feature: "request body compression", transport: "fetch"})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @type {Record<string, any>} */
|
|
44
|
+
const init = {
|
|
45
|
+
method: request.method,
|
|
46
|
+
headers: request.headers.toObject()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (request.signal) init.signal = request.signal
|
|
50
|
+
if (request.credentials) init.credentials = request.credentials
|
|
51
|
+
|
|
52
|
+
const body = request.body
|
|
53
|
+
|
|
54
|
+
if (body.kind === "text") {
|
|
55
|
+
init.body = body.value
|
|
56
|
+
} else if (body.kind === "bytes") {
|
|
57
|
+
init.body = body.value
|
|
58
|
+
} else if (body.kind === "stream") {
|
|
59
|
+
throw new SnapReqUnsupportedFeatureError({feature: "streamed request bodies", transport: "fetch"})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {Response} */
|
|
63
|
+
let fetchResponse
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
fetchResponse = await fetch(request.url, init)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof Error && error.name === "AbortError") throw new SnapReqAbortError()
|
|
69
|
+
|
|
70
|
+
throw error
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new SnapReqResponse({
|
|
74
|
+
url: request.url,
|
|
75
|
+
method: request.method,
|
|
76
|
+
status: fetchResponse.status,
|
|
77
|
+
statusText: fetchResponse.statusText,
|
|
78
|
+
headers: this._responseHeaders(fetchResponse),
|
|
79
|
+
stream: this._responseStream(fetchResponse)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {Response} response - The fetch response.
|
|
85
|
+
* @returns {SnapReqHeaders} - The response headers.
|
|
86
|
+
*/
|
|
87
|
+
_responseHeaders(response) {
|
|
88
|
+
const headers = new SnapReqHeaders()
|
|
89
|
+
|
|
90
|
+
response.headers.forEach((value, name) => headers.set(name, value))
|
|
91
|
+
|
|
92
|
+
return headers
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Builds an async iterable of byte chunks over a fetch response. Uses the
|
|
97
|
+
* `ReadableStream` body for true streaming when present and otherwise buffers
|
|
98
|
+
* the whole body once so the stream interface stays identical everywhere.
|
|
99
|
+
* @param {Response} response - The fetch response.
|
|
100
|
+
* @returns {AsyncIterable<Uint8Array>} - The response body stream.
|
|
101
|
+
*/
|
|
102
|
+
_responseStream(response) {
|
|
103
|
+
const body = response.body
|
|
104
|
+
|
|
105
|
+
if (body && typeof body.getReader === "function") {
|
|
106
|
+
return (async function* () {
|
|
107
|
+
const reader = body.getReader()
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
while (true) {
|
|
111
|
+
const {done, value} = await reader.read()
|
|
112
|
+
|
|
113
|
+
if (done) break
|
|
114
|
+
if (value) yield value instanceof Uint8Array ? value : new Uint8Array(value)
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
reader.releaseLock?.()
|
|
118
|
+
}
|
|
119
|
+
})()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (async function* () {
|
|
123
|
+
const buffer = await response.arrayBuffer()
|
|
124
|
+
|
|
125
|
+
yield new Uint8Array(buffer)
|
|
126
|
+
})()
|
|
127
|
+
}
|
|
128
|
+
}
|