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
package/src/response.js DELETED
@@ -1,197 +0,0 @@
1
- // @ts-check
2
-
3
- import SnapReqHeaders from "./headers.js"
4
-
5
- /**
6
- * Concatenates a list of byte chunks into a single `Uint8Array`.
7
- * @param {Uint8Array[]} chunks - Byte chunks in order.
8
- * @returns {Uint8Array} - The concatenated bytes.
9
- */
10
- function concatChunks(chunks) {
11
- let total = 0
12
-
13
- for (const chunk of chunks) total += chunk.byteLength
14
-
15
- const result = new Uint8Array(total)
16
- let offset = 0
17
-
18
- for (const chunk of chunks) {
19
- result.set(chunk, offset)
20
- offset += chunk.byteLength
21
- }
22
-
23
- return result
24
- }
25
-
26
- /**
27
- * A platform-agnostic response. Transports build it with either a fully-read
28
- * body (`bytes`) or a `stream` (an async iterable of `Uint8Array`) that the
29
- * read helpers buffer on first use. The body can be read exactly once as a
30
- * stream; the buffering helpers may be called repeatedly because they cache.
31
- */
32
- export default class SnapReqResponse {
33
- /**
34
- * @param {object} options - Response data.
35
- * @param {string} options.url - Fully resolved request URL.
36
- * @param {string} options.method - HTTP method used for the request.
37
- * @param {number} options.status - HTTP status code.
38
- * @param {string} [options.statusText] - HTTP status text.
39
- * @param {SnapReqHeaders} [options.headers] - Response headers.
40
- * @param {Uint8Array} [options.bytes] - Fully-read body, when the transport already buffered it.
41
- * @param {AsyncIterable<Uint8Array>} [options.stream] - Streamed body, when the transport supports streaming.
42
- * @param {import("node:stream").Readable} [options.nodeStream] - Raw Node stream, when available, for advanced consumers.
43
- * @param {() => void} [options.onBodyDone] - Callback fired when body reading finishes or fails.
44
- * @param {(error: unknown) => unknown} [options.mapBodyError] - Maps body read errors before rethrowing.
45
- */
46
- constructor({url, method, status, statusText = "", headers, bytes, stream, nodeStream, onBodyDone, mapBodyError}) {
47
- this.url = url
48
- this.method = method
49
- this.status = status
50
- this.statusText = statusText
51
- this.headers = headers || new SnapReqHeaders()
52
- /** @type {Uint8Array | null} */
53
- this._bytes = bytes ?? null
54
- /** @type {AsyncIterable<Uint8Array> | null} */
55
- this._stream = stream ?? null
56
- /** @type {import("node:stream").Readable | undefined} */
57
- this.nodeStream = nodeStream
58
- this._streamConsumed = false
59
- this._bodyDone = false
60
- this._onBodyDone = onBodyDone
61
- this._mapBodyError = mapBodyError
62
-
63
- if (bytes !== undefined) this._finishBody()
64
- }
65
-
66
- /** @returns {boolean} - Whether the status is in the 2xx range. */
67
- get ok() {
68
- return this.status >= 200 && this.status < 300
69
- }
70
-
71
- /**
72
- * Returns the response body as an async iterable of byte chunks. Can only be
73
- * called once and only when the transport provided a stream.
74
- * @returns {AsyncIterable<Uint8Array>} - The streamed body.
75
- */
76
- stream() {
77
- if (!this._stream) {
78
- throw new Error("This response has no readable stream (the body was already buffered by the transport).")
79
- }
80
-
81
- if (this._streamConsumed) {
82
- throw new Error("This response stream has already been consumed.")
83
- }
84
-
85
- this._streamConsumed = true
86
-
87
- return this._wrappedStream(this._stream)
88
- }
89
-
90
- /** @returns {boolean} - Whether the body is available as a stream that has not been read yet. */
91
- get streamable() {
92
- return Boolean(this._stream) && !this._streamConsumed
93
- }
94
-
95
- /**
96
- * Reads the whole body into a `Uint8Array`, buffering the stream if needed.
97
- * @returns {Promise<Uint8Array>} - The full response body.
98
- */
99
- async bytes() {
100
- if (this._bytes) return this._bytes
101
-
102
- if (!this._stream) {
103
- this._bytes = new Uint8Array(0)
104
- this._finishBody()
105
-
106
- return this._bytes
107
- }
108
-
109
- /** @type {Uint8Array[]} */
110
- const chunks = []
111
-
112
- for await (const chunk of this.stream()) {
113
- chunks.push(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))
114
- }
115
-
116
- this._bytes = concatChunks(chunks)
117
-
118
- return this._bytes
119
- }
120
-
121
- /**
122
- * Reads the whole body as a Node `Buffer`. Node-only convenience; throws when
123
- * the `Buffer` global is unavailable.
124
- * @returns {Promise<Buffer>} - The full response body as a Buffer.
125
- */
126
- async buffer() {
127
- if (typeof Buffer === "undefined") {
128
- throw new Error("Buffer is not available on this platform; use bytes() instead.")
129
- }
130
-
131
- const bytes = await this.bytes()
132
-
133
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
134
- }
135
-
136
- /**
137
- * Reads the whole body and decodes it as a UTF-8 string.
138
- * @returns {Promise<string>} - The decoded response body.
139
- */
140
- async text() {
141
- const bytes = await this.bytes()
142
-
143
- return new TextDecoder("utf-8").decode(bytes)
144
- }
145
-
146
- /**
147
- * Reads the whole body and parses it as JSON. Returns `null` for an empty
148
- * body.
149
- * @returns {Promise<any>} - The parsed JSON body.
150
- */
151
- async json() {
152
- const text = await this.text()
153
-
154
- if (!text) return null
155
-
156
- return JSON.parse(text)
157
- }
158
-
159
- /**
160
- * @param {AsyncIterable<Uint8Array>} source - Source response stream.
161
- * @returns {AsyncIterable<Uint8Array>} - Stream with cleanup/error mapping.
162
- */
163
- _wrappedStream(source) {
164
- const response = this
165
-
166
- return (async function* () {
167
- try {
168
- for await (const chunk of source) {
169
- yield chunk
170
- }
171
- } catch (error) {
172
- throw response._mappedBodyError(error)
173
- } finally {
174
- response._finishBody()
175
- }
176
- })()
177
- }
178
-
179
- /**
180
- * @param {unknown} error - Body read error.
181
- * @returns {unknown} - Error to rethrow.
182
- */
183
- _mappedBodyError(error) {
184
- if (this._mapBodyError) return this._mapBodyError(error)
185
-
186
- return error
187
- }
188
-
189
- /** @returns {void} */
190
- _finishBody() {
191
- if (this._bodyDone) return
192
-
193
- this._bodyDone = true
194
-
195
- if (this._onBodyDone) this._onBodyDone()
196
- }
197
- }
package/src/retry.js DELETED
@@ -1,107 +0,0 @@
1
- // @ts-check
2
-
3
- import {SnapReqHttpError, SnapReqTimeoutError} 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 instanceof SnapReqTimeoutError) return true
32
-
33
- if (!error || typeof error !== "object") return false
34
-
35
- if ("code" in error && typeof error.code === "string" && RETRYABLE_ERROR_CODES.has(error.code)) {
36
- return true
37
- }
38
-
39
- return error instanceof Error && error.message === "socket hang up"
40
- }
41
-
42
- /**
43
- * Normalizes the `retry` option into a complete set of retry settings, or
44
- * `null` when retries are disabled.
45
- * @param {boolean | RetryOptions | undefined} retry - Retry configuration.
46
- * @returns {NormalizedRetryOptions | null} - Normalized retry settings.
47
- */
48
- export function normalizeRetryOptions(retry) {
49
- if (!retry) return null
50
-
51
- const options = retry === true ? {} : retry
52
- const retryableStatuses = options.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES
53
- const shouldRetry = options.shouldRetry ?? ((/** @type {unknown} */ error) => defaultRetryableError(error))
54
-
55
- return {
56
- tries: options.tries ?? 3,
57
- waitMs: options.waitMs ?? 500,
58
- retryableStatuses,
59
- shouldRetry
60
- }
61
- }
62
-
63
- /**
64
- * @param {number} waitMs - Delay in milliseconds.
65
- * @returns {Promise<void>} - Resolves after the delay.
66
- */
67
- function wait(waitMs) {
68
- return new Promise((resolve) => setTimeout(resolve, waitMs))
69
- }
70
-
71
- /**
72
- * Runs a request attempt, retrying transient network errors and retryable HTTP
73
- * statuses. Retries are only used for buffered requests — the caller must not
74
- * apply this to streamed responses.
75
- * @param {() => Promise<import("./response.js").default>} attempt - Performs one request attempt.
76
- * @param {NormalizedRetryOptions} retry - Normalized retry settings.
77
- * @returns {Promise<import("./response.js").default>} - The successful (or final) response.
78
- */
79
- export async function runWithRetry(attempt, retry) {
80
- for (let tryNumber = 1; tryNumber <= retry.tries; tryNumber += 1) {
81
- /** @type {import("./response.js").default} */
82
- let response
83
-
84
- try {
85
- response = await attempt()
86
- } catch (error) {
87
- if (tryNumber >= retry.tries || !retry.shouldRetry(error, tryNumber)) throw error
88
-
89
- await wait(retry.waitMs)
90
- continue
91
- }
92
-
93
- if (tryNumber < retry.tries && retry.retryableStatuses.includes(response.status)) {
94
- await wait(retry.waitMs)
95
- continue
96
- }
97
-
98
- return response
99
- }
100
-
101
- throw new SnapReqHttpError({
102
- message: "Retry loop exited without a response.",
103
- method: "",
104
- url: "",
105
- status: 0
106
- })
107
- }
package/src/snap-req.js DELETED
@@ -1,390 +0,0 @@
1
- // @ts-check
2
-
3
- import {SnapReqHttpError, SnapReqTimeoutError, 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 {number} [timeoutMs] - Request timeout in milliseconds.
22
- * @property {string} [credentials] - Fetch credentials mode ("omit" | "same-origin" | "include").
23
- */
24
-
25
- /**
26
- * @typedef {object} RequestOptions
27
- * @property {string} [method] - HTTP method. Defaults to GET.
28
- * @property {string} [path] - Request path (joined with the client `baseUrl`) or absolute URL.
29
- * @property {string} [url] - Alias for `path`.
30
- * @property {Record<string, string | number | boolean | null | undefined>} [query] - Query parameters.
31
- * @property {Record<string, string | number> | SnapReqHeaders} [headers] - Per-request headers.
32
- * @property {any} [body] - Request body: string, object (JSON), Uint8Array/ArrayBuffer, or a stream/async-iterable.
33
- * @property {CompressionEncoding} [bodyCompression] - Compress the request body (Node transport only).
34
- * @property {AbortSignal} [signal] - Abort signal for the request.
35
- * @property {number} [timeoutMs] - Request timeout in milliseconds. Set to `0` to disable a client default.
36
- * @property {string} [credentials] - Fetch credentials mode.
37
- * @property {boolean | import("./retry.js").RetryOptions} [retry] - Retry transient failures.
38
- * @property {boolean} [throwOnError] - Throw `SnapReqHttpError` on non-2xx responses.
39
- */
40
-
41
- /**
42
- * @typedef {object} RequestTimeout
43
- * @property {AbortSignal | undefined} signal - Signal to use for the request.
44
- * @property {() => void} clear - Clears timeout resources.
45
- * @property {(response: import("./response.js").default, request: NormalizedRequest) => import("./response.js").default} response - Attaches timeout handling to a response.
46
- * @property {(error: unknown, request: NormalizedRequest) => unknown} error - Maps a thrown error.
47
- */
48
-
49
- /**
50
- * A cross-platform HTTP client with one API across Node, web, Expo and React
51
- * Native. The right transport is chosen at runtime; features a platform cannot
52
- * provide raise `SnapReqUnsupportedFeatureError` rather than silently changing
53
- * behaviour.
54
- */
55
- export default class SnapReq {
56
- /**
57
- * @param {object} [config] - Client configuration.
58
- * @param {string} [config.baseUrl] - Origin (and optional base path) prepended to relative paths.
59
- * @param {string} [config.socketPath] - Unix domain socket path (Node transport only).
60
- * @param {{ca?: string | Buffer, cert?: string | Buffer, key?: string | Buffer, rejectUnauthorized?: boolean}} [config.tls] - TLS material (Node transport only).
61
- * @param {boolean} [config.keepAlive] - Reuse connections across requests (Node transport only). Defaults to true.
62
- * @param {Record<string, string | number> | (() => Record<string, string | number>)} [config.headers] - Default headers (object or factory).
63
- * @param {boolean | import("./retry.js").RetryOptions} [config.retry] - Default retry policy.
64
- * @param {boolean} [config.throwOnError] - Throw `SnapReqHttpError` on non-2xx responses by default. Defaults to false.
65
- * @param {number} [config.timeoutMs] - Default request timeout in milliseconds. Set per-request `timeoutMs: 0` to disable.
66
- * @param {string} [config.credentials] - Default fetch credentials mode.
67
- * @param {import("./transports/select.js").TransportName | import("./transports/select.js").Transport} [config.transport] - Transport preference or instance. Defaults to "auto".
68
- */
69
- constructor({baseUrl, socketPath, tls, keepAlive = true, headers, retry, throwOnError = false, timeoutMs, credentials, transport = "auto"} = {}) {
70
- this.baseUrl = baseUrl
71
- this.defaultHeaders = headers
72
- this.defaultRetry = retry
73
- this.throwOnError = throwOnError
74
- this.timeoutMs = timeoutMs
75
- this.credentials = credentials
76
- this._transportPreference = transport
77
- this._nodeConfig = {socketPath, tls, keepAlive}
78
- /** @type {Promise<import("./transports/select.js").Transport> | null} */
79
- this._transportPromise = null
80
- /** @type {import("./transports/select.js").Transport | null} */
81
- this._transport = null
82
- }
83
-
84
- /** @returns {Promise<import("./transports/select.js").Transport>} - The resolved transport. */
85
- async _resolveTransport() {
86
- this._transportPromise ||= selectTransport(this._transportPreference, this._nodeConfig)
87
- this._transport = await this._transportPromise
88
-
89
- return this._transport
90
- }
91
-
92
- /** @returns {Promise<import("./capabilities.js").TransportCapabilities>} - The active transport's capabilities. */
93
- async capabilities() {
94
- return (await this._resolveTransport()).capabilities
95
- }
96
-
97
- /** @returns {Promise<string>} - The active transport's name. */
98
- async transportName() {
99
- const transport = await this._resolveTransport()
100
-
101
- return /** @type {any} */ (transport.constructor)?.transportName || "custom"
102
- }
103
-
104
- /**
105
- * @param {RequestOptions} options - Request options.
106
- * @returns {NormalizedRequest} - The normalized request.
107
- */
108
- _normalize(options) {
109
- const headers = new SnapReqHeaders()
110
- const defaults = typeof this.defaultHeaders === "function" ? this.defaultHeaders() : this.defaultHeaders
111
-
112
- if (defaults) for (const [name, value] of new SnapReqHeaders(defaults).entries()) headers.set(name, value)
113
- if (options.headers) for (const [name, value] of new SnapReqHeaders(options.headers).entries()) headers.set(name, value)
114
-
115
- const url = buildUrl(this.baseUrl, options.path ?? options.url ?? "", options.query)
116
- const body = normalizeBody(options.body, headers)
117
-
118
- return {
119
- method: (options.method || "GET").toUpperCase(),
120
- url,
121
- headers,
122
- body,
123
- bodyCompression: options.bodyCompression || "identity",
124
- signal: options.signal,
125
- timeoutMs: options.timeoutMs ?? this.timeoutMs,
126
- credentials: options.credentials ?? this.credentials
127
- }
128
- }
129
-
130
- /**
131
- * @param {RequestOptions} options - Request options.
132
- * @returns {RequestTimeout} - Timeout handling for one request attempt.
133
- */
134
- _requestTimeout(options) {
135
- const timeoutMs = options.timeoutMs ?? this.timeoutMs
136
-
137
- if (!timeoutMs || timeoutMs <= 0) {
138
- return {
139
- signal: options.signal,
140
- clear: () => {},
141
- response: (response) => response,
142
- error: (error) => error
143
- }
144
- }
145
-
146
- const timeoutController = new AbortController()
147
- const composedSignal = this._composeSignal(options.signal, timeoutController.signal)
148
- let timedOut = false
149
- const timer = setTimeout(() => {
150
- timedOut = true
151
- timeoutController.abort()
152
- }, timeoutMs)
153
-
154
- if (typeof timer.unref === "function") timer.unref()
155
-
156
- const clear = () => {
157
- clearTimeout(timer)
158
- composedSignal.clear()
159
- }
160
- const toError = (error, request) => {
161
- if (timedOut) {
162
- return new SnapReqTimeoutError({
163
- method: request.method,
164
- url: request.url,
165
- timeoutMs
166
- })
167
- }
168
-
169
- return error
170
- }
171
-
172
- return {
173
- signal: composedSignal.signal,
174
- clear,
175
- response: (response, request) => {
176
- if (response._bodyDone) {
177
- clear()
178
-
179
- return response
180
- }
181
-
182
- response._onBodyDone = this._chainBodyDone(response._onBodyDone, clear)
183
- response._mapBodyError = this._chainBodyError(response._mapBodyError, (error) => toError(error, request))
184
-
185
- return response
186
- },
187
- error: toError
188
- }
189
- }
190
-
191
- /**
192
- * @param {AbortSignal | undefined} callerSignal - Caller-supplied signal.
193
- * @param {AbortSignal} timeoutSignal - Timeout signal.
194
- * @returns {{signal: AbortSignal, clear: () => void}} - Signal that aborts when either source aborts.
195
- */
196
- _composeSignal(callerSignal, timeoutSignal) {
197
- if (!callerSignal) return {signal: timeoutSignal, clear: () => {}}
198
-
199
- const controller = new AbortController()
200
- const abort = () => controller.abort()
201
-
202
- if (callerSignal.aborted || timeoutSignal.aborted) {
203
- controller.abort()
204
- } else {
205
- callerSignal.addEventListener("abort", abort, {once: true})
206
- timeoutSignal.addEventListener("abort", abort, {once: true})
207
- }
208
-
209
- return {
210
- signal: controller.signal,
211
- clear: () => {
212
- callerSignal.removeEventListener("abort", abort)
213
- timeoutSignal.removeEventListener("abort", abort)
214
- }
215
- }
216
- }
217
-
218
- /**
219
- * @param {(() => void) | undefined} existing - Existing body-done callback.
220
- * @param {() => void} next - Callback to add.
221
- * @returns {() => void} - Combined callback.
222
- */
223
- _chainBodyDone(existing, next) {
224
- return () => {
225
- if (existing) existing()
226
- next()
227
- }
228
- }
229
-
230
- /**
231
- * @param {((error: unknown) => unknown) | undefined} existing - Existing error mapper.
232
- * @param {(error: unknown) => unknown} next - Mapper to add.
233
- * @returns {(error: unknown) => unknown} - Combined mapper.
234
- */
235
- _chainBodyError(existing, next) {
236
- return (error) => next(existing ? existing(error) : error)
237
- }
238
-
239
- /**
240
- * Performs a request and buffers nothing eagerly — read the body via the
241
- * returned response (`json()`, `text()`, `bytes()`). Retries transient
242
- * failures when a retry policy is configured (never for streamed bodies).
243
- * @param {RequestOptions} options - Request options.
244
- * @returns {Promise<import("./response.js").default>} - The response.
245
- */
246
- async request(options) {
247
- const transport = await this._resolveTransport()
248
- const throwOnError = options.throwOnError ?? this.throwOnError
249
- const retry = normalizeRetryOptions(options.retry ?? this.defaultRetry)
250
- const body = normalizeBody(options.body, new SnapReqHeaders(options.headers))
251
- const canRetry = retry && body.kind !== "stream"
252
- const attempt = async () => this._requestWithTimeout(options, (request) => transport.performRequest(request))
253
- const response = canRetry ? await runWithRetry(attempt, /** @type {any} */ (retry)) : await attempt()
254
-
255
- if (throwOnError && !response.ok) throw await this._httpError(response, this._normalize(options))
256
-
257
- return response
258
- }
259
-
260
- /**
261
- * Performs a request and returns the response with its body available as a
262
- * stream (`response.stream()`). Requires a transport that supports response
263
- * streaming; never retries.
264
- * @param {RequestOptions} options - Request options.
265
- * @returns {Promise<import("./response.js").default>} - The streaming response.
266
- */
267
- async requestStream(options) {
268
- const transport = await this._resolveTransport()
269
-
270
- if (!transport.capabilities.responseStreaming) {
271
- throw new SnapReqUnsupportedFeatureError({
272
- feature: "response streaming",
273
- transport: /** @type {any} */ (transport.constructor)?.transportName || "custom"
274
- })
275
- }
276
-
277
- const response = await this._requestWithTimeout(options, (request) => transport.performRequest(request))
278
-
279
- if ((options.throwOnError ?? this.throwOnError) && !response.ok) {
280
- throw await this._httpError(response, this._normalize(options))
281
- }
282
-
283
- return response
284
- }
285
-
286
- /**
287
- * @param {RequestOptions} options - Request options.
288
- * @param {(request: NormalizedRequest) => Promise<import("./response.js").default>} performRequest - Transport request runner.
289
- * @returns {Promise<import("./response.js").default>} - Response with timeout handling attached.
290
- */
291
- async _requestWithTimeout(options, performRequest) {
292
- const timeout = this._requestTimeout(options)
293
- const normalized = this._normalize({...options, signal: timeout.signal})
294
-
295
- try {
296
- const response = await performRequest(normalized)
297
-
298
- return timeout.response(response, normalized)
299
- } catch (error) {
300
- timeout.clear()
301
-
302
- throw timeout.error(error, normalized)
303
- }
304
- }
305
-
306
- /**
307
- * @param {string} path - Request path or absolute URL.
308
- * @param {RequestOptions} [options] - Request options.
309
- * @returns {Promise<import("./response.js").default>} - The response.
310
- */
311
- get(path, options = {}) {
312
- return this.request({...options, method: "GET", path})
313
- }
314
-
315
- /**
316
- * @param {string} path - Request path or absolute URL.
317
- * @param {any} [body] - Request body.
318
- * @param {RequestOptions} [options] - Request options.
319
- * @returns {Promise<import("./response.js").default>} - The response.
320
- */
321
- post(path, body, options = {}) {
322
- return this.request({...options, method: "POST", path, body})
323
- }
324
-
325
- /**
326
- * @param {string} path - Request path or absolute URL.
327
- * @param {any} [body] - Request body.
328
- * @param {RequestOptions} [options] - Request options.
329
- * @returns {Promise<import("./response.js").default>} - The response.
330
- */
331
- put(path, body, options = {}) {
332
- return this.request({...options, method: "PUT", path, body})
333
- }
334
-
335
- /**
336
- * @param {string} path - Request path or absolute URL.
337
- * @param {any} [body] - Request body.
338
- * @param {RequestOptions} [options] - Request options.
339
- * @returns {Promise<import("./response.js").default>} - The response.
340
- */
341
- patch(path, body, options = {}) {
342
- return this.request({...options, method: "PATCH", path, body})
343
- }
344
-
345
- /**
346
- * @param {string} path - Request path or absolute URL.
347
- * @param {RequestOptions} [options] - Request options.
348
- * @returns {Promise<import("./response.js").default>} - The response.
349
- */
350
- delete(path, options = {}) {
351
- return this.request({...options, method: "DELETE", path})
352
- }
353
-
354
- /**
355
- * @param {import("./response.js").default} response - The failed response.
356
- * @param {NormalizedRequest} request - The request that produced it.
357
- * @returns {Promise<SnapReqHttpError>} - An error describing the failure.
358
- */
359
- async _httpError(response, request) {
360
- let responseText = ""
361
-
362
- try {
363
- responseText = await response.text()
364
- } catch (error) {
365
- if (error instanceof SnapReqTimeoutError) throw error
366
-
367
- // Body unavailable (already streamed or read error) — fall back to status text.
368
- }
369
-
370
- const detail = responseText || response.statusText || ""
371
-
372
- return new SnapReqHttpError({
373
- message: `HTTP ${response.status} ${request.method} ${request.url}${detail ? `: ${detail}` : ""}`,
374
- method: request.method,
375
- url: request.url,
376
- status: response.status,
377
- statusText: response.statusText,
378
- responseText,
379
- response
380
- })
381
- }
382
-
383
- /**
384
- * Releases transport resources (for example Node keep-alive sockets).
385
- * @returns {void}
386
- */
387
- close() {
388
- this._transport?.close?.()
389
- }
390
- }