vigor-fetch 3.1.9 → 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.
Files changed (6) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +471 -1143
  3. package/dist/index.d.ts +5717 -742
  4. package/dist/index.js +1726 -1530
  5. package/dist/index.mjs +1678 -1530
  6. package/package.json +56 -52
package/dist/index.mjs CHANGED
@@ -1,788 +1,1354 @@
1
- const VigorErrorMessageFuncs = {
2
- INVALID_TARGET: ({ expected, received }) => `Invalid Task: ${typeof received} (expected: ${expected.join(', ')})`,
3
- EXHAUSTED: ({ maxAttempts }) => `Retry exhausted, (max ${maxAttempts})`,
4
- TIMED_OUT: ({ limit, attempt }) => `Timeout: exceeded ${limit}ms (attempt: ${attempt})`,
5
- INVALID_CONTENT_TYPE: ({ expected, received, response }) => `Invalid Content Type Header: ${typeof received} (expected: ${expected.join(', ')})`,
6
- PARSER_NOT_FOUND: ({ expected, received, response }) => `Parser Not Found For Header: ${typeof received} (expected: ${expected.join(', ')})`,
7
- PARSER_ALL_FAILED: ({ tried, response }) => `All Parser Failed, Tried: ${tried.join(', ')}`,
8
- INVALID_PROTOCOL: ({ expected, received }) => `Invalid Protocol: ${typeof received} (expected: ${expected.join(', ')})`,
9
- INVALID_BODY: ({ expected, received }) => `Invalid Body: ${typeof received} (expected: ${expected.join(', ')})`,
10
- FETCH_FAILED: ({ status, response, url, headers, body, statusText }) => `Fetch Failed: ${status}`,
11
- EMPTY_TARGET: ({}) => `Empty Body`
1
+ /**
2
+ * Builds the literal brand string used by {@link VigorBrand}, preserving the
3
+ * `Branch`/`Name` literal types for use as a `__brand` value.
4
+ */
5
+ function createBrand(b, n) {
6
+ return `@vigor-fetch:::${b} <- ${n}`;
7
+ }
8
+ const VigorDefault = Symbol("VigorDefault");
9
+ /** Type guard checking whether a value is the {@link VigorDefault} sentinel. */
10
+ const isVigorDefault = (v) => v === VigorDefault;
11
+ const VigorEmpty = Symbol("VigorEmpty");
12
+ /** Type guard checking whether a value is the {@link VigorEmpty} sentinel. */
13
+ const isVigorEmpty = (v) => v === VigorEmpty;
14
+
15
+ /**
16
+ * Base class for Vigor's immutable, chainable builder objects.
17
+ *
18
+ * Each subclass holds a `base` config and a current `config`, and exposes
19
+ * `_next()` to derive a new instance with a patch merged in. Subclasses
20
+ * implement `_create()` to construct the concrete instance type, and the
21
+ * return type of `_next()` is computed through the `TL` type lambda so
22
+ * chained calls stay precisely typed.
23
+ *
24
+ * @typeParam Base - Shape of the full config for this builder.
25
+ * @typeParam Config - Concrete config type held by this instance (extends `Base`).
26
+ * @typeParam TL - Type lambda mapping a config type to the concrete instance type.
27
+ */
28
+ class VigorStatus {
29
+ _base;
30
+ _config;
31
+ /**
32
+ * @param base - The default/base config for this builder.
33
+ * @param config - The (possibly partial) config to merge onto `base`.
34
+ */
35
+ constructor(base, config) {
36
+ this._base = base;
37
+ this._config = this._merge(base, config);
38
+ }
39
+ /**
40
+ * Derives a new instance by merging `patch` onto the current config.
41
+ *
42
+ * @param patch - Values to overwrite/merge in for this step.
43
+ * @param strategy - Optional tree mirroring the shape of `patch`, whose
44
+ * leaves may be `"replace" | "concat" | "merge"` to force the merge
45
+ * behavior at that path. Paths without a strategy fall back to the
46
+ * default behavior (deep merge for objects, concat for arrays).
47
+ */
48
+ _next(patch, strategy) {
49
+ const merged = this._merge(this._config, patch, strategy);
50
+ return this._create(merged);
51
+ }
52
+ /**
53
+ * Deep-merges `target` onto `source`, honoring an optional per-path
54
+ * `strategy` tree. Objects merge key by key, arrays concatenate (unless
55
+ * `"replace"` is specified), and any other value is overwritten.
56
+ */
57
+ _merge(source, target, strategy) {
58
+ const isObj = (v) => v !== null &&
59
+ typeof v === "object" &&
60
+ Object.getPrototypeOf(v) === Object.prototype;
61
+ const merge = (a, b, s) => {
62
+ if (b === undefined || b === null)
63
+ return a;
64
+ if (s === "replace")
65
+ return b;
66
+ if (Array.isArray(a) && Array.isArray(b)) {
67
+ return s === "replace" ? b : [...a, ...b];
68
+ }
69
+ if (isObj(a) && isObj(b)) {
70
+ const r = { ...a };
71
+ const sObj = isObj(s) ? s : undefined;
72
+ for (const k of Object.keys(b)) {
73
+ r[k] = merge(a[k], b[k], sObj?.[k]);
74
+ }
75
+ return r;
76
+ }
77
+ return b;
78
+ };
79
+ return merge(source, target, strategy);
80
+ }
81
+ /** The default/base config this builder was constructed with. */
82
+ get base() {
83
+ return this._base;
84
+ }
85
+ /** The current, merged config for this builder instance. */
86
+ get config() {
87
+ return this._config;
88
+ }
89
+ }
90
+
91
+ const VigorAllPolicySettingsBase = {
92
+ __brand: createBrand("All", "Policy<Settings<Schema"),
93
+ concurrency: 5,
94
+ onlySuccess: false
12
95
  };
96
+ /** Immutable builder for an all-policy's general settings. */
97
+ class VigorAllPolicySettings extends VigorStatus {
98
+ constructor(config = VigorAllBase) {
99
+ super(VigorAllBase, config);
100
+ }
101
+ _create(config) {
102
+ return new VigorAllPolicySettings(config);
103
+ }
104
+ /** Sets the maximum number of tasks run concurrently. */
105
+ concurrency(num) {
106
+ return this._next({ policy: { settings: { concurrency: num } } });
107
+ }
108
+ /** When enabled, filters the final result down to only the tasks that succeeded. */
109
+ onlySuccess(bool) {
110
+ return this._next({ policy: { settings: { onlySuccess: bool } } });
111
+ }
112
+ }
113
+
114
+ const VigorAllPolicyMiddlewaresBase = {
115
+ __brand: createBrand("All", "Policy<Middlewares<Schema"),
116
+ before: [],
117
+ after: [],
118
+ onError: []
119
+ };
120
+ /** Immutable builder for an all-policy's per-task middleware pipeline (`before`, `after`, `onError`). */
121
+ class VigorAllPolicyMiddlewares extends VigorStatus {
122
+ constructor(config = VigorAllBase) {
123
+ super(VigorAllBase, config);
124
+ }
125
+ _create(config) {
126
+ return new VigorAllPolicyMiddlewares(config);
127
+ }
128
+ /** Registers a middleware that runs before each task is invoked. */
129
+ before(func) {
130
+ return this._next({ policy: { middlewares: { before: [func] } } });
131
+ }
132
+ /** Registers a middleware that runs after each task settles successfully. */
133
+ after(func) {
134
+ return this._next({ policy: { middlewares: { after: [func] } } });
135
+ }
136
+ /** Registers a middleware that runs when an individual task fails. */
137
+ onError(func) {
138
+ return this._next({ policy: { middlewares: { onError: [func] } } });
139
+ }
140
+ }
141
+
142
+ const VigorAllPolicyBase = {
143
+ __brand: createBrand("All", "Policy<Schema"),
144
+ target: [],
145
+ settings: VigorAllPolicySettingsBase,
146
+ middlewares: VigorAllPolicyMiddlewaresBase
147
+ };
148
+
149
+ const VigorAllContextBase = {
150
+ __brand: createBrand("All", "Context<Schema"),
151
+ result: VigorDefault,
152
+ policy: VigorAllPolicyBase,
153
+ record: {}
154
+ };
155
+ const VigorAllEachContextBase = {
156
+ __brand: createBrand("All", "EachContext<Schema"),
157
+ result: VigorDefault,
158
+ error: VigorDefault,
159
+ flags: { overrided: false },
160
+ record: {}
161
+ };
162
+
163
+ /**
164
+ * Base class for all Vigor error types. Subclasses (`VigorRetryError`,
165
+ * `VigorFetchError`, `VigorParseError`, `VigorAllError`) fix `C`, `D`, and
166
+ * `T` to their own error-code union, data shape, and context shape.
167
+ *
168
+ * @typeParam C - Union of error codes for the subclass.
169
+ * @typeParam D - Shape of the structured error data.
170
+ * @typeParam T - Shape of the domain context.
171
+ */
13
172
  class VigorError extends Error {
14
173
  timestamp = new Date();
15
174
  cause;
16
175
  code;
17
176
  data;
18
177
  method;
19
- stats;
20
178
  context;
21
- constructor(code, options) {
22
- const messageFn = VigorErrorMessageFuncs[code];
23
- const message = `[${code}] ${messageFn(options?.data)}`;
24
- super(message, { cause: options?.cause });
179
+ /**
180
+ * @param code - Machine-readable error code.
181
+ * @param message - Human-readable message (rendered after the `[code]` prefix).
182
+ * @param options - Additional error metadata (cause, data, method, context).
183
+ */
184
+ constructor(code, message, options) {
185
+ super(`[${code}] ${message}`);
25
186
  this.name = new.target.name;
26
187
  this.code = code;
27
188
  this.cause = options.cause;
28
189
  this.data = options.data;
29
190
  this.method = options.method;
30
- this.stats = options.stats;
31
191
  this.context = options.context;
32
192
  Object.setPrototypeOf(this, new.target.prototype);
33
193
  Error.captureStackTrace?.(this, new.target);
34
194
  }
35
195
  }
36
- class VigorRetryError extends VigorError {
196
+
197
+ const VigorAllErrorMessageFuncs = {
198
+ EMPTY_TARGET: (_) => `Empty target list`,
199
+ };
200
+ /** Error type thrown by the all module. */
201
+ class VigorAllError extends VigorError {
202
+ /**
203
+ * @param code - One of the all module's error codes.
204
+ * @param options - Error metadata, including the `data` payload for `code`.
205
+ */
37
206
  constructor(code, options) {
38
- super(code, options);
207
+ const messageFn = VigorAllErrorMessageFuncs[code];
208
+ super(code, messageFn(options.data), options);
39
209
  }
40
210
  }
41
- class VigorParseError extends VigorError {
42
- constructor(code, options) {
43
- super(code, options);
211
+
212
+ const VigorAllBase = {
213
+ __brand: createBrand("All", "Schema"),
214
+ policy: VigorAllPolicyBase,
215
+ context: VigorAllContextBase
216
+ };
217
+ /**
218
+ * Immutable builder that runs a list of independent tasks with bounded
219
+ * concurrency, per-task middleware, and either an all-results or
220
+ * only-successes result mode.
221
+ */
222
+ class VigorAll extends VigorStatus {
223
+ constructor(config = VigorAllBase) {
224
+ super(VigorAllBase, config);
44
225
  }
45
- }
46
- class VigorFetchError extends VigorError {
47
- constructor(code, options) {
48
- super(code, options);
226
+ _create(config) {
227
+ return new VigorAll(config);
49
228
  }
50
- }
51
- class VigorAllError extends VigorError {
52
- constructor(code, options) {
53
- super(code, options);
229
+ /** Sets the list of tasks to run. */
230
+ target(...funcs) {
231
+ return this._next({ policy: { target: funcs } });
54
232
  }
55
- }
56
- class VigorStatus {
57
- _base;
58
- ctor;
59
- _config;
60
- constructor(config = {}, _base, ctor) {
61
- this._base = _base;
62
- this.ctor = ctor;
63
- this._config = { ...this._base, ...(config || {}) };
64
- }
65
- _mergeConfig(source, target) {
66
- const isPlainObject = (val) => val !== null && typeof val === 'object' && Object.getPrototypeOf(val) === Object.prototype;
67
- if (target === undefined || target === null) {
68
- return source;
233
+ /** Configures the all-policy's general settings (concurrency, onlySuccess). */
234
+ settings(input) {
235
+ if (typeof input === 'function' || input instanceof VigorAllPolicySettings) {
236
+ const result = typeof input === 'function'
237
+ ? input(new VigorAllPolicySettings())
238
+ : input;
239
+ return this._next(result.config);
69
240
  }
70
- if (isPlainObject(source) && isPlainObject(target)) {
71
- const result = { ...source };
72
- Object.keys(target).forEach((key) => {
73
- result[key] = this._mergeConfig(result[key], target[key]);
74
- });
75
- return result;
241
+ return this._next({ policy: { settings: input } });
242
+ }
243
+ /** Configures the all-policy's per-task middleware pipeline. */
244
+ middlewares(input) {
245
+ if (typeof input === 'function' || input instanceof VigorAllPolicyMiddlewares) {
246
+ const result = typeof input === 'function'
247
+ ? input(new VigorAllPolicyMiddlewares())
248
+ : input;
249
+ return this._next(result.config);
76
250
  }
77
- if (Array.isArray(source) && Array.isArray(target)) {
78
- return [...source, ...target];
251
+ return this._next({ policy: { middlewares: input } });
252
+ }
253
+ /** Runs a single task through the before/after/onError middleware pipeline. */
254
+ async _runTask(task, policy) {
255
+ let ctx = { ...VigorAllEachContextBase };
256
+ try {
257
+ for (const before of policy.middlewares.before)
258
+ await before(ctx);
259
+ ctx.result = await task();
260
+ for (const after of policy.middlewares.after)
261
+ await after(ctx);
262
+ return { success: true, value: ctx.result };
263
+ }
264
+ catch (error) {
265
+ ctx.error = error;
266
+ const setResult = (r) => { ctx.flags.overrided = true; ctx.result = r; };
267
+ for (const onError of policy.middlewares.onError)
268
+ await onError(ctx, { setResult });
269
+ if (ctx.flags.overrided)
270
+ return { success: true, value: ctx.result };
271
+ return { success: false, value: error };
272
+ }
273
+ }
274
+ /** Runs all tasks and returns the results array directly, throwing on final failure. */
275
+ async request() {
276
+ const res = await this.requestVerbose();
277
+ if (res.success)
278
+ return res.data;
279
+ throw res.error;
280
+ }
281
+ /** Runs all tasks and returns a result/error envelope instead of throwing. */
282
+ async requestVerbose() {
283
+ return await this.requestRuntime();
284
+ }
285
+ /** Internal execution loop implementing the bounded-concurrency worker pool. */
286
+ async requestRuntime(restartAttempt = 1) {
287
+ const config = this._merge(this.config, {});
288
+ const policy = config.policy;
289
+ try {
290
+ if (policy.target.length === 0)
291
+ throw new VigorAllError("EMPTY_TARGET", {
292
+ method: "request",
293
+ data: {}
294
+ });
295
+ const concurrency = Math.max(1, policy.settings.concurrency);
296
+ const results = new Array(policy.target.length);
297
+ let cursor = 0;
298
+ const worker = async () => {
299
+ while (true) {
300
+ const i = cursor++;
301
+ if (i >= policy.target.length)
302
+ return;
303
+ results[i] = await this._runTask(policy.target[i], policy);
304
+ }
305
+ };
306
+ await Promise.all(Array.from({ length: Math.min(concurrency, policy.target.length) }, () => worker()));
307
+ const result = policy.settings.onlySuccess
308
+ ? results.filter(r => r.success).map(r => r.value)
309
+ : results.map(r => r.value);
310
+ return {
311
+ success: true,
312
+ data: result,
313
+ error: null
314
+ };
315
+ }
316
+ catch (error) {
317
+ return {
318
+ success: false,
319
+ data: null,
320
+ error
321
+ };
79
322
  }
80
- return target;
81
323
  }
82
- _next(config) { return this.ctor(this._mergeConfig(this._config, config)); }
83
- _getConfig() { return this._config; }
84
- _getBase() { return this._base; }
85
324
  }
86
- const VigorDefault = Symbol("DEFAULT");
87
- class VigorRetrySettings extends VigorStatus {
88
- constructor(config) {
89
- const base = {
90
- default: VigorDefault,
91
- timeout: 20 * 1000,
92
- attempt: 5,
93
- jitter: 1000
94
- };
95
- super(config, base, (c) => new VigorRetrySettings(c));
325
+
326
+ const VigorFetchPolicySettingsBase = {
327
+ __brand: createBrand("Fetch", "Policy<Settings<Schema"),
328
+ retryHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset", "x-retry-after", "x-amz-retry-after", "chrome-proxy-next-link"],
329
+ unretryStatus: [400, 401, 403, 404, 405, 413, 422],
330
+ maxRestarts: 3,
331
+ default: VigorEmpty
332
+ };
333
+ /** Immutable builder for a fetch policy's general settings. */
334
+ class VigorFetchPolicySettings extends VigorStatus {
335
+ constructor(config = VigorFetchBase) {
336
+ super(VigorFetchBase, config);
337
+ }
338
+ _create(config) {
339
+ return new VigorFetchPolicySettings(config);
340
+ }
341
+ /** Sets response headers consulted to compute a server-suggested retry delay. */
342
+ retryHeaders(...strs) {
343
+ return this._next({ policy: { settings: { retryHeaders: strs } } });
344
+ }
345
+ /** Sets HTTP status codes that should never be retried. */
346
+ unretryStatus(...nums) {
347
+ return this._next({ policy: { settings: { unretryStatus: nums } } });
348
+ }
349
+ /** Sets the maximum number of full restarts allowed. */
350
+ maxRestarts(num) {
351
+ return this._next({ policy: { settings: { maxRestarts: num } } });
352
+ }
353
+ /** Sets the fallback value/factory used when the request ultimately fails. */
354
+ default(func) {
355
+ return this._next({ policy: { settings: { default: func } } });
96
356
  }
97
- default(unk) { return this._next({ default: unk }); }
98
- timeout(num) { return this._next({ timeout: num }); }
99
- attempt(num) { return this._next({ attempt: num }); }
100
- jitter(num) { return this._next({ jitter: num }); }
101
357
  }
102
- class VigorRetryInterceptors extends VigorStatus {
103
- constructor(config) {
104
- const base = {
105
- before: [],
106
- after: [],
107
- result: [],
108
- retryIf: [],
109
- onRetry: [],
110
- onError: []
111
- };
112
- super(config, base, (c) => new VigorRetryInterceptors(c));
113
- }
114
- before(...funcs) { return this._next({ before: funcs.flat() }); }
115
- after(...funcs) { return this._next({ after: funcs.flat() }); }
116
- result(...funcs) { return this._next({ result: funcs.flat() }); }
117
- retryIf(...funcs) { return this._next({ retryIf: funcs.flat() }); }
118
- onRetry(...funcs) { return this._next({ onRetry: funcs.flat() }); }
119
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
358
+
359
+ const VigorFetchPolicyMiddlewaresBase = {
360
+ __brand: createBrand("Fetch", "Policy<Middlewares<Schema"),
361
+ before: [],
362
+ after: [],
363
+ onResult: [],
364
+ onError: []
365
+ };
366
+ /**
367
+ * Immutable builder for a fetch policy's middleware pipeline (`before`,
368
+ * `after`, `onResult`, `onError`). Each stage accepts either "fluent"
369
+ * middleware (returns a plain value merged into the context) or
370
+ * "intercept" middleware (receives an explicit API to mutate control flow).
371
+ */
372
+ class VigorFetchPolicyMiddlewares extends VigorStatus {
373
+ constructor(config = VigorFetchBase) {
374
+ super(VigorFetchBase, config);
375
+ }
376
+ _create(config) {
377
+ return new VigorFetchPolicyMiddlewares(config);
378
+ }
379
+ before(type, func) {
380
+ return this._next({ policy: { middlewares: { before: [{ _mode: type, func }] } } });
381
+ }
382
+ after(type, func) {
383
+ return this._next({ policy: { middlewares: { after: [{ _mode: type, func }] } } });
384
+ }
385
+ onResult(type, func) {
386
+ return this._next({ policy: { middlewares: { onResult: [{ _mode: type, func }] } } });
387
+ }
388
+ onError(type, func) {
389
+ return this._next({ policy: { middlewares: { onError: [{ _mode: type, func }] } } });
390
+ }
120
391
  }
121
- class VigorRetryAlgorithmsConstant extends VigorStatus {
122
- constructor(config) {
123
- const base = {
124
- interval: 2000
125
- };
126
- super(config, base, (c) => new VigorRetryAlgorithmsConstant(c));
392
+
393
+ const VigorRetryPolicySettingsBase = {
394
+ __brand: createBrand("Retry", "Policy<Settings<Schema"),
395
+ default: VigorEmpty,
396
+ maxAttempts: 5,
397
+ maxRestarts: 3,
398
+ timeout: 20000
399
+ };
400
+ /** Immutable builder for a retry policy's general settings. */
401
+ class VigorRetryPolicySettings extends VigorStatus {
402
+ constructor(config = VigorRetryBase) {
403
+ super(VigorRetryBase, config);
127
404
  }
128
- interval(num) { return this._next({ interval: num }); }
129
- _calculateDelay(attempt) {
130
- return this._config.interval;
405
+ _create(config) {
406
+ return new VigorRetryPolicySettings(config);
407
+ }
408
+ /** Sets the fallback value/factory used when the target ultimately fails. */
409
+ default(func) {
410
+ return this._next({
411
+ policy: { settings: { default: func } }
412
+ });
413
+ }
414
+ /** Sets the maximum number of retry attempts before giving up. */
415
+ maxAttempts(num) {
416
+ return this._next({
417
+ policy: { settings: { maxAttempts: num } }
418
+ });
419
+ }
420
+ /** Sets the maximum number of full restarts allowed. */
421
+ maxRestarts(num) {
422
+ return this._next({
423
+ policy: { settings: { maxRestarts: num } }
424
+ });
425
+ }
426
+ /** Sets the timeout, in milliseconds, for a single attempt. */
427
+ timeout(num) {
428
+ return this._next({
429
+ policy: { settings: { timeout: num } }
430
+ });
131
431
  }
132
432
  }
133
- class VigorRetryAlgorithmsLinear extends VigorStatus {
134
- constructor(config) {
135
- const base = {
136
- initial: 1000,
137
- increment: 1000,
138
- minDelay: 500,
139
- maxDelay: 20 * 1000
140
- };
141
- super(config, base, (c) => new VigorRetryAlgorithmsLinear(c));
433
+
434
+ const VigorRetryPolicyMiddlewaresBase = {
435
+ __brand: createBrand("Retry", "Policy<Middlewares<Schema"),
436
+ before: [],
437
+ after: [],
438
+ onResult: [],
439
+ retryIf: [],
440
+ onRetry: [],
441
+ onError: []
442
+ };
443
+ /**
444
+ * Immutable builder for a retry policy's middleware pipeline (`before`,
445
+ * `after`, `onResult`, `retryIf`, `onRetry`, `onError`). Each stage accepts
446
+ * either "fluent" middleware (returns a plain value merged into the
447
+ * context) or "intercept" middleware (receives an explicit API to mutate
448
+ * control flow).
449
+ */
450
+ class VigorRetryPolicyMiddlewares extends VigorStatus {
451
+ constructor(config = VigorRetryBase) {
452
+ super(VigorRetryBase, config);
453
+ }
454
+ _create(config) {
455
+ return new VigorRetryPolicyMiddlewares(config);
456
+ }
457
+ before(type, func) {
458
+ return this._next({
459
+ policy: { middlewares: { before: [{ _mode: type, func }] } }
460
+ });
461
+ }
462
+ after(type, func) {
463
+ return this._next({
464
+ policy: { middlewares: { after: [{ _mode: type, func }] } }
465
+ });
466
+ }
467
+ onResult(type, func) {
468
+ return this._next({
469
+ policy: { middlewares: { onResult: [{ _mode: type, func }] } }
470
+ });
471
+ }
472
+ retryIf(type, func) {
473
+ return this._next({
474
+ policy: { middlewares: { retryIf: [{ _mode: type, func }] } }
475
+ });
476
+ }
477
+ onRetry(type, func) {
478
+ return this._next({
479
+ policy: { middlewares: { onRetry: [{ _mode: type, func }] } }
480
+ });
142
481
  }
143
- initial(num) { return this._next({ initial: num }); }
144
- increment(num) { return this._next({ increment: num }); }
145
- minDelay(num) { return this._next({ minDelay: num }); }
146
- maxDelay(num) { return this._next({ maxDelay: num }); }
147
- _calculateDelay(attempt) {
148
- const { initial, increment, minDelay, maxDelay } = this._config;
149
- return Math.max(minDelay, Math.min(maxDelay, initial + increment * attempt));
482
+ onError(type, func) {
483
+ return this._next({
484
+ policy: { middlewares: { onError: [{ _mode: type, func }] } }
485
+ });
150
486
  }
151
487
  }
152
- class VigorRetryAlgorithmsBackoff extends VigorStatus {
153
- constructor(config) {
154
- const base = {
155
- initial: 1000,
156
- multiplier: 1.7,
157
- unit: 1000,
158
- minDelay: 500,
159
- maxDelay: 20 * 1000
160
- };
161
- super(config, base, (c) => new VigorRetryAlgorithmsBackoff(c));
488
+
489
+ const VigorRetryErrorMessageFuncs = {
490
+ VALUE_REQUIRED: ({ key }) => `Value Required (missing ${key})`,
491
+ RETRY_EXHAUSTED: ({ maxAttempts }) => `Retry exhausted, (max ${maxAttempts})`,
492
+ RESTART_EXHAUSTED: ({ maxAttempts }) => `Restart exhausted, (max ${maxAttempts})`,
493
+ TIMED_OUT: ({ limit }) => `Timeout: exceeded ${limit}ms`,
494
+ };
495
+ /** Error type thrown by the retry module. */
496
+ class VigorRetryError extends VigorError {
497
+ /**
498
+ * @param code - One of the retry module's error codes.
499
+ * @param options - Error metadata, including the `data` payload for `code`.
500
+ */
501
+ constructor(code, options) {
502
+ const messageFn = VigorRetryErrorMessageFuncs[code];
503
+ super(code, messageFn(options.data), options);
504
+ }
505
+ }
506
+
507
+ /** Returns a random offset in the range `[-jitter, +jitter]` to randomize a computed delay. */
508
+ function VigorJitter(jitter) {
509
+ return (Math.random() * 2 - 1) * jitter;
510
+ }
511
+ const VigorRetryPolicyAlgorithmsConstantBase = {
512
+ __brand: createBrand("Retry", "Policy<Algorithms<Constant<Schema"),
513
+ _tag: "constant",
514
+ jitter: 1000,
515
+ interval: 2000,
516
+ _calculateDelay: (att, config) => {
517
+ const { interval, jitter } = config;
518
+ const min = jitter;
519
+ let delay = Math.max(min, interval);
520
+ delay += VigorJitter(jitter);
521
+ return delay;
162
522
  }
163
- initial(num) { return this._next({ initial: num }); }
164
- multiplier(num) { return this._next({ multiplier: num }); }
165
- unit(num) { return this._next({ unit: num }); }
166
- minDelay(num) { return this._next({ minDelay: num }); }
167
- maxDelay(num) { return this._next({ maxDelay: num }); }
168
- _calculateDelay(attempt) {
169
- const { initial, multiplier, unit, minDelay, maxDelay } = this._config;
170
- return Math.max(minDelay, Math.min(maxDelay, initial + unit * Math.pow(multiplier, attempt)));
523
+ };
524
+ /** Immutable builder for the constant-interval retry delay algorithm. */
525
+ class VigorRetryPolicyAlgorithmsConstant extends VigorStatus {
526
+ constructor(config = VigorRetryPolicyAlgorithmsConstantBase) {
527
+ super(VigorRetryPolicyAlgorithmsConstantBase, config);
528
+ }
529
+ _create(config) {
530
+ return new VigorRetryPolicyAlgorithmsConstant(config);
531
+ }
532
+ /** Sets the amount of random jitter added to the computed delay. */
533
+ jitter(num) {
534
+ return this._next({ jitter: num });
535
+ }
536
+ /** Sets the fixed delay interval, in milliseconds. */
537
+ interval(num) {
538
+ return this._next({ interval: num });
171
539
  }
172
540
  }
173
- class VigorRetryAlgorithmsCustom extends VigorStatus {
174
- constructor(config) {
175
- const base = {
176
- func: (attempt) => attempt * 1000,
177
- minDelay: 500,
178
- maxDelay: 20 * 1000
179
- };
180
- super(config, base, (c) => new VigorRetryAlgorithmsCustom(c));
541
+ const VigorRetryPolicyAlgorithmsLinearBase = {
542
+ __brand: createBrand("Retry", "Policy<Algorithms<Linear<Schema"),
543
+ _tag: "linear",
544
+ jitter: 1000,
545
+ initial: 500,
546
+ increment: 1000,
547
+ minDelay: 0,
548
+ maxDelay: 10000,
549
+ _calculateDelay: (att, config) => {
550
+ const { initial, jitter, increment, minDelay, maxDelay } = config;
551
+ const range = maxDelay - minDelay;
552
+ const j = Math.min(jitter, range / 2);
553
+ const min = minDelay + j;
554
+ const max = maxDelay - j;
555
+ let delay = initial + increment * att;
556
+ delay = Math.max(min, Math.min(max, delay));
557
+ delay += VigorJitter(j);
558
+ return delay;
559
+ }
560
+ };
561
+ /** Immutable builder for the linearly-increasing retry delay algorithm. */
562
+ class VigorRetryPolicyAlgorithmsLinear extends VigorStatus {
563
+ constructor(config = VigorRetryPolicyAlgorithmsLinearBase) {
564
+ super(VigorRetryPolicyAlgorithmsLinearBase, config);
565
+ }
566
+ _create(config) {
567
+ return new VigorRetryPolicyAlgorithmsLinear(config);
568
+ }
569
+ /** Sets the amount of random jitter added to the computed delay. */
570
+ jitter(num) {
571
+ return this._next({ jitter: num });
572
+ }
573
+ /** Sets the initial delay, in milliseconds, before the first retry. */
574
+ initial(num) {
575
+ return this._next({ initial: num });
181
576
  }
182
- func(num) { return this._next({ func: num }); }
183
- _calculateDelay(attempt) {
184
- const { func, minDelay, maxDelay } = this._config;
185
- return Math.max(minDelay, Math.min(maxDelay, func(attempt)));
577
+ /** Sets how much the delay grows, in milliseconds, per attempt. */
578
+ increment(num) {
579
+ return this._next({ increment: num });
580
+ }
581
+ /** Sets the minimum allowed delay, in milliseconds. */
582
+ minDelay(num) {
583
+ return this._next({ minDelay: num });
584
+ }
585
+ /** Sets the maximum allowed delay, in milliseconds. */
586
+ maxDelay(num) {
587
+ return this._next({ maxDelay: num });
186
588
  }
187
589
  }
188
- class VigorRetry extends VigorStatus {
189
- constructor(config) {
190
- const base = {
191
- target: VigorDefault,
192
- settings: new VigorRetrySettings()._getBase(),
193
- interceptors: new VigorRetryInterceptors()._getBase(),
194
- algorithm: (attempt) => new VigorRetryAlgorithmsBackoff()._calculateDelay(attempt),
195
- abortSignals: []
196
- };
197
- super(config, base, (c) => new VigorRetry(c));
198
- }
199
- RetryAlgorithms = {
200
- constant: (config) => new VigorRetryAlgorithmsConstant(config),
201
- linear: (config) => new VigorRetryAlgorithmsLinear(config),
202
- backoff: (config) => new VigorRetryAlgorithmsBackoff(config),
203
- custom: (config) => new VigorRetryAlgorithmsCustom(config)
204
- };
205
- _createTimelineHandler(timeline) {
206
- return (action, content) => {
207
- timeline.push({
208
- action: action,
209
- content: content,
210
- time: Date.now()
211
- });
212
- };
590
+ const VigorRetryPolicyAlgorithmsBackoffBase = {
591
+ __brand: createBrand("Retry", "Policy<Algorithms<Backoff<Schema"),
592
+ _tag: "backoff",
593
+ jitter: 800,
594
+ initial: 0,
595
+ multiplier: 1.7,
596
+ unit: 1000,
597
+ minDelay: 500,
598
+ maxDelay: 10000,
599
+ _calculateDelay: (att, config) => {
600
+ const { jitter, initial, multiplier, unit, minDelay, maxDelay } = config;
601
+ const range = maxDelay - minDelay;
602
+ const j = Math.min(jitter, range / 2);
603
+ const min = minDelay + j;
604
+ const max = maxDelay - j;
605
+ let delay = initial + unit * Math.pow(multiplier, att);
606
+ delay = Math.max(min, Math.min(max, delay));
607
+ delay += VigorJitter(j);
608
+ return delay;
213
609
  }
214
- _createInterceptorHandler(ctx, addTimeline) {
215
- return async (interceptorType, api) => {
216
- const interceptorsConfig = ctx["stats"]["interceptors"];
217
- const interceptors = interceptorsConfig[interceptorType];
218
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
219
- interceptorType: interceptorType,
220
- interceptors,
221
- });
222
- const startTime = performance.now();
223
- for (const func of interceptors) {
224
- const scopedApi = api(interceptorType, func);
225
- await func(ctx, scopedApi);
226
- }
227
- const endTime = performance.now();
228
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
229
- interceptorType: interceptorType,
230
- interceptors,
231
- took: endTime - startTime
610
+ };
611
+ /** Immutable builder for the exponential-backoff retry delay algorithm. */
612
+ class VigorRetryPolicyAlgorithmsBackoff extends VigorStatus {
613
+ constructor(config = VigorRetryPolicyAlgorithmsBackoffBase) {
614
+ super(VigorRetryPolicyAlgorithmsBackoffBase, config);
615
+ }
616
+ _create(config) {
617
+ return new VigorRetryPolicyAlgorithmsBackoff(config);
618
+ }
619
+ /** Sets the amount of random jitter added to the computed delay. */
620
+ jitter(num) {
621
+ return this._next({ jitter: num });
622
+ }
623
+ /** Sets the initial delay, in milliseconds, before backoff growth is applied. */
624
+ initial(num) {
625
+ return this._next({ initial: num });
626
+ }
627
+ /** Sets the exponential growth factor applied per attempt. */
628
+ multiplier(num) {
629
+ return this._next({ multiplier: num });
630
+ }
631
+ /** Sets the base unit, in milliseconds, the multiplier is scaled by. */
632
+ unit(num) {
633
+ return this._next({ unit: num });
634
+ }
635
+ /** Sets the minimum allowed delay, in milliseconds. */
636
+ minDelay(num) {
637
+ return this._next({ minDelay: num });
638
+ }
639
+ /** Sets the maximum allowed delay, in milliseconds. */
640
+ maxDelay(num) {
641
+ return this._next({ maxDelay: num });
642
+ }
643
+ }
644
+ const VigorRetryPolicyAlgorithmsCustomBase = {
645
+ __brand: createBrand("Retry", "Policy<Algorithms<Custom<Schema"),
646
+ _tag: "custom",
647
+ jitter: 800,
648
+ minDelay: 500,
649
+ maxDelay: 10000,
650
+ target: VigorEmpty,
651
+ _calculateDelay: (att, config) => {
652
+ const { jitter, minDelay, maxDelay, target } = config;
653
+ if (isVigorEmpty(target))
654
+ throw new VigorRetryError("VALUE_REQUIRED", {
655
+ method: "_calculateDelay",
656
+ data: {
657
+ key: "target"
658
+ }
232
659
  });
233
- };
660
+ const range = maxDelay - minDelay;
661
+ const j = Math.min(jitter, range / 2);
662
+ const min = minDelay + j;
663
+ const max = maxDelay - j;
664
+ let delay = target(att);
665
+ delay = Math.max(min, Math.min(max, delay));
666
+ delay += VigorJitter(j);
667
+ return delay;
234
668
  }
235
- target(func) { return this._next({ target: func }); }
236
- settings(func) {
237
- if (func instanceof VigorRetrySettings) {
238
- return this._next({ settings: func._getConfig() });
239
- }
240
- if (typeof func === 'function') {
241
- return this._next({ settings: func(new VigorRetrySettings(this._config.settings))._getConfig() });
669
+ };
670
+ /** Immutable builder for a user-supplied custom retry delay algorithm. */
671
+ class VigorRetryPolicyAlgorithmsCustom extends VigorStatus {
672
+ constructor(config = VigorRetryPolicyAlgorithmsCustomBase) {
673
+ super(VigorRetryPolicyAlgorithmsCustomBase, config);
674
+ }
675
+ _create(config) {
676
+ return new VigorRetryPolicyAlgorithmsCustom(config);
677
+ }
678
+ /** Sets the amount of random jitter added to the computed delay. */
679
+ jitter(num) {
680
+ return this._next({ jitter: num });
681
+ }
682
+ /** Sets the minimum allowed delay, in milliseconds. */
683
+ minDelay(num) {
684
+ return this._next({ minDelay: num });
685
+ }
686
+ /** Sets the maximum allowed delay, in milliseconds. */
687
+ maxDelay(num) {
688
+ return this._next({ maxDelay: num });
689
+ }
690
+ /** Sets the user-supplied function computing the raw delay for an attempt. */
691
+ target(func) {
692
+ return this._next({ target: func });
693
+ }
694
+ }
695
+ const VigorRetryPolicyAlgorithmsBase = VigorRetryPolicyAlgorithmsConstantBase;
696
+ /**
697
+ * Immutable builder for selecting a retry policy's delay algorithm.
698
+ *
699
+ * Switching algorithms (`constant`/`linear`/`backoff`/`custom`) always
700
+ * replaces the previous algorithm's config wholesale via the `"replace"`
701
+ * merge strategy, since the different algorithms' fields are not
702
+ * compatible with one another.
703
+ */
704
+ class VigorRetryPolicyAlgorithms extends VigorStatus {
705
+ constructor(config = VigorRetryBase) {
706
+ super(VigorRetryBase, config);
707
+ }
708
+ _create(config) {
709
+ return new VigorRetryPolicyAlgorithms(config);
710
+ }
711
+ /** Switches to the constant-interval delay algorithm. */
712
+ constant(input) {
713
+ const leaf = this._merge(VigorRetryPolicyAlgorithmsConstantBase, input ?? {});
714
+ return this._next({
715
+ policy: { algorithms: leaf }
716
+ }, { policy: { algorithms: "replace" } });
717
+ }
718
+ /** Switches to the linearly-increasing delay algorithm. */
719
+ linear(input) {
720
+ const leaf = this._merge(VigorRetryPolicyAlgorithmsLinearBase, input ?? {});
721
+ return this._next({
722
+ policy: { algorithms: leaf }
723
+ }, { policy: { algorithms: "replace" } });
724
+ }
725
+ /** Switches to the exponential-backoff delay algorithm. */
726
+ backoff(input) {
727
+ const leaf = this._merge(VigorRetryPolicyAlgorithmsBackoffBase, input ?? {});
728
+ return this._next({
729
+ policy: { algorithms: leaf }
730
+ }, { policy: { algorithms: "replace" } });
731
+ }
732
+ /** Switches to a user-supplied custom delay algorithm. */
733
+ custom(input) {
734
+ const leaf = this._merge(VigorRetryPolicyAlgorithmsCustomBase, input ?? {});
735
+ return this._next({
736
+ policy: { algorithms: leaf }
737
+ }, { policy: { algorithms: "replace" } });
738
+ }
739
+ }
740
+
741
+ const VigorRetryPolicyBase = {
742
+ __brand: createBrand("Retry", "Policy<Schema"),
743
+ target: VigorEmpty,
744
+ abortSignals: [],
745
+ settings: VigorRetryPolicySettingsBase,
746
+ middlewares: VigorRetryPolicyMiddlewaresBase,
747
+ algorithms: VigorRetryPolicyAlgorithmsBase
748
+ };
749
+
750
+ const VigorRetryContextBase = {
751
+ __brand: createBrand("Retry", "Context<Schema"),
752
+ result: VigorDefault,
753
+ error: VigorDefault,
754
+ system: {
755
+ delay: VigorDefault,
756
+ attempt: 0
757
+ },
758
+ flags: {
759
+ doRetry: true,
760
+ doRestart: false,
761
+ brokeRetry: false,
762
+ overridden: false
763
+ },
764
+ record: {},
765
+ policy: VigorRetryPolicyBase
766
+ };
767
+
768
+ const VigorRetryBase = {
769
+ __brand: createBrand("Retry", "Schema"),
770
+ policy: VigorRetryPolicyBase,
771
+ context: VigorRetryContextBase
772
+ };
773
+ /**
774
+ * Immutable builder that executes a target function with retry, restart,
775
+ * timeout, and abort support, driven by a configurable delay algorithm and
776
+ * middleware pipeline.
777
+ */
778
+ class VigorRetry extends VigorStatus {
779
+ constructor(config = VigorRetryBase) {
780
+ super(VigorRetryBase, config);
781
+ }
782
+ _create(config) {
783
+ return new VigorRetry(config);
784
+ }
785
+ /** Sets the target function to invoke, with retry/timeout applied. */
786
+ target(func) {
787
+ return this._next({
788
+ policy: { target: func }
789
+ });
790
+ }
791
+ /** Sets external abort signals that, when aborted, stop retrying. */
792
+ abortSignals(...sig) {
793
+ return this._next({
794
+ policy: { abortSignals: sig }
795
+ });
796
+ }
797
+ /** Configures the retry policy's general settings (max attempts, timeout, etc). */
798
+ settings(input) {
799
+ if (typeof input === 'function' || input instanceof VigorRetryPolicySettings) {
800
+ const result = typeof input === 'function'
801
+ ? input(new VigorRetryPolicySettings())
802
+ : input;
803
+ return this._next(result.config);
242
804
  }
243
- return this._next({ settings: func });
805
+ return this._next({
806
+ policy: { settings: input }
807
+ });
244
808
  }
245
- interceptors(func) {
246
- if (func instanceof VigorRetryInterceptors) {
247
- return this._next({ interceptors: func._getConfig() });
809
+ /** Configures the retry policy's middleware pipeline. */
810
+ middlewares(input) {
811
+ if (typeof input === 'function' || input instanceof VigorRetryPolicyMiddlewares) {
812
+ const result = typeof input === 'function'
813
+ ? input(new VigorRetryPolicyMiddlewares())
814
+ : input;
815
+ return this._next(result.config);
248
816
  }
249
- if (typeof func === 'function') {
250
- return this._next({ interceptors: func(new VigorRetryInterceptors(this._config.interceptors))._getConfig() });
817
+ return this._next({
818
+ policy: { middlewares: input }
819
+ });
820
+ }
821
+ /** Configures the retry policy's delay algorithm. */
822
+ algorithms(input) {
823
+ if (typeof input === 'function') {
824
+ const result = input(new VigorRetryPolicyAlgorithms());
825
+ const algoSlice = result.config.policy.algorithms;
826
+ return this._next({
827
+ policy: { algorithms: algoSlice }
828
+ }, { policy: { algorithms: "replace" } });
251
829
  }
252
- return this._next({ interceptors: func });
830
+ return this._next({
831
+ policy: { algorithms: input }
832
+ }, { policy: { algorithms: "replace" } });
253
833
  }
254
- algorithms(func) {
255
- const instance = func(this.RetryAlgorithms);
256
- return this._next({ algorithm: (attempt) => instance._calculateDelay(attempt) });
834
+ /** Runs the target and returns its result directly, throwing on final failure. */
835
+ async request() {
836
+ const res = await this.requestVerbose();
837
+ if (res.success)
838
+ return res.data;
839
+ throw res.error;
257
840
  }
258
- abortSignals(...abortSignals) {
259
- return this._next({ abortSignals: abortSignals.flat() });
841
+ /** Runs the target and returns a result/error envelope instead of throwing. */
842
+ async requestVerbose() {
843
+ return await this.requestRuntime();
260
844
  }
261
- async request(config, timeline = []) {
262
- const stats = this._mergeConfig(this._config, config);
845
+ /** Internal execution loop implementing the attempt/retry/restart lifecycle. */
846
+ async requestRuntime(restartAttempt = 1) {
847
+ const config = this._merge(this.config, {});
263
848
  let ctx = {
264
- result: VigorDefault,
265
- error: VigorDefault,
266
- attempt: 0,
267
- delay: 0,
268
- controller: VigorDefault,
269
- timeline: timeline,
270
- stats,
271
- flag: {
272
- broke: false,
273
- overwritten: false,
274
- restarted: false
275
- }
849
+ ...config.context,
850
+ policy: config.policy
851
+ };
852
+ if (isVigorEmpty(ctx.policy.target))
853
+ throw new VigorRetryError("VALUE_REQUIRED", {
854
+ method: "request",
855
+ data: {
856
+ key: "target"
857
+ },
858
+ context: ctx
859
+ });
860
+ const target = ctx.policy.target;
861
+ const throwError = (err) => {
862
+ throw err;
276
863
  };
277
- const addTimeline = this._createTimelineHandler(ctx.timeline);
278
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
279
- addTimeline("PROCESS_HANDLING", {
280
- type: "REQUEST_START",
281
- data: {}
282
- });
283
864
  try {
284
- if (typeof stats.target !== 'function')
285
- throw new VigorRetryError("INVALID_TARGET", {
286
- method: "request",
287
- data: {
288
- expected: ["function"],
289
- received: stats.target
290
- },
291
- stats: stats,
292
- context: ctx
293
- });
294
- while (ctx.attempt < stats.settings.attempt) {
295
- ctx.attempt++;
296
- addTimeline("ATTEMPT_INCREASED", {
297
- attempt: ctx.attempt
298
- });
865
+ while (ctx.system.attempt < ctx.policy.settings.maxAttempts) {
866
+ ctx.system.attempt++;
299
867
  try {
300
- addTimeline("PROCESS_HANDLING", {
301
- type: "RETRY_START",
302
- data: {}
303
- });
304
- const controller = new AbortController();
868
+ const breakRetry = (err) => {
869
+ ctx.flags.brokeRetry = true;
870
+ throw err;
871
+ };
872
+ const userController = new AbortController();
305
873
  const timeoutController = new AbortController();
306
- const signal = AbortSignal.any([controller.signal, timeoutController.signal, ...stats.abortSignals]);
307
- await handleInterceptor("before", (interceptorType, func) => ({
308
- abort: (error) => {
309
- addTimeline("INTERCEPTOR_API_CALLED", {
310
- interceptorType,
311
- interceptor: func,
312
- method: "abort",
313
- args: [error]
314
- });
315
- controller.abort(error);
316
- throw error;
317
- },
318
- breakRetry: (error) => {
319
- addTimeline("INTERCEPTOR_API_CALLED", {
320
- interceptorType,
321
- interceptor: func,
322
- method: "breakRetry",
323
- args: [error]
324
- });
325
- ctx.flag.broke = true;
326
- throw error;
327
- },
328
- throwError: (error) => {
329
- addTimeline("INTERCEPTOR_API_CALLED", {
330
- interceptorType,
331
- interceptor: func,
332
- method: "throwError",
333
- args: [error]
334
- });
335
- throw error;
874
+ const innerSignals = AbortSignal.any([
875
+ userController.signal,
876
+ timeoutController.signal
877
+ ]);
878
+ const outerSignals = AbortSignal.any(ctx.policy.abortSignals);
879
+ const autoBreak = () => {
880
+ ctx.flags.brokeRetry = true;
881
+ };
882
+ outerSignals.addEventListener("abort", autoBreak);
883
+ const mergedSignals = AbortSignal.any([
884
+ innerSignals,
885
+ outerSignals
886
+ ]);
887
+ let onAbort;
888
+ let timeoutId;
889
+ try {
890
+ for (const middleware of ctx.policy.middlewares.before) {
891
+ if (middleware._mode === "fluent") {
892
+ await middleware.func(ctx);
893
+ }
894
+ else {
895
+ ctx = await middleware.func(ctx, {
896
+ abort: (reason) => userController.abort(reason),
897
+ throwError,
898
+ breakRetry
899
+ });
900
+ }
336
901
  }
337
- }));
338
- const timeoutTimer = setTimeout(() => {
339
- clearTimeout(timeoutTimer);
340
- timeoutController.abort(new VigorRetryError("TIMED_OUT", {
902
+ mergedSignals.throwIfAborted();
903
+ timeoutId = setTimeout(() => timeoutController.abort(new VigorRetryError("TIMED_OUT", {
341
904
  method: "request",
342
905
  data: {
343
- limit: stats.settings.timeout,
344
- attempt: ctx.attempt
345
- },
346
- }));
347
- }, stats.settings.timeout);
348
- signal.throwIfAborted();
349
- let onAbort;
350
- try {
351
- addTimeline("TARGET_REQUEST_STARTED", {
352
- target: stats.target
353
- });
354
- const abort = (error) => {
355
- addTimeline("TARGET_API_CALLED", {
356
- target: stats.target,
357
- method: "abort"
358
- });
359
- controller.abort(error);
360
- throw error;
361
- };
362
- const started = performance.now();
906
+ limit: ctx.policy.settings.timeout
907
+ }
908
+ })), ctx.policy.settings.timeout);
363
909
  ctx.result = await Promise.race([
364
- stats.target(ctx, { abort, signal }),
910
+ target(mergedSignals),
365
911
  new Promise((_, rej) => {
366
- onAbort = () => rej(signal.reason);
367
- signal.addEventListener("abort", onAbort);
912
+ onAbort = () => rej(mergedSignals.reason);
913
+ mergedSignals.addEventListener("abort", onAbort);
368
914
  })
369
915
  ]);
370
- const endTime = performance.now();
371
- addTimeline("TARGET_REQUEST_ENDED", {
372
- target: stats.target,
373
- took: endTime - started
374
- });
916
+ for (const middleware of ctx.policy.middlewares.after) {
917
+ if (middleware._mode === "fluent") {
918
+ await middleware.func(ctx);
919
+ }
920
+ else {
921
+ ctx = await middleware.func(ctx, {
922
+ throwError
923
+ });
924
+ }
925
+ }
926
+ const setResult = (res) => {
927
+ ctx.result = res;
928
+ };
929
+ for (const middleware of ctx.policy.middlewares.onResult) {
930
+ if (middleware._mode === "fluent") {
931
+ ctx.result = await middleware.func(ctx.result);
932
+ }
933
+ else {
934
+ ctx = await middleware.func(ctx, {
935
+ setResult,
936
+ throwError
937
+ });
938
+ }
939
+ }
940
+ return {
941
+ success: true,
942
+ data: ctx.result,
943
+ error: null
944
+ };
375
945
  }
376
946
  finally {
377
- clearTimeout(timeoutTimer);
947
+ clearTimeout(timeoutId);
948
+ outerSignals.removeEventListener("abort", autoBreak);
378
949
  if (onAbort)
379
- signal.removeEventListener("abort", onAbort);
950
+ mergedSignals.removeEventListener("abort", onAbort);
380
951
  }
381
- await handleInterceptor("after", (interceptorType, func) => ({
382
- setResult: (unknown) => {
383
- addTimeline("INTERCEPTOR_API_CALLED", {
384
- interceptorType,
385
- interceptor: func,
386
- method: "setResult",
387
- args: [unknown]
388
- });
389
- ctx.result = unknown;
390
- return unknown;
391
- },
392
- throwError: (error) => {
393
- addTimeline("INTERCEPTOR_API_CALLED", {
394
- interceptorType,
395
- interceptor: func,
396
- method: "throwError",
397
- args: [error]
398
- });
399
- throw error;
400
- },
401
- breakRetry: (error) => {
402
- addTimeline("INTERCEPTOR_API_CALLED", {
403
- interceptorType,
404
- interceptor: func,
405
- method: "breakRetry",
406
- args: [error]
407
- });
408
- ctx.flag.broke = true;
409
- throw error;
410
- },
411
- }));
412
- await handleInterceptor("result", (interceptorType, func) => ({
413
- setResult: (unknown) => {
414
- addTimeline("INTERCEPTOR_API_CALLED", {
415
- interceptorType,
416
- interceptor: func,
417
- method: "setResult",
418
- args: [unknown]
419
- });
420
- ctx.result = unknown;
421
- return unknown;
422
- },
423
- throwError: (error) => {
424
- addTimeline("INTERCEPTOR_API_CALLED", {
425
- interceptorType,
426
- interceptor: func,
427
- method: "throwError",
428
- args: [error]
429
- });
430
- throw error;
431
- },
432
- }));
433
- return ctx.result;
434
952
  }
435
953
  catch (error) {
436
954
  ctx.error = error;
437
- addTimeline("PROCESS_HANDLING", {
438
- type: "RETRY_ERROR",
439
- data: {
440
- error
955
+ if (ctx.flags.brokeRetry)
956
+ throw ctx.error;
957
+ ctx.flags.doRetry = true;
958
+ const proceedRetry = () => ctx.flags.doRetry = true;
959
+ const cancelRetry = () => ctx.flags.doRetry = false;
960
+ for (const middleware of ctx.policy.middlewares.retryIf) {
961
+ if (middleware._mode === "fluent") {
962
+ ctx.flags.doRetry = await middleware.func(ctx.error);
441
963
  }
442
- });
443
- if (ctx.flag.broke)
444
- throw error;
445
- let proceed = true;
446
- await handleInterceptor("retryIf", (interceptorType, func) => ({
447
- proceedRetry: () => {
448
- addTimeline("INTERCEPTOR_API_CALLED", {
449
- interceptorType,
450
- interceptor: func,
451
- method: "proceedRetry",
452
- args: []
453
- });
454
- return proceed = true;
455
- },
456
- cancelRetry: () => {
457
- addTimeline("INTERCEPTOR_API_CALLED", {
458
- interceptorType,
459
- interceptor: func,
460
- method: "cancelRetry",
461
- args: []
964
+ else {
965
+ ctx = await middleware.func(ctx, {
966
+ proceedRetry,
967
+ cancelRetry
462
968
  });
463
- return proceed = false;
464
969
  }
465
- }));
466
- if (!proceed)
467
- throw error;
468
- ctx.delay = VigorDefault;
469
- await handleInterceptor("onRetry", (interceptorType, func) => ({
470
- throwError: (error) => {
471
- addTimeline("INTERCEPTOR_API_CALLED", {
472
- interceptorType,
473
- interceptor: func,
474
- method: "throwError",
475
- args: [error]
476
- });
477
- throw error;
478
- },
479
- setDelay: (number) => {
480
- addTimeline("INTERCEPTOR_API_CALLED", {
481
- interceptorType,
482
- interceptor: func,
483
- method: "setDelay",
484
- args: [number]
485
- });
486
- return ctx.delay = number;
487
- },
488
- setAttempt: (number) => {
489
- addTimeline("INTERCEPTOR_API_CALLED", {
490
- interceptorType,
491
- interceptor: func,
492
- method: "setAttempt",
493
- args: [number]
970
+ }
971
+ if (!ctx.flags.doRetry)
972
+ throw ctx.error;
973
+ ctx.system.delay = ctx.policy.algorithms._calculateDelay(ctx.system.attempt, ctx.policy.algorithms);
974
+ const setDelay = (num) => {
975
+ ctx.system.delay = num;
976
+ };
977
+ for (const middleware of ctx.policy.middlewares.onRetry) {
978
+ if (middleware._mode === "fluent") {
979
+ await middleware.func(ctx.error);
980
+ }
981
+ else {
982
+ ctx = await middleware.func(ctx, {
983
+ throwError,
984
+ setDelay
494
985
  });
495
- return ctx.attempt = number;
496
986
  }
497
- }));
498
- if (typeof ctx.delay !== 'number')
499
- ctx.delay = stats.algorithm(ctx.attempt) + Math.random() * stats.settings.jitter;
500
- const delay = ctx.delay;
501
- await new Promise(r => setTimeout(r, delay));
987
+ }
988
+ const delay = ctx.system.delay;
989
+ await new Promise(res => setTimeout(res, delay));
502
990
  }
503
991
  }
504
- throw new VigorRetryError("EXHAUSTED", {
992
+ throw new VigorRetryError("RETRY_EXHAUSTED", {
505
993
  method: "request",
506
994
  data: {
507
- maxAttempts: stats.settings.attempt,
508
- },
509
- context: ctx
995
+ maxAttempts: ctx.policy.settings.maxAttempts
996
+ }
510
997
  });
511
998
  }
512
999
  catch (error) {
513
1000
  ctx.error = error;
514
- addTimeline("PROCESS_HANDLING", {
515
- type: "REQUEST_ERROR",
516
- data: {
517
- error
1001
+ ctx.flags.doRestart = false;
1002
+ ctx.flags.overridden = false;
1003
+ const setResult = (res) => {
1004
+ ctx.flags.overridden = true;
1005
+ ctx.result = res;
1006
+ };
1007
+ const proceedRestart = () => ctx.flags.doRestart = true;
1008
+ const cancelRestart = () => ctx.flags.doRestart = false;
1009
+ for (const middleware of ctx.policy.middlewares.onError) {
1010
+ if (middleware._mode === "fluent") {
1011
+ await middleware.func(ctx.error);
518
1012
  }
519
- });
520
- await handleInterceptor("onError", (interceptorType, func) => ({
521
- setResult: (unknown) => {
522
- addTimeline("INTERCEPTOR_API_CALLED", {
523
- interceptorType,
524
- interceptor: func,
525
- method: "setResult",
526
- args: [unknown]
527
- });
528
- ctx.result = unknown;
529
- ctx.flag.overwritten = true;
530
- return unknown;
531
- },
532
- throwError: (error) => {
533
- addTimeline("INTERCEPTOR_API_CALLED", {
534
- interceptorType,
535
- interceptor: func,
536
- method: "throwError",
537
- args: [error]
538
- });
539
- throw error;
540
- },
541
- restart: () => {
542
- addTimeline("INTERCEPTOR_API_CALLED", {
543
- interceptorType,
544
- interceptor: func,
545
- method: "restart",
546
- args: []
1013
+ else {
1014
+ ctx = await middleware.func(ctx, {
1015
+ setResult,
1016
+ throwError,
1017
+ proceedRestart,
1018
+ cancelRestart
547
1019
  });
548
- ctx.flag.restarted = true;
549
1020
  }
550
- }));
551
- if (ctx.flag.restarted) {
552
- return await this.request(stats, ctx.timeline);
553
1021
  }
554
- if (ctx.flag.overwritten)
555
- return ctx.result;
556
- if (stats.settings.default !== VigorDefault)
557
- return stats.settings.default;
558
- throw error;
1022
+ if (ctx.flags.doRestart) {
1023
+ if (restartAttempt + 1 > ctx.policy.settings.maxRestarts)
1024
+ throw new VigorRetryError("RESTART_EXHAUSTED", {
1025
+ method: "request",
1026
+ data: {
1027
+ maxAttempts: ctx.policy.settings.maxRestarts
1028
+ }
1029
+ });
1030
+ return await this.requestRuntime(restartAttempt + 1);
1031
+ }
1032
+ if (ctx.flags.overridden)
1033
+ return {
1034
+ success: true,
1035
+ data: ctx.result,
1036
+ error: null
1037
+ };
1038
+ if (!isVigorEmpty(ctx.policy.settings.default)) {
1039
+ const data = await ctx.policy.settings.default(ctx);
1040
+ return {
1041
+ success: true,
1042
+ data,
1043
+ error: null
1044
+ };
1045
+ }
1046
+ return {
1047
+ success: false,
1048
+ data: null,
1049
+ error: ctx.error
1050
+ };
559
1051
  }
560
1052
  }
561
1053
  }
562
- class VigorParseSettings extends VigorStatus {
563
- constructor(config) {
564
- const base = {
565
- raw: false,
566
- default: VigorDefault
567
- };
568
- super(config, base, (c) => new VigorParseSettings(c));
1054
+
1055
+ const VigorParsePolicySettingsBase = {
1056
+ __brand: createBrand("Parse", "Policy<Settings<Schema"),
1057
+ raw: false,
1058
+ default: VigorEmpty,
1059
+ maxRestarts: 3
1060
+ };
1061
+ /** Immutable builder for a parse policy's general settings. */
1062
+ class VigorParsePolicySettings extends VigorStatus {
1063
+ constructor(config = VigorParseBase) {
1064
+ super(VigorParseBase, config);
1065
+ }
1066
+ _create(config) {
1067
+ return new VigorParsePolicySettings(config);
1068
+ }
1069
+ /** When enabled, skips content-type based parsing and returns the raw response. */
1070
+ raw(bool) {
1071
+ return this._next({
1072
+ policy: { settings: { raw: bool } }
1073
+ });
1074
+ }
1075
+ /** Sets the fallback value/factory used when parsing ultimately fails. */
1076
+ default(func) {
1077
+ return this._next({
1078
+ policy: { settings: { default: func } }
1079
+ });
1080
+ }
1081
+ /** Sets the maximum number of full restarts allowed. */
1082
+ maxRestarts(num) {
1083
+ return this._next({
1084
+ policy: { settings: { maxRestarts: num } }
1085
+ });
569
1086
  }
570
- original(bool) { return this._next({ raw: bool }); }
571
- default(unk) { return this._next({ default: unk }); }
572
1087
  }
573
- class VigorParseStrategies extends VigorStatus {
574
- constructor(config) {
575
- const base = {
576
- funcs: []
577
- };
578
- super(config, base, (c) => new VigorParseStrategies(c));
579
- }
580
- ParseAutoHeaders = [
581
- { header: "application/json", regExp: /application\/(.+\+)?json(.+\+)?/i, method: (res) => res.json() },
582
- { header: "application/xml", regExp: /application\/(.+\+)?xml(.+\+)?/i, method: (res) => res.text() },
583
- { header: "application/x-www-form-urlencoded", regExp: /application\/(.+\+)?x-www-form-urlencoded(.+\+)?/i, method: (res) => res.formData() },
584
- { header: "application/octet-stream", regExp: /application\/(.+\+)?octet-stream(.+\+)?/i, method: (res) => res.arrayBuffer() },
585
- { header: "image/*", regExp: /^image\/.+/i, method: (res) => res.blob() },
586
- { header: "audio/*", regExp: /^audio\/.+/i, method: (res) => res.blob() },
587
- { header: "video/*", regExp: /^video\/.+/i, method: (res) => res.blob() },
588
- { header: "multipart/form-data", regExp: /multipart\/(.+\+)?form-data(.+\+)?/i, method: (res) => res.formData() },
589
- { header: "text/*", regExp: /^text\/.+/i, method: (res) => res.text() },
590
- ];
591
- ParseAutoMethods = [
592
- { title: "json", method: (res) => res.json() },
593
- { title: "formData", method: (res) => res.formData() },
594
- { title: "text", method: (res) => res.text() },
595
- { title: "blob", method: (res) => res.blob() },
596
- ];
597
- ParseAutoAlgorithms = {
598
- contentType: async (response) => {
599
- const parsers = this.ParseAutoHeaders;
600
- const contentTypeHeader = response.headers.get("content-type");
601
- if (!contentTypeHeader)
602
- throw new VigorParseError("INVALID_CONTENT_TYPE", {
603
- method: "ParseAutoAlgorithms.contentType",
604
- data: {
605
- expected: ["string"],
606
- received: contentTypeHeader,
607
- response: response
608
- }
609
- });
610
- const toDo = parsers.find(parser => parser.regExp.test(contentTypeHeader));
611
- if (!toDo)
612
- throw new VigorParseError("PARSER_NOT_FOUND", {
613
- method: "ParseAutoAlgorithms.contentType",
614
- data: {
615
- expected: parsers.map(parser => parser.header),
616
- received: contentTypeHeader,
617
- response: response
618
- }
619
- });
620
- return await toDo.method(response);
621
- },
622
- sniff: async (response) => {
623
- const parsers = this.ParseAutoMethods;
624
- for (const [i, parser] of parsers.entries()) {
625
- const cloned = (i === parsers.length - 1)
626
- ? response
627
- : response.clone();
628
- try {
629
- const data = await parser.method(cloned);
630
- return data;
631
- }
632
- catch { }
1088
+
1089
+ const VigorParsePolicyMiddlewaresBase = {
1090
+ __brand: createBrand("Parse", "Policy<Middlewares<Schema"),
1091
+ before: [],
1092
+ after: [],
1093
+ onResult: [],
1094
+ onError: []
1095
+ };
1096
+ /**
1097
+ * Immutable builder for a parse policy's middleware pipeline (`before`,
1098
+ * `after`, `onResult`, `onError`). Each stage accepts either "fluent"
1099
+ * middleware (returns a plain value merged into the context) or
1100
+ * "intercept" middleware (receives an explicit API to mutate control flow).
1101
+ */
1102
+ class VigorParsePolicyMiddlewares extends VigorStatus {
1103
+ constructor(config = VigorParseBase) {
1104
+ super(VigorParseBase, config);
1105
+ }
1106
+ _create(config) {
1107
+ return new VigorParsePolicyMiddlewares(config);
1108
+ }
1109
+ before(type, func) {
1110
+ return this._next({ policy: { middlewares: { before: [{ _mode: type, func }] } } });
1111
+ }
1112
+ after(type, func) {
1113
+ return this._next({ policy: { middlewares: { after: [{ _mode: type, func }] } } });
1114
+ }
1115
+ onResult(type, func) {
1116
+ return this._next({ policy: { middlewares: { onResult: [{ _mode: type, func }] } } });
1117
+ }
1118
+ onError(type, func) {
1119
+ return this._next({ policy: { middlewares: { onError: [{ _mode: type, func }] } } });
1120
+ }
1121
+ }
1122
+
1123
+ const VigorParseErrorMessageFuncs = {
1124
+ VALUE_REQUIRED: ({ key }) => `Value Required (missing ${key})`,
1125
+ INVALID_CONTENT_TYPE: ({ received }) => `Invalid Content-Type header: ${String(received)}`,
1126
+ PARSER_NOT_FOUND: ({ received, expected }) => `Parser not found for Content-Type "${String(received)}" (known: ${expected.join(", ")})`,
1127
+ PARSER_ALL_FAILED: ({ tried }) => `All parsers failed, tried: ${tried.join(", ")}`,
1128
+ RESTART_EXHAUSTED: ({ maxAttempts }) => `Restart exhausted, (max ${maxAttempts})`,
1129
+ };
1130
+ /** Error type thrown by the parse module. */
1131
+ class VigorParseError extends VigorError {
1132
+ /**
1133
+ * @param code - One of the parse module's error codes.
1134
+ * @param options - Error metadata, including the `data` payload for `code`.
1135
+ */
1136
+ constructor(code, options) {
1137
+ const messageFn = VigorParseErrorMessageFuncs[code];
1138
+ super(code, messageFn(options.data), options);
1139
+ }
1140
+ }
1141
+
1142
+ const VigorParsePolicyStrategiesBase = {
1143
+ __brand: createBrand("Parse", "Policy<Strategies<Schema"),
1144
+ funcs: []
1145
+ };
1146
+ const ContentTypeParsers = [
1147
+ { header: "application/json", regExp: /application\/(.+\+)?json(.+\+)?/i, method: (res) => res.json() },
1148
+ { header: "application/xml", regExp: /application\/(.+\+)?xml(.+\+)?/i, method: (res) => res.text() },
1149
+ { header: "application/x-www-form-urlencoded", regExp: /application\/(.+\+)?x-www-form-urlencoded(.+\+)?/i, method: (res) => res.formData() },
1150
+ { header: "application/octet-stream", regExp: /application\/(.+\+)?octet-stream(.+\+)?/i, method: (res) => res.arrayBuffer() },
1151
+ { header: "image/*", regExp: /^image\/.+/i, method: (res) => res.blob() },
1152
+ { header: "audio/*", regExp: /^audio\/.+/i, method: (res) => res.blob() },
1153
+ { header: "video/*", regExp: /^video\/.+/i, method: (res) => res.blob() },
1154
+ { header: "multipart/form-data", regExp: /multipart\/(.+\+)?form-data(.+\+)?/i, method: (res) => res.formData() },
1155
+ { header: "text/*", regExp: /^text\/.+/i, method: (res) => res.text() },
1156
+ ];
1157
+ const SniffMethods = [
1158
+ { title: "json", method: (res) => res.json() },
1159
+ { title: "formData", method: (res) => res.formData() },
1160
+ { title: "text", method: (res) => res.text() },
1161
+ { title: "blob", method: (res) => res.blob() },
1162
+ ];
1163
+ /** Parses a response by matching its `Content-Type` header against a known parser table. */
1164
+ const VigorParseContentTypeStrategy = async (response) => {
1165
+ const contentTypeHeader = response.headers.get("content-type");
1166
+ if (!contentTypeHeader)
1167
+ throw new VigorParseError("INVALID_CONTENT_TYPE", {
1168
+ method: "VigorParseContentTypeStrategy",
1169
+ data: { received: contentTypeHeader }
1170
+ });
1171
+ const toDo = ContentTypeParsers.find(parser => parser.regExp.test(contentTypeHeader));
1172
+ if (!toDo)
1173
+ throw new VigorParseError("PARSER_NOT_FOUND", {
1174
+ method: "VigorParseContentTypeStrategy",
1175
+ data: {
1176
+ expected: ContentTypeParsers.map(p => p.header),
1177
+ received: contentTypeHeader
633
1178
  }
634
- throw new VigorParseError("PARSER_ALL_FAILED", {
635
- method: "ParseAutoAlgorithms.sniff",
636
- data: {
637
- tried: parsers.map(parser => parser.title),
638
- response: response
639
- }
640
- });
1179
+ });
1180
+ return await toDo.method(response);
1181
+ };
1182
+ /** Parses a response by trying a sequence of body-reading methods until one succeeds. */
1183
+ const VigorParseSniffStrategy = async (response) => {
1184
+ for (const [i, parser] of SniffMethods.entries()) {
1185
+ const cloned = (i === SniffMethods.length - 1) ? response : response.clone();
1186
+ try {
1187
+ return await parser.method(cloned);
641
1188
  }
642
- };
643
- contentType() { return this._next({ funcs: [this.ParseAutoAlgorithms.contentType] }); }
644
- sniff() { return this._next({ funcs: [this.ParseAutoAlgorithms.sniff] }); }
645
- json() { return this._next({ funcs: [(res) => res.json()] }); }
646
- text() { return this._next({ funcs: [(res) => res.text()] }); }
647
- arrayBuffer() { return this._next({ funcs: [(res) => res.arrayBuffer()] }); }
648
- blob() { return this._next({ funcs: [(res) => res.blob()] }); }
649
- bytes() { return this._next({ funcs: [(res) => res.arrayBuffer().then(r => new Uint8Array(r))] }); }
650
- formData() { return this._next({ funcs: [(res) => res.formData()] }); }
651
- }
652
- class VigorParseInterceptors extends VigorStatus {
653
- constructor(config) {
654
- const base = {
655
- before: [],
656
- after: [],
657
- result: [],
658
- onError: []
659
- };
660
- super(config, base, (c) => new VigorParseInterceptors(c));
1189
+ catch { }
1190
+ }
1191
+ throw new VigorParseError("PARSER_ALL_FAILED", {
1192
+ method: "VigorParseSniffStrategy",
1193
+ data: { tried: SniffMethods.map(p => p.title) }
1194
+ });
1195
+ };
1196
+ /** Immutable builder for a parse policy's fallback chain of parsing strategies. */
1197
+ class VigorParsePolicyStrategies extends VigorStatus {
1198
+ constructor(config = VigorParseBase) {
1199
+ super(VigorParseBase, config);
1200
+ }
1201
+ _create(config) {
1202
+ return new VigorParsePolicyStrategies(config);
1203
+ }
1204
+ /**
1205
+ * Adds strategy functions to the fallback chain.
1206
+ *
1207
+ * @param replace - When `true`, replaces the existing chain instead of
1208
+ * appending to it (the default is to concat, since strategies are
1209
+ * tried in sequence as a fallback chain).
1210
+ */
1211
+ _set(funcs, replace) {
1212
+ return this._next({ policy: { strategies: { funcs } } }, replace ? { policy: { strategies: { funcs: "replace" } } } : undefined);
661
1213
  }
662
- before(...funcs) { return this._next({ before: funcs.flat() }); }
663
- after(...funcs) { return this._next({ after: funcs.flat() }); }
664
- result(...funcs) { return this._next({ result: funcs.flat() }); }
665
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
1214
+ /** Adds the content-type based parsing strategy. */
1215
+ contentType(replace) { return this._set([VigorParseContentTypeStrategy], replace); }
1216
+ /** Adds the sniffing (try-each-method) parsing strategy. */
1217
+ sniff(replace) { return this._set([VigorParseSniffStrategy], replace); }
1218
+ /** Adds a strategy that always parses the response as JSON. */
1219
+ json(replace) { return this._set([(res) => res.json()], replace); }
1220
+ /** Adds a strategy that always parses the response as text. */
1221
+ text(replace) { return this._set([(res) => res.text()], replace); }
1222
+ /** Adds a strategy that always parses the response as an `ArrayBuffer`. */
1223
+ arrayBuffer(replace) { return this._set([(res) => res.arrayBuffer()], replace); }
1224
+ /** Adds a strategy that always parses the response as a `Blob`. */
1225
+ blob(replace) { return this._set([(res) => res.blob()], replace); }
1226
+ /** Adds a strategy that always parses the response as a `Uint8Array`. */
1227
+ bytes(replace) { return this._set([(res) => res.arrayBuffer().then(b => new Uint8Array(b))], replace); }
1228
+ /** Adds a strategy that always parses the response as `FormData`. */
1229
+ formData(replace) { return this._set([(res) => res.formData()], replace); }
1230
+ /** Adds a user-supplied custom parsing strategy. */
1231
+ custom(func, replace) { return this._set([func], replace); }
666
1232
  }
1233
+
1234
+ const VigorParsePolicyBase = {
1235
+ __brand: createBrand("Parse", "Policy<Schema"),
1236
+ target: VigorDefault,
1237
+ settings: VigorParsePolicySettingsBase,
1238
+ middlewares: VigorParsePolicyMiddlewaresBase,
1239
+ strategies: VigorParsePolicyStrategiesBase
1240
+ };
1241
+
1242
+ const VigorParseContextBase = {
1243
+ __brand: createBrand("Parse", "Context<Schema"),
1244
+ result: VigorDefault,
1245
+ error: VigorDefault,
1246
+ response: VigorDefault,
1247
+ flags: {
1248
+ overrided: false,
1249
+ restarted: false
1250
+ },
1251
+ record: {},
1252
+ policy: VigorParsePolicyBase
1253
+ };
1254
+
1255
+ const VigorParseBase = {
1256
+ __brand: createBrand("Parse", "Schema"),
1257
+ policy: VigorParsePolicyBase,
1258
+ context: VigorParseContextBase
1259
+ };
1260
+ /**
1261
+ * Immutable builder that parses an HTTP response into a result value,
1262
+ * trying each configured strategy in sequence with an optional middleware
1263
+ * pipeline and restart-on-failure support.
1264
+ */
667
1265
  class VigorParse extends VigorStatus {
668
- constructor(config) {
669
- const base = {
670
- target: VigorDefault,
671
- settings: new VigorParseSettings()._getBase(),
672
- strategies: new VigorParseStrategies()._getBase(),
673
- interceptors: new VigorParseInterceptors()._getBase()
674
- };
675
- super(config, base, (c) => new VigorParse(c));
676
- }
677
- _createTimelineHandler(timeline) {
678
- return (action, content) => {
679
- timeline.push({
680
- action: action,
681
- content: content,
682
- time: Date.now()
683
- });
684
- };
1266
+ constructor(config = VigorParseBase) {
1267
+ super(VigorParseBase, config);
1268
+ }
1269
+ _create(config) {
1270
+ return new VigorParse(config);
685
1271
  }
686
- _createInterceptorHandler(ctx, addTimeline) {
687
- return async (interceptorType, api) => {
688
- const interceptorsConfig = ctx["stats"]["interceptors"];
689
- const interceptors = interceptorsConfig[interceptorType];
690
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
691
- interceptorType: interceptorType,
692
- interceptors,
693
- });
694
- const startTime = performance.now();
695
- for (const func of interceptors) {
696
- const scopedApi = api(interceptorType, func);
697
- await func(ctx, scopedApi);
698
- }
699
- const endTime = performance.now();
700
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
701
- interceptorType: interceptorType,
702
- interceptors,
703
- took: endTime - startTime
704
- });
705
- };
1272
+ /** Sets the `Response` object to parse. */
1273
+ target(response) {
1274
+ return this._next({ policy: { target: response } });
706
1275
  }
707
- target(response) { return this._next({ target: response }); }
708
- settings(func) {
709
- if (func instanceof VigorParseSettings) {
710
- return this._next({ settings: func._getConfig() });
1276
+ /** Configures the parse policy's general settings. */
1277
+ settings(input) {
1278
+ if (typeof input === 'function' || input instanceof VigorParsePolicySettings) {
1279
+ const result = typeof input === 'function'
1280
+ ? input(new VigorParsePolicySettings())
1281
+ : input;
1282
+ return this._next(result.config);
711
1283
  }
712
- if (typeof func === 'function') {
713
- return this._next({ settings: func(new VigorParseSettings(this._config.settings))._getConfig() });
714
- }
715
- return this._next({ settings: func });
1284
+ return this._next({ policy: { settings: input } });
716
1285
  }
717
- strategies(func) {
718
- if (func instanceof VigorParseStrategies) {
719
- return this._next({ strategies: func._getConfig() });
720
- }
721
- if (typeof func === 'function') {
722
- return this._next({ strategies: func(new VigorParseStrategies(this._config.strategies))._getConfig() });
1286
+ /** Configures the parse policy's middleware pipeline. */
1287
+ middlewares(input) {
1288
+ if (typeof input === 'function' || input instanceof VigorParsePolicyMiddlewares) {
1289
+ const result = typeof input === 'function'
1290
+ ? input(new VigorParsePolicyMiddlewares())
1291
+ : input;
1292
+ return this._next(result.config);
723
1293
  }
724
- return this._next({ strategies: func });
1294
+ return this._next({ policy: { middlewares: input } });
725
1295
  }
726
- interceptors(func) {
727
- if (func instanceof VigorParseInterceptors) {
728
- return this._next({ interceptors: func._getConfig() });
1296
+ /** Configures the parse policy's fallback chain of parsing strategies. */
1297
+ strategies(input) {
1298
+ if (typeof input === 'function' || input instanceof VigorParsePolicyStrategies) {
1299
+ const result = typeof input === 'function'
1300
+ ? input(new VigorParsePolicyStrategies())
1301
+ : input;
1302
+ return this._next(result.config);
729
1303
  }
730
- if (typeof func === 'function') {
731
- return this._next({ interceptors: func(new VigorParseInterceptors(this._config.interceptors))._getConfig() });
732
- }
733
- return this._next({ interceptors: func });
1304
+ return this._next({ policy: { strategies: input } });
1305
+ }
1306
+ /** Runs parsing and returns the result directly, throwing on final failure. */
1307
+ async request() {
1308
+ const res = await this.requestVerbose();
1309
+ if (res.success)
1310
+ return res.data;
1311
+ throw res.error;
734
1312
  }
735
- async request(config, timeline = []) {
736
- const stats = this._mergeConfig(this._config, config);
737
- const target = stats.target;
1313
+ /** Runs parsing and returns a result/error envelope instead of throwing. */
1314
+ async requestVerbose() {
1315
+ return await this.requestRuntime();
1316
+ }
1317
+ /** Internal execution loop implementing the strategy fallback and restart handling. */
1318
+ async requestRuntime(restartAttempt = 1) {
1319
+ const config = this._merge(this.config, {});
738
1320
  let ctx = {
739
- timeline: timeline,
740
- stats,
741
- response: target,
742
- result: VigorDefault,
743
- error: VigorDefault,
744
- flag: {
745
- overwritten: false
746
- }
1321
+ ...config.context,
1322
+ policy: config.policy
747
1323
  };
748
- const addTimeline = this._createTimelineHandler(ctx.timeline);
749
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
750
- addTimeline("PROCESS_HANDLING", {
751
- type: "REQUEST_START",
752
- data: {}
753
- });
1324
+ if (isVigorDefault(ctx.policy.target))
1325
+ throw new VigorParseError("VALUE_REQUIRED", {
1326
+ method: "request",
1327
+ data: { key: "target" },
1328
+ context: ctx
1329
+ });
1330
+ ctx.response = ctx.policy.target;
1331
+ const throwError = (err) => { throw err; };
754
1332
  try {
755
- if (target === VigorDefault)
756
- throw new VigorParseError("INVALID_TARGET", {
757
- method: "request",
758
- data: {
759
- expected: ["Response"],
760
- received: target
761
- },
762
- context: ctx
763
- });
764
- await handleInterceptor("before", (interceptorType, func) => ({
765
- throwError: (error) => {
766
- addTimeline("INTERCEPTOR_API_CALLED", {
767
- interceptorType,
768
- interceptor: func,
769
- method: "throwError",
770
- args: [error]
771
- });
772
- throw error;
773
- },
774
- }));
775
- if (stats.settings.raw) {
776
- ctx.result = ctx.response;
1333
+ for (const middleware of ctx.policy.middlewares.before) {
1334
+ if (middleware._mode === "fluent") {
1335
+ await middleware.func(ctx);
1336
+ }
1337
+ else {
1338
+ ctx = await middleware.func(ctx, { throwError });
1339
+ }
1340
+ }
1341
+ const response = ctx.response;
1342
+ if (ctx.policy.settings.raw) {
1343
+ ctx.result = response;
777
1344
  }
778
1345
  else {
1346
+ const funcs = ctx.policy.strategies.funcs.length > 0
1347
+ ? ctx.policy.strategies.funcs
1348
+ : [VigorParseContentTypeStrategy];
779
1349
  let parsed = false;
780
- for (const [i, func] of stats.strategies.funcs.length > 0
781
- ? stats.strategies.funcs.entries()
782
- : new VigorParseStrategies().contentType()._getConfig().funcs.entries()) {
783
- const cloned = (i === stats.strategies.funcs.length - 1)
784
- ? ctx.response
785
- : ctx.response.clone();
1350
+ for (const [i, func] of funcs.entries()) {
1351
+ const cloned = (i === funcs.length - 1) ? response : response.clone();
786
1352
  try {
787
1353
  ctx.result = await func(cloned);
788
1354
  parsed = true;
@@ -793,947 +1359,529 @@ class VigorParse extends VigorStatus {
793
1359
  if (!parsed)
794
1360
  throw new VigorParseError("PARSER_ALL_FAILED", {
795
1361
  method: "request",
796
- data: {
797
- tried: stats.strategies.funcs,
798
- response: ctx.response
799
- },
1362
+ data: { tried: funcs.map((_, i) => `strategy#${i}`) },
800
1363
  context: ctx
801
1364
  });
802
1365
  }
803
- await handleInterceptor("after", (interceptorType, func) => ({
804
- setResult: (unknown) => {
805
- addTimeline("INTERCEPTOR_API_CALLED", {
806
- interceptorType,
807
- interceptor: func,
808
- method: "setResult",
809
- args: [unknown]
810
- });
811
- ctx.result = unknown;
812
- return unknown;
813
- },
814
- throwError: (error) => {
815
- addTimeline("INTERCEPTOR_API_CALLED", {
816
- interceptorType,
817
- interceptor: func,
818
- method: "throwError",
819
- args: [error]
820
- });
821
- throw error;
822
- },
823
- }));
824
- await handleInterceptor("result", (interceptorType, func) => ({
825
- setResult: (unknown) => {
826
- addTimeline("INTERCEPTOR_API_CALLED", {
827
- interceptorType,
828
- interceptor: func,
829
- method: "setResult",
830
- args: [unknown]
831
- });
832
- ctx.result = unknown;
833
- return unknown;
834
- },
835
- throwError: (error) => {
836
- addTimeline("INTERCEPTOR_API_CALLED", {
837
- interceptorType,
838
- interceptor: func,
839
- method: "throwError",
840
- args: [error]
841
- });
842
- throw error;
843
- },
844
- }));
845
- return ctx.result;
1366
+ const setResult = (res) => { ctx.result = res; };
1367
+ for (const middleware of ctx.policy.middlewares.after) {
1368
+ if (middleware._mode === "fluent") {
1369
+ await middleware.func(ctx);
1370
+ }
1371
+ else {
1372
+ ctx = await middleware.func(ctx, { setResult, throwError });
1373
+ }
1374
+ }
1375
+ for (const middleware of ctx.policy.middlewares.onResult) {
1376
+ if (middleware._mode === "fluent") {
1377
+ ctx.result = await middleware.func(ctx.result);
1378
+ }
1379
+ else {
1380
+ ctx = await middleware.func(ctx, { setResult, throwError });
1381
+ }
1382
+ }
1383
+ return {
1384
+ success: true,
1385
+ data: ctx.result,
1386
+ error: null
1387
+ };
846
1388
  }
847
1389
  catch (error) {
848
1390
  ctx.error = error;
849
- addTimeline("PROCESS_HANDLING", {
850
- type: "REQUEST_ERROR",
851
- data: {
852
- error
1391
+ ctx.flags.overrided = false;
1392
+ ctx.flags.restarted = false;
1393
+ const setResult = (res) => {
1394
+ ctx.flags.overrided = true;
1395
+ ctx.result = res;
1396
+ };
1397
+ const proceedRestart = () => ctx.flags.restarted = true;
1398
+ const cancelRestart = () => ctx.flags.restarted = false;
1399
+ for (const middleware of ctx.policy.middlewares.onError) {
1400
+ if (middleware._mode === "fluent") {
1401
+ await middleware.func(ctx.error);
853
1402
  }
854
- });
855
- await handleInterceptor("onError", (interceptorType, func) => ({
856
- setResult: (unknown) => {
857
- addTimeline("INTERCEPTOR_API_CALLED", {
858
- interceptorType,
859
- interceptor: func,
860
- method: "setResult",
861
- args: [unknown]
862
- });
863
- ctx.result = unknown;
864
- ctx.flag.overwritten = true;
865
- return unknown;
866
- },
867
- throwError: (error) => {
868
- addTimeline("INTERCEPTOR_API_CALLED", {
869
- interceptorType,
870
- interceptor: func,
871
- method: "throwError",
872
- args: [error]
1403
+ else {
1404
+ ctx = await middleware.func(ctx, { setResult, throwError, proceedRestart, cancelRestart });
1405
+ }
1406
+ }
1407
+ if (ctx.flags.restarted) {
1408
+ if (restartAttempt + 1 > ctx.policy.settings.maxRestarts)
1409
+ throw new VigorParseError("RESTART_EXHAUSTED", {
1410
+ method: "request",
1411
+ data: {
1412
+ maxAttempts: ctx.policy.settings.maxRestarts
1413
+ }
873
1414
  });
874
- throw error;
875
- },
876
- }));
877
- if (ctx.flag.overwritten)
878
- return ctx.result;
879
- if (stats.settings.default !== VigorDefault)
880
- return stats.settings.default;
881
- throw error;
1415
+ return await this.requestRuntime(restartAttempt + 1);
1416
+ }
1417
+ if (ctx.flags.overrided)
1418
+ return {
1419
+ success: true,
1420
+ data: ctx.result,
1421
+ error: null
1422
+ };
1423
+ if (!isVigorEmpty(ctx.policy.settings.default)) {
1424
+ const data = await ctx.policy.settings.default(ctx);
1425
+ return {
1426
+ success: true,
1427
+ data,
1428
+ error: null
1429
+ };
1430
+ }
1431
+ return {
1432
+ success: false,
1433
+ data: null,
1434
+ error: ctx.error
1435
+ };
882
1436
  }
883
1437
  }
884
1438
  }
885
- class VigorFetchSettings extends VigorStatus {
886
- constructor(config) {
887
- const base = {
888
- retryHeaders: ["retry-after", "ratelimit-reset", "x-ratelimit-reset", "x-retry-after", "x-amz-retry-after", "chrome-proxy-next-link"],
889
- unretryStatus: [400, 401, 403, 404, 405, 413, 422],
890
- default: VigorDefault
891
- };
892
- super(config, base, (c) => new VigorFetchSettings(c));
893
- }
894
- retryHeaders(...strs) { return this._next({ retryHeaders: strs.flat() }); }
895
- unretryStatus(...nums) { return this._next({ unretryStatus: nums.flat() }); }
896
- default(unk) { return this._next({ default: unk }); }
897
- }
898
- class VigorFetchInterceptors extends VigorStatus {
899
- constructor(config) {
900
- const base = {
901
- before: [],
902
- after: [],
903
- result: [],
904
- onError: []
905
- };
906
- super(config, base, (c) => new VigorFetchInterceptors(c));
1439
+
1440
+ const VigorFetchPolicyBase = {
1441
+ __brand: createBrand("Fetch", "Policy<Schema"),
1442
+ method: VigorEmpty,
1443
+ origin: VigorEmpty,
1444
+ path: [],
1445
+ query: [],
1446
+ hash: "",
1447
+ headers: {},
1448
+ body: VigorEmpty,
1449
+ extra: {},
1450
+ settings: VigorFetchPolicySettingsBase,
1451
+ middlewares: VigorFetchPolicyMiddlewaresBase,
1452
+ retry: VigorRetryBase,
1453
+ parse: VigorParseBase
1454
+ };
1455
+
1456
+ const VigorFetchContextBase = {
1457
+ __brand: createBrand("Fetch", "Context<Schema"),
1458
+ href: "",
1459
+ response: VigorDefault,
1460
+ result: VigorDefault,
1461
+ error: VigorDefault,
1462
+ options: VigorDefault,
1463
+ flags: {
1464
+ overrided: false,
1465
+ restarted: false
1466
+ },
1467
+ record: {},
1468
+ policy: VigorFetchPolicyBase
1469
+ };
1470
+
1471
+ const VigorFetchErrorMessageFuncs = {
1472
+ VALUE_REQUIRED: ({ key }) => `Value Required (missing ${key})`,
1473
+ INVALID_BODY: ({ received }) => `Invalid Body type: ${typeof received}`,
1474
+ INVALID_PROTOCOL: ({ origin, base }) => base
1475
+ ? `Invalid URL: could not resolve "${origin}" against base "${base}"`
1476
+ : `Invalid URL: "${origin}" is not an absolute URL, and no base (window.location) is available to resolve it against — pass an absolute origin (e.g. "https://api.example.com") when running outside a browser`,
1477
+ FETCH_FAILED: ({ status, statusText }) => `Fetch Failed: ${status} ${statusText}`,
1478
+ RESTART_EXHAUSTED: ({ maxAttempts }) => `Restart exhausted, (max ${maxAttempts})`,
1479
+ };
1480
+ /** Error type thrown by the fetch module. */
1481
+ class VigorFetchError extends VigorError {
1482
+ /**
1483
+ * @param code - One of the fetch module's error codes.
1484
+ * @param options - Error metadata, including the `data` payload for `code`.
1485
+ */
1486
+ constructor(code, options) {
1487
+ const messageFn = VigorFetchErrorMessageFuncs[code];
1488
+ super(code, messageFn(options.data), options);
907
1489
  }
908
- before(...funcs) { return this._next({ before: funcs.flat() }); }
909
- after(...funcs) { return this._next({ after: funcs.flat() }); }
910
- result(...funcs) { return this._next({ result: funcs.flat() }); }
911
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
912
1490
  }
1491
+
1492
+ const VigorFetchBase = {
1493
+ __brand: createBrand("Fetch", "Schema"),
1494
+ policy: VigorFetchPolicyBase,
1495
+ context: VigorFetchContextBase
1496
+ };
1497
+ /**
1498
+ * Immutable builder that performs an HTTP request, composing an internal
1499
+ * {@link VigorRetry} engine (for retry/timeout/backoff) and a
1500
+ * {@link VigorParse} engine (for response parsing), with a configurable
1501
+ * middleware pipeline around request building and response handling.
1502
+ */
913
1503
  class VigorFetch extends VigorStatus {
914
- constructor(config) {
915
- const base = {
916
- origin: VigorDefault,
917
- path: [],
918
- query: [],
919
- hash: "",
920
- options: {
921
- headers: {},
922
- body: VigorDefault
923
- },
924
- settings: new VigorFetchSettings()._getBase(),
925
- interceptors: new VigorFetchInterceptors()._getBase(),
926
- retryConfig: new VigorRetry()._getBase(),
927
- parseConfig: new VigorParse()._getBase()
928
- };
929
- super(config, base, (c) => new VigorFetch(c));
930
- }
931
- _createTimelineHandler(timeline) {
932
- return (action, content) => {
933
- timeline.push({
934
- action: action,
935
- content: content,
936
- time: Date.now()
937
- });
938
- };
1504
+ constructor(config = VigorFetchBase) {
1505
+ super(VigorFetchBase, config);
939
1506
  }
940
- _createInterceptorHandler(ctx, addTimeline) {
941
- return async (interceptorType, api) => {
942
- const interceptorsConfig = ctx["stats"]["interceptors"];
943
- const interceptors = interceptorsConfig[interceptorType];
944
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
945
- interceptorType: interceptorType,
946
- interceptors,
947
- });
948
- const startTime = performance.now();
949
- for (const func of interceptors) {
950
- const scopedApi = api(interceptorType, func);
951
- await func(ctx, scopedApi);
952
- }
953
- const endTime = performance.now();
954
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
955
- interceptorType: interceptorType,
956
- interceptors,
957
- took: endTime - startTime
958
- });
959
- };
1507
+ _create(config) {
1508
+ return new VigorFetch(config);
1509
+ }
1510
+ /** Sets the HTTP method. Defaults to `POST` when a body is set, otherwise `GET`. */
1511
+ method(str) { return this._next({ policy: { method: str } }); }
1512
+ /** Sets the request origin, either an absolute URL or a path relative to the current page. */
1513
+ origin(str) { return this._next({ policy: { origin: str } }); }
1514
+ /** Appends path segments to the request URL. */
1515
+ path(...strs) {
1516
+ return this._next({ policy: { path: this._stringifyList(strs) } });
1517
+ }
1518
+ /** Adds query-string parameter objects to the request URL. */
1519
+ query(...objs) {
1520
+ return this._next({ policy: { query: objs } });
1521
+ }
1522
+ /** Sets the URL fragment ("hash"), replacing any previous value. */
1523
+ hash(str) { return this._next({ policy: { hash: str } }, { policy: { hash: "replace" } }); }
1524
+ /**
1525
+ * Sets request headers.
1526
+ *
1527
+ * @param mode - `"overwrite"` discards all previously configured headers
1528
+ * and keeps only the ones passed here. `"merge"` behaves like
1529
+ * `Object.assign`, overwriting only the keys that overlap and keeping
1530
+ * the rest.
1531
+ */
1532
+ headers(mode, obj) {
1533
+ if (mode === "overwrite")
1534
+ return this._next({ policy: { headers: obj } }, { policy: { headers: "replace" } });
1535
+ return this._next({ policy: { headers: obj } });
960
1536
  }
1537
+ /**
1538
+ * Sets the request body.
1539
+ *
1540
+ * @param mode - `"overwrite"` discards the previous body and fully
1541
+ * replaces it with this value. `"merge"` shallow-merges (via
1542
+ * `Object.assign`) when both the previous and new body are plain
1543
+ * objects; for any other body type (string/Blob/FormData/etc) it is
1544
+ * simply replaced.
1545
+ */
1546
+ body(mode, obj) {
1547
+ if (mode === "overwrite")
1548
+ return this._next({ policy: { body: obj } }, { policy: { body: "replace" } });
1549
+ return this._next({ policy: { body: obj } });
1550
+ }
1551
+ /**
1552
+ * Sets additional `RequestInit` options other than headers/body/method/signal
1553
+ * (e.g. `credentials`, `mode`, `cache`, `redirect`, `referrer`,
1554
+ * `referrerPolicy`, `keepalive`, `integrity`).
1555
+ *
1556
+ * @param mode - `"overwrite"` discards all previously configured options
1557
+ * and replaces them with this value. `"merge"` shallow-merges this
1558
+ * value onto the previous options.
1559
+ */
1560
+ options(mode, obj) {
1561
+ if (mode === "overwrite")
1562
+ return this._next({ policy: { extra: obj } }, { policy: { extra: "replace" } });
1563
+ return this._next({ policy: { extra: obj } });
1564
+ }
1565
+ /** Configures the fetch policy's general settings. */
1566
+ settings(input) {
1567
+ if (typeof input === 'function' || input instanceof VigorFetchPolicySettings) {
1568
+ const result = typeof input === 'function'
1569
+ ? input(new VigorFetchPolicySettings())
1570
+ : input;
1571
+ return this._next(result.config);
1572
+ }
1573
+ return this._next({ policy: { settings: input } });
1574
+ }
1575
+ /** Configures the fetch policy's middleware pipeline. */
1576
+ middlewares(input) {
1577
+ if (typeof input === 'function' || input instanceof VigorFetchPolicyMiddlewares) {
1578
+ const result = typeof input === 'function'
1579
+ ? input(new VigorFetchPolicyMiddlewares())
1580
+ : input;
1581
+ return this._next(result.config);
1582
+ }
1583
+ return this._next({ policy: { middlewares: input } });
1584
+ }
1585
+ /** Configures the retry engine used to send the underlying request. */
1586
+ retry(input) {
1587
+ if (typeof input === 'function' || input instanceof VigorRetry) {
1588
+ const result = typeof input === 'function'
1589
+ ? input(new VigorRetry())
1590
+ : input;
1591
+ return this._next({ policy: { retry: result.config } }, { policy: { retry: "replace" } });
1592
+ }
1593
+ return this._next({ policy: { retry: input } }, { policy: { retry: "replace" } });
1594
+ }
1595
+ /** Configures the parse engine used to interpret the response. */
1596
+ parse(input) {
1597
+ if (typeof input === 'function' || input instanceof VigorParse) {
1598
+ const result = typeof input === 'function'
1599
+ ? input(new VigorParse())
1600
+ : input;
1601
+ return this._next({ policy: { parse: result.config } }, { policy: { parse: "replace" } });
1602
+ }
1603
+ return this._next({ policy: { parse: input } }, { policy: { parse: "replace" } });
1604
+ }
1605
+ /** Converts a list of stringable values into strings, dropping `null`/`undefined` entries. */
961
1606
  _stringifyList(unkList) {
962
1607
  return unkList
963
1608
  .filter(unk => unk !== null && unk !== undefined)
964
- .map(unk => {
965
- if (unk instanceof Date)
966
- return unk.toISOString();
967
- return String(unk);
968
- });
1609
+ .map(unk => unk instanceof Date ? unk.toISOString() : String(unk));
1610
+ }
1611
+ /**
1612
+ * Resolves the base URL used to interpret a relative `origin`. In a
1613
+ * browser environment this is the current page location; in
1614
+ * environments without `location` (e.g. Node), no base is available and
1615
+ * `origin` must be an absolute URL.
1616
+ */
1617
+ _resolveBase() {
1618
+ if (typeof globalThis !== 'undefined' && globalThis.location?.href) {
1619
+ return globalThis.location.href;
1620
+ }
1621
+ return undefined;
969
1622
  }
970
- method(str) { return this._next({ method: str }); }
971
- origin(str) { return this._next({ origin: str }); }
972
- path(...strs) { return this._next({ path: this._stringifyList(strs.flat()) }); }
973
- query(...strs) { return this._next({ query: strs.flat() }); }
974
- hash(str) { return this._next({ hash: str }); }
975
- options(obj) { return this._next({ options: obj }); }
976
- headers(obj) { return this._next({ options: { headers: obj } }); }
977
- body(obj) { return this._next({ options: { headers: this._config.options.headers, body: obj } }); }
1623
+ /**
1624
+ * Builds the final request URL from the configured origin, path
1625
+ * segments, query parameters, and hash. An absolute `origin` ignores
1626
+ * the resolved base (per the `URL` spec); a relative `origin` is
1627
+ * resolved against it. Throws `INVALID_PROTOCOL` if neither is possible.
1628
+ */
978
1629
  _buildUrl(origin, path, query, hash) {
979
- const originObj = new URL(origin);
980
- const baseStr = originObj.origin;
981
- const pathObj = [originObj.pathname.replace(/^\/+|\/+$/g, '')];
982
- for (const str of path) {
983
- pathObj.push(str.replace(/^\/+|\/+$/g, ''));
1630
+ const base = this._resolveBase();
1631
+ let originObj;
1632
+ try {
1633
+ originObj = new URL(origin, base);
1634
+ }
1635
+ catch {
1636
+ throw new VigorFetchError("INVALID_PROTOCOL", {
1637
+ method: "_buildUrl",
1638
+ data: { origin, base }
1639
+ });
984
1640
  }
985
- const pathStr = pathObj.join('/');
1641
+ const baseStr = originObj.origin;
1642
+ const pathParts = [originObj.pathname.replace(/^\/+|\/+$/g, '')];
1643
+ for (const p of path)
1644
+ pathParts.push(p.replace(/^\/+|\/+$/g, ''));
1645
+ const pathStr = pathParts.filter(Boolean).join('/');
986
1646
  const mainObj = new URL(pathStr, baseStr);
987
- const parseVal = (val) => {
988
- if (val instanceof Date)
989
- return val.toISOString();
990
- return String(val);
991
- };
992
- const queryObj = [...Array.from(originObj.searchParams.entries()), ...query.flatMap(qu => Object.entries(qu))];
993
- for (const [key, val] of queryObj) {
1647
+ const parseVal = (val) => val instanceof Date ? val.toISOString() : String(val);
1648
+ const queryEntries = [...originObj.searchParams.entries(), ...query.flatMap(q => Object.entries(q))];
1649
+ for (const [key, val] of queryEntries) {
994
1650
  if (val === undefined || val === null)
995
1651
  continue;
996
- if (Array.isArray(val))
997
- for (const e of val) {
1652
+ if (Array.isArray(val)) {
1653
+ for (const e of val)
998
1654
  mainObj.searchParams.append(key, parseVal(e));
999
- }
1655
+ }
1000
1656
  else {
1001
1657
  mainObj.searchParams.append(key, parseVal(val));
1002
1658
  }
1003
1659
  }
1004
- mainObj.hash = hash ?? originObj.hash;
1660
+ mainObj.hash = hash || originObj.hash;
1005
1661
  return mainObj.href;
1006
1662
  }
1007
- _normalizeOptions(body) {
1663
+ /** Infers the `Content-Type` header and serializes `body` into a `BodyInit`. */
1664
+ _normalizeBody(body) {
1008
1665
  if (body == null)
1009
- return { isJson: false, headers: {}, body };
1666
+ return { headers: {}, body: body };
1010
1667
  if (typeof body === "string")
1011
- return { isJson: false, headers: {
1012
- "Content-Type": "text/plain;charset=UTF-8"
1013
- }, body };
1668
+ return { headers: { "Content-Type": "text/plain;charset=UTF-8" }, body };
1014
1669
  if (body instanceof Blob)
1015
- return { isJson: false, headers: {
1016
- ...(body.type && { "Content-Type": body.type })
1017
- }, body };
1670
+ return { headers: body.type ? { "Content-Type": body.type } : {}, body };
1018
1671
  if (body instanceof ArrayBuffer)
1019
- return { isJson: false, headers: {
1020
- "Content-Type": "application/octet-stream"
1021
- }, body };
1672
+ return { headers: { "Content-Type": "application/octet-stream" }, body };
1022
1673
  if (body instanceof URLSearchParams)
1023
- return { isJson: false, headers: {
1024
- "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
1025
- }, body };
1674
+ return { headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" }, body };
1026
1675
  if (body instanceof FormData)
1027
- return { isJson: false, headers: {}, body };
1028
- if (typeof body === "object") {
1029
- return { isJson: true, headers: {
1030
- "Content-Type": "application/json"
1031
- }, body: JSON.stringify(body) };
1032
- }
1676
+ return { headers: {}, body };
1677
+ if (typeof body === "object")
1678
+ return { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
1033
1679
  throw new VigorFetchError("INVALID_BODY", {
1034
1680
  method: "_normalizeBody",
1035
- data: {
1036
- expected: ["string", "Blob", "ArrayBuffer", "URLSearchParams", "FormData"],
1037
- received: body
1038
- }
1681
+ data: { received: body }
1039
1682
  });
1040
1683
  }
1041
- settings(func) {
1042
- if (func instanceof VigorFetchSettings) {
1043
- return this._next({ settings: func._getConfig() });
1044
- }
1045
- if (typeof func === 'function') {
1046
- return this._next({ settings: func(new VigorFetchSettings(this._config.settings))._getConfig() });
1047
- }
1048
- return this._next({ settings: func });
1049
- }
1050
- interceptors(func) {
1051
- if (func instanceof VigorFetchInterceptors) {
1052
- return this._next({ interceptors: func._getConfig() });
1053
- }
1054
- if (typeof func === 'function') {
1055
- return this._next({ interceptors: func(new VigorFetchInterceptors(this._config.interceptors))._getConfig() });
1056
- }
1057
- return this._next({ interceptors: func });
1058
- }
1059
- retryConfig(func) {
1060
- if (func instanceof VigorRetry) {
1061
- return this._next({ retryConfig: func._getConfig() });
1062
- }
1063
- if (typeof func === 'function') {
1064
- return this._next({ retryConfig: func(new VigorRetry(this._config.retryConfig))._getConfig() });
1065
- }
1066
- return this._next({ retryConfig: func });
1684
+ /** Runs the request and returns its parsed result directly, throwing on final failure. */
1685
+ async request() {
1686
+ const res = await this.requestVerbose();
1687
+ if (res.success)
1688
+ return res.data;
1689
+ throw res.error;
1067
1690
  }
1068
- parseConfig(func) {
1069
- if (func instanceof VigorParse) {
1070
- return this._next({ parseConfig: func._getConfig() });
1071
- }
1072
- if (typeof func === 'function') {
1073
- return this._next({ parseConfig: func(new VigorParse(this._config.parseConfig))._getConfig() });
1074
- }
1075
- return this._next({ parseConfig: func });
1691
+ /** Runs the request and returns a result/error envelope instead of throwing. */
1692
+ async requestVerbose() {
1693
+ return await this.requestRuntime();
1076
1694
  }
1077
- async request(config, timeline = []) {
1078
- const stats = this._mergeConfig(this._config, config);
1695
+ /** Internal execution loop implementing URL building, retry, parsing, and restart handling. */
1696
+ async requestRuntime(restartAttempt = 1) {
1697
+ const config = this._merge(this.config, {});
1079
1698
  let ctx = {
1080
- href: "",
1081
- result: VigorDefault,
1082
- response: VigorDefault,
1083
- options: {
1084
- headers: VigorDefault,
1085
- body: VigorDefault
1086
- },
1087
- error: VigorDefault,
1088
- timeline: timeline,
1089
- stats,
1090
- flag: {
1091
- overwritten: false,
1092
- restarted: false
1093
- }
1699
+ ...config.context,
1700
+ policy: config.policy
1094
1701
  };
1095
- const addTimeline = this._createTimelineHandler(ctx.timeline);
1096
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
1097
- addTimeline("PROCESS_HANDLING", {
1098
- type: "REQUEST_START",
1099
- data: {}
1100
- });
1702
+ const throwError = (err) => { throw err; };
1101
1703
  try {
1102
- try {
1103
- new URL(stats.origin);
1104
- }
1105
- catch {
1106
- throw new VigorFetchError("INVALID_PROTOCOL", {
1704
+ if (isVigorEmpty(ctx.policy.origin))
1705
+ throw new VigorFetchError("VALUE_REQUIRED", {
1107
1706
  method: "request",
1108
- data: {
1109
- expected: ["valid URL protocol"],
1110
- received: stats.origin
1111
- }
1707
+ data: { key: "origin" }
1112
1708
  });
1113
- }
1114
- ctx.href = this._buildUrl(stats.origin, stats.path, stats.query, stats.hash);
1115
- addTimeline("BUILT_URL", {
1116
- url: ctx.href
1117
- });
1118
- const { headers, body, ...others } = stats.options;
1119
- const hasBody = body !== VigorDefault &&
1120
- body !== undefined;
1121
- const method = stats.method || (hasBody ? 'POST' : 'GET');
1122
- ctx.options = {
1123
- ...others,
1709
+ ctx.href = this._buildUrl(ctx.policy.origin, ctx.policy.path, ctx.policy.query, ctx.policy.hash);
1710
+ const hasBody = !isVigorEmpty(ctx.policy.body) && ctx.policy.body !== undefined;
1711
+ const method = ctx.policy.method || (hasBody ? "POST" : "GET");
1712
+ let options = {
1713
+ ...ctx.policy.extra,
1124
1714
  method: method,
1125
1715
  headers: {}
1126
1716
  };
1127
1717
  if (hasBody) {
1128
- const normalized = this._normalizeOptions(body);
1129
- if (normalized.body !== undefined) {
1130
- ctx.options.body = normalized.body;
1718
+ const normalized = this._normalizeBody(ctx.policy.body);
1719
+ if (normalized.body !== undefined)
1720
+ options.body = normalized.body;
1721
+ Object.assign(options.headers, normalized.headers);
1722
+ }
1723
+ Object.assign(options.headers, ctx.policy.headers);
1724
+ for (const middleware of ctx.policy.middlewares.before) {
1725
+ if (middleware._mode === "fluent") {
1726
+ await middleware.func(ctx);
1727
+ }
1728
+ else {
1729
+ ctx = await middleware.func(ctx, {
1730
+ throwError,
1731
+ setOptions: (o) => { options = o; },
1732
+ setHeaders: (h) => { options.headers = h; },
1733
+ setBody: (b) => { options.body = b; }
1734
+ });
1131
1735
  }
1132
- Object.assign(ctx.options.headers, normalized.headers);
1133
1736
  }
1134
- Object.assign(ctx.options.headers, headers);
1135
- addTimeline("SET_OPTIONS", {
1136
- options: ctx.options
1137
- });
1138
- const fetchTask = async (ctx2, { abort, signal }) => {
1139
- ctx.options.signal = signal;
1140
- const result = await fetch(ctx.href, ctx.options);
1141
- return result;
1142
- };
1143
- const throwStatus = async (ctx2, api) => {
1144
- const response = ctx2.result;
1737
+ ctx.options = options;
1738
+ const retryEngine = new VigorRetry(ctx.policy.retry)
1739
+ .target(async (signal) => {
1740
+ return await fetch(ctx.href, { ...options, signal });
1741
+ })
1742
+ .middlewares(m => m
1743
+ .after("intercept", async (rctx, api) => {
1744
+ const response = rctx.result;
1145
1745
  if (!response.ok) {
1746
+ let parsed = undefined;
1747
+ try {
1748
+ parsed = await new VigorParse(ctx.policy.parse).target(response.clone()).request();
1749
+ }
1750
+ catch {
1751
+ }
1146
1752
  api.throwError(new VigorFetchError("FETCH_FAILED", {
1147
1753
  method: "request",
1148
1754
  data: {
1149
1755
  status: response.status,
1150
- response: response,
1756
+ statusText: response.statusText,
1757
+ response,
1151
1758
  url: response.url,
1152
- headers: response.headers,
1153
- body: response.body,
1154
- statusText: response.statusText
1759
+ parsed
1155
1760
  }
1156
1761
  }));
1157
1762
  }
1158
- };
1159
- const handleBlacklist = async (ctx2, api) => {
1160
- const response = ctx2.result;
1161
- ctx.error = ctx2.error;
1162
- if (response instanceof Response) {
1163
- if (stats.settings.unretryStatus.includes(response.status))
1164
- api.cancelRetry();
1165
- else
1166
- api.proceedRetry();
1167
- }
1168
- };
1169
- const handleRatelimit = async (ctx2, api) => {
1170
- const response = ctx2.result;
1171
- ctx.error = ctx2.error;
1172
- if (response instanceof Response) {
1173
- if (response.status === 429) {
1174
- let retryHeader = null;
1175
- for (const header of stats.settings.retryHeaders) {
1176
- retryHeader = response.headers.get(header);
1177
- if (retryHeader)
1178
- break;
1179
- }
1180
- if (retryHeader) {
1181
- const toNumber = Number(retryHeader);
1182
- const delay = !isNaN(toNumber)
1183
- ? toNumber * 1000
1184
- : (() => {
1185
- const toDate = new Date(retryHeader).getTime();
1186
- return !isNaN(toDate)
1187
- ? toDate - Date.now()
1188
- : null;
1189
- })();
1190
- if (delay !== null && delay > 0)
1191
- api.setDelay(delay + Math.random() * ctx2.stats.settings.jitter);
1192
- }
1763
+ return rctx;
1764
+ })
1765
+ .retryIf("intercept", async (rctx, api) => {
1766
+ const response = rctx.error instanceof VigorFetchError
1767
+ ? rctx.error.data?.response
1768
+ : undefined;
1769
+ if (response && ctx.policy.settings.unretryStatus.includes(response.status))
1770
+ api.cancelRetry();
1771
+ else
1772
+ api.proceedRetry();
1773
+ return rctx;
1774
+ })
1775
+ .onRetry("intercept", async (rctx, api) => {
1776
+ const response = rctx.error instanceof VigorFetchError
1777
+ ? rctx.error.data?.response
1778
+ : undefined;
1779
+ if (response?.status === 429) {
1780
+ let retryHeader = null;
1781
+ for (const header of ctx.policy.settings.retryHeaders) {
1782
+ retryHeader = response.headers.get(header);
1783
+ if (retryHeader)
1784
+ break;
1785
+ }
1786
+ if (retryHeader) {
1787
+ const toNumber = Number(retryHeader);
1788
+ const delay = !isNaN(toNumber)
1789
+ ? toNumber * 1000
1790
+ : (() => {
1791
+ const toDate = new Date(retryHeader).getTime();
1792
+ return !isNaN(toDate) ? toDate - Date.now() : null;
1793
+ })();
1794
+ if (delay !== null && delay > 0)
1795
+ api.setDelay(delay);
1193
1796
  }
1194
1797
  }
1195
- };
1196
- stats.retryConfig.interceptors.after = [throwStatus, ...stats.retryConfig.interceptors.after];
1197
- stats.retryConfig.interceptors.retryIf = [handleBlacklist, ...stats.retryConfig.interceptors.retryIf];
1198
- stats.retryConfig.interceptors.onRetry = [handleRatelimit, ...stats.retryConfig.interceptors.onRetry];
1199
- const retryEngine = new VigorRetry(stats.retryConfig)
1200
- .target(fetchTask);
1201
- const parseEngine = new VigorParse(stats.parseConfig);
1202
- addTimeline("ENGINE_CREATED", {
1203
- retryEngine,
1204
- parseEngine
1205
- });
1206
- await handleInterceptor("before", (interceptorType, func) => ({
1207
- throwError: (error) => {
1208
- addTimeline("INTERCEPTOR_API_CALLED", {
1209
- interceptorType,
1210
- interceptor: func,
1211
- method: "throwError",
1212
- args: [error]
1213
- });
1214
- throw error;
1215
- },
1216
- setOptions: (unknown) => {
1217
- addTimeline("INTERCEPTOR_API_CALLED", {
1218
- interceptorType,
1219
- interceptor: func,
1220
- method: "setOptions",
1221
- args: [unknown]
1222
- });
1223
- return ctx.options = unknown;
1224
- },
1225
- setHeaders: (unknown) => {
1226
- addTimeline("INTERCEPTOR_API_CALLED", {
1227
- interceptorType,
1228
- interceptor: func,
1229
- method: "setHeaders",
1230
- args: [unknown]
1231
- });
1232
- return ctx.options.headers = unknown;
1233
- },
1234
- setBody: (unknown) => {
1235
- addTimeline("INTERCEPTOR_API_CALLED", {
1236
- interceptorType,
1237
- interceptor: func,
1238
- method: "setBody",
1239
- args: [unknown]
1240
- });
1241
- return ctx.options.body = unknown;
1242
- }
1798
+ return rctx;
1243
1799
  }));
1244
- addTimeline("RETRY_STARTED", {
1245
- engine: retryEngine
1246
- });
1247
- const retryStart = performance.now();
1248
- const retryTimeline = [];
1249
- ctx.response = await retryEngine.request(undefined, retryTimeline);
1250
- const retryEnd = performance.now();
1251
- addTimeline("RETRY_ENDED", {
1252
- engine: retryEngine,
1253
- timeline: retryTimeline,
1254
- took: retryEnd - retryStart,
1255
- response: ctx.response
1256
- });
1257
- addTimeline("PARSE_STARTED", {
1258
- engine: parseEngine
1259
- });
1260
- const parseStart = performance.now();
1261
- const parseTimeline = [];
1262
- ctx.result = await parseEngine.target(ctx.response).request(undefined, parseTimeline);
1263
- const parseEnd = performance.now();
1264
- addTimeline("PARSE_ENDED", {
1265
- engine: parseEngine,
1266
- timeline: parseTimeline,
1267
- took: parseEnd - parseStart,
1268
- result: ctx.result
1269
- });
1270
- await handleInterceptor("after", (interceptorType, func) => ({
1271
- setResult: (unknown) => {
1272
- addTimeline("INTERCEPTOR_API_CALLED", {
1273
- interceptorType,
1274
- interceptor: func,
1275
- method: "setResult",
1276
- args: [unknown]
1277
- });
1278
- ctx.result = unknown;
1279
- return unknown;
1280
- },
1281
- throwError: (error) => {
1282
- addTimeline("INTERCEPTOR_API_CALLED", {
1283
- interceptorType,
1284
- interceptor: func,
1285
- method: "throwError",
1286
- args: [error]
1287
- });
1288
- throw error;
1289
- },
1290
- }));
1291
- await handleInterceptor("result", (interceptorType, func) => ({
1292
- setResult: (unknown) => {
1293
- addTimeline("INTERCEPTOR_API_CALLED", {
1294
- interceptorType,
1295
- interceptor: func,
1296
- method: "setResult",
1297
- args: [unknown]
1298
- });
1299
- ctx.result = unknown;
1300
- return unknown;
1301
- },
1302
- throwError: (error) => {
1303
- addTimeline("INTERCEPTOR_API_CALLED", {
1304
- interceptorType,
1305
- interceptor: func,
1306
- method: "throwError",
1307
- args: [error]
1308
- });
1309
- throw error;
1310
- },
1311
- }));
1312
- return ctx.result;
1313
- }
1314
- catch (error) {
1315
- ctx.error = error;
1316
- addTimeline("PROCESS_HANDLING", {
1317
- type: "REQUEST_ERROR",
1318
- data: {
1319
- error
1800
+ ctx.response = await retryEngine.request();
1801
+ const parseEngine = new VigorParse(ctx.policy.parse).target(ctx.response);
1802
+ ctx.result = await parseEngine.request();
1803
+ for (const middleware of ctx.policy.middlewares.after) {
1804
+ if (middleware._mode === "fluent") {
1805
+ await middleware.func(ctx);
1320
1806
  }
1321
- });
1322
- await handleInterceptor("onError", (interceptorType, func) => ({
1323
- setResult: (unknown) => {
1324
- addTimeline("INTERCEPTOR_API_CALLED", {
1325
- interceptorType,
1326
- interceptor: func,
1327
- method: "setResult",
1328
- args: [unknown]
1329
- });
1330
- ctx.result = unknown;
1331
- ctx.flag.overwritten = true;
1332
- return unknown;
1333
- },
1334
- throwError: (error) => {
1335
- addTimeline("INTERCEPTOR_API_CALLED", {
1336
- interceptorType,
1337
- interceptor: func,
1338
- method: "throwError",
1339
- args: [error]
1340
- });
1341
- throw error;
1342
- },
1343
- restart: () => {
1344
- addTimeline("INTERCEPTOR_API_CALLED", {
1345
- interceptorType,
1346
- interceptor: func,
1347
- method: "restart",
1348
- args: []
1349
- });
1350
- ctx.flag.restarted = true;
1807
+ else {
1808
+ ctx = await middleware.func(ctx, { throwError, setResult: (r) => { ctx.result = r; } });
1351
1809
  }
1352
- }));
1353
- if (ctx.flag.restarted) {
1354
- return await this.request(stats, ctx.timeline);
1355
- }
1356
- if (ctx.flag.overwritten)
1357
- return ctx.result;
1358
- if (stats.settings.default !== VigorDefault)
1359
- return stats.settings.default;
1360
- throw error;
1361
- }
1362
- }
1363
- }
1364
- class VigorAllSettings extends VigorStatus {
1365
- constructor(config) {
1366
- const base = {
1367
- concurrency: 5,
1368
- onlySuccess: false
1369
- };
1370
- super(config, base, (c) => new VigorAllSettings(c));
1371
- }
1372
- concurrency(num) { return this._next({ concurrency: num }); }
1373
- onlySuccess(num) { return this._next({ onlySuccess: num }); }
1374
- }
1375
- class VigorAllInterceptors extends VigorStatus {
1376
- constructor(config) {
1377
- const base = {
1378
- before: [],
1379
- after: [],
1380
- result: [],
1381
- onError: []
1382
- };
1383
- super(config, base, (c) => new VigorAllInterceptors(c));
1384
- }
1385
- before(...funcs) { return this._next({ before: funcs.flat() }); }
1386
- after(...funcs) { return this._next({ after: funcs.flat() }); }
1387
- result(...funcs) { return this._next({ result: funcs.flat() }); }
1388
- onError(...funcs) { return this._next({ onError: funcs.flat() }); }
1389
- }
1390
- class VigorAll extends VigorStatus {
1391
- constructor(config) {
1392
- const base = {
1393
- target: [],
1394
- settings: new VigorAllSettings()._getBase(),
1395
- interceptors: new VigorAllInterceptors()._getBase()
1396
- };
1397
- super(config, base, (c) => new VigorAll(c));
1398
- }
1399
- _createTimelineHandler(timeline) {
1400
- return (action, content) => {
1401
- timeline.push({
1402
- action: action,
1403
- content: content,
1404
- time: Date.now()
1405
- });
1406
- };
1407
- }
1408
- _createInterceptorHandler(ctx, addTimeline) {
1409
- return async (interceptorType, api) => {
1410
- const interceptorsConfig = ctx["stats"]["interceptors"];
1411
- const interceptors = interceptorsConfig[interceptorType];
1412
- addTimeline("INTERCEPTOR_LOOP_STARTED", {
1413
- interceptorType: interceptorType,
1414
- interceptors,
1415
- });
1416
- const startTime = performance.now();
1417
- for (const func of interceptors) {
1418
- const scopedApi = api(interceptorType, func);
1419
- await func(ctx, scopedApi);
1420
- }
1421
- const endTime = performance.now();
1422
- addTimeline("INTERCEPTOR_LOOP_ENDED", {
1423
- interceptorType: interceptorType,
1424
- interceptors,
1425
- took: endTime - startTime
1426
- });
1427
- };
1428
- }
1429
- _createEachTimelineHandler(timeline) {
1430
- return (action, content) => {
1431
- timeline.push({
1432
- action: action,
1433
- content: content,
1434
- time: Date.now()
1435
- });
1436
- };
1437
- }
1438
- _createEachInterceptorHandler(ctx, addEachTimeline) {
1439
- return async (interceptorType, api) => {
1440
- const interceptorsConfig = ctx["stats"]["interceptors"];
1441
- const interceptors = interceptorsConfig[interceptorType];
1442
- addEachTimeline("INTERCEPTOR_LOOP_STARTED", {
1443
- interceptorType: interceptorType,
1444
- interceptors,
1445
- });
1446
- const startTime = performance.now();
1447
- for (const func of interceptors) {
1448
- const scopedApi = api(interceptorType, func);
1449
- await func(ctx, scopedApi);
1450
- }
1451
- const endTime = performance.now();
1452
- addEachTimeline("INTERCEPTOR_LOOP_ENDED", {
1453
- interceptorType: interceptorType,
1454
- interceptors,
1455
- took: endTime - startTime
1456
- });
1457
- };
1458
- }
1459
- target(...funcs) { return this._next({ target: funcs.flat() }); }
1460
- settings(func) {
1461
- if (func instanceof VigorAllSettings) {
1462
- return this._next({ settings: func._getConfig() });
1463
- }
1464
- if (typeof func === 'function') {
1465
- return this._next({ settings: func(new VigorAllSettings(this._config.settings))._getConfig() });
1466
- }
1467
- return this._next({ settings: func });
1468
- }
1469
- interceptors(func) {
1470
- if (func instanceof VigorAllInterceptors) {
1471
- return this._next({ interceptors: func._getConfig() });
1472
- }
1473
- if (typeof func === 'function') {
1474
- return this._next({ interceptors: func(new VigorAllInterceptors(this._config.interceptors))._getConfig() });
1475
- }
1476
- return this._next({ interceptors: func });
1477
- }
1478
- async runTask(task, { stats, root }, semaphore) {
1479
- let ctx = {
1480
- result: VigorDefault,
1481
- error: VigorDefault,
1482
- timeline: [],
1483
- stats,
1484
- root,
1485
- target: task,
1486
- semaphore,
1487
- flag: {
1488
- overwritten: false
1489
- }
1490
- };
1491
- const addEachTimeline = this._createEachTimelineHandler(ctx.timeline);
1492
- const handleEachInterceptor = this._createEachInterceptorHandler(ctx, addEachTimeline);
1493
- addEachTimeline("PROCESS_HANDLING", {
1494
- type: "TASK_START",
1495
- data: {}
1496
- });
1497
- try {
1498
- try {
1499
- await semaphore.acquire();
1500
- addEachTimeline("TASK_ACQUIRED", {
1501
- target: ctx.target
1502
- });
1503
- await handleEachInterceptor("before", (interceptorType, func) => ({
1504
- throwError: (error) => {
1505
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1506
- interceptorType,
1507
- interceptor: func,
1508
- method: "throwError",
1509
- args: [error]
1510
- });
1511
- throw error;
1512
- }
1513
- }));
1514
- addEachTimeline("TASK_STARTED", {
1515
- target: ctx.target
1516
- });
1517
- const startTime = performance.now();
1518
- ctx.result = await ctx.target(ctx);
1519
- const endTime = performance.now();
1520
- addEachTimeline("TASK_ENDED", {
1521
- target: ctx.target,
1522
- took: endTime - startTime
1523
- });
1524
- await handleEachInterceptor("after", (interceptorType, func) => ({
1525
- setResult: (unknown) => {
1526
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1527
- interceptorType,
1528
- interceptor: func,
1529
- method: "setResult",
1530
- args: [unknown]
1531
- });
1532
- ctx.result = unknown;
1533
- return unknown;
1534
- },
1535
- throwError: (error) => {
1536
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1537
- interceptorType,
1538
- interceptor: func,
1539
- method: "throwError",
1540
- args: [error]
1541
- });
1542
- throw error;
1543
- }
1544
- }));
1545
1810
  }
1546
- finally {
1547
- semaphore.release();
1548
- addEachTimeline("TASK_RELEASED", {
1549
- target: ctx.target
1550
- });
1811
+ const setResult = (r) => { ctx.result = r; };
1812
+ for (const middleware of ctx.policy.middlewares.onResult) {
1813
+ if (middleware._mode === "fluent") {
1814
+ ctx.result = await middleware.func(ctx.result);
1815
+ }
1816
+ else {
1817
+ ctx = await middleware.func(ctx, { setResult, throwError });
1818
+ }
1551
1819
  }
1820
+ return {
1821
+ success: true,
1822
+ data: ctx.result,
1823
+ error: null
1824
+ };
1552
1825
  }
1553
1826
  catch (error) {
1554
1827
  ctx.error = error;
1555
- addEachTimeline("PROCESS_HANDLING", {
1556
- type: "TASK_ERROR",
1557
- data: {
1558
- error
1828
+ ctx.flags.overrided = false;
1829
+ ctx.flags.restarted = false;
1830
+ const setResult = (r) => {
1831
+ ctx.flags.overrided = true;
1832
+ ctx.result = r;
1833
+ };
1834
+ const proceedRestart = () => ctx.flags.restarted = true;
1835
+ const cancelRestart = () => ctx.flags.restarted = false;
1836
+ for (const middleware of ctx.policy.middlewares.onError) {
1837
+ if (middleware._mode === "fluent") {
1838
+ await middleware.func(ctx.error);
1559
1839
  }
1560
- });
1561
- await handleEachInterceptor("onError", (interceptorType, func) => ({
1562
- setResult: (unknown) => {
1563
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1564
- interceptorType,
1565
- interceptor: func,
1566
- method: "setResult",
1567
- args: [unknown]
1568
- });
1569
- ctx.result = unknown;
1570
- ctx.flag.overwritten = true;
1571
- return unknown;
1572
- },
1573
- throwError: (error) => {
1574
- addEachTimeline("INTERCEPTOR_API_CALLED", {
1575
- interceptorType,
1576
- interceptor: func,
1577
- method: "throwError",
1578
- args: [error]
1840
+ else {
1841
+ ctx = await middleware.func(ctx, { setResult, throwError, proceedRestart, cancelRestart });
1842
+ }
1843
+ }
1844
+ if (ctx.flags.restarted) {
1845
+ if (restartAttempt + 1 > ctx.policy.settings.maxRestarts)
1846
+ throw new VigorFetchError("RESTART_EXHAUSTED", {
1847
+ method: "request",
1848
+ data: {
1849
+ maxAttempts: ctx.policy.settings.maxRestarts
1850
+ }
1579
1851
  });
1580
- throw error;
1581
- },
1582
- }));
1583
- if (ctx.flag.overwritten)
1584
- return ctx.result;
1585
- throw error;
1586
- }
1587
- return ctx.result;
1588
- }
1589
- async request(config, timeline = []) {
1590
- const stats = this._mergeConfig(this._config, config);
1591
- let ctx = {
1592
- result: VigorDefault,
1593
- timeline,
1594
- stats,
1595
- queue: new Set(),
1596
- active: 0
1597
- };
1598
- const addTimeline = this._createTimelineHandler(ctx.timeline);
1599
- const handleInterceptor = this._createInterceptorHandler(ctx, addTimeline);
1600
- addTimeline("PROCESS_HANDLING", {
1601
- type: "REQUEST_START",
1602
- data: {}
1603
- });
1604
- if (stats.target.length === 0)
1605
- throw new VigorAllError("EMPTY_TARGET", {
1606
- method: "request",
1607
- data: {}
1608
- });
1609
- const waitQueue = [];
1610
- const acquire = () => {
1611
- if (ctx.active < stats.settings.concurrency) {
1612
- ctx.active++;
1613
- return Promise.resolve();
1852
+ return await this.requestRuntime(restartAttempt + 1);
1614
1853
  }
1615
- return new Promise((res) => waitQueue.push(() => { ctx.active++; res(); }));
1616
- };
1617
- const release = () => {
1618
- ctx.active--;
1619
- if (waitQueue.length > 0) {
1620
- const next = waitQueue.shift();
1621
- if (next)
1622
- next();
1854
+ if (ctx.flags.overrided)
1855
+ return {
1856
+ success: true,
1857
+ data: ctx.result,
1858
+ error: null
1859
+ };
1860
+ if (!isVigorEmpty(ctx.policy.settings.default)) {
1861
+ const data = await ctx.policy.settings.default(ctx);
1862
+ return {
1863
+ success: true,
1864
+ data,
1865
+ error: null
1866
+ };
1623
1867
  }
1624
- };
1625
- for (const task of stats.target) {
1626
- let promise;
1627
- promise = this.runTask(task, { stats, root: ctx }, { acquire, release })
1628
- .then(res => ({ success: true, value: res }))
1629
- .catch(err => ({ success: false, value: err }))
1630
- .finally(() => ctx.queue.delete(promise));
1631
- ctx.queue.add(promise);
1868
+ return {
1869
+ success: false,
1870
+ data: null,
1871
+ error
1872
+ };
1632
1873
  }
1633
- addTimeline("QUEUE_REQUEST_STARTED", {
1634
- queue: ctx.queue
1635
- });
1636
- const startTime = performance.now();
1637
- const raw = await Promise.all(ctx.queue);
1638
- const endTime = performance.now();
1639
- addTimeline("QUEUE_REQUEST_ENDED", {
1640
- queue: ctx.queue,
1641
- took: endTime - startTime
1642
- });
1643
- ctx.result = stats.settings.onlySuccess
1644
- ? raw.filter(r => r.success).map(r => r.value)
1645
- : raw.map(r => r.value);
1646
- await handleInterceptor("result", (interceptorType, func) => ({
1647
- setResult: (unknown) => {
1648
- addTimeline("INTERCEPTOR_API_CALLED", {
1649
- interceptorType,
1650
- interceptor: func,
1651
- method: "setResult",
1652
- args: [unknown]
1653
- });
1654
- ctx.result = unknown;
1655
- return unknown;
1656
- },
1657
- throwError: (error) => {
1658
- addTimeline("INTERCEPTOR_API_CALLED", {
1659
- interceptorType,
1660
- interceptor: func,
1661
- method: "throwError",
1662
- args: [error]
1663
- });
1664
- throw error;
1665
- },
1666
- }));
1667
- return ctx.result;
1668
1874
  }
1669
1875
  }
1670
- const VigorEntry = {
1671
- retry: {
1672
- main: VigorRetry,
1673
- settings: VigorRetrySettings,
1674
- interceptors: VigorRetryInterceptors,
1675
- error: VigorRetryError,
1676
- algorithms: {
1677
- constant: VigorRetryAlgorithmsConstant,
1678
- linear: VigorRetryAlgorithmsLinear,
1679
- backoff: VigorRetryAlgorithmsBackoff,
1680
- custom: VigorRetryAlgorithmsCustom
1681
- }
1682
- },
1683
- parse: {
1684
- main: VigorParse,
1685
- settings: VigorParseSettings,
1686
- interceptors: VigorParseInterceptors,
1687
- error: VigorParseError,
1688
- strategies: VigorParseStrategies
1689
- },
1690
- fetch: {
1691
- main: VigorFetch,
1692
- settings: VigorFetchSettings,
1693
- interceptors: VigorFetchInterceptors,
1694
- error: VigorFetchError,
1695
- },
1696
- all: {
1697
- main: VigorAll,
1698
- settings: VigorAllSettings,
1699
- interceptors: VigorAllInterceptors,
1700
- error: VigorAllError
1701
- }
1702
- };
1876
+
1703
1877
  const vigor = {
1704
- use: async (func, config) => {
1705
- return await func(VigorEntry, config);
1706
- },
1707
- fetch: (str) => {
1708
- return new VigorFetch().origin(str);
1709
- },
1710
- retry: (target) => {
1711
- return new VigorRetry().target(target);
1878
+ use: (plugin, options) => {
1879
+ plugin.func(vigor, options);
1712
1880
  },
1713
- parse: (response) => {
1714
- return new VigorParse().target(response);
1715
- },
1716
- all: (...funcs) => {
1717
- return new VigorAll().target(...funcs);
1718
- },
1719
- builder: {
1720
- fetch: {
1721
- settings: (c) => new VigorFetchSettings(c),
1722
- interceptors: (c) => new VigorFetchInterceptors(c),
1723
- },
1724
- retry: {
1725
- settings: (c) => new VigorRetrySettings(c),
1726
- interceptors: (c) => new VigorRetryInterceptors(c),
1727
- },
1728
- parse: {
1729
- settings: (c) => new VigorParseSettings(c),
1730
- interceptors: (c) => new VigorParseInterceptors(c),
1731
- },
1732
- all: {
1733
- settings: (c) => new VigorAllSettings(c),
1734
- interceptors: (c) => new VigorAllInterceptors(c),
1735
- }
1736
- }
1881
+ retry: (task) => task ? new VigorRetry().target(task) : new VigorRetry(),
1882
+ parse: (response) => response ? new VigorParse().target(response) : new VigorParse(),
1883
+ fetch: (origin) => origin ? new VigorFetch().origin(origin) : new VigorFetch(),
1884
+ all: (...tasks) => tasks.length > 0 ? new VigorAll().target(...tasks) : new VigorAll()
1737
1885
  };
1738
1886
 
1739
- export { VigorAllError, VigorEntry, VigorFetchError, VigorParseError, VigorRetryError, vigor as default, vigor };
1887
+ export { VigorAll, VigorAllBase, VigorAllError, VigorAllErrorMessageFuncs, VigorAllPolicyBase, VigorAllPolicyMiddlewares, VigorAllPolicyMiddlewaresBase, VigorAllPolicySettings, VigorAllPolicySettingsBase, VigorError, VigorFetch, VigorFetchBase, VigorFetchError, VigorFetchErrorMessageFuncs, VigorFetchPolicyBase, VigorFetchPolicyMiddlewares, VigorFetchPolicyMiddlewaresBase, VigorFetchPolicySettings, VigorFetchPolicySettingsBase, VigorJitter, VigorParse, VigorParseBase, VigorParseContentTypeStrategy, VigorParseError, VigorParseErrorMessageFuncs, VigorParsePolicyBase, VigorParsePolicyMiddlewares, VigorParsePolicyMiddlewaresBase, VigorParsePolicySettings, VigorParsePolicySettingsBase, VigorParsePolicyStrategies, VigorParsePolicyStrategiesBase, VigorParseSniffStrategy, VigorRetry, VigorRetryBase, VigorRetryError, VigorRetryErrorMessageFuncs, VigorRetryPolicyAlgorithms, VigorRetryPolicyAlgorithmsBackoff, VigorRetryPolicyAlgorithmsBackoffBase, VigorRetryPolicyAlgorithmsBase, VigorRetryPolicyAlgorithmsConstant, VigorRetryPolicyAlgorithmsConstantBase, VigorRetryPolicyAlgorithmsCustom, VigorRetryPolicyAlgorithmsCustomBase, VigorRetryPolicyAlgorithmsLinear, VigorRetryPolicyAlgorithmsLinearBase, VigorRetryPolicyBase, VigorRetryPolicyMiddlewares, VigorRetryPolicyMiddlewaresBase, VigorRetryPolicySettings, VigorRetryPolicySettingsBase, VigorStatus, vigor as default, vigor };