snapreq 0.0.4 → 0.0.5

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.
Files changed (68) hide show
  1. package/{types → build}/capabilities.d.ts +1 -0
  2. package/build/capabilities.d.ts.map +1 -0
  3. package/{src → build}/capabilities.js +11 -12
  4. package/{types → build}/errors.d.ts +1 -0
  5. package/build/errors.d.ts.map +1 -0
  6. package/build/errors.js +82 -0
  7. package/{types → build}/headers.d.ts +1 -0
  8. package/build/headers.d.ts.map +1 -0
  9. package/build/headers.js +89 -0
  10. package/{types → build}/request.d.ts +1 -0
  11. package/build/request.d.ts.map +1 -0
  12. package/build/request.js +89 -0
  13. package/{types → build}/response.d.ts +1 -0
  14. package/build/response.d.ts.map +1 -0
  15. package/build/response.js +171 -0
  16. package/{types → build}/retry.d.ts +1 -0
  17. package/build/retry.d.ts.map +1 -0
  18. package/build/retry.js +95 -0
  19. package/{types → build}/snap-req.d.ts +1 -0
  20. package/build/snap-req.d.ts.map +1 -0
  21. package/build/snap-req.js +349 -0
  22. package/{types → build}/transports/fetch-transport.d.ts +1 -0
  23. package/build/transports/fetch-transport.d.ts.map +1 -0
  24. package/build/transports/fetch-transport.js +116 -0
  25. package/build/transports/fetch.d.ts +2 -0
  26. package/build/transports/fetch.d.ts.map +1 -0
  27. package/build/transports/fetch.js +3 -0
  28. package/{types → build}/transports/node-transport.d.ts +1 -0
  29. package/build/transports/node-transport.d.ts.map +1 -0
  30. package/build/transports/node-transport.js +287 -0
  31. package/build/transports/proxy-bounce-transport.d.ts +27 -0
  32. package/build/transports/proxy-bounce-transport.d.ts.map +1 -0
  33. package/build/transports/proxy-bounce-transport.js +116 -0
  34. package/{types → build}/transports/select.d.ts +1 -0
  35. package/build/transports/select.d.ts.map +1 -0
  36. package/build/transports/select.js +69 -0
  37. package/{types → build}/transports/xhr-transport.d.ts +1 -0
  38. package/build/transports/xhr-transport.d.ts.map +1 -0
  39. package/build/transports/xhr-transport.js +93 -0
  40. package/build/transports/xhr.d.ts +2 -0
  41. package/build/transports/xhr.d.ts.map +1 -0
  42. package/build/transports/xhr.js +3 -0
  43. package/{types → build}/websocket/websocket-channel.d.ts +1 -0
  44. package/build/websocket/websocket-channel.d.ts.map +1 -0
  45. package/build/websocket/websocket-channel.js +162 -0
  46. package/{types → build}/websocket/websocket-client.d.ts +1 -0
  47. package/build/websocket/websocket-client.d.ts.map +1 -0
  48. package/build/websocket/websocket-client.js +920 -0
  49. package/{types → build}/websocket/websocket-connection.d.ts +1 -0
  50. package/build/websocket/websocket-connection.d.ts.map +1 -0
  51. package/build/websocket/websocket-connection.js +148 -0
  52. package/build/websocket.d.ts +3 -0
  53. package/build/websocket.d.ts.map +1 -0
  54. package/build/websocket.js +4 -0
  55. package/package.json +12 -48
  56. package/src/errors.js +0 -86
  57. package/src/headers.js +0 -92
  58. package/src/request.js +0 -104
  59. package/src/response.js +0 -197
  60. package/src/retry.js +0 -107
  61. package/src/snap-req.js +0 -390
  62. package/src/transports/fetch-transport.js +0 -128
  63. package/src/transports/node-transport.js +0 -323
  64. package/src/transports/select.js +0 -73
  65. package/src/transports/xhr-transport.js +0 -112
  66. package/src/websocket/websocket-channel.js +0 -176
  67. package/src/websocket/websocket-client.js +0 -1034
  68. package/src/websocket/websocket-connection.js +0 -154
@@ -1,128 +0,0 @@
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
- }
@@ -1,323 +0,0 @@
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
- * Full-featured transport backed by Node's `http`/`https` modules. Supports
10
- * Unix sockets, client TLS, keep-alive, request-body compression, response
11
- * decompression and streaming. The `node:*` modules are loaded with a dynamic
12
- * `import()` so this file never has to be bundled by web/Expo bundlers — the
13
- * transport selector only imports it when running on Node.
14
- */
15
- export default class NodeTransport {
16
- /** @returns {string} - Transport name. */
17
- static get transportName() {
18
- return "node"
19
- }
20
-
21
- /** @returns {boolean} - Whether this transport can run in the current environment. */
22
- static isAvailable() {
23
- return typeof process !== "undefined" && Boolean(process.versions?.node)
24
- }
25
-
26
- /**
27
- * @param {object} [config] - Transport configuration.
28
- * @param {string} [config.socketPath] - Unix domain socket path.
29
- * @param {{ca?: string | Buffer, cert?: string | Buffer, key?: string | Buffer, rejectUnauthorized?: boolean}} [config.tls] - TLS material for HTTPS connections.
30
- * @param {boolean} [config.keepAlive] - Reuse connections across requests. Defaults to true.
31
- */
32
- constructor({socketPath, tls, keepAlive = true} = {}) {
33
- this.socketPath = socketPath
34
- this.tls = tls
35
- this.keepAlive = keepAlive
36
- /** @type {{http: any, https: any, zlib: any, stream: any} | null} */
37
- this._modules = null
38
- /** @type {any} */
39
- this._httpAgent = null
40
- /** @type {any} */
41
- this._httpsAgent = null
42
- }
43
-
44
- /** @returns {import("../capabilities.js").TransportCapabilities} - Supported capabilities. */
45
- get capabilities() {
46
- return buildCapabilities({
47
- unixSocket: true,
48
- tlsClientCert: true,
49
- requestCompression: true,
50
- responseStreaming: true,
51
- requestStreaming: true,
52
- keepAlive: true,
53
- abort: true
54
- })
55
- }
56
-
57
- /** @returns {Promise<{http: any, https: any, zlib: any, stream: any}>} - Lazily-loaded Node modules. */
58
- async _load() {
59
- if (!this._modules) {
60
- const [http, https, zlib, stream] = await Promise.all([
61
- import("node:http"),
62
- import("node:https"),
63
- import("node:zlib"),
64
- import("node:stream")
65
- ])
66
-
67
- this._modules = {http, https, zlib, stream}
68
- }
69
-
70
- return this._modules
71
- }
72
-
73
- /**
74
- * @param {boolean} useTls - Whether the request uses TLS.
75
- * @returns {any} - The keep-alive agent for the protocol.
76
- */
77
- _agent(useTls) {
78
- const {http, https} = /** @type {{http: any, https: any}} */ (this._modules)
79
-
80
- if (useTls) {
81
- this._httpsAgent ||= new https.Agent({
82
- keepAlive: this.keepAlive,
83
- ...(this.tls?.ca !== undefined ? {ca: this.tls.ca} : {}),
84
- ...(this.tls?.cert !== undefined ? {cert: this.tls.cert} : {}),
85
- ...(this.tls?.key !== undefined ? {key: this.tls.key} : {}),
86
- ...(this.tls?.rejectUnauthorized !== undefined ? {rejectUnauthorized: this.tls.rejectUnauthorized} : {})
87
- })
88
-
89
- return this._httpsAgent
90
- }
91
-
92
- this._httpAgent ||= new http.Agent({keepAlive: this.keepAlive})
93
-
94
- return this._httpAgent
95
- }
96
-
97
- /**
98
- * Performs a single request and resolves once the response headers arrive,
99
- * exposing the (decoded) body as a stream so callers can buffer or stream it.
100
- * @param {import("../snap-req.js").NormalizedRequest} request - Normalized request.
101
- * @returns {Promise<SnapReqResponse>} - The response.
102
- */
103
- async performRequest(request) {
104
- const modules = await this._load()
105
- const {zlib, stream} = modules
106
- const parsedUrl = new URL(request.url)
107
- const useTls = Boolean(this.tls) || parsedUrl.protocol === "https:"
108
- const httpModule = useTls ? modules.https : modules.http
109
- const headers = new SnapReqHeaders(request.headers)
110
- const requestBody = this._prepareRequestBody(request, headers, {zlib, stream})
111
-
112
- /** @type {Record<string, any>} */
113
- const requestOptions = {
114
- method: request.method,
115
- path: `${parsedUrl.pathname}${parsedUrl.search}`,
116
- headers: headers.toObject(),
117
- agent: this._agent(useTls)
118
- }
119
-
120
- if (this.socketPath) {
121
- requestOptions.socketPath = this.socketPath
122
- } else {
123
- requestOptions.hostname = parsedUrl.hostname
124
- requestOptions.port = parsedUrl.port || (useTls ? 443 : 80)
125
- }
126
-
127
- return await new Promise((resolve, reject) => {
128
- if (request.signal?.aborted) {
129
- reject(new SnapReqAbortError())
130
- return
131
- }
132
-
133
- let settled = false
134
- const abort = () => req.destroy(new SnapReqAbortError())
135
- const removeAbortListener = () => request.signal?.removeEventListener("abort", abort)
136
-
137
- const req = httpModule.request(requestOptions, (res) => {
138
- /** @type {import("node:stream").Readable} */
139
- let responseStream
140
-
141
- try {
142
- responseStream = this._decodeResponseStream(res, zlib)
143
- } catch (error) {
144
- if (!settled) {
145
- settled = true
146
- removeAbortListener()
147
- reject(error)
148
- }
149
-
150
- return
151
- }
152
-
153
- // Re-point the abort listener at the response stream so aborting after
154
- // headers arrive tears down the body stream rather than the request.
155
- removeAbortListener()
156
- const abortStream = () => responseStream.destroy(new SnapReqAbortError())
157
-
158
- request.signal?.addEventListener("abort", abortStream, {once: true})
159
- responseStream.on("close", () => request.signal?.removeEventListener("abort", abortStream))
160
-
161
- settled = true
162
- resolve(new SnapReqResponse({
163
- url: request.url,
164
- method: request.method,
165
- status: res.statusCode,
166
- statusText: res.statusMessage || "",
167
- headers: this._responseHeaders(res),
168
- stream: responseStream,
169
- nodeStream: responseStream
170
- }))
171
- })
172
-
173
- request.signal?.addEventListener("abort", abort, {once: true})
174
-
175
- req.on("error", (/** @type {unknown} */ error) => {
176
- removeAbortListener()
177
-
178
- if (!settled) {
179
- settled = true
180
- reject(error)
181
- }
182
- })
183
-
184
- if (requestBody.stream) {
185
- requestBody.stream.pipe(req)
186
- } else {
187
- if (requestBody.buffer) req.write(requestBody.buffer)
188
-
189
- req.end()
190
- }
191
- })
192
- }
193
-
194
- /**
195
- * @param {import("../snap-req.js").NormalizedRequest} request - Normalized request.
196
- * @param {SnapReqHeaders} headers - Headers, mutated with Content-Length / Content-Encoding.
197
- * @param {{zlib: any, stream: any}} modules - Node modules.
198
- * @returns {{buffer: Buffer | null, stream: import("node:stream").Readable | null}} - Prepared body.
199
- */
200
- _prepareRequestBody(request, headers, {zlib, stream}) {
201
- const compression = request.bodyCompression || "identity"
202
- const body = request.body
203
-
204
- if (body.kind === "none") return {buffer: null, stream: null}
205
-
206
- /** @type {Buffer | null} */
207
- let buffer = null
208
- /** @type {import("node:stream").Readable | null} */
209
- let bodyStream = null
210
-
211
- if (body.kind === "stream") {
212
- const value = /** @type {any} */ (body.value)
213
-
214
- bodyStream = typeof value.pipe === "function" ? value : stream.Readable.from(value)
215
- } else if (body.kind === "bytes") {
216
- buffer = Buffer.from(/** @type {Uint8Array} */ (body.value))
217
- } else {
218
- buffer = Buffer.from(/** @type {string} */ (body.value))
219
- }
220
-
221
- if (compression === "identity") {
222
- if (buffer) headers.set("Content-Length", String(buffer.length))
223
-
224
- return {buffer, stream: bodyStream}
225
- }
226
-
227
- if (headers.has("content-encoding")) {
228
- throw new SnapReqUnsupportedFeatureError({
229
- feature: "bodyCompression",
230
- transport: "node",
231
- detail: "cannot combine bodyCompression with an explicit Content-Encoding header"
232
- })
233
- }
234
-
235
- headers.set("Content-Encoding", compression)
236
-
237
- const compressor = this._requestCompressor(compression, zlib)
238
- const source = bodyStream || stream.Readable.from(/** @type {Buffer} */ (buffer))
239
-
240
- source.on("error", (/** @type {Error} */ error) => compressor.destroy(error))
241
- source.pipe(compressor)
242
-
243
- return {buffer: null, stream: compressor}
244
- }
245
-
246
- /**
247
- * @param {string} encoding - Compression encoding.
248
- * @param {any} zlib - The zlib module.
249
- * @returns {import("node:stream").Transform} - A compressor transform.
250
- */
251
- _requestCompressor(encoding, zlib) {
252
- if (encoding === "gzip") return zlib.createGzip()
253
- if (encoding === "deflate") return zlib.createDeflate()
254
- if (encoding === "br") return zlib.createBrotliCompress()
255
- if (encoding === "zstd" && typeof zlib.createZstdCompress === "function") return zlib.createZstdCompress()
256
-
257
- throw new SnapReqUnsupportedFeatureError({feature: `bodyCompression "${encoding}"`, transport: "node"})
258
- }
259
-
260
- /**
261
- * @param {import("node:http").IncomingMessage} response - The raw response.
262
- * @param {any} zlib - The zlib module.
263
- * @returns {import("node:stream").Readable} - The decoded response body stream.
264
- */
265
- _decodeResponseStream(response, zlib) {
266
- const header = response.headers["content-encoding"]
267
- const headerValue = Array.isArray(header) ? header.join(",") : header
268
- const encodings = (headerValue || "")
269
- .split(",")
270
- .map((encoding) => encoding.trim().toLowerCase())
271
- .filter((encoding) => encoding && encoding !== "identity")
272
-
273
- /** @type {import("node:stream").Readable} */
274
- let decoded = response
275
-
276
- for (let index = encodings.length - 1; index >= 0; index -= 1) {
277
- decoded = decoded.pipe(this._responseDecoder(encodings[index], zlib))
278
- }
279
-
280
- return decoded
281
- }
282
-
283
- /**
284
- * @param {string} encoding - Content encoding.
285
- * @param {any} zlib - The zlib module.
286
- * @returns {import("node:stream").Transform} - A decompressor transform.
287
- */
288
- _responseDecoder(encoding, zlib) {
289
- if (encoding === "gzip" || encoding === "x-gzip") return zlib.createGunzip()
290
- if (encoding === "deflate") return zlib.createInflate()
291
- if (encoding === "br") return zlib.createBrotliDecompress()
292
- if (encoding === "zstd" && typeof zlib.createZstdDecompress === "function") return zlib.createZstdDecompress()
293
-
294
- throw new SnapReqUnsupportedFeatureError({feature: `response content-encoding "${encoding}"`, transport: "node"})
295
- }
296
-
297
- /**
298
- * @param {import("node:http").IncomingMessage} response - The raw response.
299
- * @returns {SnapReqHeaders} - The response headers.
300
- */
301
- _responseHeaders(response) {
302
- const headers = new SnapReqHeaders()
303
-
304
- for (const [name, value] of Object.entries(response.headers)) {
305
- if (value === undefined) continue
306
-
307
- headers.set(name, Array.isArray(value) ? value.join(", ") : value)
308
- }
309
-
310
- return headers
311
- }
312
-
313
- /**
314
- * Destroys the keep-alive agents, closing all persistent connections.
315
- * @returns {void}
316
- */
317
- close() {
318
- this._httpAgent?.destroy()
319
- this._httpsAgent?.destroy()
320
- this._httpAgent = null
321
- this._httpsAgent = null
322
- }
323
- }
@@ -1,73 +0,0 @@
1
- // @ts-check
2
-
3
- import {SnapReqError} from "../errors.js"
4
- import FetchTransport from "./fetch-transport.js"
5
- import XhrTransport from "./xhr-transport.js"
6
-
7
- /**
8
- * @typedef {"auto" | "node" | "fetch" | "xhr"} TransportName
9
- */
10
-
11
- /**
12
- * @typedef {object} Transport
13
- * @property {import("../capabilities.js").TransportCapabilities} capabilities - Supported capabilities.
14
- * @property {(request: import("../snap-req.js").NormalizedRequest) => Promise<import("../response.js").default>} performRequest - Perform a request.
15
- * @property {() => void} [close] - Optional resource cleanup.
16
- */
17
-
18
- /**
19
- * Detects the JavaScript runtime so `auto` can pick the right transport.
20
- * @returns {"node" | "react-native" | "browser" | "unknown"} - Detected runtime.
21
- */
22
- export function detectRuntime() {
23
- const navigatorRef = /** @type {any} */ (globalThis).navigator
24
-
25
- if (navigatorRef && navigatorRef.product === "ReactNative") return "react-native"
26
- if (typeof process !== "undefined" && Boolean(process.versions?.node)) return "node"
27
- if (typeof window !== "undefined" && typeof document !== "undefined") return "browser"
28
-
29
- return "unknown"
30
- }
31
-
32
- /**
33
- * @param {object} config - Node transport configuration.
34
- * @returns {Promise<Transport>} - A Node transport instance.
35
- */
36
- async function createNodeTransport(config) {
37
- const {default: NodeTransport} = await import("./node-transport.js")
38
-
39
- return new NodeTransport(config)
40
- }
41
-
42
- /**
43
- * Resolves a transport for the requested preference. Returns the preference
44
- * untouched when it is already a transport instance. The Node transport is
45
- * imported dynamically so web/Expo bundlers never pull in `node:*` modules.
46
- * @param {TransportName | Transport | undefined} preference - Requested transport.
47
- * @param {object} nodeConfig - Configuration forwarded to the Node transport.
48
- * @returns {Promise<Transport>} - The resolved transport.
49
- */
50
- export async function selectTransport(preference, nodeConfig) {
51
- if (preference && typeof preference === "object" && typeof (/** @type {any} */ (preference).performRequest) === "function") {
52
- return /** @type {Transport} */ (preference)
53
- }
54
-
55
- const choice = /** @type {TransportName} */ (preference || "auto")
56
-
57
- if (choice === "node") return await createNodeTransport(nodeConfig)
58
- if (choice === "fetch") return new FetchTransport()
59
- if (choice === "xhr") return new XhrTransport()
60
-
61
- if (choice !== "auto") {
62
- throw new SnapReqError(`Unknown transport "${choice}". Use "auto", "node", "fetch", "xhr" or a transport instance.`)
63
- }
64
-
65
- const runtime = detectRuntime()
66
-
67
- if (runtime === "node") return await createNodeTransport(nodeConfig)
68
- if (FetchTransport.isAvailable()) return new FetchTransport()
69
- if (XhrTransport.isAvailable()) return new XhrTransport()
70
- if (typeof process !== "undefined" && Boolean(process.versions?.node)) return await createNodeTransport(nodeConfig)
71
-
72
- throw new SnapReqError("No suitable transport found: this platform has no fetch, XMLHttpRequest or Node http support.")
73
- }
@@ -1,112 +0,0 @@
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 `XMLHttpRequest`. A fallback for web environments that
10
- * lack `fetch`. Buffers the whole response (no incremental streaming) and, like
11
- * `fetch`, cannot do Unix sockets, client TLS or request-body compression.
12
- */
13
- export default class XhrTransport {
14
- /** @returns {string} - Transport name. */
15
- static get transportName() {
16
- return "xhr"
17
- }
18
-
19
- /** @returns {boolean} - Whether this transport can run in the current environment. */
20
- static isAvailable() {
21
- return typeof XMLHttpRequest === "function"
22
- }
23
-
24
- /** @returns {import("../capabilities.js").TransportCapabilities} - Supported capabilities. */
25
- get capabilities() {
26
- return buildCapabilities({abort: true})
27
- }
28
-
29
- /**
30
- * @param {import("../snap-req.js").NormalizedRequest} request - Normalized request.
31
- * @returns {Promise<SnapReqResponse>} - The response.
32
- */
33
- performRequest(request) {
34
- if (request.bodyCompression && request.bodyCompression !== "identity") {
35
- throw new SnapReqUnsupportedFeatureError({feature: "request body compression", transport: "xhr"})
36
- }
37
-
38
- if (request.body.kind === "stream") {
39
- throw new SnapReqUnsupportedFeatureError({feature: "streamed request bodies", transport: "xhr"})
40
- }
41
-
42
- return new Promise((resolve, reject) => {
43
- if (request.signal?.aborted) {
44
- reject(new SnapReqAbortError())
45
- return
46
- }
47
-
48
- const xhr = new XMLHttpRequest()
49
-
50
- xhr.open(request.method, request.url, true)
51
- xhr.responseType = "arraybuffer"
52
-
53
- if (request.credentials === "include") xhr.withCredentials = true
54
-
55
- for (const [name, value] of request.headers.entries()) xhr.setRequestHeader(name, value)
56
-
57
- const abort = () => xhr.abort()
58
-
59
- request.signal?.addEventListener("abort", abort, {once: true})
60
-
61
- const cleanup = () => request.signal?.removeEventListener("abort", abort)
62
-
63
- xhr.onload = () => {
64
- cleanup()
65
- resolve(new SnapReqResponse({
66
- url: request.url,
67
- method: request.method,
68
- status: xhr.status,
69
- statusText: xhr.statusText,
70
- headers: this._parseHeaders(xhr.getAllResponseHeaders()),
71
- bytes: new Uint8Array(/** @type {ArrayBuffer} */ (xhr.response) || new ArrayBuffer(0))
72
- }))
73
- }
74
-
75
- xhr.onerror = () => {
76
- cleanup()
77
- reject(new Error(`XMLHttpRequest failed for ${request.method} ${request.url}`))
78
- }
79
-
80
- xhr.onabort = () => {
81
- cleanup()
82
- reject(new SnapReqAbortError())
83
- }
84
-
85
- const body = request.body
86
-
87
- if (body.kind === "none") {
88
- xhr.send()
89
- } else {
90
- xhr.send(/** @type {any} */ (body.value))
91
- }
92
- })
93
- }
94
-
95
- /**
96
- * @param {string} rawHeaders - Raw header block from `getAllResponseHeaders`.
97
- * @returns {SnapReqHeaders} - The parsed response headers.
98
- */
99
- _parseHeaders(rawHeaders) {
100
- const headers = new SnapReqHeaders()
101
-
102
- for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
103
- const separatorIndex = line.indexOf(":")
104
-
105
- if (separatorIndex === -1) continue
106
-
107
- headers.set(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim())
108
- }
109
-
110
- return headers
111
- }
112
- }