@d1g1tal/transportr 2.2.0 → 3.0.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/CHANGELOG.md +304 -0
- package/README.md +846 -491
- package/dist/{media-types.d.ts → content-type.d.ts} +2 -2
- package/dist/{media-types.js → content-type.js} +2 -2
- package/dist/content-type.js.map +7 -0
- package/dist/{headers.d.ts → request-header.d.ts} +2 -2
- package/dist/{headers.js → request-header.js} +2 -2
- package/dist/request-header.js.map +7 -0
- package/dist/{methods.d.ts → request-method.d.ts} +3 -3
- package/dist/{methods.js → request-method.js} +2 -2
- package/dist/request-method.js.map +7 -0
- package/dist/{response-headers.d.ts → response-header.d.ts} +2 -2
- package/dist/{response-headers.js → response-header.js} +2 -2
- package/dist/response-header.js.map +7 -0
- package/dist/transportr.d.ts +368 -282
- package/dist/transportr.js +7 -2
- package/dist/transportr.js.map +4 -4
- package/package.json +19 -19
- package/dist/headers.js.map +0 -7
- package/dist/media-types.js.map +0 -7
- package/dist/methods.js.map +0 -7
- package/dist/response-headers.js.map +0 -7
package/README.md
CHANGED
|
@@ -1,491 +1,846 @@
|
|
|
1
|
-
# transportr
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@d1g1tal/transportr)
|
|
4
|
-
[](https://www.npmjs.com/package/@d1g1tal/transportr)
|
|
5
|
-
[](https://github.com/D1g1talEntr0py/transportr/actions/workflows/ci.yml)
|
|
6
|
-
[](https://codecov.io/gh/D1g1talEntr0py/transportr)
|
|
7
|
-
[](https://github.com/D1g1talEntr0py/transportr/blob/main/LICENSE)
|
|
8
|
-
[](https://nodejs.org)
|
|
9
|
-
[
|
|
17
|
-
- **Abort & timeout management** — Per-request timeouts, `AbortController` integration, and `abortAll()` for cleanup
|
|
18
|
-
- **Event-driven** — Global and instance-level lifecycle events (`configured`, `success`, `error`, `complete`, etc.)
|
|
19
|
-
- **Retry logic** — Configurable retry with exponential backoff, status code filtering, and method filtering
|
|
20
|
-
- **Request deduplication** — Identical in-flight GET/HEAD requests share a single fetch
|
|
21
|
-
- **Lifecycle hooks** — `beforeRequest`, `afterResponse`, `beforeError` hooks at global, instance, and per-request levels
|
|
22
|
-
- **XSRF/CSRF protection** — Automatic cookie-to-header token injection
|
|
23
|
-
- **HTML selectors** — Extract specific elements from HTML responses with CSS selectors
|
|
24
|
-
- **FormData auto-detection** — Automatically handles FormData, Blob, ArrayBuffer, and stream bodies
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
Transportr.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
await api.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
Transportr.
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
1
|
+
# transportr
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@d1g1tal/transportr)
|
|
4
|
+
[](https://www.npmjs.com/package/@d1g1tal/transportr)
|
|
5
|
+
[](https://github.com/D1g1talEntr0py/transportr/actions/workflows/ci.yml)
|
|
6
|
+
[](https://codecov.io/gh/D1g1talEntr0py/transportr)
|
|
7
|
+
[](https://github.com/D1g1talEntr0py/transportr/blob/main/LICENSE)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
A TypeScript Fetch API wrapper providing type-safe HTTP requests with advanced abort/timeout handling, event-driven architecture, and automatic content-type based response processing.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Type-safe** — Full TypeScript support with strict types, branded JSON strings, and typed headers
|
|
16
|
+
- **Automatic response handling** — Content-type based response parsing (JSON, HTML, XML, images, streams, etc.)
|
|
17
|
+
- **Abort & timeout management** — Per-request timeouts, `AbortController` integration, and `abortAll()` for cleanup
|
|
18
|
+
- **Event-driven** — Global and instance-level lifecycle events (`configured`, `success`, `error`, `complete`, etc.)
|
|
19
|
+
- **Retry logic** — Configurable retry with exponential backoff, status code filtering, and method filtering
|
|
20
|
+
- **Request deduplication** — Identical in-flight GET/HEAD requests share a single fetch
|
|
21
|
+
- **Lifecycle hooks** — `beforeRequest`, `afterResponse`, `beforeError` hooks at global, instance, and per-request levels
|
|
22
|
+
- **XSRF/CSRF protection** — Automatic cookie-to-header token injection
|
|
23
|
+
- **HTML selectors** — Extract specific elements from HTML responses with CSS selectors
|
|
24
|
+
- **FormData auto-detection** — Automatically handles FormData, Blob, ArrayBuffer, and stream bodies
|
|
25
|
+
- **Streaming** — SSE (`getEventStream`) and NDJSON (`getJsonStream`) as `AsyncIterable`
|
|
26
|
+
- **Progress tracking** — Download and upload progress callbacks with loaded/total/percentage
|
|
27
|
+
- **Safe results** — `unwrap: false` option returns `Result<T>` tuples instead of throwing
|
|
28
|
+
- **Concurrent helpers** — `Transportr.all()` and `Transportr.race()` with auto-abort for race losers
|
|
29
|
+
|
|
30
|
+
## Why Transportr?
|
|
31
|
+
|
|
32
|
+
The HTTP client space in JavaScript/TypeScript is crowded. Here's where Transportr fits and why it may be the right choice for your project.
|
|
33
|
+
|
|
34
|
+
### The Competition at a Glance
|
|
35
|
+
|
|
36
|
+
| Library | Minified | Gzipped | Engine | Request API | Philosophy |
|
|
37
|
+
|---------|----------|---------|--------|-------------|------------|
|
|
38
|
+
| axios | 35.4 kB | 13.9 kB | ≥ 10 | XMLHttpRequest / http | Kitchen-sink, XHR-based |
|
|
39
|
+
| ky | 13.7 kB | 4.9 kB | ≥ 18 | Fetch | Tiny fetch wrapper, browser-first |
|
|
40
|
+
| ofetch | 9.3 kB | 3.8 kB | ≥ 18 | Fetch | Universal, minimal API |
|
|
41
|
+
| wretch | 4.8 kB | 1.9 kB | ≥ 14 | Fetch | Fluent-chain, middleware-based |
|
|
42
|
+
| got | — | ~43 kB | ≥ 18 | http/https | Node-only, feature-rich |
|
|
43
|
+
| transportr | 27 kB | 8.5 kB* | ≥ 20 | Fetch | Content-type-aware, event-driven |
|
|
44
|
+
|
|
45
|
+
Sizes from [Bundlephobia](https://bundlephobia.com). \*Transportr bundles `@d1g1tal/media-type`, `@d1g1tal/subscribr`, and DOMPurify. Optional `jsdom` peer dependency is **not** included — only needed for HTML/XML/DOM features in Node.js.
|
|
46
|
+
|
|
47
|
+
### What Every Fetch Wrapper Gives You
|
|
48
|
+
|
|
49
|
+
All of these libraries wrap `fetch` and add roughly the same core set of features:
|
|
50
|
+
|
|
51
|
+
- JSON body serialization + parsing
|
|
52
|
+
- Error on non-2xx responses
|
|
53
|
+
- Request timeout
|
|
54
|
+
- Base URL configuration
|
|
55
|
+
- TypeScript types
|
|
56
|
+
|
|
57
|
+
If you only need those basics, any of them will do. ky at 3.5 kB is a perfectly fine choice.
|
|
58
|
+
|
|
59
|
+
### What Only Transportr Gives You
|
|
60
|
+
|
|
61
|
+
#### 1. Content-Type-Aware Response Handling
|
|
62
|
+
|
|
63
|
+
No other HTTP client knows *what it fetched* and processes it accordingly. Transportr maps response `Content-Type` directly to strongly-typed return values:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const api = new Transportr('https://example.com');
|
|
67
|
+
|
|
68
|
+
// Returns Promise<Json> — parsed and typed
|
|
69
|
+
const data = await api.getJson('/api/users');
|
|
70
|
+
|
|
71
|
+
// Returns Promise<Document> — fully parsed, DOMPurify-sanitized HTML
|
|
72
|
+
const page = await api.getHtml('/page');
|
|
73
|
+
|
|
74
|
+
// Returns Promise<Document> — sanitized and parsed XML
|
|
75
|
+
const feed = await api.getXml('/feed.xml');
|
|
76
|
+
|
|
77
|
+
// Returns Promise<DocumentFragment> — isolated fragment, no full document
|
|
78
|
+
const fragment = await api.getHtmlFragment('/partial');
|
|
79
|
+
|
|
80
|
+
// Returns Promise<void> — script fetched, verified, injected into <head>
|
|
81
|
+
await api.getScript('https://cdn.example.com/widget.js');
|
|
82
|
+
|
|
83
|
+
// Returns Promise<void> — stylesheet fetched and injected into <head>
|
|
84
|
+
await api.getStylesheet('https://cdn.example.com/theme.css');
|
|
85
|
+
|
|
86
|
+
// Returns Promise<HTMLImageElement> — decoded, memory-safe
|
|
87
|
+
const img = await api.getImage('/assets/photo.webp');
|
|
88
|
+
|
|
89
|
+
// Returns Promise<ReadableStream> — for large payloads
|
|
90
|
+
const stream = await api.getStream('/export/data.csv');
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The type system enforces what you get back. You can't accidentally call `.querySelector()` on a JSON response.
|
|
94
|
+
|
|
95
|
+
#### 2. Automatic DOMPurify Sanitization
|
|
96
|
+
|
|
97
|
+
`getHtml()`, `getXml()`, and `getHtmlFragment()` sanitize the response through **DOMPurify before parsing**. You don't have to remember to sanitize — it's built into the transport layer.
|
|
98
|
+
|
|
99
|
+
This is the correct place to sanitize: as close to the network boundary as possible, before the content ever reaches a parser or your application code.
|
|
100
|
+
|
|
101
|
+
#### 3. HTML Selector Extraction
|
|
102
|
+
|
|
103
|
+
Fetch a page and get back exactly the element you want, not the whole document:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Returns Promise<Element | null>
|
|
107
|
+
const nav = await api.getHtml('/page', {}, 'nav.main');
|
|
108
|
+
const price = await api.getHtmlFragment('/product', {}, '.price');
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Useful for partial page loading, widget hydration, and scraping structured content without a separate HTML parsing step.
|
|
112
|
+
|
|
113
|
+
#### 4. Script & Stylesheet Injection with Cleanup
|
|
114
|
+
|
|
115
|
+
`getScript()` and `getStylesheet()` use `URL.createObjectURL()` to inject remote assets, then automatically revoke the object URL after load/error. No memory leaks, no dangling blob URLs:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Loads, verifies, injects, and cleans up automatically
|
|
119
|
+
await api.getScript('https://partner.example.com/sdk.js');
|
|
120
|
+
await api.getStylesheet('https://cdn.example.com/theme.css');
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This is a real pattern for micro-frontend loaders and dynamic plugin systems. No other HTTP client handles it.
|
|
124
|
+
|
|
125
|
+
#### 5. Full Lifecycle Event System
|
|
126
|
+
|
|
127
|
+
Transportr has a two-tier event system (global + per-instance) with a defined lifecycle:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
configured → success | error | aborted | timeout → complete → all-complete
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Register handlers globally (across all instances) or per-instance:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// All requests, all instances
|
|
137
|
+
Transportr.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
|
|
138
|
+
analytics.track('api_success', { url: data.url });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Just this instance
|
|
142
|
+
api.register(Transportr.RequestEvent.TIMEOUT, (event, error) => {
|
|
143
|
+
toast.error('Request timed out');
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
ky has hooks. ofetch has hooks. Neither has a persistent, named event system you can subscribe to and unsubscribe from independently.
|
|
148
|
+
|
|
149
|
+
#### 6. Request Deduplication
|
|
150
|
+
|
|
151
|
+
Identical in-flight GET/HEAD requests share a single fetch. Each caller receives a cloned `Response`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Only one network request — both get independent Response clones
|
|
155
|
+
const [a, b] = await Promise.all([
|
|
156
|
+
api.get('/config', { dedupe: true }),
|
|
157
|
+
api.get('/config', { dedupe: true }),
|
|
158
|
+
]);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This is commonly left to userland caching or state management. Transportr makes it a first-class option.
|
|
162
|
+
|
|
163
|
+
#### 7. `abortAll()` for Clean Teardown
|
|
164
|
+
|
|
165
|
+
Cancel every in-flight request across all instances in one call:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Route change, unmount, logout — kill everything
|
|
169
|
+
Transportr.abortAll();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Useful in SPAs for route transitions or session expiry. ky and ofetch require manual `AbortController` management per-request.
|
|
173
|
+
|
|
174
|
+
#### 8. Structured Hook Layers
|
|
175
|
+
|
|
176
|
+
Hooks run in a deterministic order: **global → instance → per-request**. This lets you separate concerns cleanly:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
Transportr.addHooks({ beforeRequest: [addRequestId] }); // Always runs
|
|
180
|
+
api.addHooks({ afterResponse: [logLatency] }); // Runs for this API
|
|
181
|
+
await api.get('/data', { hooks: { beforeError: [notify] } }); // Only this call
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Feature Comparison
|
|
185
|
+
|
|
186
|
+
| Feature | transportr | ky | ofetch | wretch | axios |
|
|
187
|
+
|---------|:----------:|:--:|:------:|:------:|:-----:|
|
|
188
|
+
| JSON request/response | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
189
|
+
| TypeScript (first-class) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
190
|
+
| Timeout | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
191
|
+
| Retry with backoff | ✅ | ✅ | ✅ | ✅ | ⚠️ plugin |
|
|
192
|
+
| SSE / NDJSON streaming | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
193
|
+
| Download / upload progress | ✅ | ✅ | ❌ | ❌ | ✅ |
|
|
194
|
+
| Safe result tuples | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
195
|
+
| Concurrent helpers with auto-abort | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
196
|
+
| Request deduplication | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
197
|
+
| Abort all in-flight | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
198
|
+
| Lifecycle event system | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
199
|
+
| HTML response → `Document` | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
200
|
+
| XML response → `Document` | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
201
|
+
| HTML fragment with selector | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
202
|
+
| Auto DOMPurify sanitization | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
203
|
+
| Script injection + cleanup | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
204
|
+
| Stylesheet injection + cleanup | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
205
|
+
| Image → `HTMLImageElement` | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
206
|
+
| XSRF/CSRF protection | ✅ | ❌ | ❌ | ⚠️ plugin | ✅ |
|
|
207
|
+
| `beforeRequest` hooks | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
208
|
+
| `afterResponse` hooks | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
209
|
+
| Global + instance hook layers | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
210
|
+
| Branded JSON types | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
211
|
+
| Custom content-type handlers | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
212
|
+
|
|
213
|
+
### When to Choose Transportr
|
|
214
|
+
|
|
215
|
+
**Choose Transportr if you are building:**
|
|
216
|
+
|
|
217
|
+
- A **micro-frontend loader** that fetches and injects remote scripts or stylesheets
|
|
218
|
+
- An **SSR/ISR application** that fetches HTML partials and extracts fragments server-side
|
|
219
|
+
- A **content aggregator** that deals with mixed response types (JSON, HTML, XML, images)
|
|
220
|
+
- A **dashboard or scraper** that parses HTML with CSS selectors and needs built-in sanitization
|
|
221
|
+
- An application where **abort-all on route change** or **request deduplication** are requirements
|
|
222
|
+
- A project that wants **typed lifecycle events** rather than ad-hoc error handling
|
|
223
|
+
|
|
224
|
+
**Choose ky or ofetch if you are building:**
|
|
225
|
+
|
|
226
|
+
- A pure JSON API client where bundle size is the primary constraint
|
|
227
|
+
- A project that has no DOM-manipulation requirements
|
|
228
|
+
- A project that already has its own event/observability layer
|
|
229
|
+
|
|
230
|
+
## Requirements
|
|
231
|
+
|
|
232
|
+
- **Node.js** ≥ 20.0.0 or a modern browser with native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and `AbortController` support
|
|
233
|
+
- `jsdom` is an **optional peer dependency** — only needed for HTML/XML/DOM features in Node.js. Install it separately if you use `getHtml()`, `getXml()`, `getHtmlFragment()`, `getScript()`, `getStylesheet()`, or `getImage()` in a non-browser environment:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pnpm add jsdom
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Installation
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# With pnpm:
|
|
243
|
+
pnpm add @d1g1tal/transportr
|
|
244
|
+
|
|
245
|
+
# Or with npm:
|
|
246
|
+
npm install @d1g1tal/transportr
|
|
247
|
+
|
|
248
|
+
# Or with yarn
|
|
249
|
+
yarn add @d1g1tal/transportr
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Quick Start
|
|
253
|
+
|
|
254
|
+
Only the main module is required — the submodule constants (`RequestHeader`, `ContentType`, etc.) are optional conveniences. Anywhere a constant is used, a plain string works just as well.
|
|
255
|
+
|
|
256
|
+
Out of the box, every instance defaults to:
|
|
257
|
+
- `Content-Type: application/json; charset=utf-8`
|
|
258
|
+
- `Accept: application/json; charset=utf-8`
|
|
259
|
+
- `timeout`: 30 000 ms
|
|
260
|
+
- `cache`: `no-store`
|
|
261
|
+
- `credentials`: `same-origin`
|
|
262
|
+
- `mode`: `cors`
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
266
|
+
|
|
267
|
+
const api = new Transportr('https://api.example.com');
|
|
268
|
+
|
|
269
|
+
// GET JSON — default Accept header is already application/json
|
|
270
|
+
const data = await api.getJson('/users/1');
|
|
271
|
+
|
|
272
|
+
// POST with JSON body — automatically serialized, no Content-Type needed
|
|
273
|
+
const created = await api.post('/users', { body: { name: 'Alice' } });
|
|
274
|
+
|
|
275
|
+
// GET with search params
|
|
276
|
+
const results = await api.getJson('/search', { searchParams: { q: 'term', page: 1 } });
|
|
277
|
+
|
|
278
|
+
// Typed response using generics
|
|
279
|
+
interface User { id: number; name: string; }
|
|
280
|
+
const user = await api.get<User>('/users/1');
|
|
281
|
+
|
|
282
|
+
// Plain strings work anywhere — constants are just for convenience
|
|
283
|
+
const api2 = new Transportr('https://api.example.com', {
|
|
284
|
+
headers: { 'authorization': 'Bearer token', 'accept-language': 'en-US' }
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Browser / CDN Usage
|
|
289
|
+
|
|
290
|
+
The package is published as pure ESM and works directly in modern browsers — no bundler required. All dependencies (`@d1g1tal/media-type`, `@d1g1tal/subscribr`, DOMPurify) are bundled into the output, so there are no external module URLs to manage. `jsdom` is not needed in a browser environment.
|
|
291
|
+
|
|
292
|
+
### With an import map (recommended)
|
|
293
|
+
|
|
294
|
+
An [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap) mirrors the package's named submodule exports and keeps your code identical to the Node.js form — use bare specifiers exactly as you would in a bundled project:
|
|
295
|
+
|
|
296
|
+
```html
|
|
297
|
+
<script type="importmap">
|
|
298
|
+
{
|
|
299
|
+
"imports": {
|
|
300
|
+
"@d1g1tal/transportr": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/transportr.js",
|
|
301
|
+
"@d1g1tal/transportr/request-header": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-header.js",
|
|
302
|
+
"@d1g1tal/transportr/request-method": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-method.js",
|
|
303
|
+
"@d1g1tal/transportr/content-type": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/content-type.js",
|
|
304
|
+
"@d1g1tal/transportr/response-header": "https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/response-header.js"
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
</script>
|
|
308
|
+
|
|
309
|
+
<script type="module">
|
|
310
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
311
|
+
import { RequestHeader } from '@d1g1tal/transportr/request-header';
|
|
312
|
+
import { RequestMethod } from '@d1g1tal/transportr/request-method';
|
|
313
|
+
import { ContentType } from '@d1g1tal/transportr/content-type';
|
|
314
|
+
import { ResponseHeader } from '@d1g1tal/transportr/response-header';
|
|
315
|
+
|
|
316
|
+
const api = new Transportr('https://api.example.com', {
|
|
317
|
+
headers: {
|
|
318
|
+
[RequestHeader.AUTHORIZATION]: 'Bearer token',
|
|
319
|
+
[RequestHeader.ACCEPT]: ContentType.JSON
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const data = await api.getJson('/users/1');
|
|
324
|
+
console.log(data);
|
|
325
|
+
</script>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Without an import map
|
|
329
|
+
|
|
330
|
+
The CDN resolves the `"."` entry in `exports` automatically, so no explicit file path is needed for the main module. Submodules use their CDN paths directly:
|
|
331
|
+
|
|
332
|
+
```html
|
|
333
|
+
<script type="module">
|
|
334
|
+
import { Transportr } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr';
|
|
335
|
+
import { RequestHeader } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/request-header.js';
|
|
336
|
+
import { ContentType } from 'https://cdn.jsdelivr.net/npm/@d1g1tal/transportr/dist/content-type.js';
|
|
337
|
+
|
|
338
|
+
const api = new Transportr('https://api.example.com', {
|
|
339
|
+
headers: {
|
|
340
|
+
[RequestHeader.AUTHORIZATION]: 'Bearer token',
|
|
341
|
+
[RequestHeader.ACCEPT]: ContentType.JSON
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const data = await api.getJson('/users/1');
|
|
346
|
+
console.log(data);
|
|
347
|
+
</script>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Import map support is available in all browsers covered by this project's `browserslist` configuration (Chrome 89+, Firefox 108+, Safari 16.4+).
|
|
351
|
+
|
|
352
|
+
## Migrating from v2
|
|
353
|
+
|
|
354
|
+
v3.0 contains breaking renames plus new features. The renames are all find-and-replace — no behavior changed.
|
|
355
|
+
|
|
356
|
+
### Submodule paths & exported symbols
|
|
357
|
+
|
|
358
|
+
| Before (v2) | After (v3) |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `@d1g1tal/transportr/headers` | `@d1g1tal/transportr/request-header` |
|
|
361
|
+
| `@d1g1tal/transportr/methods` | `@d1g1tal/transportr/request-method` |
|
|
362
|
+
| `@d1g1tal/transportr/media-types` | `@d1g1tal/transportr/content-type` |
|
|
363
|
+
| `@d1g1tal/transportr/response-headers` | `@d1g1tal/transportr/response-header` |
|
|
364
|
+
| `HttpRequestHeader` | `RequestHeader` |
|
|
365
|
+
| `HttpRequestMethod` | `RequestMethod` |
|
|
366
|
+
| `HttpMediaType` | `ContentType` |
|
|
367
|
+
| `HttpResponseHeader` | `ResponseHeader` |
|
|
368
|
+
|
|
369
|
+
### Static properties on `Transportr`
|
|
370
|
+
|
|
371
|
+
| Before (v2) | After (v3) |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `Transportr.RequestEvents` | `Transportr.RequestEvent` |
|
|
374
|
+
| `Transportr.RequestModes` | `Transportr.RequestMode` |
|
|
375
|
+
| `Transportr.RequestPriorities` | `Transportr.RequestPriority` |
|
|
376
|
+
| `Transportr.RedirectPolicies` | `Transportr.RedirectPolicy` |
|
|
377
|
+
|
|
378
|
+
### New in v3
|
|
379
|
+
|
|
380
|
+
- **Streaming responses** — `getEventStream()` returns an `AsyncIterable<ServerSentEvent>` for SSE endpoints; `getJsonStream<T>()` returns an `AsyncIterable<T>` for NDJSON feeds.
|
|
381
|
+
- **Progress tracking** — `onDownloadProgress` and `onUploadProgress` callbacks in request options provide `{ loaded, total, percentage }` updates.
|
|
382
|
+
- **Safe results** — Pass `unwrap: false` per-request or in the constructor to get `Result<T>` tuples (`[true, data]` or `[false, error]`) instead of thrown errors.
|
|
383
|
+
- **Concurrent helpers** — `Transportr.all()` for parallel requests; `Transportr.race()` races requests and auto-aborts the losers.
|
|
384
|
+
- **Typed events** — `register()` now narrows the event data type based on the event name.
|
|
385
|
+
|
|
386
|
+
## API
|
|
387
|
+
|
|
388
|
+
### Constructor
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
new Transportr(url?: URL | string | RequestOptions, options?: RequestOptions)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Creates a new instance. When `url` is omitted, defaults to `globalThis.location.origin`.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// With base URL
|
|
398
|
+
const api = new Transportr('https://api.example.com/v2');
|
|
399
|
+
|
|
400
|
+
// With URL and default options
|
|
401
|
+
const api = new Transportr('https://api.example.com', {
|
|
402
|
+
timeout: 10000,
|
|
403
|
+
headers: { 'Authorization': 'Bearer token' }
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// With options only (uses current origin)
|
|
407
|
+
const api = new Transportr({ timeout: 5000 });
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Updating Instance Options
|
|
411
|
+
|
|
412
|
+
Update any option that was set at construction time, without creating a new instance. `configure()` accepts the same shape as the constructor options. Headers and searchParams are **merged** onto existing defaults; all other options **overwrite** the current value; hooks are appended.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
const api = new Transportr('https://api.example.com', {
|
|
416
|
+
timeout: 30000,
|
|
417
|
+
credentials: 'same-origin'
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// After login — inject auth token and tighten timeout
|
|
421
|
+
api.configure({
|
|
422
|
+
timeout: 10000,
|
|
423
|
+
credentials: 'include',
|
|
424
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Add default search params for all subsequent requests
|
|
428
|
+
api.configure({ searchParams: { version: '2', locale: 'en' } });
|
|
429
|
+
|
|
430
|
+
// Chainable
|
|
431
|
+
api
|
|
432
|
+
.configure({ timeout: 5000 })
|
|
433
|
+
.configure({ headers: { 'X-Tenant': 'acme' } })
|
|
434
|
+
.addHooks({ beforeRequest: [logHook] });
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Request Methods
|
|
438
|
+
|
|
439
|
+
| Method | Description |
|
|
440
|
+
|--------|-------------|
|
|
441
|
+
| `get(path?, options?)` | GET request with auto content-type handling |
|
|
442
|
+
| `post(path?, options?)` | POST request |
|
|
443
|
+
| `put(path?, options?)` | PUT request |
|
|
444
|
+
| `patch(path?, options?)` | PATCH request |
|
|
445
|
+
| `delete(path?, options?)` | DELETE request |
|
|
446
|
+
| `head(path?, options?)` | HEAD request |
|
|
447
|
+
| `options(path?, options?)` | OPTIONS request (returns allowed methods) |
|
|
448
|
+
| `request(path?, options?)` | Raw request returning `TypedResponse<T>` |
|
|
449
|
+
|
|
450
|
+
### Typed Response Methods
|
|
451
|
+
|
|
452
|
+
| Method | Returns | Accept Header |
|
|
453
|
+
|--------|---------|---------------|
|
|
454
|
+
| `getJson(path?, options?)` | `Json` | `application/json` |
|
|
455
|
+
| `getHtml(path?, options?, selector?)` | `Document \| Element` | `text/html` |
|
|
456
|
+
| `getHtmlFragment(path?, options?, selector?)` | `DocumentFragment \| Element` | `text/html` |
|
|
457
|
+
| `getXml(path?, options?)` | `Document` | `application/xml` |
|
|
458
|
+
| `getScript(path?, options?)` | `void` (injected into DOM) | `application/javascript` |
|
|
459
|
+
| `getStylesheet(path?, options?)` | `void` (injected into DOM) | `text/css` |
|
|
460
|
+
| `getBlob(path?, options?)` | `Blob` | `application/octet-stream` |
|
|
461
|
+
| `getImage(path?, options?)` | `HTMLImageElement` | `image/*` |
|
|
462
|
+
| `getBuffer(path?, options?)` | `ArrayBuffer` | `application/octet-stream` |
|
|
463
|
+
| `getStream(path?, options?)` | `ReadableStream` | `application/octet-stream` |
|
|
464
|
+
| `getEventStream(path?, options?)` | `AsyncIterable<ServerSentEvent>` | `text/event-stream` |
|
|
465
|
+
| `getJsonStream<T>(path?, options?)` | `AsyncIterable<T>` | `application/x-ndjson` |
|
|
466
|
+
|
|
467
|
+
### Request Options
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
type RequestOptions = {
|
|
471
|
+
headers?: RequestHeaders;
|
|
472
|
+
searchParams?: URLSearchParams | string | Record<string, string | number | boolean>;
|
|
473
|
+
timeout?: number; // Default: 30000ms
|
|
474
|
+
global?: boolean; // Emit global events (default: true)
|
|
475
|
+
body?: BodyInit | JsonObject; // Auto-serialized for JSON content-type
|
|
476
|
+
retry?: number | RetryOptions;
|
|
477
|
+
dedupe?: boolean; // Deduplicate identical GET/HEAD requests
|
|
478
|
+
xsrf?: boolean | XsrfOptions;
|
|
479
|
+
hooks?: HooksOptions;
|
|
480
|
+
unwrap?: boolean; // false → return Result<T> tuple instead of throwing
|
|
481
|
+
onDownloadProgress?: (progress: DownloadProgress) => void;
|
|
482
|
+
onUploadProgress?: (progress: DownloadProgress) => void;
|
|
483
|
+
// ...all standard RequestInit properties (cache, credentials, mode, etc.)
|
|
484
|
+
};
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Retry
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
// Simple: retry up to 3 times with default settings
|
|
491
|
+
await api.get('/data', { retry: 3 });
|
|
492
|
+
|
|
493
|
+
// Advanced configuration
|
|
494
|
+
await api.get('/data', {
|
|
495
|
+
retry: {
|
|
496
|
+
limit: 3,
|
|
497
|
+
statusCodes: [408, 413, 429, 500, 502, 503, 504],
|
|
498
|
+
methods: ['GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'],
|
|
499
|
+
delay: 300, // ms before first retry
|
|
500
|
+
backoffFactor: 2 // exponential backoff multiplier
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Request Deduplication
|
|
506
|
+
|
|
507
|
+
When `dedupe: true`, identical in-flight GET/HEAD requests share a single fetch call. Each consumer receives a cloned response.
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
// Only one fetch call is made
|
|
511
|
+
const [a, b] = await Promise.all([
|
|
512
|
+
api.get('/data', { dedupe: true }),
|
|
513
|
+
api.get('/data', { dedupe: true })
|
|
514
|
+
]);
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Lifecycle Hooks
|
|
518
|
+
|
|
519
|
+
Hooks run in order: global → instance → per-request.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// Global hooks (all instances)
|
|
523
|
+
Transportr.addHooks({
|
|
524
|
+
beforeRequest: [async (options, url) => {
|
|
525
|
+
options.headers.set('X-Request-ID', crypto.randomUUID());
|
|
526
|
+
return options;
|
|
527
|
+
}],
|
|
528
|
+
afterResponse: [async (response, options) => response],
|
|
529
|
+
beforeError: [(error) => error]
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Instance hooks
|
|
533
|
+
api.addHooks({
|
|
534
|
+
afterResponse: [async (response) => {
|
|
535
|
+
console.log(`Response: ${response.status}`);
|
|
536
|
+
return response;
|
|
537
|
+
}]
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// Per-request hooks
|
|
541
|
+
await api.get('/data', {
|
|
542
|
+
hooks: { beforeRequest: [async (opts) => opts] }
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Events
|
|
547
|
+
|
|
548
|
+
```typescript
|
|
549
|
+
// Global events (all instances)
|
|
550
|
+
const reg = Transportr.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
|
|
551
|
+
console.log('Request succeeded:', data);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Instance events
|
|
555
|
+
const reg = api.register(Transportr.RequestEvent.ERROR, (event, error) => {
|
|
556
|
+
console.error('Request failed:', error);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// Unregister
|
|
560
|
+
api.unregister(reg); // Returns `this` for chaining
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
**Event lifecycle**: `configured` → `success | error | aborted | timeout` → `complete` → `all-complete`
|
|
564
|
+
|
|
565
|
+
Additional events: `retry` (emitted on each retry attempt)
|
|
566
|
+
|
|
567
|
+
### Error Handling
|
|
568
|
+
|
|
569
|
+
Non-2xx responses throw an error with `name === 'HttpError'`. Aborted and timed-out requests also produce an `HttpError` with synthetic status codes.
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
import type { HttpError } from '@d1g1tal/transportr';
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const user = await api.getJson('/users/1');
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (error instanceof Error && error.name === 'HttpError') {
|
|
578
|
+
const httpError = error as unknown as HttpError;
|
|
579
|
+
console.error(httpError.statusCode); // HTTP status code
|
|
580
|
+
console.error(httpError.statusText); // HTTP status text
|
|
581
|
+
console.error(httpError.entity); // parsed response body (if any)
|
|
582
|
+
console.error(httpError.url?.href); // request URL
|
|
583
|
+
console.error(httpError.method); // HTTP method used
|
|
584
|
+
console.error(httpError.timing); // { start, end, duration } in ms
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Synthetic status codes for non-HTTP failures:**
|
|
590
|
+
|
|
591
|
+
| Code | Text | Cause |
|
|
592
|
+
|------|------|-------|
|
|
593
|
+
| `499` | `Aborted` | Cancelled via `controller.abort()` or `Transportr.abortAll()` |
|
|
594
|
+
| `504` | `Request Timeout` | `timeout` option exceeded |
|
|
595
|
+
|
|
596
|
+
### Abort & Timeout
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
// Per-request timeout
|
|
600
|
+
await api.get('/slow', { timeout: 5000 });
|
|
601
|
+
|
|
602
|
+
// Manual abort via AbortController
|
|
603
|
+
const controller = new AbortController();
|
|
604
|
+
api.get('/data', { signal: controller.signal });
|
|
605
|
+
controller.abort();
|
|
606
|
+
|
|
607
|
+
// Abort all in-flight requests
|
|
608
|
+
Transportr.abortAll();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### XSRF/CSRF Protection
|
|
612
|
+
|
|
613
|
+
```typescript
|
|
614
|
+
// Default: reads 'XSRF-TOKEN' cookie, sets 'X-XSRF-TOKEN' header
|
|
615
|
+
await api.post('/data', { body: payload, xsrf: true });
|
|
616
|
+
|
|
617
|
+
// Custom cookie/header names
|
|
618
|
+
await api.post('/data', {
|
|
619
|
+
body: payload,
|
|
620
|
+
xsrf: { cookieName: 'MY-CSRF', headerName: 'X-MY-CSRF' }
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Streaming
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
// Server-Sent Events (SSE)
|
|
628
|
+
for await (const event of await api.getEventStream('/chat/completions', { body: { prompt } })) {
|
|
629
|
+
console.log(event.event, event.data);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// NDJSON (Newline Delimited JSON)
|
|
633
|
+
interface LogEntry { ts: number; message: string; }
|
|
634
|
+
for await (const entry of await api.getJsonStream<LogEntry>('/logs/stream')) {
|
|
635
|
+
processEntry(entry);
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### Progress Tracking
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// Download progress
|
|
643
|
+
await api.getBlob('/large-file', {
|
|
644
|
+
onDownloadProgress: ({ loaded, total, percentage }) => {
|
|
645
|
+
console.log(`${percentage}% (${loaded}/${total})`);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Upload progress
|
|
650
|
+
await api.post('/upload', {
|
|
651
|
+
body: largeBlob,
|
|
652
|
+
onUploadProgress: ({ loaded, total, percentage }) => {
|
|
653
|
+
console.log(`Uploading: ${percentage}%`);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
### Safe Results (unwrap: false)
|
|
659
|
+
|
|
660
|
+
Pass `unwrap: false` to get a `Result<T>` tuple instead of thrown errors. The tuple is `[true, data]` on success or `[false, HttpError]` on failure.
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
// Per-request
|
|
664
|
+
const [ok, result] = await api.getJson('/users/1', { unwrap: false });
|
|
665
|
+
if (ok) {
|
|
666
|
+
console.log(result); // typed as Json
|
|
667
|
+
} else {
|
|
668
|
+
console.error(result.statusCode); // typed as HttpError
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Constructor-level default
|
|
672
|
+
const safeApi = new Transportr('https://api.example.com', { unwrap: false });
|
|
673
|
+
const [ok, data] = await safeApi.getJson('/data');
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Concurrent Requests
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// Run multiple requests in parallel
|
|
680
|
+
const [users, posts] = await Transportr.all([
|
|
681
|
+
api.getJson('/users'),
|
|
682
|
+
api.getJson('/posts')
|
|
683
|
+
]);
|
|
684
|
+
|
|
685
|
+
// Race requests — first to settle wins, losers are aborted
|
|
686
|
+
const fastest = await Transportr.race([
|
|
687
|
+
(signal) => api.getJson('/primary-cdn/data', { signal }),
|
|
688
|
+
(signal) => api.getJson('/fallback-cdn/data', { signal })
|
|
689
|
+
]);
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### HTML Selector Support
|
|
693
|
+
|
|
694
|
+
```typescript
|
|
695
|
+
// Get a specific element from HTML response
|
|
696
|
+
const nav = await api.getHtml('/page', {}, 'nav.main');
|
|
697
|
+
const item = await api.getHtmlFragment('/partial', {}, '.item:first-child');
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### FormData & Raw Bodies
|
|
701
|
+
|
|
702
|
+
FormData, Blob, ArrayBuffer, ReadableStream, TypedArray, and URLSearchParams are sent as-is. The `Content-Type` header is automatically removed so the runtime can set it (e.g., multipart boundary for FormData).
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
const form = new FormData();
|
|
706
|
+
form.append('file', fileBlob, 'photo.jpg');
|
|
707
|
+
await api.post('/upload', { body: form });
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Custom Content-Type Handlers
|
|
711
|
+
|
|
712
|
+
```typescript
|
|
713
|
+
// Register a custom handler (takes priority over built-in)
|
|
714
|
+
Transportr.registerContentTypeHandler('csv', async (response) => {
|
|
715
|
+
const text = await response.text();
|
|
716
|
+
return text.split('\n').map(row => row.split(','));
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Remove a handler
|
|
720
|
+
Transportr.unregisterContentTypeHandler('csv');
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Cleanup
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
// Tear down a single instance
|
|
727
|
+
api.destroy();
|
|
728
|
+
|
|
729
|
+
// Tear down all global state
|
|
730
|
+
Transportr.unregisterAll();
|
|
731
|
+
|
|
732
|
+
// Clear only global hooks without aborting in-flight requests
|
|
733
|
+
Transportr.clearHooks();
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
### Method Chaining
|
|
737
|
+
|
|
738
|
+
Instance methods `configure()`, `unregister()`, `addHooks()`, and `clearHooks()` return `this`:
|
|
739
|
+
|
|
740
|
+
```typescript
|
|
741
|
+
api
|
|
742
|
+
.configure({ timeout: 5000, credentials: 'include' })
|
|
743
|
+
.configure({ headers: { 'Authorization': `Bearer ${token}` } })
|
|
744
|
+
.addHooks({ beforeRequest: [myHook] })
|
|
745
|
+
.clearHooks()
|
|
746
|
+
.addHooks({ afterResponse: [logHook] });
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Instance Properties
|
|
750
|
+
|
|
751
|
+
| Property | Type | Description |
|
|
752
|
+
|----------|------|-------------|
|
|
753
|
+
| `baseUrl` | `URL` | The base URL used for all requests from this instance |
|
|
754
|
+
|
|
755
|
+
### Static Properties
|
|
756
|
+
|
|
757
|
+
| Property | Description |
|
|
758
|
+
|----------|-------------|
|
|
759
|
+
| `Transportr.CredentialsPolicy` | Credentials policy constants |
|
|
760
|
+
| `Transportr.RequestMode` | Request mode constants |
|
|
761
|
+
| `Transportr.RequestPriority` | Request priority constants |
|
|
762
|
+
| `Transportr.RedirectPolicy` | Redirect policy constants |
|
|
763
|
+
| `Transportr.ReferrerPolicy` | Referrer policy constants |
|
|
764
|
+
| `Transportr.RequestEvent` | Event name constants |
|
|
765
|
+
| `Transportr.all(requests)` | Run requests in parallel (`Promise.all` with tuple typing) |
|
|
766
|
+
| `Transportr.race(requests)` | Race request factories; auto-aborts losers |
|
|
767
|
+
| `Transportr.abortAll()` | Abort all in-flight requests across all instances |
|
|
768
|
+
| `Transportr.addHooks(hooks)` | Add global lifecycle hooks |
|
|
769
|
+
| `Transportr.clearHooks()` | Remove all global hooks |
|
|
770
|
+
| `Transportr.register(event, handler)` | Register a global event handler |
|
|
771
|
+
| `Transportr.unregister(registration)` | Remove a global event handler |
|
|
772
|
+
|
|
773
|
+
### Submodule Imports
|
|
774
|
+
|
|
775
|
+
HTTP constant objects are available as named submodule imports. Each is a tree-shakeable, side-effect-free object of string constants — useful for avoiding magic strings and getting autocomplete.
|
|
776
|
+
|
|
777
|
+
#### `@d1g1tal/transportr/request-header`
|
|
778
|
+
|
|
779
|
+
Request header name constants.
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
783
|
+
import { RequestHeader } from '@d1g1tal/transportr/request-header';
|
|
784
|
+
|
|
785
|
+
const api = new Transportr('https://api.example.com', {
|
|
786
|
+
headers: {
|
|
787
|
+
[RequestHeader.AUTHORIZATION]: 'Bearer token',
|
|
788
|
+
[RequestHeader.CONTENT_TYPE]: 'application/json',
|
|
789
|
+
[RequestHeader.ACCEPT_LANGUAGE]: 'en-US'
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
#### `@d1g1tal/transportr/request-method`
|
|
795
|
+
|
|
796
|
+
HTTP method string constants.
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
800
|
+
import { RequestMethod } from '@d1g1tal/transportr/request-method';
|
|
801
|
+
|
|
802
|
+
const api = new Transportr('https://api.example.com');
|
|
803
|
+
const response = await api.request('/data', { method: RequestMethod.PATCH });
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### `@d1g1tal/transportr/content-type`
|
|
807
|
+
|
|
808
|
+
MIME type string constants covering common content types (JSON, HTML, XML, CSS, images, audio, video, and more).
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
812
|
+
import { RequestHeader } from '@d1g1tal/transportr/request-header';
|
|
813
|
+
import { ContentType } from '@d1g1tal/transportr/content-type';
|
|
814
|
+
|
|
815
|
+
const api = new Transportr('https://api.example.com', {
|
|
816
|
+
headers: { [RequestHeader.ACCEPT]: ContentType.JSON }
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Use as a content-type value
|
|
820
|
+
const csvData = 'id,name\n1,Alice';
|
|
821
|
+
await api.post('/upload', {
|
|
822
|
+
body: csvData,
|
|
823
|
+
headers: { [RequestHeader.CONTENT_TYPE]: ContentType.CSV }
|
|
824
|
+
});
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
#### `@d1g1tal/transportr/response-header`
|
|
828
|
+
|
|
829
|
+
Response header name constants — useful when reading headers from a response.
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
833
|
+
import { ResponseHeader } from '@d1g1tal/transportr/response-header';
|
|
834
|
+
|
|
835
|
+
const api = new Transportr('https://api.example.com');
|
|
836
|
+
const reg = api.register(Transportr.RequestEvent.SUCCESS, (event, data) => {
|
|
837
|
+
const response = data as Response;
|
|
838
|
+
const etag = response.headers.get(ResponseHeader.ETAG);
|
|
839
|
+
const retryAfter = response.headers.get(ResponseHeader.RETRY_AFTER);
|
|
840
|
+
const location = response.headers.get(ResponseHeader.LOCATION);
|
|
841
|
+
});
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
## License
|
|
845
|
+
|
|
846
|
+
[MIT](LICENSE)
|