@ahriknow/afetch 0.0.1

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/README_zh-CN.md +454 -0
  4. package/dist/afetch.d.ts +15 -0
  5. package/dist/afetch.d.ts.map +1 -0
  6. package/dist/afetch.js +284 -0
  7. package/dist/afetch.js.map +1 -0
  8. package/dist/error.d.ts +47 -0
  9. package/dist/error.d.ts.map +1 -0
  10. package/dist/error.js +75 -0
  11. package/dist/error.js.map +1 -0
  12. package/dist/events.d.ts +31 -0
  13. package/dist/events.d.ts.map +1 -0
  14. package/dist/events.js +63 -0
  15. package/dist/events.js.map +1 -0
  16. package/dist/index.d.ts +16 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +18 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/plugin.d.ts +42 -0
  21. package/dist/plugin.d.ts.map +1 -0
  22. package/dist/plugin.js +46 -0
  23. package/dist/plugin.js.map +1 -0
  24. package/dist/plugins/event-bus.d.ts +19 -0
  25. package/dist/plugins/event-bus.d.ts.map +1 -0
  26. package/dist/plugins/event-bus.js +27 -0
  27. package/dist/plugins/event-bus.js.map +1 -0
  28. package/dist/plugins/index.d.ts +8 -0
  29. package/dist/plugins/index.d.ts.map +1 -0
  30. package/dist/plugins/index.js +6 -0
  31. package/dist/plugins/index.js.map +1 -0
  32. package/dist/plugins/retry.d.ts +18 -0
  33. package/dist/plugins/retry.d.ts.map +1 -0
  34. package/dist/plugins/retry.js +151 -0
  35. package/dist/plugins/retry.js.map +1 -0
  36. package/dist/types.d.ts +221 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +15 -0
  39. package/dist/types.js.map +1 -0
  40. package/dist/utils.d.ts +57 -0
  41. package/dist/utils.d.ts.map +1 -0
  42. package/dist/utils.js +378 -0
  43. package/dist/utils.js.map +1 -0
  44. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ahriknow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,452 @@
1
+ <div align="center">
2
+
3
+ # afetch
4
+
5
+ **English** | [δΈ­ζ–‡](./README_zh-CN.md)
6
+
7
+ A lightweight, type-safe, plugin-based fetch API wrapper for modern JavaScript/TypeScript.
8
+
9
+ [![npm version](https://img.shields.io/npm/v/@ahriknow/afetch.svg)](https://www.npmjs.com/package/@ahriknow/afetch)
10
+ [![license](https://img.shields.io/npm/l/afetch.svg)](./LICENSE)
11
+ [![codecov](https://codecov.io/gh/ahriknow/afetch/branch/main/graph/badge.svg)](https://codecov.io/gh/ahriknow/afetch)
12
+ [![typescript](https://img.shields.io/badge/TypeScript-7.0-blue.svg)](https://www.typescriptlang.org/)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## Features
19
+
20
+ - πŸš€ **Lightweight** β€” Zero dependencies, minimal bundle size
21
+ - πŸ”’ **Type-safe** β€” Full TypeScript support with strict types
22
+ - 🧩 **Plugin System** β€” Extensible via `beforeRequest`, `afterResponse`, `onError` hooks
23
+ - πŸ” **Retry Plugin** β€” Automatic retry with exponential backoff, status matching, and custom hooks
24
+ - πŸ“‘ **Event Bus Plugin** β€” Observe request lifecycle via events
25
+ - ⏱️ **Timeout** β€” Request timeout with automatic abort
26
+ - ❌ **Cancellation** β€” AbortController support + Task API for fine-grained control
27
+ - πŸ“Š **Progress** β€” Upload and download progress tracking
28
+ - πŸ—οΈ **Instances** β€” Create pre-configured instances for different APIs
29
+ - πŸ”§ **Transforms** β€” Request and response data transformation
30
+ - 🌐 **Universal** β€” Works in browsers (Chrome 42+, Firefox 39+, Safari 10.1+) and Node.js 18+
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install afetch
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```typescript
41
+ import { afetch } from 'afetch';
42
+
43
+ // GET request
44
+ const { data } = await afetch.get<User[]>('/api/users');
45
+
46
+ // POST request
47
+ const { data: user } = await afetch.post<User>('/api/users', {
48
+ name: 'John Doe',
49
+ email: 'john@example.com',
50
+ });
51
+
52
+ // With options
53
+ const { data: item } = await afetch.get<Item>('/api/items/1', {
54
+ headers: { Authorization: 'Bearer token' },
55
+ timeout: 5000,
56
+ params: { fields: 'name,email' },
57
+ });
58
+ ```
59
+
60
+ ## Creating Instances
61
+
62
+ ```typescript
63
+ import { createInstance } from 'afetch';
64
+
65
+ const api = createInstance({
66
+ baseURL: 'https://api.example.com',
67
+ timeout: 10000,
68
+ headers: { 'Content-Type': 'application/json' },
69
+ });
70
+
71
+ const { data: users } = await api.get<User[]>('/users');
72
+ const { data: user } = await api.post<User>('/users', { name: 'John' });
73
+ ```
74
+
75
+ ## Plugin System
76
+
77
+ afetch uses a plugin architecture. Core functionality is minimal β€” features like retry and event observation are provided as plugins.
78
+
79
+ ### Built-in Plugins
80
+
81
+ #### Retry Plugin
82
+
83
+ ```typescript
84
+ import { createRetryPlugin } from 'afetch';
85
+
86
+ const api = createInstance({ baseURL: 'https://api.example.com' });
87
+ api.use(createRetryPlugin());
88
+
89
+ // Basic retry
90
+ await api.get('/api/data', {
91
+ meta: { retry: { maxRetries: 3, delay: 1000 } },
92
+ });
93
+
94
+ // Retry on specific status codes
95
+ await api.get('/api/data', {
96
+ meta: {
97
+ retry: {
98
+ maxRetries: 3,
99
+ delay: 1000,
100
+ retryOn: [500, 502, 503, 504],
101
+ },
102
+ },
103
+ });
104
+
105
+ // Exponential backoff
106
+ await api.get('/api/data', {
107
+ meta: {
108
+ retry: {
109
+ maxRetries: 5,
110
+ delay: (attempt) => Math.pow(2, attempt) * 1000,
111
+ },
112
+ },
113
+ });
114
+
115
+ // Advanced: hook + call for token refresh on 401
116
+ await api.get('/api/protected', {
117
+ meta: {
118
+ retry: {
119
+ maxRetries: 3,
120
+ delay: 1000,
121
+ retryOn: [
122
+ 500,
123
+ {
124
+ hook: async (error) => error.status === 401,
125
+ retryDelay: 0,
126
+ call: async () => {
127
+ const token = await refreshToken();
128
+ api.defaults.headers!['Authorization'] = `Bearer ${token}`;
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ },
134
+ });
135
+
136
+ // Custom condition function
137
+ await api.get('/api/data', {
138
+ meta: {
139
+ retry: {
140
+ maxRetries: 3,
141
+ condition: (attempt, error) => error.status === 503 && attempt < 2,
142
+ },
143
+ },
144
+ });
145
+ ```
146
+
147
+ #### Event Bus Plugin
148
+
149
+ ```typescript
150
+ import { createEventBusPlugin } from 'afetch';
151
+
152
+ const api = createInstance({ baseURL: 'https://api.example.com' });
153
+ const eventBus = createEventBusPlugin();
154
+ api.use(eventBus);
155
+
156
+ // Listen to lifecycle events
157
+ const unsub = eventBus.on('request', ({ config }) => {
158
+ console.log(`β†’ ${config.method} ${config.url}`);
159
+ });
160
+
161
+ eventBus.on('response', ({ config, response }) => {
162
+ console.log(`← ${response.status} ${config.url}`);
163
+ });
164
+
165
+ eventBus.on('error', ({ config, error }) => {
166
+ console.error(`βœ— ${error.code} ${config.url}`);
167
+ });
168
+
169
+ // Unsubscribe
170
+ unsub();
171
+
172
+ // Remove all listeners for an event
173
+ eventBus.off('response');
174
+ ```
175
+
176
+ ### Writing Custom Plugins
177
+
178
+ ```typescript
179
+ import type { AFetchPlugin } from 'afetch';
180
+
181
+ const loggerPlugin: AFetchPlugin = {
182
+ name: 'logger',
183
+ install(api) {
184
+ api.addHook('beforeRequest', ({ config }) => {
185
+ console.log(`[REQ] ${config.method} ${config.baseURL}${config.url}`);
186
+ });
187
+
188
+ api.addHook('afterResponse', ({ response }) => {
189
+ console.log(`[RES] ${response.status} ${response.statusText}`);
190
+ });
191
+
192
+ api.addHook('onError', ({ error }) => {
193
+ console.error(`[ERR] ${error.code}: ${error.message}`);
194
+ });
195
+ },
196
+ };
197
+
198
+ api.use(loggerPlugin);
199
+ ```
200
+
201
+ #### Plugin Lifecycle Hooks
202
+
203
+ | Hook | When | Return |
204
+ |------|------|--------|
205
+ | `beforeRequest` | Before sending request | `void` |
206
+ | `afterResponse` | After receiving response | `AResponse` (replace) or `void` |
207
+ | `onError` | On request error | `AResponse` (retry/replace) or `void` (propagate) |
208
+
209
+ Plugins are installed once per instance β€” calling `use()` with the same plugin name is a no-op.
210
+
211
+ ## Error Handling
212
+
213
+ ```typescript
214
+ import { AFetchError, AFetchErrorType } from 'afetch';
215
+
216
+ try {
217
+ await api.get('/api/data');
218
+ } catch (error) {
219
+ if (error instanceof AFetchError) {
220
+ switch (error.code) {
221
+ case AFetchErrorType.TIMEOUT:
222
+ console.log('Request timed out');
223
+ break;
224
+ case AFetchErrorType.HTTP:
225
+ console.log(`HTTP ${error.status}: ${error.message}`);
226
+ break;
227
+ case AFetchErrorType.NETWORK:
228
+ console.log('Network error');
229
+ break;
230
+ case AFetchErrorType.ABORT:
231
+ console.log('Request aborted');
232
+ break;
233
+ }
234
+ }
235
+ }
236
+ ```
237
+
238
+ ## Request Cancellation
239
+
240
+ ### Task API (Recommended)
241
+
242
+ The Task API starts the request immediately and returns a handle to control it:
243
+
244
+ ```typescript
245
+ // Create a task β€” request starts immediately
246
+ const task = api.task.get('/api/data', {
247
+ headers: { Authorization: 'Bearer token' },
248
+ });
249
+
250
+ // Check status
251
+ console.log(task.done); // false
252
+ console.log(task.aborted); // false
253
+
254
+ // Cancel the request
255
+ task.abort();
256
+ console.log(task.aborted); // true
257
+
258
+ // Wait for response β€” throws AFetchError(ABORT) if cancelled
259
+ try {
260
+ const response = await task.wait();
261
+ console.log(response.data);
262
+ } catch (error) {
263
+ if (error.code === 'EABORT') {
264
+ console.log('Request was aborted');
265
+ }
266
+ }
267
+ ```
268
+
269
+ Supports all HTTP methods:
270
+
271
+ ```typescript
272
+ const getTask = api.task.get('/api/users');
273
+ const postTask = api.task.post('/api/users', { name: 'John' });
274
+ const putTask = api.task.put('/api/users/1', { name: 'Updated' });
275
+ const deleteTask = api.task.delete('/api/users/1');
276
+ const patchTask = api.task.patch('/api/users/1', { name: 'Patched' });
277
+ ```
278
+
279
+ ### AbortController
280
+
281
+ You can also use the standard `AbortController` approach:
282
+
283
+ ```typescript
284
+ const controller = new AbortController();
285
+
286
+ const { data } = await api.get('/api/data', {
287
+ signal: controller.signal,
288
+ timeout: 5000, // auto-abort after 5s
289
+ });
290
+
291
+ // Cancel manually
292
+ controller.abort();
293
+ ```
294
+
295
+ ## Request & Response Transforms
296
+
297
+ ```typescript
298
+ // Transform request data before sending
299
+ await api.post('/api/data', rawData, {
300
+ transformRequest: (data) => ({
301
+ ...(data as object),
302
+ timestamp: Date.now(),
303
+ }),
304
+ });
305
+
306
+ // Transform response data after receiving
307
+ const { data } = await api.get<Item[]>('/api/items', {
308
+ transformResponse: (data) => (data as any).items,
309
+ });
310
+ ```
311
+
312
+ ## TypeScript Support
313
+
314
+ Full generic type support:
315
+
316
+ ```typescript
317
+ interface User {
318
+ id: number;
319
+ name: string;
320
+ email: string;
321
+ }
322
+
323
+ // Response data is fully typed
324
+ const { data } = await api.get<User[]>('/users');
325
+ // ^ User[]
326
+
327
+ const { data: user } = await api.post<User>('/users', { name: 'John' });
328
+ // ^ User
329
+ ```
330
+
331
+ ## Configuration Options
332
+
333
+ ```typescript
334
+ const api = createInstance({
335
+ baseURL: 'https://api.example.com', // Base URL for all requests
336
+ timeout: 10000, // Default timeout (ms)
337
+ headers: { // Default headers
338
+ 'Content-Type': 'application/json',
339
+ },
340
+ responseType: 'json', // Default response type
341
+ cache: 'default', // Request cache mode
342
+ credentials: 'same-origin', // Credentials mode
343
+ throwOnError: true, // Throw on non-2xx (default: true)
344
+ fetchAdapter: customFetch, // Custom fetch implementation
345
+ plugins: [createRetryPlugin()], // Plugins to install
346
+ });
347
+ ```
348
+
349
+ ### Per-request Options
350
+
351
+ ```typescript
352
+ await api.get('/data', {
353
+ headers: { 'X-Custom': 'value' },
354
+ params: { page: 1, limit: 20 },
355
+ timeout: 3000,
356
+ signal: controller.signal,
357
+ responseType: 'text',
358
+ cache: 'no-cache',
359
+ throwOnError: false,
360
+ meta: { requestId: '123' },
361
+ transformRequest: fn,
362
+ transformResponse: fn,
363
+ onUploadProgress: fn,
364
+ onDownloadProgress: fn,
365
+ });
366
+ ```
367
+
368
+ ## API Reference
369
+
370
+ ### Instance Methods
371
+
372
+ | Method | Description |
373
+ |--------|-------------|
374
+ | `afetch.get<T>(url, options?)` | GET request |
375
+ | `afetch.post<T>(url, data?, options?)` | POST request |
376
+ | `afetch.put<T>(url, data?, options?)` | PUT request |
377
+ | `afetch.delete<T>(url, options?)` | DELETE request |
378
+ | `afetch.patch<T>(url, data?, options?)` | PATCH request |
379
+ | `afetch.head<T>(url, options?)` | HEAD request |
380
+ | `afetch.options<T>(url, options?)` | OPTIONS request |
381
+ | `afetch.request<T>(url, options?)` | Custom method request |
382
+ | `afetch.create(config?)` | Create a new instance |
383
+ | `afetch.use(plugin)` | Install a plugin |
384
+ | `afetch.task` | Task API for cancellable requests |
385
+ | `afetch.defaults` | Default configuration |
386
+
387
+ ### Response Object (`AResponse<T>`)
388
+
389
+ | Property | Type | Description |
390
+ |----------|------|-------------|
391
+ | `data` | `T` | Parsed response data |
392
+ | `status` | `number` | HTTP status code |
393
+ | `statusText` | `string` | HTTP status text |
394
+ | `headers` | `Headers` | Response headers |
395
+ | `config` | `ResolvedRequestConfig` | Request config |
396
+ | `raw` | `Response` | Original Response object |
397
+ | `ok` | `boolean` | `status >= 200 && status < 300` |
398
+
399
+ ### Request Task (`RequestTask<T>`)
400
+
401
+ | Property / Method | Type | Description |
402
+ |-------------------|------|-------------|
403
+ | `abort()` | `() => void` | Cancel the request |
404
+ | `wait()` | `() => Promise<AResponse<T>>` | Wait for the response (throws if aborted) |
405
+ | `aborted` | `boolean` | Whether the request has been aborted |
406
+ | `done` | `boolean` | Whether the request has completed |
407
+
408
+ ### Error Types (`AFetchErrorType`)
409
+
410
+ | Code | Description |
411
+ |------|-------------|
412
+ | `TIMEOUT` | Request timed out |
413
+ | `NETWORK` | Network error |
414
+ | `ABORT` | Request aborted |
415
+ | `HTTP` | Non-2xx response |
416
+ | `PARSE` | Response parse error |
417
+ | `CONFIG` | Configuration error |
418
+
419
+ ## Project Structure
420
+
421
+ ```
422
+ afetch/
423
+ β”œβ”€β”€ src/
424
+ β”‚ β”œβ”€β”€ index.ts # Entry point
425
+ β”‚ β”œβ”€β”€ afetch.ts # Core implementation
426
+ β”‚ β”œβ”€β”€ types.ts # Type definitions
427
+ β”‚ β”œβ”€β”€ plugin.ts # Plugin system (HookRunner)
428
+ β”‚ β”œβ”€β”€ events.ts # Event emitter
429
+ β”‚ β”œβ”€β”€ error.ts # AFetchError class
430
+ β”‚ β”œβ”€β”€ utils.ts # Utilities
431
+ β”‚ └── plugins/
432
+ β”‚ β”œβ”€β”€ index.ts # Plugin exports
433
+ β”‚ β”œβ”€β”€ retry.ts # Retry plugin
434
+ β”‚ └── event-bus.ts # Event bus plugin
435
+ β”œβ”€β”€ test/
436
+ β”‚ β”œβ”€β”€ afetch.test.ts # Unit tests
437
+ β”‚ └── coverage.test.ts # Coverage tests
438
+ β”œβ”€β”€ examples/
439
+ β”‚ β”œβ”€β”€ basic.ts # Basic usage
440
+ β”‚ β”œβ”€β”€ plugins.ts # Plugin examples
441
+ β”‚ └── task.ts # Task API examples
442
+ β”œβ”€β”€ .github/
443
+ β”‚ └── workflows/
444
+ β”‚ └── publish.yml # CI/CD
445
+ β”œβ”€β”€ package.json
446
+ β”œβ”€β”€ tsconfig.json
447
+ └── README.md
448
+ ```
449
+
450
+ ## License
451
+
452
+ [MIT](./LICENSE)