@d1g1tal/transportr 1.4.4 → 2.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/README.md +329 -76
- package/dist/transportr.d.ts +1799 -0
- package/dist/transportr.js +2753 -1503
- package/dist/transportr.js.map +7 -0
- package/package.json +72 -49
- package/dist/iife/transportr.js +0 -2656
- package/dist/iife/transportr.min.js +0 -3
- package/dist/iife/transportr.min.js.map +0 -7
- package/dist/transportr.min.js +0 -3
- package/dist/transportr.min.js.map +0 -7
- package/src/abort-signal.js +0 -124
- package/src/constants.js +0 -61
- package/src/http-error.js +0 -75
- package/src/http-media-type.js +0 -162
- package/src/http-request-headers.js +0 -312
- package/src/http-request-methods.js +0 -277
- package/src/http-response-headers.js +0 -350
- package/src/parameter-map.js +0 -221
- package/src/response-status.js +0 -62
- package/src/transportr.js +0 -838
package/README.md
CHANGED
|
@@ -1,90 +1,343 @@
|
|
|
1
1
|
# transportr
|
|
2
|
-
|
|
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
|
+
|
|
3
26
|
## Installation
|
|
27
|
+
|
|
4
28
|
```bash
|
|
5
|
-
|
|
29
|
+
pnpm add @d1g1tal/transportr
|
|
6
30
|
```
|
|
7
|
-
## Usage
|
|
8
|
-
```javascript
|
|
9
|
-
import Transportr from '@d1g1tal/transportr';
|
|
10
31
|
|
|
11
|
-
|
|
12
|
-
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- **Node.js** ≥ 22.0.0 or a modern browser with native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and `AbortController` support
|
|
35
|
+
- `jsdom` is bundled to polyfill the DOM in Node.js environments automatically
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { Transportr } from '@d1g1tal/transportr';
|
|
41
|
+
|
|
42
|
+
const api = new Transportr('https://api.example.com');
|
|
43
|
+
|
|
44
|
+
// GET JSON
|
|
45
|
+
const data = await api.getJson('/users/1');
|
|
46
|
+
|
|
47
|
+
// POST with JSON body
|
|
48
|
+
const created = await api.post('/users', { body: { name: 'Alice' } });
|
|
13
49
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
50
|
+
// GET with search params
|
|
51
|
+
const results = await api.getJson('/search', { searchParams: { q: 'term', page: 1 } });
|
|
52
|
+
|
|
53
|
+
// Typed response using generics
|
|
54
|
+
interface User { id: number; name: string; }
|
|
55
|
+
const user = await api.get<User>('/users/1');
|
|
17
56
|
```
|
|
18
|
-
Or
|
|
19
57
|
|
|
20
|
-
|
|
21
|
-
const transportr = new Transportr('https://jsonplaceholder.typicode.com');
|
|
58
|
+
## API
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
60
|
+
### Constructor
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
new Transportr(url?: URL | string | RequestOptions, options?: RequestOptions)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Creates a new instance. When `url` is omitted, defaults to `globalThis.location.origin`.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// With base URL
|
|
70
|
+
const api = new Transportr('https://api.example.com/v2');
|
|
71
|
+
|
|
72
|
+
// With URL and default options
|
|
73
|
+
const api = new Transportr('https://api.example.com', {
|
|
74
|
+
timeout: 10000,
|
|
75
|
+
headers: { 'Authorization': 'Bearer token' }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// With options only (uses current origin)
|
|
79
|
+
const api = new Transportr({ timeout: 5000 });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Request Methods
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `get(path?, options?)` | GET request with auto content-type handling |
|
|
87
|
+
| `post(path?, options?)` | POST request |
|
|
88
|
+
| `put(path?, options?)` | PUT request |
|
|
89
|
+
| `patch(path?, options?)` | PATCH request |
|
|
90
|
+
| `delete(path?, options?)` | DELETE request |
|
|
91
|
+
| `head(path?, options?)` | HEAD request |
|
|
92
|
+
| `options(path?, options?)` | OPTIONS request (returns allowed methods) |
|
|
93
|
+
| `request(path?, options?)` | Raw request returning `TypedResponse<T>` |
|
|
94
|
+
|
|
95
|
+
### Typed Response Methods
|
|
96
|
+
|
|
97
|
+
| Method | Returns | Accept Header |
|
|
98
|
+
|--------|---------|---------------|
|
|
99
|
+
| `getJson(path?, options?)` | `Json` | `application/json` |
|
|
100
|
+
| `getHtml(path?, options?, selector?)` | `Document \| Element` | `text/html` |
|
|
101
|
+
| `getHtmlFragment(path?, options?, selector?)` | `DocumentFragment \| Element` | `text/html` |
|
|
102
|
+
| `getXml(path?, options?)` | `Document` | `application/xml` |
|
|
103
|
+
| `getScript(path?, options?)` | `void` (injected into DOM) | `application/javascript` |
|
|
104
|
+
| `getStylesheet(path?, options?)` | `void` (injected into DOM) | `text/css` |
|
|
105
|
+
| `getBlob(path?, options?)` | `Blob` | `application/octet-stream` |
|
|
106
|
+
| `getImage(path?, options?)` | `HTMLImageElement` | `image/*` |
|
|
107
|
+
| `getBuffer(path?, options?)` | `ArrayBuffer` | `application/octet-stream` |
|
|
108
|
+
| `getStream(path?, options?)` | `ReadableStream` | `application/octet-stream` |
|
|
109
|
+
|
|
110
|
+
### Request Options
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
type RequestOptions = {
|
|
114
|
+
headers?: RequestHeaders;
|
|
115
|
+
searchParams?: URLSearchParams | string | Record<string, string | number | boolean>;
|
|
116
|
+
timeout?: number; // Default: 30000ms
|
|
117
|
+
global?: boolean; // Emit global events (default: true)
|
|
118
|
+
body?: BodyInit | JsonObject; // Auto-serialized for JSON content-type
|
|
119
|
+
retry?: number | RetryOptions;
|
|
120
|
+
dedupe?: boolean; // Deduplicate identical GET/HEAD requests
|
|
121
|
+
xsrf?: boolean | XsrfOptions;
|
|
122
|
+
hooks?: HooksOptions;
|
|
123
|
+
// ...all standard RequestInit properties (cache, credentials, mode, etc.)
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Retry
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Simple: retry up to 3 times with default settings
|
|
131
|
+
await api.get('/data', { retry: 3 });
|
|
132
|
+
|
|
133
|
+
// Advanced configuration
|
|
134
|
+
await api.get('/data', {
|
|
135
|
+
retry: {
|
|
136
|
+
limit: 3,
|
|
137
|
+
statusCodes: [408, 413, 429, 500, 502, 503, 504],
|
|
138
|
+
methods: ['GET', 'PUT', 'HEAD', 'DELETE', 'OPTIONS'],
|
|
139
|
+
delay: 300, // ms before first retry
|
|
140
|
+
backoffFactor: 2 // exponential backoff multiplier
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Request Deduplication
|
|
146
|
+
|
|
147
|
+
When `dedupe: true`, identical in-flight GET/HEAD requests share a single fetch call. Each consumer receives a cloned response.
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Only one fetch call is made
|
|
151
|
+
const [a, b] = await Promise.all([
|
|
152
|
+
api.get('/data', { dedupe: true }),
|
|
153
|
+
api.get('/data', { dedupe: true })
|
|
154
|
+
]);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Lifecycle Hooks
|
|
158
|
+
|
|
159
|
+
Hooks run in order: global → instance → per-request.
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Global hooks (all instances)
|
|
163
|
+
Transportr.addHooks({
|
|
164
|
+
beforeRequest: [async (options, url) => {
|
|
165
|
+
options.headers.set('X-Request-ID', crypto.randomUUID());
|
|
166
|
+
return options;
|
|
167
|
+
}],
|
|
168
|
+
afterResponse: [async (response, options) => response],
|
|
169
|
+
beforeError: [(error) => error]
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Instance hooks
|
|
173
|
+
api.addHooks({
|
|
174
|
+
afterResponse: [async (response) => {
|
|
175
|
+
console.log(`Response: ${response.status}`);
|
|
176
|
+
return response;
|
|
177
|
+
}]
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Per-request hooks
|
|
181
|
+
await api.get('/data', {
|
|
182
|
+
hooks: { beforeRequest: [async (opts) => opts] }
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Events
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Global events (all instances)
|
|
190
|
+
const reg = Transportr.register(Transportr.RequestEvents.SUCCESS, (event, data) => {
|
|
191
|
+
console.log('Request succeeded:', data);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Instance events
|
|
195
|
+
const reg = api.register(Transportr.RequestEvents.ERROR, (event, error) => {
|
|
196
|
+
console.error('Request failed:', error);
|
|
197
|
+
});
|
|
26
198
|
|
|
27
|
-
|
|
28
|
-
|
|
199
|
+
// Unregister
|
|
200
|
+
api.unregister(reg); // Returns `this` for chaining
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Event lifecycle**: `configured` → `success | error | aborted | timeout` → `complete` → `all-complete`
|
|
204
|
+
|
|
205
|
+
Additional events: `retry` (emitted on each retry attempt)
|
|
206
|
+
|
|
207
|
+
### Error Handling
|
|
208
|
+
|
|
209
|
+
Non-2xx responses throw an error with `name === 'HttpError'`. Aborted and timed-out requests also produce an `HttpError` with synthetic status codes.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import type { HttpError } from '@d1g1tal/transportr';
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const user = await api.getJson('/users/1');
|
|
29
216
|
} catch (error) {
|
|
30
|
-
|
|
217
|
+
if (error instanceof Error && error.name === 'HttpError') {
|
|
218
|
+
const httpError = error as unknown as HttpError;
|
|
219
|
+
console.error(httpError.statusCode); // HTTP status code
|
|
220
|
+
console.error(httpError.statusText); // HTTP status text
|
|
221
|
+
console.error(httpError.entity); // parsed response body (if any)
|
|
222
|
+
console.error(httpError.url?.href); // request URL
|
|
223
|
+
console.error(httpError.method); // HTTP method used
|
|
224
|
+
console.error(httpError.timing); // { start, end, duration } in ms
|
|
225
|
+
}
|
|
31
226
|
}
|
|
32
227
|
```
|
|
33
228
|
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
229
|
+
**Synthetic status codes for non-HTTP failures:**
|
|
230
|
+
|
|
231
|
+
| Code | Text | Cause |
|
|
232
|
+
|------|------|-------|
|
|
233
|
+
| `499` | `Aborted` | Cancelled via `controller.abort()` or `Transportr.abortAll()` |
|
|
234
|
+
| `504` | `Request Timeout` | `timeout` option exceeded |
|
|
235
|
+
|
|
236
|
+
### Abort & Timeout
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Per-request timeout
|
|
240
|
+
await api.get('/slow', { timeout: 5000 });
|
|
241
|
+
|
|
242
|
+
// Manual abort via AbortController
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
api.get('/data', { signal: controller.signal });
|
|
245
|
+
controller.abort();
|
|
246
|
+
|
|
247
|
+
// Abort all in-flight requests
|
|
248
|
+
Transportr.abortAll();
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### XSRF/CSRF Protection
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Default: reads 'XSRF-TOKEN' cookie, sets 'X-XSRF-TOKEN' header
|
|
255
|
+
await api.post('/data', { body: payload, xsrf: true });
|
|
256
|
+
|
|
257
|
+
// Custom cookie/header names
|
|
258
|
+
await api.post('/data', {
|
|
259
|
+
body: payload,
|
|
260
|
+
xsrf: { cookieName: 'MY-CSRF', headerName: 'X-MY-CSRF' }
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### HTML Selector Support
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Get a specific element from HTML response
|
|
268
|
+
const nav = await api.getHtml('/page', {}, 'nav.main');
|
|
269
|
+
const item = await api.getHtmlFragment('/partial', {}, '.item:first-child');
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### FormData & Raw Bodies
|
|
273
|
+
|
|
274
|
+
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).
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
const form = new FormData();
|
|
278
|
+
form.append('file', fileBlob, 'photo.jpg');
|
|
279
|
+
await api.post('/upload', { body: form });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Custom Content-Type Handlers
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// Register a custom handler (takes priority over built-in)
|
|
286
|
+
Transportr.registerContentTypeHandler('csv', async (response) => {
|
|
287
|
+
const text = await response.text();
|
|
288
|
+
return text.split('\n').map(row => row.split(','));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Remove a handler
|
|
292
|
+
Transportr.unregisterContentTypeHandler('csv');
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Cleanup
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
// Tear down a single instance
|
|
299
|
+
api.destroy();
|
|
300
|
+
|
|
301
|
+
// Tear down all global state
|
|
302
|
+
Transportr.unregisterAll();
|
|
303
|
+
|
|
304
|
+
// Clear only global hooks without aborting in-flight requests
|
|
305
|
+
Transportr.clearHooks();
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Method Chaining
|
|
309
|
+
|
|
310
|
+
Instance methods `unregister()`, `addHooks()`, and `clearHooks()` return `this`:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
api
|
|
314
|
+
.addHooks({ beforeRequest: [myHook] })
|
|
315
|
+
.clearHooks()
|
|
316
|
+
.addHooks({ afterResponse: [logHook] });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Instance Properties
|
|
320
|
+
|
|
321
|
+
| Property | Type | Description |
|
|
322
|
+
|----------|------|-------------|
|
|
323
|
+
| `baseUrl` | `URL` | The base URL used for all requests from this instance |
|
|
324
|
+
|
|
325
|
+
### Static Properties
|
|
326
|
+
|
|
327
|
+
| Property | Description |
|
|
328
|
+
|----------|-------------|
|
|
329
|
+
| `Transportr.MediaType` | HTTP media type constants |
|
|
330
|
+
| `Transportr.RequestMethod` | HTTP method constants |
|
|
331
|
+
| `Transportr.RequestHeader` | Request header constants |
|
|
332
|
+
| `Transportr.ResponseHeader` | Response header constants |
|
|
333
|
+
| `Transportr.CachingPolicy` | Cache policy constants |
|
|
334
|
+
| `Transportr.CredentialsPolicy` | Credentials policy constants |
|
|
335
|
+
| `Transportr.RequestModes` | Request mode constants |
|
|
336
|
+
| `Transportr.RequestPriorities` | Request priority constants |
|
|
337
|
+
| `Transportr.RedirectPolicies` | Redirect policy constants |
|
|
338
|
+
| `Transportr.ReferrerPolicy` | Referrer policy constants |
|
|
339
|
+
| `Transportr.RequestEvents` | Event name constants |
|
|
340
|
+
|
|
341
|
+
## License
|
|
342
|
+
|
|
343
|
+
[ISC](LICENSE)
|