rust-fetch 2.1.0 → 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/README.md +464 -105
- package/llms.txt +91 -0
- package/node.js +143 -10
- package/package.json +31 -6
- package/rust_fetch.d.ts +152 -11
- package/rust_fetch.js +281 -12
- package/rust_fetch_bg.wasm +0 -0
- package/rust_fetch_bg.wasm.d.ts +13 -0
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,70 @@
|
|
|
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
|
+
|
|
47
|
+
// Node's default global agents open a socket for every request that finds no
|
|
48
|
+
// idle one, but keep only 256 of them idle afterwards. Under a burst the rest
|
|
49
|
+
// are closed and sit in TIME_WAIT, and once the ephemeral port range is used
|
|
50
|
+
// up every connect fails with EADDRNOTAVAIL. A bounded pool that keeps all of
|
|
51
|
+
// its sockets queues requests instead, reusing the same connections.
|
|
52
|
+
const MAX_SOCKETS = 256;
|
|
53
|
+
const POOL_OPTIONS = { keepAlive: true, scheduling: 'lifo', timeout: 5000, maxSockets: MAX_SOCKETS, maxFreeSockets: MAX_SOCKETS };
|
|
54
|
+
const pools = new Map([
|
|
55
|
+
[http, new http.Agent(POOL_OPTIONS)],
|
|
56
|
+
[https, new https.Agent(POOL_OPTIONS)],
|
|
57
|
+
]);
|
|
58
|
+
// The options Node's own global agents are created with. Newer versions also
|
|
59
|
+
// list unset ones (e.g. `proxyEnv: undefined` unless NODE_USE_ENV_PROXY is on).
|
|
60
|
+
const NODE_AGENT_OPTIONS = { keepAlive: true, scheduling: 'lifo', timeout: 5000, noDelay: true, defaultPort: 443, protocol: 'https:' };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The bounded pool while `globalAgent` is still Node's unbounded default;
|
|
64
|
+
* an agent the application installed (a proxy, TLS options, its own limits)
|
|
65
|
+
* is used as is.
|
|
66
|
+
*/
|
|
67
|
+
function agentFor(transport) {
|
|
68
|
+
const global = transport.globalAgent;
|
|
69
|
+
const isNodeDefault =
|
|
70
|
+
global.constructor === transport.Agent &&
|
|
71
|
+
global.maxSockets === Infinity &&
|
|
72
|
+
Object.entries(global.options).every(([key, value]) => value == null || NODE_AGENT_OPTIONS[key] === value);
|
|
73
|
+
return isNodeDefault ? pools.get(transport) : global;
|
|
74
|
+
}
|
|
75
|
+
|
|
20
76
|
const MAX_REDIRECTS = 20;
|
|
21
77
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
22
78
|
const ACCEPT_ENCODING = typeof zlib.zstdDecompressSync === 'function' ? 'gzip, deflate, br, zstd' : 'gzip, deflate, br';
|
|
@@ -117,6 +173,36 @@ function ownedBytes(bytes) {
|
|
|
117
173
|
: new Uint8Array(bytes);
|
|
118
174
|
}
|
|
119
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
|
+
|
|
120
206
|
function withoutHeaders(headers, names) {
|
|
121
207
|
const copy = { ...headers };
|
|
122
208
|
for (const name of names) delete copy[name];
|
|
@@ -126,7 +212,7 @@ function withoutHeaders(headers, names) {
|
|
|
126
212
|
function send(state, method, url, headers, body, redirects, resolve, reject) {
|
|
127
213
|
if (state.cancelled) return;
|
|
128
214
|
const transport = url.protocol === 'https:' ? https : http;
|
|
129
|
-
const request = transport.request(url, { method, headers }, (response) => {
|
|
215
|
+
const request = transport.request(url, { method, headers, agent: agentFor(transport) }, (response) => {
|
|
130
216
|
const status = response.statusCode;
|
|
131
217
|
const location = response.headers.location;
|
|
132
218
|
|
|
@@ -165,10 +251,13 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
|
|
|
165
251
|
const buffer = preallocated ? new Uint8Array(expected) : undefined;
|
|
166
252
|
let received = 0;
|
|
167
253
|
const chunks = [];
|
|
254
|
+
const onProgress = status >= 200 && status < 300 ? state.onDownloadProgress : undefined;
|
|
255
|
+
const total = Number.isSafeInteger(expected) ? expected : null;
|
|
168
256
|
response.on('data', (chunk) => {
|
|
169
257
|
if (buffer && received + chunk.length <= expected) buffer.set(chunk, received);
|
|
170
258
|
else chunks.push(chunk);
|
|
171
259
|
received += chunk.length;
|
|
260
|
+
if (onProgress) report(onProgress, received, total);
|
|
172
261
|
});
|
|
173
262
|
response.on('error', reject);
|
|
174
263
|
// A socket closed mid-body may emit 'close' without 'end' (a settled promise ignores this)
|
|
@@ -192,13 +281,13 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
|
|
|
192
281
|
});
|
|
193
282
|
});
|
|
194
283
|
request.on('error', reject);
|
|
195
|
-
request.
|
|
284
|
+
writeBody(request, body, state.onUploadProgress);
|
|
196
285
|
state.request = request;
|
|
197
286
|
}
|
|
198
287
|
|
|
199
288
|
/**
|
|
200
289
|
* One attempt, called by the Rust core with
|
|
201
|
-
* `{ method, url, headers, body?, timeout?, bodyType }`. Resolves to the
|
|
290
|
+
* `{ method, url, headers, body?, timeout?, bodyType, onUploadProgress?, onDownloadProgress? }`. Resolves to the
|
|
202
291
|
* finished `{ status, statusText, ok, url, headers, body }` (the core fills
|
|
203
292
|
* in statusText). Rejects with code RUST_FETCH_TIMEOUT or RUST_FETCH_PARSE,
|
|
204
293
|
* or any other error for network failures.
|
|
@@ -208,7 +297,16 @@ function send(state, method, url, headers, body, redirects, resolve, reject) {
|
|
|
208
297
|
* AbortSignal into every request.
|
|
209
298
|
*/
|
|
210
299
|
function nodeTransport(request) {
|
|
211
|
-
|
|
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 };
|
|
212
310
|
let timer;
|
|
213
311
|
const promise = new Promise((resolve, reject) => {
|
|
214
312
|
if (request.timeout > 0) {
|
|
@@ -225,16 +323,11 @@ function nodeTransport(request) {
|
|
|
225
323
|
clearTimeout(timer);
|
|
226
324
|
return reject(error);
|
|
227
325
|
}
|
|
228
|
-
const headers = { ...request.headers };
|
|
229
|
-
headers['accept'] ??= '*/*';
|
|
230
|
-
headers['accept-encoding'] ??= ACCEPT_ENCODING;
|
|
231
|
-
headers['user-agent'] ??= 'rust-fetch';
|
|
232
|
-
if (request.body) headers['content-length'] ??= String(request.body.length);
|
|
233
326
|
const settle = (fn) => (value) => {
|
|
234
327
|
clearTimeout(timer);
|
|
235
328
|
fn(value);
|
|
236
329
|
};
|
|
237
|
-
send(state, request.method, url,
|
|
330
|
+
send(state, request.method, url, withDefaultHeaders(request), request.body, 0, settle(resolve), settle(reject));
|
|
238
331
|
});
|
|
239
332
|
const cancel = () => {
|
|
240
333
|
state.cancelled = true;
|
|
@@ -251,6 +344,46 @@ function nodeTransport(request) {
|
|
|
251
344
|
return finished;
|
|
252
345
|
}
|
|
253
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
|
+
|
|
254
387
|
/** The response object handed to the Rust core (which fills in statusText) */
|
|
255
388
|
function finishResponse({ status, url, headers, body }, bodyType) {
|
|
256
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": "
|
|
8
|
-
"version": "2.
|
|
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
|
-
"
|
|
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
|
|