rust-fetch 2.1.1 → 2.2.0

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/llms.txt ADDED
@@ -0,0 +1,91 @@
1
+ # rust-fetch
2
+
3
+ > Resilient HTTP client for JavaScript and TypeScript (npm package `rust-fetch`), with a Rust core compiled to WebAssembly that behaves the same in browsers, Web Workers and Node.js 20+. It adds what `fetch()` lacks: typed errors, per-attempt timeouts, retries with backoff, interceptors, upload and download progress, streaming, and WebSockets with reconnection. Also usable as a Rust crate.
4
+
5
+ Use rust-fetch when you want `fetch()` plus production policy (timeouts, retries, error handling) in one client, the same in the browser and in Node.js. Prefer `undici.request()` for maximum raw throughput in Node.js only, and native `fetch()` when bundle size matters most (rust-fetch adds about 207 KB brotli to a browser bundle).
6
+
7
+ ## Install and set up
8
+
9
+ ```js
10
+ // npm install rust-fetch
11
+ import { WasmClient } from 'rust-fetch'; // Node.js: ready on import
12
+ // Browsers and bundlers: import init, { WasmClient } from 'rust-fetch'; await init();
13
+
14
+ const client = new WasmClient({
15
+ baseUrl: 'https://api.example.com/v1', // relative URLs are appended to it
16
+ headers: { authorization: 'Bearer …' }, // sent with every request
17
+ timeoutMs: 10_000, // per attempt, including the body; default 30000; 0 disables
18
+ retries: 3, // or { maxRetries, initialDelayMs, maxDelayMs, multiplier, retryOn, retryOnNetworkError, retryOnTimeout, retryNonIdempotent }
19
+ });
20
+ ```
21
+
22
+ ## Requests
23
+
24
+ - `client.get(url, options?)`, `head`, `delete`: no body
25
+ - `client.post(url, { body, ...options })`, `put`, `patch`; `client.fetch(url, { method, body, ...options })`
26
+ - `client.post_json(url, body, options?)`, `put_json`, `patch_json`: always JSON
27
+ - TypeScript: `client.get<User>('/users/42')` resolves to `HttpResponse<User>` (the type isn't validated at runtime)
28
+ - Bodies: string, `Uint8Array`/`ArrayBuffer`/`DataView`, `Blob`/`File`, `FormData` (multipart), `URLSearchParams`, or any value sent as JSON
29
+ - Options: `headers`, `params` (query string), `timeoutMs`, `responseType` (`'auto' | 'json' | 'text' | 'binary' | 'stream'`), `signal` (AbortSignal), `onDownloadProgress({ loaded, total })`, `onUploadProgress({ loaded, total })`
30
+
31
+ Resolves (2xx only) to `{ status, statusText, ok, url, headers, body }`. With `responseType: 'auto'`, `body` is parsed JSON for `application/json` and `*+json`, a string for text types, a `Uint8Array` otherwise, `null` when empty.
32
+
33
+ ## Errors
34
+
35
+ Every failure rejects with an `Error` whose `kind` (and `name`) is one of:
36
+ - `HttpError`: non-2xx; has `status`, `statusText`, `body` (response text)
37
+ - `TimeoutError`: an attempt exceeded `timeoutMs`; has `timeoutMs`
38
+ - `NetworkError`: DNS, connection, TLS, CORS, truncated body, or a WebSocket failure
39
+ - `CancelledError`: the `signal` aborted
40
+ - `InvalidInputError`: bad URL, header, option or body, before sending; has `parameter`
41
+ - `ParseError`: `responseType: 'json'` and the body isn't JSON
42
+ - `JsInteropError`: unexpected JS/Wasm conversion failure
43
+
44
+ Retries are off by default. When on, POST and PATCH aren't retried unless `retryNonIdempotent: true`; `Retry-After` (seconds) is honoured; only `retryOn` statuses (default 408, 429, 500, 502, 503, 504), network errors and timeouts are retried. A request that fails every attempt rejects with the last attempt's error.
45
+
46
+ ## Interceptors
47
+
48
+ ```js
49
+ client.addRequestInterceptor((request) => { request.headers.authorization = `Bearer ${token}`; });
50
+ client.addResponseInterceptor((response) => response, (error) => { throw error; });
51
+ // both return an id for client.ejectInterceptor(id); client.clearInterceptors() removes all
52
+ ```
53
+
54
+ ## Streaming and progress
55
+
56
+ ```js
57
+ const res = await client.get('/export.csv', { responseType: 'stream' }); // res.body is a ReadableStream<Uint8Array>
58
+ await client.post('/upload', { body: formData, onUploadProgress: ({ loaded, total }) => {} });
59
+ ```
60
+
61
+ Upload progress uses `XMLHttpRequest` in browsers (cross-origin requests are preflighted) and the `node:http` transport in Node.js.
62
+
63
+ ## WebSockets
64
+
65
+ ```js
66
+ const socket = client.websocket('/live', { // https base URL → wss://…/live
67
+ params: { room: 'blue' },
68
+ messageType: 'json', // 'auto' (default) or 'json'
69
+ reconnect: true, // or a number, or { maxRetries, initialDelayMs, maxDelayMs, multiplier }
70
+ onMessage: (message) => {},
71
+ onError: (error) => {}, // typed errors, as for requests
72
+ });
73
+ await socket.opened; // rejects if it never connects
74
+ socket.sendJson({ type: 'subscribe' }); // socket.send(string | ArrayBuffer | typed array | Blob)
75
+ socket.close(); // stops reconnecting
76
+ const { code, reason, wasClean } = await socket.closed;
77
+ ```
78
+
79
+ Needs a built-in `WebSocket`: browsers and Node.js 22+ (Node.js 20 with `--experimental-websocket`). Handshake `headers` work in Node.js only.
80
+
81
+ ## Docs
82
+
83
+ - [README](https://github.com/asemqaffaf/rust-fetch#readme): full guide, performance results and limitations
84
+ - [CHANGELOG](https://github.com/asemqaffaf/rust-fetch/blob/main/CHANGELOG.md): changes per version
85
+ - [Roadmap](https://github.com/asemqaffaf/rust-fetch/blob/main/ROADMAP.md): planned work
86
+
87
+ ## Optional
88
+
89
+ - [Performance](https://github.com/asemqaffaf/rust-fetch#performance): Node.js benchmarks against `fetch()`, `undici.request()` and axios, including an independent benchmark
90
+ - [Using it from Rust](https://github.com/asemqaffaf/rust-fetch#using-it-from-rust): the same client as a Rust crate (no interceptors, upload progress or WebSockets there)
91
+ - [npm package](https://www.npmjs.com/package/rust-fetch)
package/node.js CHANGED
@@ -9,14 +9,41 @@
9
9
  // fetch() implements browser semantics (WHATWG streams) on top of the same
10
10
  // sockets and is considerably slower; this transport only moves bytes.
11
11
  // Retries, timeouts, validation and decoding stay in the Rust core.
12
+ //
13
+ // Where the native addon (rust-fetch-native.<platform>-<arch>.node) loads,
14
+ // requests go through it instead: the same rules, implemented in Rust on
15
+ // Tokio threads, so the JS thread only builds requests and decodes bodies.
12
16
  import { readFileSync } from 'node:fs';
13
17
  import http from 'node:http';
14
18
  import https from 'node:https';
19
+ import { createRequire } from 'node:module';
15
20
  import zlib from 'node:zlib';
16
21
  import { initSync } from './rust_fetch.js';
17
22
 
18
23
  initSync({ module: readFileSync(new URL('./rust_fetch_bg.wasm', import.meta.url)) });
19
24
 
25
+ /**
26
+ * The native addon, or undefined. RUST_FETCH_NATIVE overrides its path;
27
+ * anything that doesn't load (e.g. `0`) keeps the node:http transport.
28
+ */
29
+ function loadNative() {
30
+ const path = process.env.RUST_FETCH_NATIVE ?? `./rust-fetch-native.${process.platform}-${process.arch}.node`;
31
+ try {
32
+ const addon = createRequire(import.meta.url)(path);
33
+ const extraCaCerts = process.env.NODE_EXTRA_CA_CERTS;
34
+ addon.configure({
35
+ zstd: typeof zlib.zstdDecompressSync === 'function',
36
+ rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
37
+ extraCaCerts: extraCaCerts ? readFileSync(extraCaCerts) : undefined,
38
+ });
39
+ return addon;
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ const native = loadNative();
46
+
20
47
  // Node's default global agents open a socket for every request that finds no
21
48
  // idle one, but keep only 256 of them idle afterwards. Under a burst the rest
22
49
  // are closed and sit in TIME_WAIT, and once the ephemeral port range is used
@@ -146,6 +173,36 @@ function ownedBytes(bytes) {
146
173
  : new Uint8Array(bytes);
147
174
  }
148
175
 
176
+ /** Call a progress callback with `{ loaded, total }`, ignoring its exceptions */
177
+ function report(callback, loaded, total) {
178
+ try {
179
+ callback({ loaded, total });
180
+ } catch {}
181
+ }
182
+
183
+ // Request bodies are written in chunks of this size when upload progress is reported
184
+ const UPLOAD_CHUNK = 64 * 1024;
185
+
186
+ /**
187
+ * Send the body and end the request. With `onProgress`, the body is written
188
+ * a chunk at a time, reporting each chunk once it has been handed to the OS.
189
+ */
190
+ function writeBody(request, body, onProgress) {
191
+ if (!onProgress || !body) return request.end(body);
192
+ let loaded = 0;
193
+ const next = () => {
194
+ const chunk = body.subarray(loaded, loaded + UPLOAD_CHUNK);
195
+ request.write(chunk, (error) => {
196
+ if (error) return; // the request's 'error' event rejects
197
+ loaded += chunk.length;
198
+ report(onProgress, loaded, body.length);
199
+ if (loaded < body.length) next();
200
+ else request.end();
201
+ });
202
+ };
203
+ next();
204
+ }
205
+
149
206
  function withoutHeaders(headers, names) {
150
207
  const copy = { ...headers };
151
208
  for (const name of names) delete copy[name];
@@ -194,10 +251,13 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
194
251
  const buffer = preallocated ? new Uint8Array(expected) : undefined;
195
252
  let received = 0;
196
253
  const chunks = [];
254
+ const onProgress = status >= 200 && status < 300 ? state.onDownloadProgress : undefined;
255
+ const total = Number.isSafeInteger(expected) ? expected : null;
197
256
  response.on('data', (chunk) => {
198
257
  if (buffer && received + chunk.length <= expected) buffer.set(chunk, received);
199
258
  else chunks.push(chunk);
200
259
  received += chunk.length;
260
+ if (onProgress) report(onProgress, received, total);
201
261
  });
202
262
  response.on('error', reject);
203
263
  // A socket closed mid-body may emit 'close' without 'end' (a settled promise ignores this)
@@ -221,13 +281,13 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
221
281
  });
222
282
  });
223
283
  request.on('error', reject);
224
- request.end(body);
284
+ writeBody(request, body, state.onUploadProgress);
225
285
  state.request = request;
226
286
  }
227
287
 
228
288
  /**
229
289
  * One attempt, called by the Rust core with
230
- * `{ method, url, headers, body?, timeout?, bodyType }`. Resolves to the
290
+ * `{ method, url, headers, body?, timeout?, bodyType, onUploadProgress?, onDownloadProgress? }`. Resolves to the
231
291
  * finished `{ status, statusText, ok, url, headers, body }` (the core fills
232
292
  * in statusText). Rejects with code RUST_FETCH_TIMEOUT or RUST_FETCH_PARSE,
233
293
  * or any other error for network failures.
@@ -237,7 +297,16 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
237
297
  * AbortSignal into every request.
238
298
  */
239
299
  function nodeTransport(request) {
240
- const state = { cancelled: false, request: undefined, bodyType: request.bodyType ?? AUTO };
300
+ // The native transport makes its own connections, so only where the
301
+ // node:http transport would use its own pool too, not an installed agent.
302
+ // Progress callbacks run on this thread, so those requests stay here.
303
+ const { onUploadProgress, onDownloadProgress } = request;
304
+ const transport = request.url.startsWith('https:') ? https : request.url.startsWith('http:') ? http : undefined;
305
+ if (native && transport && !onUploadProgress && !onDownloadProgress && agentFor(transport) === pools.get(transport)) {
306
+ return nativeTransport(request);
307
+ }
308
+
309
+ const state = { cancelled: false, request: undefined, bodyType: request.bodyType ?? AUTO, onUploadProgress, onDownloadProgress };
241
310
  let timer;
242
311
  const promise = new Promise((resolve, reject) => {
243
312
  if (request.timeout > 0) {
@@ -254,16 +323,11 @@ function nodeTransport(request) {
254
323
  clearTimeout(timer);
255
324
  return reject(error);
256
325
  }
257
- const headers = { ...request.headers };
258
- headers['accept'] ??= '*/*';
259
- headers['accept-encoding'] ??= ACCEPT_ENCODING;
260
- headers['user-agent'] ??= 'rust-fetch';
261
- if (request.body) headers['content-length'] ??= String(request.body.length);
262
326
  const settle = (fn) => (value) => {
263
327
  clearTimeout(timer);
264
328
  fn(value);
265
329
  };
266
- send(state, request.method, url, headers, request.body, 0, settle(resolve), settle(reject));
330
+ send(state, request.method, url, withDefaultHeaders(request), request.body, 0, settle(resolve), settle(reject));
267
331
  });
268
332
  const cancel = () => {
269
333
  state.cancelled = true;
@@ -280,6 +344,46 @@ function nodeTransport(request) {
280
344
  return finished;
281
345
  }
282
346
 
347
+ /** The request's headers plus fetch()-like defaults */
348
+ function withDefaultHeaders(request) {
349
+ const headers = { ...request.headers };
350
+ headers['accept'] ??= '*/*';
351
+ headers['accept-encoding'] ??= ACCEPT_ENCODING;
352
+ headers['user-agent'] ??= 'rust-fetch';
353
+ if (request.body) headers['content-length'] ??= String(request.body.length);
354
+ return headers;
355
+ }
356
+
357
+ let nextNativeId = 0;
358
+
359
+ /** `nodeTransport` through the native addon: same contract, same rules */
360
+ function nativeTransport(request) {
361
+ const id = (nextNativeId = (nextNativeId + 1) >>> 0);
362
+ const headers = withDefaultHeaders(request);
363
+ // Names and values alternating; a repeated header is sent once per value
364
+ const pairs = [];
365
+ for (const name in headers) {
366
+ const value = headers[name];
367
+ if (Array.isArray(value)) for (const item of value) pairs.push(name, item);
368
+ else pairs.push(name, value);
369
+ }
370
+ const bodyType = request.bodyType ?? AUTO;
371
+ const finished = native
372
+ .send({ id, method: request.method, url: request.url, headers: pairs, body: request.body, timeout: request.timeout })
373
+ .then((response) => {
374
+ if (response.error != null) throw coded(new Error(response.error), response.code);
375
+ const headers = {};
376
+ const raw = response.headers;
377
+ for (let i = 0; i < raw.length; i += 2) {
378
+ const name = raw[i];
379
+ headers[name] = Object.hasOwn(headers, name) ? `${headers[name]}, ${raw[i + 1]}` : raw[i + 1];
380
+ }
381
+ return finishResponse({ status: response.status, url: response.url, headers, body: ownedBytes(response.body) }, bodyType);
382
+ });
383
+ finished.cancel = () => native.cancel(id);
384
+ return finished;
385
+ }
386
+
283
387
  /** The response object handed to the Rust core (which fills in statusText) */
284
388
  function finishResponse({ status, url, headers, body }, bodyType) {
285
389
  const ok = status >= 200 && status < 300;
package/package.json CHANGED
@@ -4,8 +4,8 @@
4
4
  "collaborators": [
5
5
  "Asem Qaffaf <asem@qaffaf.com>"
6
6
  ],
7
- "description": "A reliable HTTP client for JavaScript and TypeScript, written in Rust and compiled to WebAssembly: timeouts, retries, typed errors and interceptors, faster than fetch() in Node.js",
8
- "version": "2.1.1",
7
+ "description": "Resilient HTTP client for JavaScript, TypeScript and Rust: fetch() with timeouts, retries, typed errors, interceptors, upload and download progress, and WebSockets, from one Rust core compiled to WebAssembly for browsers and Node.js",
8
+ "version": "2.2.0",
9
9
  "license": "MIT OR Apache-2.0",
10
10
  "repository": {
11
11
  "type": "git",
@@ -17,20 +17,46 @@
17
17
  "rust_fetch.js",
18
18
  "rust_fetch.d.ts",
19
19
  "node.js",
20
+ "llms.txt",
20
21
  "LICENSE-MIT",
21
22
  "LICENSE-APACHE"
22
23
  ],
23
24
  "main": "rust_fetch.js",
25
+ "homepage": "https://github.com/asemqaffaf/rust-fetch#readme",
24
26
  "types": "rust_fetch.d.ts",
25
27
  "sideEffects": [
26
28
  "./snippets/*"
27
29
  ],
28
30
  "keywords": [
29
- "wasm",
30
31
  "http",
31
- "client",
32
+ "http-client",
32
33
  "fetch",
33
- "webassembly"
34
+ "fetch-api",
35
+ "request",
36
+ "rest",
37
+ "api-client",
38
+ "axios-alternative",
39
+ "retry",
40
+ "retries",
41
+ "backoff",
42
+ "timeout",
43
+ "interceptors",
44
+ "typed-errors",
45
+ "abortcontroller",
46
+ "upload-progress",
47
+ "download-progress",
48
+ "streaming",
49
+ "websocket",
50
+ "websockets",
51
+ "reconnect",
52
+ "typescript",
53
+ "nodejs",
54
+ "browser",
55
+ "isomorphic",
56
+ "webassembly",
57
+ "wasm",
58
+ "rust",
59
+ "napi"
34
60
  ],
35
61
  "exports": {
36
62
  ".": {
@@ -48,7 +74,6 @@
48
74
  "engines": {
49
75
  "node": ">=20"
50
76
  },
51
- "homepage": "https://github.com/asemqaffaf/rust-fetch#readme",
52
77
  "bugs": {
53
78
  "url": "https://github.com/asemqaffaf/rust-fetch/issues"
54
79
  }
package/rust_fetch.d.ts CHANGED
@@ -50,6 +50,12 @@ export interface DownloadProgress {
50
50
  total: number | null;
51
51
  }
52
52
 
53
+ /** Upload progress: bytes of the request body sent so far, and the body's size. */
54
+ export interface UploadProgress {
55
+ loaded: number;
56
+ total: number;
57
+ }
58
+
53
59
  export interface RetryOptions {
54
60
  /** Maximum number of retries after the first attempt. Default: 3 */
55
61
  maxRetries?: number;
@@ -93,6 +99,12 @@ export interface RequestOptions {
93
99
  signal?: AbortSignal;
94
100
  /** Called after each received chunk of a successful response */
95
101
  onDownloadProgress?: (progress: DownloadProgress) => void;
102
+ /**
103
+ * Called as the request body is sent; starts again from 0 if the request is retried.
104
+ * Browsers send these requests with `XMLHttpRequest` (cross-origin ones are preflighted).
105
+ * Can't be combined with `responseType: 'stream'`.
106
+ */
107
+ onUploadProgress?: (progress: UploadProgress) => void;
96
108
  }
97
109
 
98
110
  export interface FetchInit extends RequestOptions {
@@ -155,6 +167,70 @@ export interface RustFetchError extends Error {
155
167
 
156
168
 
157
169
 
170
+ /** Reconnection after a connection fails or drops, with jittered exponential backoff. */
171
+ export interface ReconnectOptions {
172
+ /** Reconnection attempts in a row before giving up; reset once a connection opens. Default: unlimited */
173
+ maxRetries?: number;
174
+ /** Delay before the first attempt, in ms. Default: 1000 */
175
+ initialDelayMs?: number;
176
+ /** Upper bound for any delay, in ms. Default: 30000 */
177
+ maxDelayMs?: number;
178
+ /** Exponential backoff multiplier. Default: 2 */
179
+ multiplier?: number;
180
+ }
181
+
182
+ /** What `send()` accepts; use `sendJson()` for other values. */
183
+ export type WebSocketData = string | ArrayBuffer | ArrayBufferView | Blob;
184
+
185
+ export type WebSocketState = 'connecting' | 'open' | 'reconnecting' | 'closing' | 'closed';
186
+
187
+ export interface WebSocketCloseInfo {
188
+ code: number;
189
+ reason: string;
190
+ wasClean: boolean;
191
+ }
192
+
193
+ export interface WebSocketReconnectInfo {
194
+ /** 1 for the first attempt after a failure */
195
+ attempt: number;
196
+ delayMs: number;
197
+ /** Why the previous connection failed or dropped */
198
+ error: RustFetchError;
199
+ }
200
+
201
+ export interface WebSocketOptions {
202
+ /** Subprotocols to offer */
203
+ protocols?: string | string[];
204
+ params?: QueryParams;
205
+ /**
206
+ * Handshake headers, merged over the client's headers. Node.js only: browsers don't allow
207
+ * WebSocket headers, so there this rejects with an `InvalidInputError` (client headers are skipped).
208
+ */
209
+ headers?: HeadersLike;
210
+ /** Time allowed for each connection attempt, in ms. `0` disables it. Default: the client's `timeoutMs` */
211
+ timeoutMs?: number;
212
+ /**
213
+ * Reconnect when a connection fails or drops unexpectedly (close codes 1001, 1006, 1011-1014):
214
+ * `true` for the defaults, a maximum number of attempts in a row, or options. Default: off
215
+ */
216
+ reconnect?: boolean | number | ReconnectOptions;
217
+ /** Closes the connection and stops reconnecting when aborted */
218
+ signal?: AbortSignal;
219
+ /** `auto` (default): text messages as strings, binary ones as `Uint8Array`. `json`: text messages parsed as JSON */
220
+ messageType?: 'auto' | 'json';
221
+ /** Called each time a connection opens, including after reconnecting */
222
+ onOpen?: () => void;
223
+ onMessage?: (data: any) => void;
224
+ /** Called on every failure: a failed or timed-out attempt, an unexpected drop, a message that isn't JSON */
225
+ onError?: (error: RustFetchError) => void;
226
+ /** Called before waiting to reconnect */
227
+ onReconnect?: (info: WebSocketReconnectInfo) => void;
228
+ /** Called once, when the connection is closed for good */
229
+ onClose?: (info: WebSocketCloseInfo) => void;
230
+ }
231
+
232
+
233
+
158
234
  export class IntoUnderlyingByteSource {
159
235
  private constructor();
160
236
  free(): void;
@@ -205,7 +281,7 @@ export class WasmClient {
205
281
  /**
206
282
  * Make a DELETE request
207
283
  */
208
- delete(url: string, options?: RequestOptions | null): Promise<HttpResponse>;
284
+ delete<T = any>(url: string, options?: RequestOptions | null): Promise<HttpResponse<T>>;
209
285
  /**
210
286
  * Remove an interceptor; returns whether it was registered
211
287
  */
@@ -213,15 +289,15 @@ export class WasmClient {
213
289
  /**
214
290
  * Make a request described by a `fetch`-style init object
215
291
  */
216
- fetch(url: string, init?: FetchInit | null): Promise<HttpResponse>;
292
+ fetch<T = any>(url: string, init?: FetchInit | null): Promise<HttpResponse<T>>;
217
293
  /**
218
294
  * Make a GET request
219
295
  */
220
- get(url: string, options?: RequestOptions | null): Promise<HttpResponse>;
296
+ get<T = any>(url: string, options?: RequestOptions | null): Promise<HttpResponse<T>>;
221
297
  /**
222
298
  * Make a HEAD request
223
299
  */
224
- head(url: string, options?: RequestOptions | null): Promise<HttpResponse>;
300
+ head<T = any>(url: string, options?: RequestOptions | null): Promise<HttpResponse<T>>;
225
301
  /**
226
302
  * Create a new client
227
303
  */
@@ -229,31 +305,83 @@ export class WasmClient {
229
305
  /**
230
306
  * Make a PATCH request; the body is taken from `init.body`
231
307
  */
232
- patch(url: string, init?: FetchInit | null): Promise<HttpResponse>;
308
+ patch<T = any>(url: string, init?: FetchInit | null): Promise<HttpResponse<T>>;
233
309
  /**
234
310
  * Make a PATCH request with a JSON body
235
311
  */
236
- patch_json(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse>;
312
+ patch_json<T = any>(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse<T>>;
237
313
  /**
238
314
  * Make a POST request; the body is taken from `init.body`
239
315
  */
240
- post(url: string, init?: FetchInit | null): Promise<HttpResponse>;
316
+ post<T = any>(url: string, init?: FetchInit | null): Promise<HttpResponse<T>>;
241
317
  /**
242
318
  * Make a POST request with a JSON body
243
319
  */
244
- post_json(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse>;
320
+ post_json<T = any>(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse<T>>;
245
321
  /**
246
322
  * Make a PUT request; the body is taken from `init.body`
247
323
  */
248
- put(url: string, init?: FetchInit | null): Promise<HttpResponse>;
324
+ put<T = any>(url: string, init?: FetchInit | null): Promise<HttpResponse<T>>;
249
325
  /**
250
326
  * Make a PUT request with a JSON body
251
327
  */
252
- put_json(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse>;
328
+ put_json<T = any>(url: string, body: any, options?: RequestOptions | null): Promise<HttpResponse<T>>;
253
329
  /**
254
330
  * Make a request with any method, headers and body
255
331
  */
256
- request(method: string, url: string, headers?: HeadersLike | null, body?: RequestBody | null, options?: RequestOptions | null): Promise<HttpResponse>;
332
+ request<T = any>(method: string, url: string, headers?: HeadersLike | null, body?: RequestBody | null, options?: RequestOptions | null): Promise<HttpResponse<T>>;
333
+ /**
334
+ * Open a WebSocket connection. Relative URLs are resolved against the
335
+ * base URL, with `http:` and `https:` becoming `ws:` and `wss:`.
336
+ */
337
+ websocket(url: string, options?: WebSocketOptions | null): WebSocketConnection;
338
+ }
339
+
340
+ /**
341
+ * A WebSocket connection created by `WasmClient.websocket()`
342
+ */
343
+ export class WebSocketConnection {
344
+ private constructor();
345
+ free(): void;
346
+ [Symbol.dispose](): void;
347
+ /**
348
+ * Close the connection and stop reconnecting. `code` is 1000 (the
349
+ * default) or 3000-4999; `reason` is at most 123 bytes of UTF-8.
350
+ */
351
+ close(code?: number | null, reason?: string | null): void;
352
+ /**
353
+ * Send a value as JSON text
354
+ */
355
+ sendJson(value: any): void;
356
+ /**
357
+ * Send a message. Before the connection opens (or while reconnecting),
358
+ * messages are queued and sent once it does.
359
+ */
360
+ send(data: WebSocketData): void;
361
+ /**
362
+ * Bytes queued by the socket but not yet sent
363
+ */
364
+ readonly bufferedAmount: number;
365
+ /**
366
+ * Resolves with the close code and reason once the connection is closed for good
367
+ */
368
+ readonly closed: Promise<WebSocketCloseInfo>;
369
+ /**
370
+ * Resolves when the first connection opens; rejects if it never does
371
+ */
372
+ readonly opened: Promise<void>;
373
+ /**
374
+ * The subprotocol the server selected, or `""`
375
+ */
376
+ readonly protocol: string;
377
+ /**
378
+ * `connecting`, `open`, `reconnecting`, `closing` or `closed`
379
+ */
380
+ readonly state: WebSocketState;
381
+ /**
382
+ * The connection URL (`ws:` or `wss:`)
383
+ */
384
+ readonly url: string;
257
385
  }
258
386
 
259
387
  /**
@@ -312,6 +440,7 @@ export interface InitOutput {
312
440
  readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void;
313
441
  readonly __wbg_intounderlyingsource_free: (a: number, b: number) => void;
314
442
  readonly __wbg_wasmclient_free: (a: number, b: number) => void;
443
+ readonly __wbg_websocketconnection_free: (a: number, b: number) => void;
315
444
  readonly create_client: () => [number, number, number];
316
445
  readonly fetch_json: (a: number, b: number) => any;
317
446
  readonly fetch_json_promise: (a: number, b: number) => any;
@@ -347,9 +476,20 @@ export interface InitOutput {
347
476
  readonly wasmclient_put: (a: number, b: number, c: number, d: number) => any;
348
477
  readonly wasmclient_put_json: (a: number, b: number, c: number, d: any, e: number) => any;
349
478
  readonly wasmclient_request: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => any;
479
+ readonly wasmclient_websocket: (a: number, b: number, c: number, d: number) => [number, number, number];
480
+ readonly websocketconnection_buffered_amount: (a: number) => number;
481
+ readonly websocketconnection_close: (a: number, b: number, c: number, d: number) => [number, number];
482
+ readonly websocketconnection_closed: (a: number) => any;
483
+ readonly websocketconnection_opened: (a: number) => any;
484
+ readonly websocketconnection_protocol: (a: number) => [number, number];
485
+ readonly websocketconnection_send: (a: number, b: any) => [number, number];
486
+ readonly websocketconnection_sendJson: (a: number, b: any) => [number, number];
487
+ readonly websocketconnection_state: (a: number) => any;
488
+ readonly websocketconnection_url: (a: number) => [number, number];
350
489
  readonly wasm_bindgen_6b82e471f4a4091b___convert__closures_____invoke___js_sys_6c670598d03a71fc___Function_fn_wasm_bindgen_6b82e471f4a4091b___JsValue_____wasm_bindgen_6b82e471f4a4091b___sys__Undefined___js_sys_6c670598d03a71fc___Function_fn_wasm_bindgen_6b82e471f4a4091b___JsValue_____wasm_bindgen_6b82e471f4a4091b___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
351
490
  readonly wasm_bindgen_6b82e471f4a4091b___convert__closures_____invoke___wasm_bindgen_6b82e471f4a4091b___JsValue__core_37ee0ce0c4cd57d5___result__Result_____wasm_bindgen_6b82e471f4a4091b___JsError___true_: (a: number, b: number, c: any) => [number, number];
352
491
  readonly wasm_bindgen_6b82e471f4a4091b___convert__closures_____invoke___wasm_bindgen_6b82e471f4a4091b___JsValue______true_: (a: number, b: number, c: any) => void;
492
+ readonly wasm_bindgen_6b82e471f4a4091b___convert__closures_____invoke___web_sys_697c1c02b79bdc2c___features__gen_ProgressEvent__ProgressEvent______true_: (a: number, b: number, c: any) => void;
353
493
  readonly wasm_bindgen_6b82e471f4a4091b___convert__closures_____invoke_______true_: (a: number, b: number) => void;
354
494
  readonly __wbindgen_malloc: (a: number, b: number) => number;
355
495
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
@@ -358,6 +498,7 @@ export interface InitOutput {
358
498
  readonly __wbindgen_externrefs: WebAssembly.Table;
359
499
  readonly __wbindgen_destroy_closure: (a: number, b: number) => void;
360
500
  readonly __externref_table_dealloc: (a: number) => void;
501
+ readonly __wbindgen_free: (a: number, b: number, c: number) => void;
361
502
  readonly __wbindgen_start: () => void;
362
503
  }
363
504