@d1g1tal/transportr 1.4.2 → 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 CHANGED
@@ -1,90 +1,343 @@
1
1
  # transportr
2
- JavaScript wrapper for Fetch API
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@d1g1tal/transportr?color=blue)](https://www.npmjs.com/package/@d1g1tal/transportr)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@d1g1tal/transportr)](https://www.npmjs.com/package/@d1g1tal/transportr)
5
+ [![CI](https://github.com/D1g1talEntr0py/transportr/actions/workflows/ci.yml/badge.svg)](https://github.com/D1g1talEntr0py/transportr/actions/workflows/ci.yml)
6
+ [![codecov](https://codecov.io/gh/D1g1talEntr0py/transportr/graph/badge.svg)](https://codecov.io/gh/D1g1talEntr0py/transportr)
7
+ [![License: ISC](https://img.shields.io/github/license/D1g1talEntr0py/transportr)](https://github.com/D1g1talEntr0py/transportr/blob/main/LICENSE)
8
+ [![Node.js](https://img.shields.io/node/v/@d1g1tal/transportr)](https://nodejs.org)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?logo=typescript&logoColor=white)](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
- npm install @d1g1tal/transportr
29
+ pnpm add @d1g1tal/transportr
6
30
  ```
7
- ## Usage
8
- ```javascript
9
- import Transportr from '@d1g1tal/transportr';
10
31
 
11
- // Creates default instance configured for JSON requests using UTF-8 encoding.
12
- const transportr = new Transportr('https://jsonplaceholder.typicode.com', { headers: { [Transportr.RequestHeader.CONTENT_TYPE]: Transportr.MediaType.JSON }, encoding: 'utf-8', });
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
- transportr.get('/todos/1')
15
- .then(json => console.log(json))
16
- .catch(error => console.error(error.message));
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
- ```javascript
21
- const transportr = new Transportr('https://jsonplaceholder.typicode.com');
58
+ ## API
22
59
 
23
- try {
24
- const todo1 = await transportr.getJson('/todos/1');
25
- console.log(todo1);
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
- const todo2 = await transportr.getJson('/todos/2');
28
- console.log(todo2);
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
- console.error(error.message);
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
- ## API
35
- ### Transportr
36
- #### constructor(options)
37
- ##### options
38
- Type: `Object`
39
-
40
- ###### options.baseURL
41
- Type: `String`
42
- Base URL for all requests.
43
-
44
- ###### options.headers
45
- Type: `Object`
46
- Default headers for all requests.
47
-
48
- ###### options.timeout
49
- Type: `Number`
50
- Default timeout for all requests.
51
-
52
- ###### options.credentials
53
- Type: `String`
54
- Default credentials for all requests.
55
-
56
- ###### options.mode
57
- Type: `String`
58
- Default mode for all requests.
59
-
60
- ###### options.cache
61
- Type: `String`
62
- Default cache for all requests.
63
-
64
- ###### options.redirect
65
- Type: `String`
66
- Default redirect for all requests.
67
-
68
- ###### options.referrer
69
- Type: `String`
70
- Default referrer for all requests.
71
-
72
- ###### options.integrity
73
- Type: `String`
74
- Default integrity for all requests.
75
-
76
- ###### options.keepalive
77
- Type: `Boolean`
78
- Default keepalive for all requests.
79
-
80
- ###### options.signal
81
- Type: `AbortSignal`
82
- Default signal for all requests.
83
-
84
- ###### options.encoding
85
- Type: `String`
86
- Default encoding for all requests.
87
-
88
- ###### options.body
89
- Type: `Object|String|FormData|URLSearchParams|Blob|BufferSource|ReadableStream`
90
- Default body for all requests.
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)