@gcmdev/faxios 0.2.0 → 0.2.2

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 (186) hide show
  1. package/LICENSE +8 -0
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/adapters/fetch-helpers.d.ts +2 -1
  7. package/dist/lib/adapters/fetch-helpers.d.ts.map +1 -0
  8. package/dist/lib/adapters/fetch.d.ts +1 -0
  9. package/dist/lib/adapters/fetch.d.ts.map +1 -0
  10. package/dist/lib/adapters/fetch.js +86 -26
  11. package/dist/lib/adapters/fetch.js.map +1 -1
  12. package/dist/lib/cancel/CanceledError.d.ts +2 -1
  13. package/dist/lib/cancel/CanceledError.d.ts.map +1 -0
  14. package/dist/lib/cancel/isCancel.d.ts +1 -0
  15. package/dist/lib/cancel/isCancel.d.ts.map +1 -0
  16. package/dist/lib/core/Faxios.d.ts +4 -3
  17. package/dist/lib/core/Faxios.d.ts.map +1 -0
  18. package/dist/lib/core/Faxios.js +8 -4
  19. package/dist/lib/core/Faxios.js.map +1 -1
  20. package/dist/lib/core/FaxiosError.d.ts +3 -2
  21. package/dist/lib/core/FaxiosError.d.ts.map +1 -0
  22. package/dist/lib/core/FaxiosHeaders.d.ts +9 -8
  23. package/dist/lib/core/FaxiosHeaders.d.ts.map +1 -0
  24. package/dist/lib/core/buildFullPath.d.ts +1 -0
  25. package/dist/lib/core/buildFullPath.d.ts.map +1 -0
  26. package/dist/lib/core/define.d.ts +20 -3
  27. package/dist/lib/core/define.d.ts.map +1 -0
  28. package/dist/lib/core/define.js.map +1 -1
  29. package/dist/lib/core/dispatchRequest.d.ts +2 -1
  30. package/dist/lib/core/dispatchRequest.d.ts.map +1 -0
  31. package/dist/lib/core/mergeConfig.d.ts +3 -2
  32. package/dist/lib/core/mergeConfig.d.ts.map +1 -0
  33. package/dist/lib/core/mergeConfig.js +40 -2
  34. package/dist/lib/core/mergeConfig.js.map +1 -1
  35. package/dist/lib/core/prepareRequest.d.ts +1 -0
  36. package/dist/lib/core/prepareRequest.d.ts.map +1 -0
  37. package/dist/lib/core/route.d.ts +10 -1
  38. package/dist/lib/core/route.d.ts.map +1 -0
  39. package/dist/lib/core/route.js +2 -5
  40. package/dist/lib/core/route.js.map +1 -1
  41. package/dist/lib/core/settle.d.ts +1 -0
  42. package/dist/lib/core/settle.d.ts.map +1 -0
  43. package/dist/lib/core/transformData.d.ts +3 -2
  44. package/dist/lib/core/transformData.d.ts.map +1 -0
  45. package/dist/lib/core/validateSchema.d.ts +3 -2
  46. package/dist/lib/core/validateSchema.d.ts.map +1 -0
  47. package/dist/lib/defaults/index.d.ts +1 -0
  48. package/dist/lib/defaults/index.d.ts.map +1 -0
  49. package/dist/lib/defaults/transitional.d.ts +1 -0
  50. package/dist/lib/defaults/transitional.d.ts.map +1 -0
  51. package/dist/lib/env/data.d.ts +2 -1
  52. package/dist/lib/env/data.d.ts.map +1 -0
  53. package/dist/lib/env/data.js +1 -1
  54. package/dist/lib/faxios.d.ts +59 -38
  55. package/dist/lib/faxios.d.ts.map +1 -0
  56. package/dist/lib/faxios.js.map +1 -1
  57. package/dist/lib/helpers/FaxiosURLSearchParams.d.ts +1 -0
  58. package/dist/lib/helpers/FaxiosURLSearchParams.d.ts.map +1 -0
  59. package/dist/lib/helpers/HttpStatusCode.d.ts +1 -0
  60. package/dist/lib/helpers/HttpStatusCode.d.ts.map +1 -0
  61. package/dist/lib/helpers/buildURL.d.ts +1 -0
  62. package/dist/lib/helpers/buildURL.d.ts.map +1 -0
  63. package/dist/lib/helpers/cloneConfig.d.ts +1 -1
  64. package/dist/lib/helpers/cloneConfig.d.ts.map +1 -0
  65. package/dist/lib/helpers/cloneConfig.js +2 -2
  66. package/dist/lib/helpers/cloneConfig.js.map +1 -1
  67. package/dist/lib/helpers/combineURLs.d.ts +1 -0
  68. package/dist/lib/helpers/combineURLs.d.ts.map +1 -0
  69. package/dist/lib/helpers/composeSignals.d.ts +1 -0
  70. package/dist/lib/helpers/composeSignals.d.ts.map +1 -0
  71. package/dist/lib/helpers/cookies.d.ts +2 -1
  72. package/dist/lib/helpers/cookies.d.ts.map +1 -0
  73. package/dist/lib/helpers/estimateDataURLDecodedBytes.d.ts +1 -0
  74. package/dist/lib/helpers/estimateDataURLDecodedBytes.d.ts.map +1 -0
  75. package/dist/lib/helpers/formDataToJSON.d.ts +2 -1
  76. package/dist/lib/helpers/formDataToJSON.d.ts.map +1 -0
  77. package/dist/lib/helpers/formDataToJSON.js +1 -1
  78. package/dist/lib/helpers/formDataToJSON.js.map +1 -1
  79. package/dist/lib/helpers/isAbsoluteURL.d.ts +1 -0
  80. package/dist/lib/helpers/isAbsoluteURL.d.ts.map +1 -0
  81. package/dist/lib/helpers/isFaxiosError.d.ts +1 -0
  82. package/dist/lib/helpers/isFaxiosError.d.ts.map +1 -0
  83. package/dist/lib/helpers/isURLSameOrigin.d.ts +1 -0
  84. package/dist/lib/helpers/isURLSameOrigin.d.ts.map +1 -0
  85. package/dist/lib/helpers/parseHeaders.d.ts +2 -1
  86. package/dist/lib/helpers/parseHeaders.d.ts.map +1 -0
  87. package/dist/lib/helpers/progressEventReducer.d.ts +1 -0
  88. package/dist/lib/helpers/progressEventReducer.d.ts.map +1 -0
  89. package/dist/lib/helpers/sanitizeHeaderValue.d.ts +1 -0
  90. package/dist/lib/helpers/sanitizeHeaderValue.d.ts.map +1 -0
  91. package/dist/lib/helpers/speedometer.d.ts +1 -0
  92. package/dist/lib/helpers/speedometer.d.ts.map +1 -0
  93. package/dist/lib/helpers/substitutePathParams.d.ts +1 -0
  94. package/dist/lib/helpers/substitutePathParams.d.ts.map +1 -0
  95. package/dist/lib/helpers/substitutePathParams.js +11 -3
  96. package/dist/lib/helpers/substitutePathParams.js.map +1 -1
  97. package/dist/lib/helpers/throttle.d.ts +1 -0
  98. package/dist/lib/helpers/throttle.d.ts.map +1 -0
  99. package/dist/lib/helpers/toFormData.d.ts +1 -0
  100. package/dist/lib/helpers/toFormData.d.ts.map +1 -0
  101. package/dist/lib/helpers/toFormData.js +4 -2
  102. package/dist/lib/helpers/toFormData.js.map +1 -1
  103. package/dist/lib/helpers/toURLEncodedForm.d.ts +1 -0
  104. package/dist/lib/helpers/toURLEncodedForm.d.ts.map +1 -0
  105. package/dist/lib/helpers/trackStream.d.ts +1 -0
  106. package/dist/lib/helpers/trackStream.d.ts.map +1 -0
  107. package/dist/lib/helpers/validator.d.ts +2 -1
  108. package/dist/lib/helpers/validator.d.ts.map +1 -0
  109. package/dist/lib/platform.d.ts +1 -0
  110. package/dist/lib/platform.d.ts.map +1 -0
  111. package/dist/lib/plugins/attempt.d.ts +1 -0
  112. package/dist/lib/plugins/attempt.d.ts.map +1 -0
  113. package/dist/lib/plugins/authBearer.d.ts +10 -5
  114. package/dist/lib/plugins/authBearer.d.ts.map +1 -0
  115. package/dist/lib/plugins/authBearer.js +6 -0
  116. package/dist/lib/plugins/authBearer.js.map +1 -1
  117. package/dist/lib/plugins/definePlugin.d.ts +4 -3
  118. package/dist/lib/plugins/definePlugin.d.ts.map +1 -0
  119. package/dist/lib/plugins/retry.d.ts +24 -23
  120. package/dist/lib/plugins/retry.d.ts.map +1 -0
  121. package/dist/lib/plugins/timing.d.ts +9 -8
  122. package/dist/lib/plugins/timing.d.ts.map +1 -0
  123. package/dist/lib/types/standard-schema.d.ts +7 -6
  124. package/dist/lib/types/standard-schema.d.ts.map +1 -0
  125. package/dist/lib/types.d.ts +104 -92
  126. package/dist/lib/types.d.ts.map +1 -0
  127. package/dist/lib/utils.d.ts +5 -3
  128. package/dist/lib/utils.d.ts.map +1 -0
  129. package/dist/lib/utils.js +9 -2
  130. package/dist/lib/utils.js.map +1 -1
  131. package/package.json +8 -4
  132. package/skills/configure-requests/SKILL.md +53 -8
  133. package/skills/configure-requests/references/plugins.md +54 -6
  134. package/src/index.ts +57 -0
  135. package/src/lib/adapters/fetch-helpers.ts +167 -0
  136. package/src/lib/adapters/fetch.ts +871 -0
  137. package/src/lib/cancel/CanceledError.ts +23 -0
  138. package/src/lib/cancel/isCancel.ts +10 -0
  139. package/src/lib/core/Faxios.ts +517 -0
  140. package/src/lib/core/FaxiosError.ts +283 -0
  141. package/src/lib/core/FaxiosHeaders.ts +529 -0
  142. package/src/lib/core/buildFullPath.ts +51 -0
  143. package/src/lib/core/define.ts +127 -0
  144. package/src/lib/core/dispatchRequest.ts +171 -0
  145. package/src/lib/core/mergeConfig.ts +213 -0
  146. package/src/lib/core/prepareRequest.ts +155 -0
  147. package/src/lib/core/route.ts +65 -0
  148. package/src/lib/core/settle.ts +20 -0
  149. package/src/lib/core/transformData.ts +23 -0
  150. package/src/lib/core/validateSchema.ts +53 -0
  151. package/src/lib/defaults/index.ts +221 -0
  152. package/src/lib/defaults/transitional.ts +9 -0
  153. package/src/lib/env/data.ts +1 -0
  154. package/src/lib/faxios.ts +306 -0
  155. package/src/lib/helpers/FaxiosURLSearchParams.ts +52 -0
  156. package/src/lib/helpers/HttpStatusCode.ts +77 -0
  157. package/src/lib/helpers/buildURL.ts +77 -0
  158. package/src/lib/helpers/cloneConfig.ts +22 -0
  159. package/src/lib/helpers/combineURLs.ts +11 -0
  160. package/src/lib/helpers/composeSignals.ts +74 -0
  161. package/src/lib/helpers/cookies.ts +68 -0
  162. package/src/lib/helpers/deprecatedMethod.ts +32 -0
  163. package/src/lib/helpers/estimateDataURLDecodedBytes.ts +95 -0
  164. package/src/lib/helpers/formDataToJSON.ts +122 -0
  165. package/src/lib/helpers/isAbsoluteURL.ts +13 -0
  166. package/src/lib/helpers/isFaxiosError.ts +19 -0
  167. package/src/lib/helpers/isURLSameOrigin.ts +16 -0
  168. package/src/lib/helpers/parseHeaders.ts +76 -0
  169. package/src/lib/helpers/progressEventReducer.ts +57 -0
  170. package/src/lib/helpers/sanitizeHeaderValue.ts +61 -0
  171. package/src/lib/helpers/speedometer.ts +55 -0
  172. package/src/lib/helpers/substitutePathParams.ts +23 -0
  173. package/src/lib/helpers/throttle.ts +45 -0
  174. package/src/lib/helpers/toFormData.ts +298 -0
  175. package/src/lib/helpers/toURLEncodedForm.ts +14 -0
  176. package/src/lib/helpers/trackStream.ts +123 -0
  177. package/src/lib/helpers/validator.ts +114 -0
  178. package/src/lib/platform.ts +44 -0
  179. package/src/lib/plugins/attempt.ts +3 -0
  180. package/src/lib/plugins/authBearer.ts +129 -0
  181. package/src/lib/plugins/definePlugin.ts +34 -0
  182. package/src/lib/plugins/retry.ts +245 -0
  183. package/src/lib/plugins/timing.ts +76 -0
  184. package/src/lib/types/standard-schema.ts +58 -0
  185. package/src/lib/types.ts +496 -0
  186. package/src/lib/utils.ts +941 -0
@@ -0,0 +1,245 @@
1
+ // @ts-self-types="./retry.d.ts" — required for Deno: maps built .js to adjacent .d.ts in dist/
2
+
3
+ import CanceledError from "../cancel/CanceledError.js";
4
+ import isCancel from "../cancel/isCancel.js";
5
+ import FaxiosError from "../core/FaxiosError.js";
6
+ import validator from "../helpers/validator.js";
7
+ import type { FaxiosContext, FaxiosPlugin, FaxiosRequestConfig, GenericAbortSignal, InternalFaxiosRequestConfig, Method } from "../types.js";
8
+ import utils from "../utils.js";
9
+ import { RETRY_ATTEMPT } from "./attempt.js";
10
+ import { definePlugin } from "./definePlugin.js";
11
+
12
+ // Config values are read as own properties only (repo rule for possibly untrusted input).
13
+ const ownValue = <T extends object, K extends keyof T>(source: T, key: K): T[K] | undefined =>
14
+ (utils.hasOwnProp(source, key) ? source[key] : undefined);
15
+
16
+ const isWait = (ms: unknown): ms is number => typeof ms === "number" && Number.isFinite(ms) && ms >= 0;
17
+
18
+ // An HTTP method is a token (RFC 9110 §9.1, §5.6.2); anything else can never match a request.
19
+ const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
20
+
21
+ const isStatus = (value: unknown) => Number.isInteger(value) && (value as number) >= 100 && (value as number) <= 599;
22
+
23
+ // Bounded on purpose: NaN or Infinity would make a failing request loop forever.
24
+ const optionsSchema = {
25
+ attempts: (value: unknown) => (Number.isInteger(value) && (value as number) >= 1) || "an integer of at least 1",
26
+ retryOn: validator.validators["function"],
27
+ delay: (value: unknown) => isWait(value) || typeof value === "function" || "a finite number of at least 0 or a function",
28
+ methods: (value: unknown) => (Array.isArray(value) && value.every(m => typeof m === "string" && TOKEN.test(m))) || "an array of HTTP method names",
29
+ statuses: (value: unknown) => (Array.isArray(value) && value.every(isStatus)) || "an array of HTTP status codes (100-599)",
30
+ maxRetryAfter: (value: unknown) => isWait(value) || "a finite number of milliseconds of at least 0",
31
+ maxDelay: (value: unknown) => isWait(value) || "a finite number of milliseconds of at least 0",
32
+ jitter: (value: unknown) => value === "full" || value === "none" || "\"full\" or \"none\"",
33
+ respectRetryAfter: validator.validators["boolean"],
34
+ onRetry: validator.validators["function"],
35
+ };
36
+
37
+ // Transient failures: timeout, rate limit, and server errors that may clear on their own. 501 and
38
+ // 505 mean the server will never handle the request, so they aren't here.
39
+ const DEFAULT_STATUSES: ReadonlyArray<number> = [ 408, 429, 500, 502, 503, 504 ];
40
+
41
+ // Statuses whose Retry-After says when to try again (RFC 9110 §10.2.3).
42
+ const RETRY_AFTER_STATUSES: ReadonlyArray<number> = [ 429, 503 ];
43
+
44
+ // Methods whose repeat has the same effect as one request (RFC 9110 §9.2.2, plus QUERY).
45
+ const IDEMPOTENT_METHODS: ReadonlyArray<string> = [ "get", "head", "options", "put", "delete", "query" ];
46
+
47
+ // One check for both the factory argument and a per-request override.
48
+ function assertRetryOptions(value: unknown, config?: InternalFaxiosRequestConfig): asserts value is RetryOptions {
49
+ if (value === null || typeof value !== "object") {
50
+ throw new FaxiosError("retry options must be an object", FaxiosError.ERR_BAD_OPTION_VALUE, config);
51
+ }
52
+ validator.assertOptions(value, optionsSchema, false);
53
+ }
54
+
55
+ export type RetryOptions = {
56
+ /** Total tries, counting the first request. Default 3. */
57
+ attempts?: number;
58
+ /**
59
+ * Milliseconds to wait before the next try, or a function of the try that failed. Default 100ms,
60
+ * doubling. Capped at `maxDelay`, then jittered. A 429 or 503 with a `Retry-After` header waits
61
+ * that long instead, unless `respectRetryAfter` is `false`.
62
+ */
63
+ delay?: ((attempt: number, error: unknown) => number) | number;
64
+ /**
65
+ * `"full"` (default) waits a random time between 0 and the capped backoff, so clients that failed
66
+ * together don't retry together; `"none"` waits the capped backoff. Doesn't apply to `Retry-After`.
67
+ */
68
+ jitter?: "full" | "none";
69
+ /** The most the computed backoff waits, in milliseconds. Default 30 000. Doesn't apply to `Retry-After`. */
70
+ maxDelay?: number;
71
+ /**
72
+ * The longest `Retry-After` (seconds or an HTTP date, on a 429 or 503) to wait, in milliseconds.
73
+ * Default 300 000 (5 minutes). A longer one rejects with the response's error instead of retrying.
74
+ * Ignored when `respectRetryAfter` is `false`.
75
+ */
76
+ maxRetryAfter?: number;
77
+ /**
78
+ * HTTP methods that may be retried, in any case. Default: GET, HEAD, OPTIONS, PUT, DELETE and
79
+ * QUERY. POST and PATCH aren't idempotent, so add them only if your API dedupes repeats. A request
80
+ * whose method isn't listed is never retried, whatever `retryOn` or `statuses` say.
81
+ */
82
+ methods?: ReadonlyArray<Method>;
83
+ /**
84
+ * Called before each wait with the error, the try that failed and the wait in milliseconds. If it
85
+ * throws, the request rejects with that error and stops retrying.
86
+ */
87
+ onRetry?: (error: unknown, attempt: number, delayMs: number) => void;
88
+ /**
89
+ * Whether a 429 or 503's `Retry-After` header sets the wait. Default `true`. With `false`, every
90
+ * retry waits the computed backoff and `maxRetryAfter` doesn't apply; which requests retry doesn't
91
+ * change. Turn it off when another plugin, such as a queue throttle, honours `Retry-After` itself.
92
+ */
93
+ respectRetryAfter?: boolean;
94
+ /**
95
+ * Whether the try that just failed should be retried. `attempt` counts from 1.
96
+ * Default: network errors, timeouts and the `statuses`; never a cancellation. A custom `retryOn`
97
+ * replaces that whole predicate, so `statuses` is ignored when both are set. retryOn decides
98
+ * only within `methods`; to retry POST, list it in `methods`.
99
+ */
100
+ retryOn?: (error: unknown, attempt: number) => boolean;
101
+ /**
102
+ * Response statuses the default predicate retries. Default 408, 429, 500, 502, 503 and 504.
103
+ * Ignored when `retryOn` is set.
104
+ */
105
+ statuses?: ReadonlyArray<number>;
106
+ };
107
+
108
+ /** Per-request override: `false` turns retries off, an object overrides the plugin's options. */
109
+ export type RetryRequestOptions = { retry?: false | RetryOptions; };
110
+
111
+ function retryByDefault(error: unknown, statuses: ReadonlyArray<number>): boolean {
112
+ if (isCancel(error) || !(error instanceof FaxiosError)) return false;
113
+ if (error.code === FaxiosError.ERR_NETWORK || error.code === FaxiosError.ETIMEDOUT) return true;
114
+ const status = error.response?.status;
115
+ return status !== undefined && statuses.includes(status);
116
+ }
117
+
118
+ // The wait a 429 or 503 asks for, in milliseconds: delay-seconds or an HTTP date (a past date
119
+ // means now). undefined when there's no usable header, so the computed backoff applies.
120
+ function retryAfterMs(error: unknown): number | undefined {
121
+ if (!(error instanceof FaxiosError) || !error.response || !RETRY_AFTER_STATUSES.includes(error.response.status)) return undefined;
122
+ const value = error.response.headers.get("retry-after");
123
+ if (typeof value !== "string") return undefined;
124
+ const trimmed = value.trim();
125
+ if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1000;
126
+ const date = Date.parse(trimmed);
127
+ return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
128
+ }
129
+
130
+ const doublingDelay = (attempt: number) => 100 * 2 ** (attempt - 1);
131
+
132
+ // A stream is consumed by the first try, so sending it again would send nothing.
133
+ // The same stream checks the fetch adapter uses: web ReadableStreams and Node-style pipe streams.
134
+ function canReplay(data: unknown): boolean {
135
+ return !utils.isReadableStream?.(data) && !utils.isStream(data);
136
+ }
137
+
138
+ function wait(ms: number, signal: AbortSignal | GenericAbortSignal | undefined, config: FaxiosRequestConfig): Promise<void> {
139
+ return new Promise((resolve, reject) => {
140
+ if (signal?.aborted) {
141
+ reject(new CanceledError(null, config));
142
+ return;
143
+ }
144
+ const onAbort = () => {
145
+ clearTimeout(timer);
146
+ signal?.removeEventListener?.("abort", onAbort);
147
+ reject(new CanceledError(null, config));
148
+ };
149
+ const timer = setTimeout(() => {
150
+ signal?.removeEventListener?.("abort", onAbort);
151
+ resolve();
152
+ }, ms);
153
+ signal?.addEventListener?.("abort", onAbort);
154
+ });
155
+ }
156
+
157
+ // Each list is lower-cased right after it is validated: the plugin's once, a per-request override's
158
+ // on the request that carries it.
159
+ function retriesMethod(config: InternalFaxiosRequestConfig, perRequest: RetryOptions | undefined, pluginMethods: ReadonlyArray<string> | undefined): boolean {
160
+ const requestMethods = perRequest ? ownValue(perRequest, "methods")?.map(m => m.toLowerCase()) : undefined;
161
+ const methods = requestMethods ?? pluginMethods ?? IDEMPOTENT_METHODS;
162
+ return methods.includes(String(ownValue(config, "method") ?? "get").toLowerCase());
163
+ }
164
+
165
+ type Policy = {
166
+ attempts: number;
167
+ delay: ((attempt: number, error: unknown) => number) | number;
168
+ jitter: "full" | "none";
169
+ maxDelay: number;
170
+ maxRetryAfter: number;
171
+ onRetry: ((error: unknown, attempt: number, delayMs: number) => void) | undefined;
172
+ respectRetryAfter: boolean;
173
+ retryOn: (error: unknown, attempt: number) => boolean;
174
+ };
175
+
176
+ // Per field, request first: no merged object, and only own properties count.
177
+ function resolvePolicy(options: RetryOptions, perRequest: RetryOptions | undefined): Policy {
178
+ const setting = <K extends keyof RetryOptions>(key: K) => (perRequest ? ownValue(perRequest, key) : undefined) ?? ownValue(options, key);
179
+ const statuses = setting("statuses") ?? DEFAULT_STATUSES;
180
+ return {
181
+ attempts: setting("attempts") ?? 3,
182
+ retryOn: setting("retryOn") ?? ((error: unknown) => retryByDefault(error, statuses)),
183
+ delay: setting("delay") ?? doublingDelay,
184
+ maxDelay: setting("maxDelay") ?? 30_000,
185
+ jitter: setting("jitter") ?? "full",
186
+ maxRetryAfter: setting("maxRetryAfter") ?? 300_000,
187
+ respectRetryAfter: setting("respectRetryAfter") ?? true,
188
+ onRetry: setting("onRetry"),
189
+ };
190
+ }
191
+
192
+ // How long to wait before the next try, or undefined when the server's Retry-After is longer
193
+ // than the caller is willing to wait, so the error is rethrown.
194
+ function waitBeforeRetry(error: unknown, attempt: number, policy: Policy, config: InternalFaxiosRequestConfig): number | undefined {
195
+ const retryAfter = policy.respectRetryAfter ? retryAfterMs(error) : undefined;
196
+ if (retryAfter !== undefined) return retryAfter > policy.maxRetryAfter ? undefined : retryAfter;
197
+ const computed: unknown = typeof policy.delay === "function" ? policy.delay(attempt, error) : policy.delay;
198
+ if (!isWait(computed)) {
199
+ throw new FaxiosError(`retry: delay must give a finite number of at least 0, got ${String(computed)}`, FaxiosError.ERR_BAD_OPTION_VALUE, config);
200
+ }
201
+ const capped = Math.min(computed, policy.maxDelay);
202
+ // Jitter only spreads out clients that failed together; it needs no unpredictability.
203
+ // eslint-disable-next-line sonarjs/pseudo-random -- see above
204
+ return policy.jitter === "full" ? Math.floor(Math.random() * capped) : capped;
205
+ }
206
+
207
+ /**
208
+ * Retries failed requests by calling `next` again. Each `next` dispatches its own copy of the
209
+ * config, so every try starts from the same input. Install it inside middleware that should run
210
+ * once per request (for example `timing` installed first measures all tries together).
211
+ */
212
+ export function retry(options: RetryOptions = {}): FaxiosPlugin<{ options: RetryRequestOptions; }> {
213
+ assertRetryOptions(options);
214
+ // Lower-cased once here, so requests compare names without re-casing the list.
215
+ const pluginMethods = ownValue(options, "methods")?.map(m => m.toLowerCase());
216
+ return definePlugin({
217
+ name: "retry",
218
+ middleware: async (ctx: FaxiosContext<RetryRequestOptions>, next) => {
219
+ const perRequest = ownValue(ctx.config, "retry");
220
+ if (perRequest === false) return next(ctx);
221
+ if (perRequest !== undefined) assertRetryOptions(perRequest, ctx.config);
222
+ const policy = resolvePolicy(options, perRequest);
223
+ if (!retriesMethod(ctx.config, perRequest, pluginMethods)) return next(ctx);
224
+
225
+ // Tries run one after another on purpose: each must finish before deciding on the next.
226
+ for (let attempt = 1; ; attempt++) {
227
+ // For timing: the try number, read after next() settles.
228
+ ctx.state[RETRY_ATTEMPT] = attempt;
229
+ try {
230
+ // eslint-disable-next-line no-await-in-loop -- see above
231
+ return await next(ctx);
232
+ }
233
+ catch (error) {
234
+ if (attempt >= policy.attempts || !canReplay(ownValue(ctx.config, "data")) || !policy.retryOn(error, attempt)) throw error;
235
+ const ms = waitBeforeRetry(error, attempt, policy, ctx.config);
236
+ if (ms === undefined) throw error;
237
+ // A throwing hook rejects the request with its own error, before any wait.
238
+ policy.onRetry?.(error, attempt, ms);
239
+ // eslint-disable-next-line no-await-in-loop -- see above
240
+ await wait(ms, ownValue(ctx.config, "signal"), ctx.config);
241
+ }
242
+ }
243
+ },
244
+ });
245
+ }
@@ -0,0 +1,76 @@
1
+ // @ts-self-types="./timing.d.ts" — required for Deno: maps built .js to adjacent .d.ts in dist/
2
+
3
+ import buildFullPath from "../core/buildFullPath.js";
4
+ import type { FaxiosContext, FaxiosPlugin } from "../types.js";
5
+ import utils from "../utils.js";
6
+ import { RETRY_ATTEMPT } from "./attempt.js";
7
+ import { definePlugin } from "./definePlugin.js";
8
+
9
+ /**
10
+ * What `timing` reports once per request: `status` on success, `error` on failure. No config,
11
+ * headers or body, so an event is safe to log.
12
+ */
13
+ export type TimingEvent = {
14
+ /**
15
+ * The try number from `retry`: each try's when installed after it, the final try's when
16
+ * installed before it. Absent without `retry`.
17
+ */
18
+ attempt?: number;
19
+ durationMs: number;
20
+ /** The request method, upper-cased (`"GET"`). */
21
+ method: string;
22
+ /** The full request URL without its query string and fragment. */
23
+ url: string;
24
+ } & ({ error: unknown; status?: undefined; } | { error?: undefined; status: number; });
25
+
26
+ const now = () => (typeof performance === "undefined" ? Date.now() : performance.now());
27
+
28
+ const own = <T extends object, K extends keyof T>(source: T, key: K): T[K] | undefined =>
29
+ (utils.hasOwnProp(source, key) ? source[key] : undefined);
30
+
31
+ // Read once the request settles, so a later middleware's changes to the config count. The query
32
+ // and fragment often carry secrets, and they'd make every URL a different metric label.
33
+ function describe({ config, state }: FaxiosContext) {
34
+ // Typed string, but undefined when the request has neither url nor baseURL.
35
+ const full: unknown = buildFullPath(own(config, "baseURL"), own(config, "url"), own(config, "allowAbsoluteUrls"));
36
+ const url = typeof full === "string" ? full : "";
37
+ const end = url.search(/[?#]/);
38
+ const attempt = state[RETRY_ATTEMPT];
39
+ return {
40
+ method: String(own(config, "method") ?? "get").toUpperCase(),
41
+ url: end === -1 ? url : url.slice(0, end),
42
+ ...(typeof attempt === "number" ? { attempt } : {}),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Measures the time around `next` and calls `onTiming` once per request with the method, URL,
48
+ * duration and status or error. Installed before `retry`, the duration covers every try;
49
+ * installed after it, each try is reported. The response is returned unchanged. If `onTiming`
50
+ * throws while reporting a failure, the request still rejects with its own error; while
51
+ * reporting a success, its error rejects the request.
52
+ */
53
+ export function timing(onTiming: (event: TimingEvent) => void): FaxiosPlugin {
54
+ return definePlugin({
55
+ name: "timing",
56
+ middleware: async (ctx, next) => {
57
+ const start = now();
58
+ let response;
59
+ try {
60
+ response = await next(ctx);
61
+ }
62
+ catch (error) {
63
+ try {
64
+ onTiming({ ...describe(ctx), durationMs: now() - start, error });
65
+ }
66
+ catch {
67
+ // The request's own error matters more than the observer's; keep it.
68
+ }
69
+ throw error;
70
+ }
71
+ // Outside the try, so an onTiming that throws isn't reported as a failed request.
72
+ onTiming({ ...describe(ctx), durationMs: now() - start, status: response.status });
73
+ return response;
74
+ },
75
+ });
76
+ }
@@ -0,0 +1,58 @@
1
+ // @ts-self-types="./standard-schema.d.ts"
2
+ /**
3
+ * Vendored Standard Schema v1 types.
4
+ * @see https://standardschema.dev
5
+ * @see https://github.com/standard-schema/standard-schema
6
+ */
7
+
8
+ /* eslint-disable sonarjs/no-redundant-optional */
9
+
10
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
11
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
12
+ }
13
+
14
+ export declare namespace StandardSchemaV1 {
15
+ interface Props<Input = unknown, Output = Input> {
16
+ readonly types?: Types<Input, Output>;
17
+ readonly validate: (
18
+ value: unknown
19
+ ) => Promise<Result<Output>> | Result<Output>;
20
+ readonly vendor: string;
21
+ readonly version: 1;
22
+ }
23
+
24
+ interface Types<Input = unknown, Output = Input> {
25
+ readonly input: Input;
26
+ readonly output: Output;
27
+ }
28
+
29
+ type Result<Output> = FailureResult | SuccessResult<Output>;
30
+
31
+ interface SuccessResult<Output> {
32
+ readonly issues?: undefined;
33
+ readonly value: Output;
34
+ }
35
+
36
+ interface FailureResult {
37
+ readonly issues: ReadonlyArray<Issue>;
38
+ }
39
+
40
+ interface Issue {
41
+ readonly message: string;
42
+ readonly path?:
43
+ | ReadonlyArray<PathSegment | PropertyKey>
44
+ | undefined;
45
+ }
46
+
47
+ interface PathSegment {
48
+ readonly key: PropertyKey;
49
+ }
50
+
51
+ type InferInput<Schema extends StandardSchemaV1> = NonNullable<
52
+ Schema["~standard"]["types"]
53
+ >["input"];
54
+
55
+ type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
56
+ Schema["~standard"]["types"]
57
+ >["output"];
58
+ }