snapreq 0.0.3 → 0.0.4

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 CHANGED
@@ -63,6 +63,7 @@ new SnapReq({
63
63
  headers, // default headers — object or a factory `() => ({...})` for dynamic auth
64
64
  retry, // default retry policy (see below)
65
65
  throwOnError, // throw SnapReqHttpError on non-2xx (default false)
66
+ timeoutMs, // default request/body timeout in milliseconds; per-request 0 disables it
66
67
  credentials, // fetch credentials mode: "omit" | "same-origin" | "include"
67
68
  transport, // "auto" (default) | "node" | "fetch" | "xhr" | a transport instance
68
69
 
@@ -73,6 +74,17 @@ new SnapReq({
73
74
  })
74
75
  ```
75
76
 
77
+ ### Timeouts
78
+
79
+ Set `timeoutMs` on the client or a single request to abort stalled requests. The timeout covers the response headers and body reads through `json()`, `text()`, `bytes()`, `buffer()`, or `stream()`. A timed-out request rejects with `SnapReqTimeoutError`.
80
+
81
+ ```js
82
+ const client = new SnapReq({baseUrl: "https://api.example.com", timeoutMs: 120000})
83
+
84
+ await client.get("/slow", {timeoutMs: 5000})
85
+ await client.get("/long-running", {timeoutMs: 0}) // disable the client default for this call
86
+ ```
87
+
76
88
  ### Retry
77
89
 
78
90
  Retries transient network errors and retryable HTTP statuses (502/503/504 by default). Only applies to buffered requests — never to streamed bodies.
package/package.json CHANGED
@@ -1,22 +1,55 @@
1
1
  {
2
2
  "name": "snapreq",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Cross-platform HTTP and WebSocket client with one API across Node, web, Expo and React Native",
5
5
  "type": "module",
6
6
  "main": "./src/snap-req.js",
7
7
  "types": "./types/snap-req.d.ts",
8
8
  "exports": {
9
- ".": {"types": "./types/snap-req.d.ts", "default": "./src/snap-req.js"},
10
- "./websocket": {"types": "./types/websocket/websocket-client.d.ts", "default": "./src/websocket/websocket-client.js"},
11
- "./errors": {"types": "./types/errors.d.ts", "default": "./src/errors.js"},
12
- "./headers": {"types": "./types/headers.d.ts", "default": "./src/headers.js"},
13
- "./response": {"types": "./types/response.d.ts", "default": "./src/response.js"},
14
- "./retry": {"types": "./types/retry.d.ts", "default": "./src/retry.js"},
15
- "./capabilities": {"types": "./types/capabilities.d.ts", "default": "./src/capabilities.js"},
16
- "./transports/select": {"types": "./types/transports/select.d.ts", "default": "./src/transports/select.js"},
17
- "./transports/fetch": {"types": "./types/transports/fetch-transport.d.ts", "default": "./src/transports/fetch-transport.js"},
18
- "./transports/xhr": {"types": "./types/transports/xhr-transport.d.ts", "default": "./src/transports/xhr-transport.js"},
19
- "./*": {"types": "./types/*.d.ts", "default": "./src/*.js"}
9
+ ".": {
10
+ "types": "./types/snap-req.d.ts",
11
+ "default": "./src/snap-req.js"
12
+ },
13
+ "./websocket": {
14
+ "types": "./types/websocket/websocket-client.d.ts",
15
+ "default": "./src/websocket/websocket-client.js"
16
+ },
17
+ "./errors": {
18
+ "types": "./types/errors.d.ts",
19
+ "default": "./src/errors.js"
20
+ },
21
+ "./headers": {
22
+ "types": "./types/headers.d.ts",
23
+ "default": "./src/headers.js"
24
+ },
25
+ "./response": {
26
+ "types": "./types/response.d.ts",
27
+ "default": "./src/response.js"
28
+ },
29
+ "./retry": {
30
+ "types": "./types/retry.d.ts",
31
+ "default": "./src/retry.js"
32
+ },
33
+ "./capabilities": {
34
+ "types": "./types/capabilities.d.ts",
35
+ "default": "./src/capabilities.js"
36
+ },
37
+ "./transports/select": {
38
+ "types": "./types/transports/select.d.ts",
39
+ "default": "./src/transports/select.js"
40
+ },
41
+ "./transports/fetch": {
42
+ "types": "./types/transports/fetch-transport.d.ts",
43
+ "default": "./src/transports/fetch-transport.js"
44
+ },
45
+ "./transports/xhr": {
46
+ "types": "./types/transports/xhr-transport.d.ts",
47
+ "default": "./src/transports/xhr-transport.js"
48
+ },
49
+ "./*": {
50
+ "types": "./types/*.d.ts",
51
+ "default": "./src/*.js"
52
+ }
20
53
  },
21
54
  "files": [
22
55
  "src/**",
@@ -43,12 +76,12 @@
43
76
  "author": "kasper@diestoeckels.de",
44
77
  "license": "ISC",
45
78
  "devDependencies": {
46
- "@eslint/js": "^9.0.0",
47
- "@types/node": "^22.0.0",
79
+ "@eslint/js": "^10.0.1",
80
+ "@types/node": "^25.9.1",
48
81
  "@types/ws": "^8.18.1",
49
82
  "eslint": "^10.3.0",
50
- "eslint-plugin-jsdoc": "^62.9.0",
51
- "globals": "^16.0.0",
83
+ "eslint-plugin-jsdoc": "^63.0.0",
84
+ "globals": "^17.6.0",
52
85
  "release-patch": "^1.0.0",
53
86
  "typescript": "^6.0.3",
54
87
  "ws": "^8.21.0"
package/src/errors.js CHANGED
@@ -67,3 +67,20 @@ export class SnapReqAbortError extends SnapReqError {
67
67
  this.name = "SnapReqAbortError"
68
68
  }
69
69
  }
70
+
71
+ /** Thrown when a request exceeds its configured timeout. */
72
+ export class SnapReqTimeoutError extends SnapReqError {
73
+ /**
74
+ * @param {object} options - Error metadata.
75
+ * @param {string} options.method - HTTP method used for the request.
76
+ * @param {string} options.url - Fully resolved request URL.
77
+ * @param {number} options.timeoutMs - Timeout in milliseconds.
78
+ */
79
+ constructor({method, url, timeoutMs}) {
80
+ super(`Request timed out after ${timeoutMs}ms: ${method} ${url}`)
81
+ this.name = "SnapReqTimeoutError"
82
+ this.method = method
83
+ this.url = url
84
+ this.timeoutMs = timeoutMs
85
+ }
86
+ }
package/src/response.js CHANGED
@@ -40,8 +40,10 @@ export default class SnapReqResponse {
40
40
  * @param {Uint8Array} [options.bytes] - Fully-read body, when the transport already buffered it.
41
41
  * @param {AsyncIterable<Uint8Array>} [options.stream] - Streamed body, when the transport supports streaming.
42
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.
43
45
  */
44
- constructor({url, method, status, statusText = "", headers, bytes, stream, nodeStream}) {
46
+ constructor({url, method, status, statusText = "", headers, bytes, stream, nodeStream, onBodyDone, mapBodyError}) {
45
47
  this.url = url
46
48
  this.method = method
47
49
  this.status = status
@@ -54,6 +56,11 @@ export default class SnapReqResponse {
54
56
  /** @type {import("node:stream").Readable | undefined} */
55
57
  this.nodeStream = nodeStream
56
58
  this._streamConsumed = false
59
+ this._bodyDone = false
60
+ this._onBodyDone = onBodyDone
61
+ this._mapBodyError = mapBodyError
62
+
63
+ if (bytes !== undefined) this._finishBody()
57
64
  }
58
65
 
59
66
  /** @returns {boolean} - Whether the status is in the 2xx range. */
@@ -77,7 +84,7 @@ export default class SnapReqResponse {
77
84
 
78
85
  this._streamConsumed = true
79
86
 
80
- return this._stream
87
+ return this._wrappedStream(this._stream)
81
88
  }
82
89
 
83
90
  /** @returns {boolean} - Whether the body is available as a stream that has not been read yet. */
@@ -94,20 +101,15 @@ export default class SnapReqResponse {
94
101
 
95
102
  if (!this._stream) {
96
103
  this._bytes = new Uint8Array(0)
104
+ this._finishBody()
97
105
 
98
106
  return this._bytes
99
107
  }
100
108
 
101
- if (this._streamConsumed) {
102
- throw new Error("Cannot buffer this response: its stream was already consumed via stream().")
103
- }
104
-
105
- this._streamConsumed = true
106
-
107
109
  /** @type {Uint8Array[]} */
108
110
  const chunks = []
109
111
 
110
- for await (const chunk of this._stream) {
112
+ for await (const chunk of this.stream()) {
111
113
  chunks.push(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))
112
114
  }
113
115
 
@@ -153,4 +155,43 @@ export default class SnapReqResponse {
153
155
 
154
156
  return JSON.parse(text)
155
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
+ }
156
197
  }
package/src/retry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import {SnapReqHttpError} from "./errors.js"
3
+ import {SnapReqHttpError, SnapReqTimeoutError} from "./errors.js"
4
4
 
5
5
  const RETRYABLE_ERROR_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "EHOSTUNREACH", "ENOENT", "ETIMEDOUT", "EPIPE"])
6
6
  const DEFAULT_RETRYABLE_STATUSES = [502, 503, 504]
@@ -28,6 +28,8 @@ const DEFAULT_RETRYABLE_STATUSES = [502, 503, 504]
28
28
  * @returns {boolean} - Whether the error is a transient network failure.
29
29
  */
30
30
  export function defaultRetryableError(error) {
31
+ if (error instanceof SnapReqTimeoutError) return true
32
+
31
33
  if (!error || typeof error !== "object") return false
32
34
 
33
35
  if ("code" in error && typeof error.code === "string" && RETRYABLE_ERROR_CODES.has(error.code)) {
package/src/snap-req.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @ts-check
2
2
 
3
- import {SnapReqHttpError, SnapReqUnsupportedFeatureError} from "./errors.js"
3
+ import {SnapReqHttpError, SnapReqTimeoutError, SnapReqUnsupportedFeatureError} from "./errors.js"
4
4
  import SnapReqHeaders from "./headers.js"
5
5
  import {buildUrl, normalizeBody} from "./request.js"
6
6
  import {normalizeRetryOptions, runWithRetry} from "./retry.js"
@@ -18,6 +18,7 @@ import {selectTransport} from "./transports/select.js"
18
18
  * @property {import("./request.js").NormalizedBody} body - Normalized request body.
19
19
  * @property {CompressionEncoding} bodyCompression - Request body compression.
20
20
  * @property {AbortSignal} [signal] - Abort signal.
21
+ * @property {number} [timeoutMs] - Request timeout in milliseconds.
21
22
  * @property {string} [credentials] - Fetch credentials mode ("omit" | "same-origin" | "include").
22
23
  */
23
24
 
@@ -31,11 +32,20 @@ import {selectTransport} from "./transports/select.js"
31
32
  * @property {any} [body] - Request body: string, object (JSON), Uint8Array/ArrayBuffer, or a stream/async-iterable.
32
33
  * @property {CompressionEncoding} [bodyCompression] - Compress the request body (Node transport only).
33
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.
34
36
  * @property {string} [credentials] - Fetch credentials mode.
35
37
  * @property {boolean | import("./retry.js").RetryOptions} [retry] - Retry transient failures.
36
38
  * @property {boolean} [throwOnError] - Throw `SnapReqHttpError` on non-2xx responses.
37
39
  */
38
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
+
39
49
  /**
40
50
  * A cross-platform HTTP client with one API across Node, web, Expo and React
41
51
  * Native. The right transport is chosen at runtime; features a platform cannot
@@ -52,14 +62,16 @@ export default class SnapReq {
52
62
  * @param {Record<string, string | number> | (() => Record<string, string | number>)} [config.headers] - Default headers (object or factory).
53
63
  * @param {boolean | import("./retry.js").RetryOptions} [config.retry] - Default retry policy.
54
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.
55
66
  * @param {string} [config.credentials] - Default fetch credentials mode.
56
67
  * @param {import("./transports/select.js").TransportName | import("./transports/select.js").Transport} [config.transport] - Transport preference or instance. Defaults to "auto".
57
68
  */
58
- constructor({baseUrl, socketPath, tls, keepAlive = true, headers, retry, throwOnError = false, credentials, transport = "auto"} = {}) {
69
+ constructor({baseUrl, socketPath, tls, keepAlive = true, headers, retry, throwOnError = false, timeoutMs, credentials, transport = "auto"} = {}) {
59
70
  this.baseUrl = baseUrl
60
71
  this.defaultHeaders = headers
61
72
  this.defaultRetry = retry
62
73
  this.throwOnError = throwOnError
74
+ this.timeoutMs = timeoutMs
63
75
  this.credentials = credentials
64
76
  this._transportPreference = transport
65
77
  this._nodeConfig = {socketPath, tls, keepAlive}
@@ -110,10 +122,120 @@ export default class SnapReq {
110
122
  body,
111
123
  bodyCompression: options.bodyCompression || "identity",
112
124
  signal: options.signal,
125
+ timeoutMs: options.timeoutMs ?? this.timeoutMs,
113
126
  credentials: options.credentials ?? this.credentials
114
127
  }
115
128
  }
116
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
+
117
239
  /**
118
240
  * Performs a request and buffers nothing eagerly — read the body via the
119
241
  * returned response (`json()`, `text()`, `bytes()`). Retries transient
@@ -122,16 +244,15 @@ export default class SnapReq {
122
244
  * @returns {Promise<import("./response.js").default>} - The response.
123
245
  */
124
246
  async request(options) {
125
- const normalized = this._normalize(options)
126
247
  const transport = await this._resolveTransport()
127
248
  const throwOnError = options.throwOnError ?? this.throwOnError
128
249
  const retry = normalizeRetryOptions(options.retry ?? this.defaultRetry)
129
- const canRetry = retry && normalized.body.kind !== "stream"
130
-
131
- const attempt = () => transport.performRequest(normalized)
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))
132
253
  const response = canRetry ? await runWithRetry(attempt, /** @type {any} */ (retry)) : await attempt()
133
254
 
134
- if (throwOnError && !response.ok) throw await this._httpError(response, normalized)
255
+ if (throwOnError && !response.ok) throw await this._httpError(response, this._normalize(options))
135
256
 
136
257
  return response
137
258
  }
@@ -144,7 +265,6 @@ export default class SnapReq {
144
265
  * @returns {Promise<import("./response.js").default>} - The streaming response.
145
266
  */
146
267
  async requestStream(options) {
147
- const normalized = this._normalize(options)
148
268
  const transport = await this._resolveTransport()
149
269
 
150
270
  if (!transport.capabilities.responseStreaming) {
@@ -154,15 +274,35 @@ export default class SnapReq {
154
274
  })
155
275
  }
156
276
 
157
- const response = await transport.performRequest(normalized)
277
+ const response = await this._requestWithTimeout(options, (request) => transport.performRequest(request))
158
278
 
159
279
  if ((options.throwOnError ?? this.throwOnError) && !response.ok) {
160
- throw await this._httpError(response, normalized)
280
+ throw await this._httpError(response, this._normalize(options))
161
281
  }
162
282
 
163
283
  return response
164
284
  }
165
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
+
166
306
  /**
167
307
  * @param {string} path - Request path or absolute URL.
168
308
  * @param {RequestOptions} [options] - Request options.
@@ -221,7 +361,9 @@ export default class SnapReq {
221
361
 
222
362
  try {
223
363
  responseText = await response.text()
224
- } catch {
364
+ } catch (error) {
365
+ if (error instanceof SnapReqTimeoutError) throw error
366
+
225
367
  // Body unavailable (already streamed or read error) — fall back to status text.
226
368
  }
227
369
 
package/types/errors.d.ts CHANGED
@@ -62,3 +62,20 @@ export class SnapReqAbortError extends SnapReqError {
62
62
  /** @param {string} [message] - Human readable description. */
63
63
  constructor(message?: string);
64
64
  }
65
+ /** Thrown when a request exceeds its configured timeout. */
66
+ export class SnapReqTimeoutError extends SnapReqError {
67
+ /**
68
+ * @param {object} options - Error metadata.
69
+ * @param {string} options.method - HTTP method used for the request.
70
+ * @param {string} options.url - Fully resolved request URL.
71
+ * @param {number} options.timeoutMs - Timeout in milliseconds.
72
+ */
73
+ constructor({ method, url, timeoutMs }: {
74
+ method: string;
75
+ url: string;
76
+ timeoutMs: number;
77
+ });
78
+ method: string;
79
+ url: string;
80
+ timeoutMs: number;
81
+ }
@@ -15,8 +15,10 @@ export default class SnapReqResponse {
15
15
  * @param {Uint8Array} [options.bytes] - Fully-read body, when the transport already buffered it.
16
16
  * @param {AsyncIterable<Uint8Array>} [options.stream] - Streamed body, when the transport supports streaming.
17
17
  * @param {import("node:stream").Readable} [options.nodeStream] - Raw Node stream, when available, for advanced consumers.
18
+ * @param {() => void} [options.onBodyDone] - Callback fired when body reading finishes or fails.
19
+ * @param {(error: unknown) => unknown} [options.mapBodyError] - Maps body read errors before rethrowing.
18
20
  */
19
- constructor({ url, method, status, statusText, headers, bytes, stream, nodeStream }: {
21
+ constructor({ url, method, status, statusText, headers, bytes, stream, nodeStream, onBodyDone, mapBodyError }: {
20
22
  url: string;
21
23
  method: string;
22
24
  status: number;
@@ -25,6 +27,8 @@ export default class SnapReqResponse {
25
27
  bytes?: Uint8Array;
26
28
  stream?: AsyncIterable<Uint8Array>;
27
29
  nodeStream?: import("node:stream").Readable;
30
+ onBodyDone?: () => void;
31
+ mapBodyError?: (error: unknown) => unknown;
28
32
  });
29
33
  url: string;
30
34
  method: string;
@@ -38,6 +42,9 @@ export default class SnapReqResponse {
38
42
  /** @type {import("node:stream").Readable | undefined} */
39
43
  nodeStream: import("node:stream").Readable | undefined;
40
44
  _streamConsumed: boolean;
45
+ _bodyDone: boolean;
46
+ _onBodyDone: () => void;
47
+ _mapBodyError: (error: unknown) => unknown;
41
48
  /** @returns {boolean} - Whether the status is in the 2xx range. */
42
49
  get ok(): boolean;
43
50
  /**
@@ -70,5 +77,17 @@ export default class SnapReqResponse {
70
77
  * @returns {Promise<any>} - The parsed JSON body.
71
78
  */
72
79
  json(): Promise<any>;
80
+ /**
81
+ * @param {AsyncIterable<Uint8Array>} source - Source response stream.
82
+ * @returns {AsyncIterable<Uint8Array>} - Stream with cleanup/error mapping.
83
+ */
84
+ _wrappedStream(source: AsyncIterable<Uint8Array>): AsyncIterable<Uint8Array>;
85
+ /**
86
+ * @param {unknown} error - Body read error.
87
+ * @returns {unknown} - Error to rethrow.
88
+ */
89
+ _mappedBodyError(error: unknown): unknown;
90
+ /** @returns {void} */
91
+ _finishBody(): void;
73
92
  }
74
93
  import SnapReqHeaders from "./headers.js";
@@ -9,6 +9,7 @@
9
9
  * @property {import("./request.js").NormalizedBody} body - Normalized request body.
10
10
  * @property {CompressionEncoding} bodyCompression - Request body compression.
11
11
  * @property {AbortSignal} [signal] - Abort signal.
12
+ * @property {number} [timeoutMs] - Request timeout in milliseconds.
12
13
  * @property {string} [credentials] - Fetch credentials mode ("omit" | "same-origin" | "include").
13
14
  */
14
15
  /**
@@ -21,10 +22,18 @@
21
22
  * @property {any} [body] - Request body: string, object (JSON), Uint8Array/ArrayBuffer, or a stream/async-iterable.
22
23
  * @property {CompressionEncoding} [bodyCompression] - Compress the request body (Node transport only).
23
24
  * @property {AbortSignal} [signal] - Abort signal for the request.
25
+ * @property {number} [timeoutMs] - Request timeout in milliseconds. Set to `0` to disable a client default.
24
26
  * @property {string} [credentials] - Fetch credentials mode.
25
27
  * @property {boolean | import("./retry.js").RetryOptions} [retry] - Retry transient failures.
26
28
  * @property {boolean} [throwOnError] - Throw `SnapReqHttpError` on non-2xx responses.
27
29
  */
30
+ /**
31
+ * @typedef {object} RequestTimeout
32
+ * @property {AbortSignal | undefined} signal - Signal to use for the request.
33
+ * @property {() => void} clear - Clears timeout resources.
34
+ * @property {(response: import("./response.js").default, request: NormalizedRequest) => import("./response.js").default} response - Attaches timeout handling to a response.
35
+ * @property {(error: unknown, request: NormalizedRequest) => unknown} error - Maps a thrown error.
36
+ */
28
37
  /**
29
38
  * A cross-platform HTTP client with one API across Node, web, Expo and React
30
39
  * Native. The right transport is chosen at runtime; features a platform cannot
@@ -41,10 +50,11 @@ export default class SnapReq {
41
50
  * @param {Record<string, string | number> | (() => Record<string, string | number>)} [config.headers] - Default headers (object or factory).
42
51
  * @param {boolean | import("./retry.js").RetryOptions} [config.retry] - Default retry policy.
43
52
  * @param {boolean} [config.throwOnError] - Throw `SnapReqHttpError` on non-2xx responses by default. Defaults to false.
53
+ * @param {number} [config.timeoutMs] - Default request timeout in milliseconds. Set per-request `timeoutMs: 0` to disable.
44
54
  * @param {string} [config.credentials] - Default fetch credentials mode.
45
55
  * @param {import("./transports/select.js").TransportName | import("./transports/select.js").Transport} [config.transport] - Transport preference or instance. Defaults to "auto".
46
56
  */
47
- constructor({ baseUrl, socketPath, tls, keepAlive, headers, retry, throwOnError, credentials, transport }?: {
57
+ constructor({ baseUrl, socketPath, tls, keepAlive, headers, retry, throwOnError, timeoutMs, credentials, transport }?: {
48
58
  baseUrl?: string;
49
59
  socketPath?: string;
50
60
  tls?: {
@@ -57,6 +67,7 @@ export default class SnapReq {
57
67
  headers?: Record<string, string | number> | (() => Record<string, string | number>);
58
68
  retry?: boolean | import("./retry.js").RetryOptions;
59
69
  throwOnError?: boolean;
70
+ timeoutMs?: number;
60
71
  credentials?: string;
61
72
  transport?: import("./transports/select.js").TransportName | import("./transports/select.js").Transport;
62
73
  });
@@ -64,6 +75,7 @@ export default class SnapReq {
64
75
  defaultHeaders: Record<string, string | number> | (() => Record<string, string | number>);
65
76
  defaultRetry: boolean | import("./retry.js").RetryOptions;
66
77
  throwOnError: boolean;
78
+ timeoutMs: number;
67
79
  credentials: string;
68
80
  _transportPreference: import("./transports/select.js").TransportName | import("./transports/select.js").Transport;
69
81
  _nodeConfig: {
@@ -91,6 +103,32 @@ export default class SnapReq {
91
103
  * @returns {NormalizedRequest} - The normalized request.
92
104
  */
93
105
  _normalize(options: RequestOptions): NormalizedRequest;
106
+ /**
107
+ * @param {RequestOptions} options - Request options.
108
+ * @returns {RequestTimeout} - Timeout handling for one request attempt.
109
+ */
110
+ _requestTimeout(options: RequestOptions): RequestTimeout;
111
+ /**
112
+ * @param {AbortSignal | undefined} callerSignal - Caller-supplied signal.
113
+ * @param {AbortSignal} timeoutSignal - Timeout signal.
114
+ * @returns {{signal: AbortSignal, clear: () => void}} - Signal that aborts when either source aborts.
115
+ */
116
+ _composeSignal(callerSignal: AbortSignal | undefined, timeoutSignal: AbortSignal): {
117
+ signal: AbortSignal;
118
+ clear: () => void;
119
+ };
120
+ /**
121
+ * @param {(() => void) | undefined} existing - Existing body-done callback.
122
+ * @param {() => void} next - Callback to add.
123
+ * @returns {() => void} - Combined callback.
124
+ */
125
+ _chainBodyDone(existing: (() => void) | undefined, next: () => void): () => void;
126
+ /**
127
+ * @param {((error: unknown) => unknown) | undefined} existing - Existing error mapper.
128
+ * @param {(error: unknown) => unknown} next - Mapper to add.
129
+ * @returns {(error: unknown) => unknown} - Combined mapper.
130
+ */
131
+ _chainBodyError(existing: ((error: unknown) => unknown) | undefined, next: (error: unknown) => unknown): (error: unknown) => unknown;
94
132
  /**
95
133
  * Performs a request and buffers nothing eagerly — read the body via the
96
134
  * returned response (`json()`, `text()`, `bytes()`). Retries transient
@@ -107,6 +145,12 @@ export default class SnapReq {
107
145
  * @returns {Promise<import("./response.js").default>} - The streaming response.
108
146
  */
109
147
  requestStream(options: RequestOptions): Promise<import("./response.js").default>;
148
+ /**
149
+ * @param {RequestOptions} options - Request options.
150
+ * @param {(request: NormalizedRequest) => Promise<import("./response.js").default>} performRequest - Transport request runner.
151
+ * @returns {Promise<import("./response.js").default>} - Response with timeout handling attached.
152
+ */
153
+ _requestWithTimeout(options: RequestOptions, performRequest: (request: NormalizedRequest) => Promise<import("./response.js").default>): Promise<import("./response.js").default>;
110
154
  /**
111
155
  * @param {string} path - Request path or absolute URL.
112
156
  * @param {RequestOptions} [options] - Request options.
@@ -178,6 +222,10 @@ export type NormalizedRequest = {
178
222
  * - Abort signal.
179
223
  */
180
224
  signal?: AbortSignal;
225
+ /**
226
+ * - Request timeout in milliseconds.
227
+ */
228
+ timeoutMs?: number;
181
229
  /**
182
230
  * - Fetch credentials mode ("omit" | "same-origin" | "include").
183
231
  */
@@ -216,6 +264,10 @@ export type RequestOptions = {
216
264
  * - Abort signal for the request.
217
265
  */
218
266
  signal?: AbortSignal;
267
+ /**
268
+ * - Request timeout in milliseconds. Set to `0` to disable a client default.
269
+ */
270
+ timeoutMs?: number;
219
271
  /**
220
272
  * - Fetch credentials mode.
221
273
  */
@@ -229,5 +281,23 @@ export type RequestOptions = {
229
281
  */
230
282
  throwOnError?: boolean;
231
283
  };
284
+ export type RequestTimeout = {
285
+ /**
286
+ * - Signal to use for the request.
287
+ */
288
+ signal: AbortSignal | undefined;
289
+ /**
290
+ * - Clears timeout resources.
291
+ */
292
+ clear: () => void;
293
+ /**
294
+ * - Attaches timeout handling to a response.
295
+ */
296
+ response: (response: import("./response.js").default, request: NormalizedRequest) => import("./response.js").default;
297
+ /**
298
+ * - Maps a thrown error.
299
+ */
300
+ error: (error: unknown, request: NormalizedRequest) => unknown;
301
+ };
232
302
  import { SnapReqHttpError } from "./errors.js";
233
303
  import SnapReqHeaders from "./headers.js";