vigor-fetch 3.1.8 → 4.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/LICENSE +20 -20
- package/README.md +471 -1143
- package/dist/index.d.ts +5717 -742
- package/dist/index.js +1726 -1530
- package/dist/index.mjs +1678 -1530
- package/package.json +56 -52
package/README.md
CHANGED
|
@@ -1,1143 +1,471 @@
|
|
|
1
|
-
# vigor-fetch
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
11
|
-
- 🧩 **
|
|
12
|
-
- 🔁 **
|
|
13
|
-
- 🌐 **Smart
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
.
|
|
71
|
-
)
|
|
72
|
-
|
|
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
|
-
.algorithms(a => a
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
```ts
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
#### Settings API
|
|
474
|
-
|
|
475
|
-
| Method | Description | Default |
|
|
476
|
-
|---|---|---|
|
|
477
|
-
| original(boolean) | Returns original Response object | `false` |
|
|
478
|
-
| fallback(value) | Fallback return value on parse failure | `throws` |
|
|
479
|
-
|
|
480
|
-
---
|
|
481
|
-
|
|
482
|
-
### strategies
|
|
483
|
-
|
|
484
|
-
Configures parser strategy.
|
|
485
|
-
|
|
486
|
-
```ts
|
|
487
|
-
.strategies(s =>
|
|
488
|
-
s.contentType()
|
|
489
|
-
)
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## Parse Strategies
|
|
495
|
-
|
|
496
|
-
### contentType
|
|
497
|
-
|
|
498
|
-
Parses response using `content-type` header.
|
|
499
|
-
|
|
500
|
-
```ts
|
|
501
|
-
.strategies(s =>
|
|
502
|
-
s.contentType()
|
|
503
|
-
)
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
Supported:
|
|
507
|
-
|
|
508
|
-
- JSON
|
|
509
|
-
- Text
|
|
510
|
-
- Blob
|
|
511
|
-
- FormData
|
|
512
|
-
- ArrayBuffer
|
|
513
|
-
- Audio
|
|
514
|
-
- Video
|
|
515
|
-
- Image
|
|
516
|
-
|
|
517
|
-
---
|
|
518
|
-
|
|
519
|
-
### sniff
|
|
520
|
-
|
|
521
|
-
Attempts all parsers until one succeeds.
|
|
522
|
-
|
|
523
|
-
```ts
|
|
524
|
-
.strategies(s =>
|
|
525
|
-
s.sniff()
|
|
526
|
-
)
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
Useful when:
|
|
530
|
-
|
|
531
|
-
- content-type is invalid
|
|
532
|
-
- server responses are inconsistent
|
|
533
|
-
- APIs return malformed headers
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
### custom
|
|
538
|
-
|
|
539
|
-
Uses custom parser strategy.
|
|
540
|
-
|
|
541
|
-
```ts
|
|
542
|
-
.strategies(s =>
|
|
543
|
-
s.custom()
|
|
544
|
-
.func(async ({ response, parsers }) => {
|
|
545
|
-
return await parsers.json();
|
|
546
|
-
})
|
|
547
|
-
)
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
#### API
|
|
551
|
-
|
|
552
|
-
| Method | Description |
|
|
553
|
-
|---|---|
|
|
554
|
-
| func(fn) | Custom parse resolver |
|
|
555
|
-
|
|
556
|
-
---
|
|
557
|
-
|
|
558
|
-
### parsers
|
|
559
|
-
|
|
560
|
-
Registers custom parsers.
|
|
561
|
-
|
|
562
|
-
```ts
|
|
563
|
-
.parsers(p =>
|
|
564
|
-
p.add("csv", async response => {
|
|
565
|
-
return parseCSV(await response.text());
|
|
566
|
-
})
|
|
567
|
-
)
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
#### Parser API
|
|
571
|
-
|
|
572
|
-
| Method | Description |
|
|
573
|
-
|---|---|
|
|
574
|
-
| add(name, parser) | Adds custom parser |
|
|
575
|
-
| remove(name) | Removes parser |
|
|
576
|
-
| clear() | Removes all parsers |
|
|
577
|
-
|
|
578
|
-
---
|
|
579
|
-
|
|
580
|
-
### request
|
|
581
|
-
|
|
582
|
-
Executes parse workflow.
|
|
583
|
-
|
|
584
|
-
```ts
|
|
585
|
-
const result = await vigor
|
|
586
|
-
.parse(response)
|
|
587
|
-
.request();
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
---
|
|
591
|
-
|
|
592
|
-
## Interceptors
|
|
593
|
-
|
|
594
|
-
| Name | API |
|
|
595
|
-
|---|:---:|
|
|
596
|
-
| before | throwError |
|
|
597
|
-
| after | setResult / throwError |
|
|
598
|
-
| result | setResult / throwError |
|
|
599
|
-
| onError | setResult / throwError |
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
### before
|
|
604
|
-
|
|
605
|
-
Runs before parsing starts.
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
.before(async (ctx, api) => {
|
|
609
|
-
console.log(ctx.response.headers);
|
|
610
|
-
})
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
#### Available APIs
|
|
614
|
-
|
|
615
|
-
| API | Description |
|
|
616
|
-
|---|---|
|
|
617
|
-
| throwError(error) | Immediately throws error |
|
|
618
|
-
|
|
619
|
-
---
|
|
620
|
-
|
|
621
|
-
### after
|
|
622
|
-
|
|
623
|
-
Runs after parser succeeds.
|
|
624
|
-
|
|
625
|
-
```ts
|
|
626
|
-
.after(async (ctx, api) => {
|
|
627
|
-
api.setResult({
|
|
628
|
-
wrapped: ctx.result
|
|
629
|
-
});
|
|
630
|
-
})
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
#### Available APIs
|
|
634
|
-
|
|
635
|
-
| API | Description |
|
|
636
|
-
|---|---|
|
|
637
|
-
| setResult(value) | Replaces parsed result |
|
|
638
|
-
| throwError(error) | Throws error |
|
|
639
|
-
|
|
640
|
-
---
|
|
641
|
-
|
|
642
|
-
### result
|
|
643
|
-
|
|
644
|
-
Runs before returning final result.
|
|
645
|
-
|
|
646
|
-
```ts
|
|
647
|
-
.result(async (ctx, api) => {
|
|
648
|
-
api.setResult(transform(ctx.result));
|
|
649
|
-
})
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
#### Available APIs
|
|
653
|
-
|
|
654
|
-
| API | Description |
|
|
655
|
-
|---|---|
|
|
656
|
-
| setResult(value) | Replaces final result |
|
|
657
|
-
| throwError(error) | Throws error |
|
|
658
|
-
|
|
659
|
-
---
|
|
660
|
-
|
|
661
|
-
### onError
|
|
662
|
-
|
|
663
|
-
Runs when parsing fails.
|
|
664
|
-
|
|
665
|
-
```ts
|
|
666
|
-
.onError(async (ctx, api) => {
|
|
667
|
-
api.setResult(null);
|
|
668
|
-
})
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
#### Available APIs
|
|
672
|
-
|
|
673
|
-
| API | Description |
|
|
674
|
-
|---|---|
|
|
675
|
-
| setResult(value) | Returns fallback value |
|
|
676
|
-
| throwError(error) | Throws error |
|
|
677
|
-
|
|
678
|
-
---
|
|
679
|
-
|
|
680
|
-
## Example
|
|
681
|
-
|
|
682
|
-
```ts
|
|
683
|
-
const response = await fetch("/api");
|
|
684
|
-
|
|
685
|
-
const data = await vigor
|
|
686
|
-
.parse(response)
|
|
687
|
-
.strategies(s =>
|
|
688
|
-
s.sniff()
|
|
689
|
-
)
|
|
690
|
-
.interceptors(i =>
|
|
691
|
-
i.onError((ctx, api) => {
|
|
692
|
-
console.log(ctx.error);
|
|
693
|
-
|
|
694
|
-
api.setResult(null);
|
|
695
|
-
})
|
|
696
|
-
)
|
|
697
|
-
.request();
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
## vigor.fetch
|
|
701
|
-
|
|
702
|
-
### Methods
|
|
703
|
-
|
|
704
|
-
| Method | Description |
|
|
705
|
-
|---|---|
|
|
706
|
-
| method(http method) | Forces the http method |
|
|
707
|
-
| origin(...paths) | Sets base URL and origin paths |
|
|
708
|
-
| path(...paths) | Appends request paths |
|
|
709
|
-
| query(params) | Sets query parameters |
|
|
710
|
-
| headers(headers) | Sets request headers |
|
|
711
|
-
| options(options) | Sets fetch options |
|
|
712
|
-
| retryConfig(fn \| config) | Configures retry engine |
|
|
713
|
-
| parseConfig(fn \| config) | Configures parse engine |
|
|
714
|
-
| interceptors(fn \| config) | Configures fetch interceptors |
|
|
715
|
-
| abortSignals(...signals) | Attaches external AbortSignals |
|
|
716
|
-
| request(config?) | Executes fetch pipeline |
|
|
717
|
-
|
|
718
|
-
---
|
|
719
|
-
|
|
720
|
-
### origin
|
|
721
|
-
|
|
722
|
-
Sets base URL and origin paths.
|
|
723
|
-
|
|
724
|
-
```ts
|
|
725
|
-
.fetch("https://api.example.com/", "/api")
|
|
726
|
-
```
|
|
727
|
-
|
|
728
|
-
Produces:
|
|
729
|
-
|
|
730
|
-
```txt
|
|
731
|
-
https://api.example.com/api
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
---
|
|
735
|
-
|
|
736
|
-
### path
|
|
737
|
-
|
|
738
|
-
Appends request paths.
|
|
739
|
-
|
|
740
|
-
```ts
|
|
741
|
-
.path("v1", "users")
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
Produces:
|
|
745
|
-
|
|
746
|
-
```txt
|
|
747
|
-
https://api.example.com/api/v1/users
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
---
|
|
751
|
-
|
|
752
|
-
### query
|
|
753
|
-
|
|
754
|
-
Adds query parameters.
|
|
755
|
-
|
|
756
|
-
```ts
|
|
757
|
-
.query({
|
|
758
|
-
page: 1,
|
|
759
|
-
limit: 10
|
|
760
|
-
})
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
Produces:
|
|
764
|
-
|
|
765
|
-
```txt
|
|
766
|
-
?page=1&limit=10
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
---
|
|
770
|
-
|
|
771
|
-
### headers
|
|
772
|
-
|
|
773
|
-
Sets request headers.
|
|
774
|
-
|
|
775
|
-
```ts
|
|
776
|
-
.headers({
|
|
777
|
-
Authorization: "Bearer token"
|
|
778
|
-
})
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
---
|
|
782
|
-
|
|
783
|
-
### options
|
|
784
|
-
|
|
785
|
-
Sets native fetch options.
|
|
786
|
-
|
|
787
|
-
```ts
|
|
788
|
-
.options({
|
|
789
|
-
method: "POST",
|
|
790
|
-
body: JSON.stringify({
|
|
791
|
-
username: "john"
|
|
792
|
-
})
|
|
793
|
-
})
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
---
|
|
797
|
-
|
|
798
|
-
### retryConfig
|
|
799
|
-
|
|
800
|
-
Configures internal retry engine.
|
|
801
|
-
|
|
802
|
-
```ts
|
|
803
|
-
.retryConfig(r => r
|
|
804
|
-
.settings(s => s
|
|
805
|
-
.attempt(5)
|
|
806
|
-
)
|
|
807
|
-
)
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
---
|
|
811
|
-
|
|
812
|
-
### parseConfig
|
|
813
|
-
|
|
814
|
-
Configures internal parse engine.
|
|
815
|
-
|
|
816
|
-
```ts
|
|
817
|
-
.parseConfig(p => p
|
|
818
|
-
.strategies(s => s
|
|
819
|
-
.sniff()
|
|
820
|
-
)
|
|
821
|
-
)
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
---
|
|
825
|
-
|
|
826
|
-
### abortSignals
|
|
827
|
-
|
|
828
|
-
Attaches external AbortSignals.
|
|
829
|
-
|
|
830
|
-
```ts
|
|
831
|
-
const controller = new AbortController();
|
|
832
|
-
|
|
833
|
-
await vigor
|
|
834
|
-
.fetch("/api")
|
|
835
|
-
.abortSignals(controller.signal)
|
|
836
|
-
.request();
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
---
|
|
840
|
-
|
|
841
|
-
### request
|
|
842
|
-
|
|
843
|
-
Executes fetch workflow.
|
|
844
|
-
|
|
845
|
-
```ts
|
|
846
|
-
const data = await vigor
|
|
847
|
-
.fetch("https://api.example.com")
|
|
848
|
-
.request();
|
|
849
|
-
```
|
|
850
|
-
|
|
851
|
-
---
|
|
852
|
-
|
|
853
|
-
## Interceptors
|
|
854
|
-
|
|
855
|
-
| Name | API |
|
|
856
|
-
|---|:---:|
|
|
857
|
-
| before | throwError / abort |
|
|
858
|
-
| after | setResponse / throwError |
|
|
859
|
-
| result | setResult / throwError |
|
|
860
|
-
| onError | setResult / throwError / retry |
|
|
861
|
-
|
|
862
|
-
---
|
|
863
|
-
|
|
864
|
-
### before
|
|
865
|
-
|
|
866
|
-
Runs before fetch execution.
|
|
867
|
-
|
|
868
|
-
```ts
|
|
869
|
-
.before(async (ctx, api) => {
|
|
870
|
-
console.log(ctx.url);
|
|
871
|
-
})
|
|
872
|
-
```
|
|
873
|
-
|
|
874
|
-
#### Available APIs
|
|
875
|
-
|
|
876
|
-
| API | Description |
|
|
877
|
-
|---|---|
|
|
878
|
-
| throwError(error) | Immediately throws error |
|
|
879
|
-
| abort(error) | Aborts request |
|
|
880
|
-
|
|
881
|
-
---
|
|
882
|
-
|
|
883
|
-
### after
|
|
884
|
-
|
|
885
|
-
Runs after receiving Response.
|
|
886
|
-
|
|
887
|
-
```ts
|
|
888
|
-
.after(async (ctx, api) => {
|
|
889
|
-
console.log(ctx.response.status);
|
|
890
|
-
})
|
|
891
|
-
```
|
|
892
|
-
|
|
893
|
-
#### Available APIs
|
|
894
|
-
|
|
895
|
-
| API | Description |
|
|
896
|
-
|---|---|
|
|
897
|
-
| setResponse(response) | Replaces Response object |
|
|
898
|
-
| throwError(error) | Throws error |
|
|
899
|
-
|
|
900
|
-
---
|
|
901
|
-
|
|
902
|
-
### result
|
|
903
|
-
|
|
904
|
-
Runs before returning parsed result.
|
|
905
|
-
|
|
906
|
-
```ts
|
|
907
|
-
.result(async (ctx, api) => {
|
|
908
|
-
api.setResult({
|
|
909
|
-
data: ctx.result
|
|
910
|
-
});
|
|
911
|
-
})
|
|
912
|
-
```
|
|
913
|
-
|
|
914
|
-
#### Available APIs
|
|
915
|
-
|
|
916
|
-
| API | Description |
|
|
917
|
-
|---|---|
|
|
918
|
-
| setResult(value) | Replaces final result |
|
|
919
|
-
| throwError(error) | Throws error |
|
|
920
|
-
|
|
921
|
-
---
|
|
922
|
-
|
|
923
|
-
### onError
|
|
924
|
-
|
|
925
|
-
Runs when fetch fails.
|
|
926
|
-
|
|
927
|
-
```ts
|
|
928
|
-
.onError(async (ctx, api) => {
|
|
929
|
-
api.retry();
|
|
930
|
-
})
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
#### Available APIs
|
|
934
|
-
|
|
935
|
-
| API | Description |
|
|
936
|
-
|---|---|
|
|
937
|
-
| setResult(value) | Returns fallback value |
|
|
938
|
-
| throwError(error) | Throws error |
|
|
939
|
-
| restart() | Re-executes fetch pipeline |
|
|
940
|
-
|
|
941
|
-
---
|
|
942
|
-
|
|
943
|
-
## Example
|
|
944
|
-
|
|
945
|
-
```ts
|
|
946
|
-
const data = await vigor
|
|
947
|
-
.fetch("https://api.example.com", "api")
|
|
948
|
-
.path("v1", "users")
|
|
949
|
-
.query({
|
|
950
|
-
page: 1
|
|
951
|
-
})
|
|
952
|
-
.headers({
|
|
953
|
-
Authorization: "Bearer token"
|
|
954
|
-
})
|
|
955
|
-
.retryConfig(r =>
|
|
956
|
-
r.settings(s =>
|
|
957
|
-
s.attempt(5)
|
|
958
|
-
)
|
|
959
|
-
)
|
|
960
|
-
.request();
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
## vigor.all
|
|
964
|
-
|
|
965
|
-
### Methods
|
|
966
|
-
|
|
967
|
-
| Method | Description |
|
|
968
|
-
|---|---|
|
|
969
|
-
| target(...tasks) | Sets async task list |
|
|
970
|
-
| settings(fn \| config) | Configures concurrency settings |
|
|
971
|
-
| interceptors(fn \| config) | Configures concurrency interceptors |
|
|
972
|
-
| abortSignals(...signals) | Attaches external AbortSignals |
|
|
973
|
-
| request(config?) | Executes concurrency pipeline |
|
|
974
|
-
|
|
975
|
-
---
|
|
976
|
-
|
|
977
|
-
### target
|
|
978
|
-
|
|
979
|
-
Sets async task list.
|
|
980
|
-
|
|
981
|
-
```ts
|
|
982
|
-
.all(
|
|
983
|
-
async () => fetch("/api/1"),
|
|
984
|
-
async () => fetch("/api/2")
|
|
985
|
-
)
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
---
|
|
989
|
-
|
|
990
|
-
### settings
|
|
991
|
-
|
|
992
|
-
Configures concurrency behavior.
|
|
993
|
-
|
|
994
|
-
```ts
|
|
995
|
-
.settings(s => s
|
|
996
|
-
.concurrency(2)
|
|
997
|
-
.onlySuccess(true)
|
|
998
|
-
)
|
|
999
|
-
```
|
|
1000
|
-
|
|
1001
|
-
#### Settings API
|
|
1002
|
-
|
|
1003
|
-
| Method | Description | Default |
|
|
1004
|
-
|---|---|---|
|
|
1005
|
-
| concurrency(number) | Maximum concurrent tasks | `Infinity` |
|
|
1006
|
-
| onlySuccess(boolean) | Returns only successful results | `false` |
|
|
1007
|
-
| fallback(value) | Fallback return value | `throws` |
|
|
1008
|
-
|
|
1009
|
-
---
|
|
1010
|
-
|
|
1011
|
-
### abortSignals
|
|
1012
|
-
|
|
1013
|
-
Attaches external AbortSignals.
|
|
1014
|
-
|
|
1015
|
-
```ts
|
|
1016
|
-
const controller = new AbortController();
|
|
1017
|
-
|
|
1018
|
-
await vigor
|
|
1019
|
-
.all(task1, task2)
|
|
1020
|
-
.abortSignals(controller.signal)
|
|
1021
|
-
.request();
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
---
|
|
1025
|
-
|
|
1026
|
-
### request
|
|
1027
|
-
|
|
1028
|
-
Executes concurrency workflow.
|
|
1029
|
-
|
|
1030
|
-
```ts
|
|
1031
|
-
const results = await vigor
|
|
1032
|
-
.all(task1, task2)
|
|
1033
|
-
.request();
|
|
1034
|
-
```
|
|
1035
|
-
|
|
1036
|
-
---
|
|
1037
|
-
|
|
1038
|
-
## Interceptors
|
|
1039
|
-
|
|
1040
|
-
| Name | API |
|
|
1041
|
-
|---|:---:|
|
|
1042
|
-
| before | throwError / abort |
|
|
1043
|
-
| afterEach | setResult / throwError |
|
|
1044
|
-
| after | setResults / throwError |
|
|
1045
|
-
| result | setResults / throwError |
|
|
1046
|
-
| onError | setResults / throwError |
|
|
1047
|
-
|
|
1048
|
-
---
|
|
1049
|
-
|
|
1050
|
-
### before
|
|
1051
|
-
|
|
1052
|
-
Runs before task execution starts.
|
|
1053
|
-
|
|
1054
|
-
```ts
|
|
1055
|
-
.before(async (ctx, api) => {
|
|
1056
|
-
console.log(ctx.tasks.length);
|
|
1057
|
-
})
|
|
1058
|
-
```
|
|
1059
|
-
|
|
1060
|
-
#### Available APIs
|
|
1061
|
-
|
|
1062
|
-
| API | Description |
|
|
1063
|
-
|---|---|
|
|
1064
|
-
| throwError(error) | Immediately throws error |
|
|
1065
|
-
| abort(error) | Aborts execution |
|
|
1066
|
-
|
|
1067
|
-
---
|
|
1068
|
-
|
|
1069
|
-
### after
|
|
1070
|
-
|
|
1071
|
-
Runs after all tasks complete.
|
|
1072
|
-
|
|
1073
|
-
```ts
|
|
1074
|
-
.after(async (ctx, api) => {
|
|
1075
|
-
console.log(ctx.results);
|
|
1076
|
-
})
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
#### Available APIs
|
|
1080
|
-
|
|
1081
|
-
| API | Description |
|
|
1082
|
-
|---|---|
|
|
1083
|
-
| setResults(value) | Replaces result array |
|
|
1084
|
-
| throwError(error) | Throws error |
|
|
1085
|
-
|
|
1086
|
-
---
|
|
1087
|
-
|
|
1088
|
-
### result
|
|
1089
|
-
|
|
1090
|
-
Runs before returning final results.
|
|
1091
|
-
|
|
1092
|
-
```ts
|
|
1093
|
-
.result(async (ctx, api) => {
|
|
1094
|
-
api.setResults(
|
|
1095
|
-
ctx.results.filter(Boolean)
|
|
1096
|
-
);
|
|
1097
|
-
})
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
#### Available APIs
|
|
1101
|
-
|
|
1102
|
-
| API | Description |
|
|
1103
|
-
|---|---|
|
|
1104
|
-
| setResults(value) | Replaces final results |
|
|
1105
|
-
| throwError(error) | Throws error |
|
|
1106
|
-
|
|
1107
|
-
---
|
|
1108
|
-
|
|
1109
|
-
### onError
|
|
1110
|
-
|
|
1111
|
-
Runs when concurrency execution fails.
|
|
1112
|
-
|
|
1113
|
-
```ts
|
|
1114
|
-
.onError(async (ctx, api) => {
|
|
1115
|
-
api.setResults([]);
|
|
1116
|
-
})
|
|
1117
|
-
```
|
|
1118
|
-
|
|
1119
|
-
#### Available APIs
|
|
1120
|
-
|
|
1121
|
-
| API | Description |
|
|
1122
|
-
|---|---|
|
|
1123
|
-
| setResults(value) | Returns fallback results |
|
|
1124
|
-
| throwError(error) | Throws error |
|
|
1125
|
-
|
|
1126
|
-
---
|
|
1127
|
-
|
|
1128
|
-
## Example
|
|
1129
|
-
|
|
1130
|
-
```ts
|
|
1131
|
-
const results = await vigor
|
|
1132
|
-
.all(
|
|
1133
|
-
async () => fetch("/api/1"),
|
|
1134
|
-
async () => fetch("/api/2"),
|
|
1135
|
-
async () => fetch("/api/3")
|
|
1136
|
-
)
|
|
1137
|
-
.settings(s =>
|
|
1138
|
-
s
|
|
1139
|
-
.concurrency(2)
|
|
1140
|
-
.onlySuccess(true)
|
|
1141
|
-
)
|
|
1142
|
-
.request();
|
|
1143
|
-
```
|
|
1
|
+
# vigor-fetch
|
|
2
|
+
|
|
3
|
+
**A composable, lightweight network workflow toolkit built on native Fetch.**
|
|
4
|
+
|
|
5
|
+
Vigor gives you an immutable, chainable API for building robust network logic: retry with configurable backoff, content-aware response parsing, and concurrency-limited task running — all composed together, or used standalone.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🧩 **Immutable, fluent builders** — every call returns a new instance; nothing is mutated in place
|
|
12
|
+
- 🔁 **Two-level retry** — per-attempt retry/backoff *and* whole-pipeline restart, each independently configurable
|
|
13
|
+
- 🌐 **Smart fetch layer** — automatic `429 Retry-After` handling, configurable "never retry" status codes
|
|
14
|
+
- 🔌 **Content-aware parsing** — auto-detects `Content-Type`, with a sniffing fallback and custom strategies
|
|
15
|
+
- ⚡ **Bounded concurrency runner** — run many tasks with a concurrency cap and per-task hooks
|
|
16
|
+
- 🪝 **Two middleware modes** — `"fluent"` (return a value) or `"intercept"` (get an explicit control API)
|
|
17
|
+
- 🧠 **TypeScript-first** — config shape is inferred and carried through the whole chain
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install vigor-fetch
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Import
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import vigor from "vigor-fetch";
|
|
31
|
+
// or
|
|
32
|
+
import { vigor } from "vigor-fetch";
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Both work — `vigor` is exported as both the default and a named export. Every internal class (`VigorFetch`, `VigorRetry`, `VigorParse`, `VigorAll`, their settings/middleware/strategy/algorithm builders, and error classes) is also exported by name if you want to construct them directly instead of going through `vigor.*`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
### Fetch
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import vigor from "vigor-fetch";
|
|
45
|
+
|
|
46
|
+
const data = await vigor
|
|
47
|
+
.fetch("https://api.example.com")
|
|
48
|
+
.path("v1", "users")
|
|
49
|
+
.query({ page: 1, limit: 10 })
|
|
50
|
+
.request();
|
|
51
|
+
// -> GET https://api.example.com/v1/users?page=1&limit=10
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Retry (standalone)
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const data = await vigor
|
|
58
|
+
.retry(async (signal) => {
|
|
59
|
+
return await db.select("...");
|
|
60
|
+
})
|
|
61
|
+
.request();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Parse (standalone)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const response = await fetch("https://api.example.com");
|
|
68
|
+
|
|
69
|
+
const data = await vigor
|
|
70
|
+
.parse(response)
|
|
71
|
+
.request();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Concurrency
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const results = await vigor
|
|
78
|
+
.all(
|
|
79
|
+
() => fetch("/api/1"),
|
|
80
|
+
() => fetch("/api/2")
|
|
81
|
+
)
|
|
82
|
+
.request();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## The `vigor` object
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
vigor.fetch(origin?) // -> VigorFetch, origin pre-set if given
|
|
91
|
+
vigor.retry(task?) // -> VigorRetry, target pre-set if given
|
|
92
|
+
vigor.parse(response?) // -> VigorParse, target pre-set if given
|
|
93
|
+
vigor.all(...tasks) // -> VigorAll, target pre-set if any tasks given
|
|
94
|
+
vigor.use(plugin, options?)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every module follows the same shape: an **immutable builder** with `.settings()`, `.middlewares()`, and a terminal `.request()` / `.requestVerbose()` pair.
|
|
98
|
+
|
|
99
|
+
- `.request()` returns the result directly and **throws** on final failure.
|
|
100
|
+
- `.requestVerbose()` never throws — it returns `{ success: true, data, error: null }` or `{ success: false, data: null, error }`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## `vigor.fetch`
|
|
105
|
+
|
|
106
|
+
### Methods
|
|
107
|
+
|
|
108
|
+
| Method | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `method(str)` | Forces the HTTP method. Default: `POST` if a body is set, otherwise `GET` |
|
|
111
|
+
| `origin(str)` | Sets the base URL (absolute) or a path relative to the current page |
|
|
112
|
+
| `path(...strs)` | Appends path segments |
|
|
113
|
+
| `query(...objs)` | Adds query-parameter objects (each call appends another object to the list) |
|
|
114
|
+
| `hash(str)` | Sets the URL fragment, replacing any previous value |
|
|
115
|
+
| `headers(mode, obj)` | Sets headers. `mode`: `"overwrite"` (discard previous) or `"merge"` (`Object.assign`-style) |
|
|
116
|
+
| `body(mode, obj)` | Sets the body. `"overwrite"` replaces it entirely; `"merge"` shallow-merges when both old and new body are plain objects, otherwise replaces |
|
|
117
|
+
| `options(mode, obj)` | Sets extra native `RequestInit` fields other than `method`/`headers`/`body`/`signal` (e.g. `credentials`, `mode`, `cache`) |
|
|
118
|
+
| `settings(fn \| config)` | Configures fetch-level settings (below) |
|
|
119
|
+
| `middlewares(fn \| config)` | Configures the fetch middleware pipeline (below) |
|
|
120
|
+
| `retry(fn \| config \| VigorRetry)` | Configures the **internal retry engine** used to send the request |
|
|
121
|
+
| `parse(fn \| config \| VigorParse)` | Configures the **internal parse engine** used to interpret the response |
|
|
122
|
+
| `request()` | Executes and returns the parsed result, throwing on failure |
|
|
123
|
+
| `requestVerbose()` | Executes and returns `{ success, data, error }` |
|
|
124
|
+
|
|
125
|
+
> `path()`, `query()`, `headers()` in merge mode, etc. are all additive per call — chain them incrementally rather than passing everything in one call.
|
|
126
|
+
|
|
127
|
+
### origin / path / query / hash
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
vigor.fetch("https://api.example.com/", "/api")
|
|
131
|
+
.path("v1", "users")
|
|
132
|
+
.query({ page: 1 })
|
|
133
|
+
.hash("section-2")
|
|
134
|
+
// -> https://api.example.com/api/v1/users?page=1#section-2
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`origin()` only accepts one argument — pass origin + first path segments as separate calls, or fold extra segments into `path()`.
|
|
138
|
+
|
|
139
|
+
### headers / body / options
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
vigor.fetch("/api")
|
|
143
|
+
.headers("merge", { Authorization: "Bearer token" })
|
|
144
|
+
.body("overwrite", { username: "john" })
|
|
145
|
+
.options("merge", { credentials: "include" })
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Body is auto-normalized based on type (string, `Blob`, `ArrayBuffer`, `URLSearchParams`, `FormData`, or plain object → JSON), and a matching `Content-Type` header is set automatically unless you've already set one via `.headers()`.
|
|
149
|
+
|
|
150
|
+
### settings
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
.settings(s => s
|
|
154
|
+
.unretryStatus(400, 401, 403, 404, 405, 413, 422)
|
|
155
|
+
.retryHeaders("retry-after", "x-ratelimit-reset")
|
|
156
|
+
.maxRestarts(3)
|
|
157
|
+
.default(ctx => ({ fallback: true }))
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| Method | Description | Default |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `retryHeaders(...strs)` | Response headers checked for a `429` retry delay | `["retry-after", "ratelimit-reset", "x-ratelimit-reset", "x-retry-after", "x-amz-retry-after", "chrome-proxy-next-link"]` |
|
|
164
|
+
| `unretryStatus(...nums)` | HTTP status codes that are never retried, regardless of the retry engine's attempt budget | `[400, 401, 403, 404, 405, 413, 422]` |
|
|
165
|
+
| `maxRestarts(num)` | Max number of full pipeline restarts (see `onError` below) | `3` |
|
|
166
|
+
| `default(fn)` | Fallback value/factory used only if the request ultimately fails and no restart/override happened | throws |
|
|
167
|
+
|
|
168
|
+
On any non-`unretryStatus` failure response, the internal retry engine retries per its own `settings`/`algorithms` (defaults: 5 attempts, 20s timeout, exponential-ish backoff jitter — see `vigor.retry` below). On a `429`, Vigor reads `retryHeaders` off the response and uses that value (seconds or an HTTP date) as the next delay automatically.
|
|
169
|
+
|
|
170
|
+
### interceptors (`middlewares`)
|
|
171
|
+
|
|
172
|
+
Every stage takes a mode as the first argument: `"fluent"` (return a plain value) or `"intercept"` (receive an explicit API object and return the context).
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
.middlewares(m => m
|
|
176
|
+
.before("intercept", async (ctx, api) => {
|
|
177
|
+
api.setHeaders({ ...ctx.options.headers, "X-Trace": crypto.randomUUID() });
|
|
178
|
+
return ctx;
|
|
179
|
+
})
|
|
180
|
+
.onError("intercept", async (ctx, api) => {
|
|
181
|
+
if (ctx.error instanceof SomeRecoverableError) api.proceedRestart();
|
|
182
|
+
return ctx;
|
|
183
|
+
})
|
|
184
|
+
)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
| Stage | Runs | Intercept API |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `before` | Before the request is sent | `throwError`, `setOptions`, `setHeaders`, `setBody` |
|
|
190
|
+
| `after` | After the response is parsed | `setResult`, `throwError` |
|
|
191
|
+
| `onResult` | Right before returning the final result | `setResult`, `throwError` |
|
|
192
|
+
| `onError` | After the request has finally failed | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
|
|
193
|
+
|
|
194
|
+
Fluent-mode signatures: `before`/`after` take `(ctx) => void`, `onResult` takes `(res) => newRes`, `onError` takes `(err) => void`.
|
|
195
|
+
|
|
196
|
+
`onError` → `proceedRestart()` re-runs the **entire** fetch pipeline from scratch (up to `settings.maxRestarts` times) — this is separate from, and on top of, the internal retry engine's per-attempt retries.
|
|
197
|
+
|
|
198
|
+
### retry / parse (nested engines)
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const data = await vigor
|
|
202
|
+
.fetch("/api/users")
|
|
203
|
+
.retry(r => r.settings(s => s.maxAttempts(8)).algorithms(a => a.backoff({ multiplier: 2 })))
|
|
204
|
+
.parse(p => p.strategies(s => s.sniff()))
|
|
205
|
+
.request();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Both accept a config object, a builder callback, or an already-built `VigorRetry`/`VigorParse` instance. The target function you'd normally set with `.target()` is ignored here — fetch always supplies its own target (the actual `fetch()` call) and its own `.target()` (`response.clone()` fed through your parse config).
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## `vigor.retry`
|
|
213
|
+
|
|
214
|
+
### Methods
|
|
215
|
+
|
|
216
|
+
| Method | Description |
|
|
217
|
+
|---|---|
|
|
218
|
+
| `target(fn)` | Sets the function to run: `(signal: AbortSignal) => any \| Promise<any>` |
|
|
219
|
+
| `abortSignals(...sigs)` | Attaches external `AbortSignal`s that cancel the current attempt when aborted |
|
|
220
|
+
| `settings(fn \| config)` | General retry settings (below) |
|
|
221
|
+
| `middlewares(fn \| config)` | Middleware pipeline (below) |
|
|
222
|
+
| `algorithms(fn)` | Selects and configures the delay algorithm (below) |
|
|
223
|
+
| `request()` / `requestVerbose()` | Executes |
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
|
|
228
|
+
await vigor
|
|
229
|
+
.retry(async (signal) => fetch("/api", { signal }))
|
|
230
|
+
.abortSignals(controller.signal)
|
|
231
|
+
.settings(s => s.maxAttempts(5).timeout(5000))
|
|
232
|
+
.request();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### settings
|
|
236
|
+
|
|
237
|
+
| Method | Description | Default |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `default(fn)` | Fallback value/factory when the target ultimately fails | throws |
|
|
240
|
+
| `maxAttempts(num)` | Max attempts before giving up on this run | `5` |
|
|
241
|
+
| `maxRestarts(num)` | Max full pipeline restarts (see `onError`) | `3` |
|
|
242
|
+
| `timeout(ms)` | Timeout per individual attempt | `20000` |
|
|
243
|
+
|
|
244
|
+
### algorithms
|
|
245
|
+
|
|
246
|
+
Delay algorithm is a **replace**, not merge — switching algorithms discards the previous one's config.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
.algorithms(a => a.backoff({ initial: 1000, multiplier: 1.7 }))
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| Algorithm | Fields (with defaults) |
|
|
253
|
+
|---|---|
|
|
254
|
+
| `constant(cfg?)` | `interval` (`2000`), `jitter` (`1000`) |
|
|
255
|
+
| `linear(cfg?)` | `initial` (`500`), `increment` (`1000`), `minDelay` (`0`), `maxDelay` (`10000`), `jitter` (`1000`) |
|
|
256
|
+
| `backoff(cfg?)` | `initial` (`0`), `multiplier` (`1.7`), `unit` (`1000`), `minDelay` (`500`), `maxDelay` (`10000`), `jitter` (`800`) |
|
|
257
|
+
| `custom(cfg?)` | `target: (attempt) => number` (required), `minDelay` (`500`), `maxDelay` (`10000`), `jitter` (`800`) |
|
|
258
|
+
|
|
259
|
+
Each computed delay is clamped to `[minDelay, maxDelay]` (except `constant`, which has no bounds) and then perturbed by `±jitter` ms.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
.algorithms(a => a.custom({
|
|
263
|
+
target: (attempt) => attempt * 3000
|
|
264
|
+
}))
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### interceptors (`middlewares`)
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
.middlewares(m => m
|
|
271
|
+
.retryIf("intercept", async (ctx, api) => {
|
|
272
|
+
if (ctx.error instanceof TypeError) api.proceedRetry();
|
|
273
|
+
else api.cancelRetry();
|
|
274
|
+
return ctx;
|
|
275
|
+
})
|
|
276
|
+
.onRetry("intercept", async (ctx, api) => {
|
|
277
|
+
api.setDelay(5000);
|
|
278
|
+
return ctx;
|
|
279
|
+
})
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
| Stage | Runs | Intercept API |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| `before` | Before each attempt | `throwError`, `abort`, `breakRetry` |
|
|
286
|
+
| `after` | After the target settles successfully | `setResult`, `throwError` |
|
|
287
|
+
| `onResult` | Right before returning the attempt's result | `setResult`, `throwError` |
|
|
288
|
+
| `retryIf` | After a failed attempt, decides whether to retry | `proceedRetry`, `cancelRetry` |
|
|
289
|
+
| `onRetry` | Right before the retry delay is awaited | `throwError`, `setDelay` |
|
|
290
|
+
| `onError` | After attempts are exhausted (or `breakRetry` was called) | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
|
|
291
|
+
|
|
292
|
+
Fluent-mode signatures: `before`/`after` take `(ctx) => void`, `onResult` takes `(res) => newRes`, `retryIf` takes `(err) => boolean`, `onRetry`/`onError` take `(err) => void`.
|
|
293
|
+
|
|
294
|
+
There are two independent resilience layers here:
|
|
295
|
+
- **`retryIf`/`onRetry`** loop through attempts (bounded by `settings.maxAttempts`) without ever fully failing.
|
|
296
|
+
- **`onError`** fires once attempts are exhausted; calling `api.proceedRestart()` there resets the attempt counter and starts over (bounded by `settings.maxRestarts`).
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## `vigor.parse`
|
|
301
|
+
|
|
302
|
+
### Methods
|
|
303
|
+
|
|
304
|
+
| Method | Description |
|
|
305
|
+
|---|---|
|
|
306
|
+
| `target(response)` | Sets the `Response` to parse |
|
|
307
|
+
| `settings(fn \| config)` | General settings (below) |
|
|
308
|
+
| `middlewares(fn \| config)` | Middleware pipeline (below) |
|
|
309
|
+
| `strategies(fn \| config)` | Configures the ordered fallback chain of parsing strategies |
|
|
310
|
+
| `request()` / `requestVerbose()` | Executes |
|
|
311
|
+
|
|
312
|
+
### settings
|
|
313
|
+
|
|
314
|
+
| Method | Description | Default |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| `raw(bool)` | When `true`, skips parsing entirely and returns the raw `Response` object | `false` |
|
|
317
|
+
| `default(fn)` | Fallback value/factory when parsing ultimately fails | throws |
|
|
318
|
+
| `maxRestarts(num)` | Max full pipeline restarts | `3` |
|
|
319
|
+
|
|
320
|
+
### strategies
|
|
321
|
+
|
|
322
|
+
Strategies form an ordered list that's tried in sequence until one succeeds (each attempt but the last clones the response first, so a failed strategy doesn't consume the body). If you never configure any, Vigor defaults to content-type detection.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
.strategies(s => s.sniff())
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| Method | Behavior |
|
|
329
|
+
|---|---|
|
|
330
|
+
| `contentType(replace?)` | Parses based on the response's `Content-Type` header (JSON / XML / form-urlencoded / octet-stream / image / audio / video / multipart / text) |
|
|
331
|
+
| `sniff(replace?)` | Tries `json → formData → text → blob` in order until one parses |
|
|
332
|
+
| `json(replace?)` | Always parses as JSON |
|
|
333
|
+
| `text(replace?)` | Always parses as text |
|
|
334
|
+
| `arrayBuffer(replace?)` | Always parses as `ArrayBuffer` |
|
|
335
|
+
| `blob(replace?)` | Always parses as `Blob` |
|
|
336
|
+
| `bytes(replace?)` | Always parses as `Uint8Array` |
|
|
337
|
+
| `formData(replace?)` | Always parses as `FormData` |
|
|
338
|
+
| `custom(func, replace?)` | Uses a custom `(response: Response) => Promise<any>` |
|
|
339
|
+
|
|
340
|
+
By default each call **appends** to the chain (they concatenate, so you can layer fallbacks: e.g. `contentType()` then `sniff()` as a last resort). Pass `replace: true` as the trailing argument to replace the whole chain instead.
|
|
341
|
+
|
|
342
|
+
### interceptors (`middlewares`)
|
|
343
|
+
|
|
344
|
+
| Stage | Runs | Intercept API |
|
|
345
|
+
|---|---|---|
|
|
346
|
+
| `before` | Before parsing starts | `throwError` |
|
|
347
|
+
| `after` | After a strategy succeeds | `setResult`, `throwError` |
|
|
348
|
+
| `onResult` | Right before returning the final result | `setResult`, `throwError` |
|
|
349
|
+
| `onError` | When every strategy in the chain failed | `setResult`, `throwError`, `proceedRestart`, `cancelRestart` |
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const data = await vigor
|
|
353
|
+
.parse(response)
|
|
354
|
+
.strategies(s => s.sniff())
|
|
355
|
+
.middlewares(m => m.onError("intercept", async (ctx, api) => {
|
|
356
|
+
api.setResult(null);
|
|
357
|
+
return ctx;
|
|
358
|
+
}))
|
|
359
|
+
.request();
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## `vigor.all`
|
|
365
|
+
|
|
366
|
+
Runs a fixed list of zero-argument tasks with bounded concurrency.
|
|
367
|
+
|
|
368
|
+
### Methods
|
|
369
|
+
|
|
370
|
+
| Method | Description |
|
|
371
|
+
|---|---|
|
|
372
|
+
| `target(...funcs)` | Sets the task list — each is `() => unknown \| Promise<unknown>` |
|
|
373
|
+
| `settings(fn \| config)` | Concurrency settings (below) |
|
|
374
|
+
| `middlewares(fn \| config)` | Per-task hooks (below) |
|
|
375
|
+
| `request()` / `requestVerbose()` | Executes |
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const results = await vigor
|
|
379
|
+
.all(
|
|
380
|
+
() => fetch("/api/1"),
|
|
381
|
+
() => fetch("/api/2"),
|
|
382
|
+
() => fetch("/api/3")
|
|
383
|
+
)
|
|
384
|
+
.settings(s => s.concurrency(2).onlySuccess(true))
|
|
385
|
+
.request();
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### settings
|
|
389
|
+
|
|
390
|
+
| Method | Description | Default |
|
|
391
|
+
|---|---|---|
|
|
392
|
+
| `concurrency(num)` | Max number of tasks running at once | `5` |
|
|
393
|
+
| `onlySuccess(bool)` | When `true`, filters the final array down to only tasks whose middleware chain resolved successfully | `false` |
|
|
394
|
+
|
|
395
|
+
> Unlike `vigor.retry`/`vigor.fetch`/`vigor.parse`, `vigor.all` has no `default`/fallback setting, and per-task failures don't throw the whole call by themselves — a task that fails (and whose `onError` middleware doesn't call `setResult`) simply contributes its error value into the results array (or is dropped, if `onlySuccess` is on). The only thing that makes `.request()` throw is an **empty** task list.
|
|
396
|
+
|
|
397
|
+
### hooks (`middlewares`)
|
|
398
|
+
|
|
399
|
+
Unlike the other modules, `all`'s per-task hooks are plain functions — there's no `"fluent"`/`"intercept"` mode split.
|
|
400
|
+
|
|
401
|
+
| Stage | Signature | Runs |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| `before(fn)` | `(ctx) => void \| Promise<void>` | Before each task is invoked |
|
|
404
|
+
| `after(fn)` | `(ctx) => void \| Promise<void>` | After a task settles successfully |
|
|
405
|
+
| `onError(fn)` | `(ctx, { setResult }) => void \| Promise<void>` | When an individual task throws |
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
.middlewares(m => m
|
|
409
|
+
.onError(async (ctx, api) => {
|
|
410
|
+
console.error(ctx.error);
|
|
411
|
+
api.setResult(null); // recover instead of letting this task register as failed
|
|
412
|
+
})
|
|
413
|
+
)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Plugins
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
export type VigorPlugin<O> = {
|
|
422
|
+
options: O // type-only anchor for inferring O — not used at runtime
|
|
423
|
+
func: (v: typeof vigor, options: O) => void
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
vigor.use(plugin, options?)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
`vigor.use(plugin, options)` simply invokes `plugin.func(vigor, options)` — a lightweight hook for setup-time side effects (e.g. registering shared config, wiring up logging) rather than a full plugin/extension system.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Error handling
|
|
434
|
+
|
|
435
|
+
Every module throws its own error subclass (`VigorFetchError`, `VigorRetryError`, `VigorParseError`, `VigorAllError`) — all extending a common `VigorError` with `.code`, `.data`, `.method`, `.context`, and `.timestamp`.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
try {
|
|
439
|
+
await vigor.fetch("/api/users").request();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
if (err instanceof VigorFetchError && err.code === "FETCH_FAILED") {
|
|
442
|
+
console.log(err.data.status, err.data.parsed); // failed response's status + best-effort parsed body
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Or avoid try/catch entirely with `requestVerbose()`:
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
const res = await vigor.fetch("/api/users").requestVerbose();
|
|
451
|
+
if (!res.success) {
|
|
452
|
+
// res.error is typed, res.data is null
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
| Module | Error codes |
|
|
457
|
+
|---|---|
|
|
458
|
+
| `VigorFetchError` | `VALUE_REQUIRED`, `INVALID_BODY`, `INVALID_PROTOCOL`, `FETCH_FAILED`, `RESTART_EXHAUSTED` |
|
|
459
|
+
| `VigorRetryError` | `VALUE_REQUIRED`, `RETRY_EXHAUSTED`, `RESTART_EXHAUSTED`, `TIMED_OUT` |
|
|
460
|
+
| `VigorParseError` | `VALUE_REQUIRED`, `INVALID_CONTENT_TYPE`, `PARSER_NOT_FOUND`, `PARSER_ALL_FAILED`, `RESTART_EXHAUSTED` |
|
|
461
|
+
| `VigorAllError` | `EMPTY_TARGET` |
|
|
462
|
+
|
|
463
|
+
`FETCH_FAILED`'s `data` includes `parsed`: Vigor best-effort parses the failed response's body (using your configured parse strategies) before throwing, so you can inspect an API's error payload without re-fetching. If parsing that body also fails, `parsed` is simply `undefined`.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## TypeScript notes
|
|
468
|
+
|
|
469
|
+
- Every builder is **immutable** — `.settings(...)`, `.path(...)`, etc. all return a *new* instance with the change merged in; the original is untouched.
|
|
470
|
+
- Array-valued config (`path`, `query`, parse `strategies`, etc.) **concatenates** across calls by default; object-valued config deep-merges. A handful of methods (`hash`, `headers("overwrite", ...)`, `body("overwrite", ...)`, `options("overwrite", ...)`, `retry(...)`, `parse(...)`, `algorithms(...)`) force a full **replace** instead, since merging wouldn't make sense there.
|
|
471
|
+
- Config shape is tracked through the whole chain via TypeScript generics, so `.settings()`/`.middlewares()` callbacks and `.request()`'s inferred return type stay accurate as you chain.
|