@foundatiofx/fetchclient 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/mod.js +83 -0
- package/esm/src/DefaultHelpers.js +5 -5
- package/esm/src/FetchClient.js +66 -41
- package/esm/src/FetchClientError.js +73 -0
- package/esm/src/FetchClientProvider.js +24 -0
- package/esm/src/RateLimitMiddleware.js +35 -1
- package/esm/src/ResponsePromise.js +163 -0
- package/esm/src/RetryMiddleware.js +179 -0
- package/esm/src/mocks/MockHistory.js +8 -0
- package/esm/src/mocks/MockRegistry.js +7 -0
- package/package.json +1 -1
- package/script/mod.js +94 -1
- package/script/src/DefaultHelpers.js +5 -5
- package/script/src/FetchClient.js +66 -41
- package/script/src/FetchClientError.js +77 -0
- package/script/src/FetchClientProvider.js +24 -0
- package/script/src/RateLimitMiddleware.js +36 -0
- package/script/src/ResponsePromise.js +167 -0
- package/script/src/RetryMiddleware.js +184 -0
- package/script/src/mocks/MockHistory.js +8 -0
- package/script/src/mocks/MockRegistry.js +7 -0
- package/types/mod.d.ts +96 -0
- package/types/mod.d.ts.map +1 -1
- package/types/src/DefaultHelpers.d.ts +5 -5
- package/types/src/FetchClient.d.ts +31 -15
- package/types/src/FetchClient.d.ts.map +1 -1
- package/types/src/FetchClientError.d.ts +29 -0
- package/types/src/FetchClientError.d.ts.map +1 -0
- package/types/src/FetchClientProvider.d.ts +11 -0
- package/types/src/FetchClientProvider.d.ts.map +1 -1
- package/types/src/RateLimitMiddleware.d.ts +27 -0
- package/types/src/RateLimitMiddleware.d.ts.map +1 -1
- package/types/src/ResponsePromise.d.ts +93 -0
- package/types/src/ResponsePromise.d.ts.map +1 -0
- package/types/src/RetryMiddleware.d.ts +88 -0
- package/types/src/RetryMiddleware.d.ts.map +1 -0
- package/types/src/mocks/MockHistory.d.ts +1 -0
- package/types/src/mocks/MockHistory.d.ts.map +1 -1
- package/types/src/mocks/MockRegistry.d.ts +5 -0
- package/types/src/mocks/MockRegistry.d.ts.map +1 -1
- package/types/src/mocks/types.d.ts +2 -0
- package/types/src/mocks/types.d.ts.map +1 -1
- package/types/src/tests/RetryMiddleware.test.d.ts.map +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FetchClientError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error wrapper for non-2xx responses.
|
|
6
|
+
* Exposes the underlying response for compatibility and debugging.
|
|
7
|
+
*/
|
|
8
|
+
class FetchClientError extends Error {
|
|
9
|
+
response;
|
|
10
|
+
constructor(response, message) {
|
|
11
|
+
super(message ??
|
|
12
|
+
response.problem?.title ??
|
|
13
|
+
`Unexpected status code: ${response.status}`);
|
|
14
|
+
this.name = "FetchClientError";
|
|
15
|
+
this.response = response;
|
|
16
|
+
}
|
|
17
|
+
get status() {
|
|
18
|
+
return this.response.status;
|
|
19
|
+
}
|
|
20
|
+
get statusText() {
|
|
21
|
+
return this.response.statusText;
|
|
22
|
+
}
|
|
23
|
+
get ok() {
|
|
24
|
+
return this.response.ok;
|
|
25
|
+
}
|
|
26
|
+
get headers() {
|
|
27
|
+
return this.response.headers;
|
|
28
|
+
}
|
|
29
|
+
get url() {
|
|
30
|
+
return this.response.url;
|
|
31
|
+
}
|
|
32
|
+
get redirected() {
|
|
33
|
+
return this.response.redirected;
|
|
34
|
+
}
|
|
35
|
+
get type() {
|
|
36
|
+
return this.response.type;
|
|
37
|
+
}
|
|
38
|
+
get body() {
|
|
39
|
+
return this.response.body;
|
|
40
|
+
}
|
|
41
|
+
get bodyUsed() {
|
|
42
|
+
return this.response.bodyUsed;
|
|
43
|
+
}
|
|
44
|
+
get data() {
|
|
45
|
+
return this.response.data;
|
|
46
|
+
}
|
|
47
|
+
get problem() {
|
|
48
|
+
return this.response.problem;
|
|
49
|
+
}
|
|
50
|
+
get meta() {
|
|
51
|
+
return this.response.meta;
|
|
52
|
+
}
|
|
53
|
+
json() {
|
|
54
|
+
return this.response.json();
|
|
55
|
+
}
|
|
56
|
+
text() {
|
|
57
|
+
return this.response.text();
|
|
58
|
+
}
|
|
59
|
+
arrayBuffer() {
|
|
60
|
+
return this.response.arrayBuffer();
|
|
61
|
+
}
|
|
62
|
+
blob() {
|
|
63
|
+
return this.response.blob();
|
|
64
|
+
}
|
|
65
|
+
formData() {
|
|
66
|
+
return this.response.formData();
|
|
67
|
+
}
|
|
68
|
+
// @ts-ignore: New in Deno 1.44
|
|
69
|
+
bytes() {
|
|
70
|
+
// @ts-ignore: New in Deno 1.44
|
|
71
|
+
return this.response.bytes();
|
|
72
|
+
}
|
|
73
|
+
clone() {
|
|
74
|
+
return this.response.clone();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.FetchClientError = FetchClientError;
|
|
@@ -9,6 +9,7 @@ const RateLimitMiddleware_js_1 = require("./RateLimitMiddleware.js");
|
|
|
9
9
|
const RateLimiter_js_1 = require("./RateLimiter.js");
|
|
10
10
|
const CircuitBreakerMiddleware_js_1 = require("./CircuitBreakerMiddleware.js");
|
|
11
11
|
const CircuitBreaker_js_1 = require("./CircuitBreaker.js");
|
|
12
|
+
const RetryMiddleware_js_1 = require("./RetryMiddleware.js");
|
|
12
13
|
/**
|
|
13
14
|
* Represents a provider for creating instances of the FetchClient class with shared default options and cache.
|
|
14
15
|
*/
|
|
@@ -20,6 +21,8 @@ class FetchClientProvider {
|
|
|
20
21
|
#rateLimitMiddlewareFunc;
|
|
21
22
|
#circuitBreakerMiddleware;
|
|
22
23
|
#circuitBreakerMiddlewareFunc;
|
|
24
|
+
#retryMiddleware;
|
|
25
|
+
#retryMiddlewareFunc;
|
|
23
26
|
#counter = new Counter_js_1.Counter();
|
|
24
27
|
#onLoading = new ObjectEvent_js_1.ObjectEvent();
|
|
25
28
|
/**
|
|
@@ -253,6 +256,27 @@ class FetchClientProvider {
|
|
|
253
256
|
this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
|
|
254
257
|
}
|
|
255
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Enables automatic retry for failed requests.
|
|
261
|
+
* Retries are performed with exponential backoff and jitter.
|
|
262
|
+
* @param options - The retry configuration options.
|
|
263
|
+
*/
|
|
264
|
+
useRetry(options) {
|
|
265
|
+
this.#retryMiddleware = new RetryMiddleware_js_1.RetryMiddleware(options);
|
|
266
|
+
this.#retryMiddlewareFunc = this.#retryMiddleware.middleware();
|
|
267
|
+
this.useMiddleware(this.#retryMiddlewareFunc);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Removes the retry middleware from all FetchClient instances created by this provider.
|
|
271
|
+
*/
|
|
272
|
+
removeRetry() {
|
|
273
|
+
const middlewareFunc = this.#retryMiddlewareFunc;
|
|
274
|
+
this.#retryMiddleware = undefined;
|
|
275
|
+
this.#retryMiddlewareFunc = undefined;
|
|
276
|
+
if (middlewareFunc) {
|
|
277
|
+
this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
256
280
|
}
|
|
257
281
|
exports.FetchClientProvider = FetchClientProvider;
|
|
258
282
|
const provider = new FetchClientProvider();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RateLimitMiddleware = exports.RateLimitError = void 0;
|
|
4
|
+
exports.createRateLimitMiddleware = createRateLimitMiddleware;
|
|
5
|
+
exports.createPerDomainRateLimitMiddleware = createPerDomainRateLimitMiddleware;
|
|
4
6
|
const ProblemDetails_js_1 = require("./ProblemDetails.js");
|
|
5
7
|
const RateLimiter_js_1 = require("./RateLimiter.js");
|
|
6
8
|
/**
|
|
@@ -118,3 +120,37 @@ class RateLimitMiddleware {
|
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
exports.RateLimitMiddleware = RateLimitMiddleware;
|
|
123
|
+
/**
|
|
124
|
+
* Creates a rate limit middleware with the given options.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const client = new FetchClient();
|
|
129
|
+
* client.use(createRateLimitMiddleware({
|
|
130
|
+
* maxRequests: 100,
|
|
131
|
+
* windowSeconds: 60,
|
|
132
|
+
* }));
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
function createRateLimitMiddleware(options) {
|
|
136
|
+
return new RateLimitMiddleware(options).middleware();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Creates a per-domain rate limit middleware where each domain is tracked separately.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const client = new FetchClient();
|
|
144
|
+
* client.use(createPerDomainRateLimitMiddleware({
|
|
145
|
+
* maxRequests: 100,
|
|
146
|
+
* windowSeconds: 60,
|
|
147
|
+
* }));
|
|
148
|
+
* // api.example.com and api.other.com will have separate rate limits
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
function createPerDomainRateLimitMiddleware(options) {
|
|
152
|
+
return new RateLimitMiddleware({
|
|
153
|
+
...options,
|
|
154
|
+
getGroupFunc: RateLimiter_js_1.groupByDomain,
|
|
155
|
+
}).middleware();
|
|
156
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResponsePromise = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A promise that resolves to a FetchClientResponse with additional helper methods
|
|
6
|
+
* for parsing the response body. This allows for a fluent API similar to ky:
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Await to get the full response
|
|
11
|
+
* const response = await client.get("/api/users");
|
|
12
|
+
*
|
|
13
|
+
* // Or use helper methods for direct access to parsed body
|
|
14
|
+
* const users = await client.get("/api/users").json<User[]>();
|
|
15
|
+
* const html = await client.get("/page").text();
|
|
16
|
+
* const file = await client.get("/file").blob();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
class ResponsePromise {
|
|
20
|
+
#responsePromise;
|
|
21
|
+
#options;
|
|
22
|
+
constructor(responsePromise, options) {
|
|
23
|
+
this.#responsePromise = responsePromise;
|
|
24
|
+
this.#options = options;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Implements PromiseLike interface so the ResponsePromise can be awaited.
|
|
28
|
+
*/
|
|
29
|
+
then(onfulfilled, onrejected) {
|
|
30
|
+
return this.#responsePromise.then(onfulfilled, onrejected);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Catches any errors from the response promise.
|
|
34
|
+
*/
|
|
35
|
+
catch(onrejected) {
|
|
36
|
+
return this.#responsePromise.catch(onrejected);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Executes a callback when the promise settles (fulfilled or rejected).
|
|
40
|
+
*/
|
|
41
|
+
finally(onfinally) {
|
|
42
|
+
return this.#responsePromise.finally(onfinally);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parses the response body as JSON.
|
|
46
|
+
*
|
|
47
|
+
* If the response was already parsed as JSON (via getJSON, postJSON, etc.),
|
|
48
|
+
* returns the parsed data directly. Otherwise, parses the response body.
|
|
49
|
+
*
|
|
50
|
+
* @template TJson - The expected type of the JSON response
|
|
51
|
+
* @returns A promise that resolves to the parsed JSON
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const user = await client.get("/api/user/1").json<User>();
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
async json() {
|
|
59
|
+
const response = await this.#responsePromise;
|
|
60
|
+
// If the response already has parsed data (from getJSON, etc.), return it
|
|
61
|
+
if (response.data !== null && response.data !== undefined) {
|
|
62
|
+
return response.data;
|
|
63
|
+
}
|
|
64
|
+
// Otherwise, parse the response body as JSON
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
// Apply reviver and date parsing if options are set
|
|
67
|
+
if (this.#options?.reviver || this.#options?.shouldParseDates) {
|
|
68
|
+
return this.#reviveJson(data);
|
|
69
|
+
}
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Returns the response body as text.
|
|
74
|
+
*
|
|
75
|
+
* @returns A promise that resolves to the response text
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const html = await client.get("/page").text();
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
async text() {
|
|
83
|
+
const response = await this.#responsePromise;
|
|
84
|
+
return response.text();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns the response body as a Blob.
|
|
88
|
+
*
|
|
89
|
+
* @returns A promise that resolves to the response as a Blob
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const imageBlob = await client.get("/image.png").blob();
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
async blob() {
|
|
97
|
+
const response = await this.#responsePromise;
|
|
98
|
+
return response.blob();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Returns the response body as an ArrayBuffer.
|
|
102
|
+
*
|
|
103
|
+
* @returns A promise that resolves to the response as an ArrayBuffer
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const buffer = await client.get("/file").arrayBuffer();
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
async arrayBuffer() {
|
|
111
|
+
const response = await this.#responsePromise;
|
|
112
|
+
return response.arrayBuffer();
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Returns the response body as FormData.
|
|
116
|
+
*
|
|
117
|
+
* @returns A promise that resolves to the response as FormData
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const formData = await client.get("/form").formData();
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
async formData() {
|
|
125
|
+
const response = await this.#responsePromise;
|
|
126
|
+
return response.formData();
|
|
127
|
+
}
|
|
128
|
+
#reviveJson(data) {
|
|
129
|
+
if (data === null || data === undefined) {
|
|
130
|
+
return data;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(data)) {
|
|
133
|
+
return data.map((item) => this.#reviveJson(item));
|
|
134
|
+
}
|
|
135
|
+
if (typeof data === "object") {
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const [key, value] of Object.entries(data)) {
|
|
138
|
+
result[key] = this.#reviveValue(key, this.#reviveJson(value));
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
return this.#reviveValue("", data);
|
|
143
|
+
}
|
|
144
|
+
#reviveValue(key, value) {
|
|
145
|
+
let revivedValue = value;
|
|
146
|
+
if (this.#options?.reviver) {
|
|
147
|
+
revivedValue = this.#options.reviver.call(this, key, revivedValue);
|
|
148
|
+
}
|
|
149
|
+
if (this.#options?.shouldParseDates) {
|
|
150
|
+
revivedValue = this.#tryParseDate(revivedValue);
|
|
151
|
+
}
|
|
152
|
+
return revivedValue;
|
|
153
|
+
}
|
|
154
|
+
#tryParseDate(value) {
|
|
155
|
+
if (typeof value !== "string") {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
159
|
+
const date = new Date(value);
|
|
160
|
+
if (!isNaN(date.getTime())) {
|
|
161
|
+
return date;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.ResponsePromise = ResponsePromise;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RetryMiddleware = void 0;
|
|
4
|
+
exports.createRetryMiddleware = createRetryMiddleware;
|
|
5
|
+
/**
|
|
6
|
+
* Default HTTP methods that are eligible for retry.
|
|
7
|
+
* These are idempotent methods that can be safely retried without side effects.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_RETRY_METHODS = [
|
|
10
|
+
"GET",
|
|
11
|
+
"HEAD",
|
|
12
|
+
"PUT",
|
|
13
|
+
"DELETE",
|
|
14
|
+
"OPTIONS",
|
|
15
|
+
"TRACE",
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Default HTTP status codes that trigger a retry.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_RETRY_STATUS_CODES = [
|
|
21
|
+
408, // Request Timeout
|
|
22
|
+
413, // Payload Too Large (rate limiting)
|
|
23
|
+
429, // Too Many Requests
|
|
24
|
+
500, // Internal Server Error
|
|
25
|
+
502, // Bad Gateway
|
|
26
|
+
503, // Service Unavailable
|
|
27
|
+
504, // Gateway Timeout
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Retry middleware that automatically retries failed requests with exponential backoff.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const provider = new FetchClientProvider();
|
|
35
|
+
* provider.useRetry({
|
|
36
|
+
* limit: 3,
|
|
37
|
+
* statusCodes: [500, 502, 503, 504],
|
|
38
|
+
* jitter: 0.1,
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* const client = provider.getFetchClient();
|
|
42
|
+
* const response = await client.getJSON('/api/data');
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
class RetryMiddleware {
|
|
46
|
+
#options;
|
|
47
|
+
constructor(options) {
|
|
48
|
+
this.#options = {
|
|
49
|
+
limit: options?.limit ?? 2,
|
|
50
|
+
methods: (options?.methods ?? DEFAULT_RETRY_METHODS).map((m) => m.toUpperCase()),
|
|
51
|
+
statusCodes: options?.statusCodes ?? DEFAULT_RETRY_STATUS_CODES,
|
|
52
|
+
maxRetryAfter: options?.maxRetryAfter ?? Infinity,
|
|
53
|
+
backoffLimit: options?.backoffLimit ?? 30000,
|
|
54
|
+
jitter: options?.jitter ?? 0.1,
|
|
55
|
+
delay: options?.delay,
|
|
56
|
+
shouldRetry: options?.shouldRetry,
|
|
57
|
+
onRetry: options?.onRetry,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates the middleware function.
|
|
62
|
+
* @returns The middleware function
|
|
63
|
+
*/
|
|
64
|
+
middleware() {
|
|
65
|
+
return async (context, next) => {
|
|
66
|
+
const method = context.request.method.toUpperCase();
|
|
67
|
+
// Check if method is eligible for retry
|
|
68
|
+
if (!this.#options.methods.includes(method)) {
|
|
69
|
+
await next();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let attemptNumber = 0;
|
|
73
|
+
while (true) {
|
|
74
|
+
// Store retry metadata in context for observability
|
|
75
|
+
if (attemptNumber > 0) {
|
|
76
|
+
context.retryAttempt = attemptNumber;
|
|
77
|
+
}
|
|
78
|
+
await next();
|
|
79
|
+
// If no response or we've exhausted retries, stop
|
|
80
|
+
if (!context.response || attemptNumber >= this.#options.limit) {
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const response = context.response;
|
|
84
|
+
// Check if status code is retryable
|
|
85
|
+
if (!this.#options.statusCodes.includes(response.status)) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// Check custom shouldRetry predicate
|
|
89
|
+
if (this.#options.shouldRetry) {
|
|
90
|
+
const shouldRetry = await this.#options.shouldRetry(response, attemptNumber);
|
|
91
|
+
if (!shouldRetry) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Calculate base delay
|
|
96
|
+
let delay = this.#calculateDelay(attemptNumber, response);
|
|
97
|
+
// Check Retry-After header
|
|
98
|
+
const retryAfterDelay = this.#parseRetryAfter(response);
|
|
99
|
+
if (retryAfterDelay !== null) {
|
|
100
|
+
// If Retry-After exceeds maxRetryAfter, don't retry
|
|
101
|
+
if (retryAfterDelay > this.#options.maxRetryAfter) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// Use the larger of computed delay or Retry-After
|
|
105
|
+
delay = Math.max(delay, retryAfterDelay);
|
|
106
|
+
}
|
|
107
|
+
// Invoke onRetry callback
|
|
108
|
+
this.#options.onRetry?.(attemptNumber, response, delay);
|
|
109
|
+
// Wait before retry
|
|
110
|
+
await this.#sleep(delay);
|
|
111
|
+
// Reset response for next attempt
|
|
112
|
+
context.response = null;
|
|
113
|
+
attemptNumber++;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Calculates the delay for a given attempt with exponential backoff and jitter.
|
|
119
|
+
*/
|
|
120
|
+
#calculateDelay(attemptNumber, response) {
|
|
121
|
+
let baseDelay;
|
|
122
|
+
if (this.#options.delay) {
|
|
123
|
+
baseDelay = this.#options.delay(attemptNumber, response);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Default exponential backoff: 1s, 2s, 4s, 8s, ...
|
|
127
|
+
baseDelay = Math.min(1000 * Math.pow(2, attemptNumber), this.#options.backoffLimit);
|
|
128
|
+
}
|
|
129
|
+
// Apply jitter
|
|
130
|
+
return this.#applyJitter(baseDelay);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Applies jitter to a delay value.
|
|
134
|
+
*/
|
|
135
|
+
#applyJitter(delay) {
|
|
136
|
+
if (this.#options.jitter <= 0) {
|
|
137
|
+
return delay;
|
|
138
|
+
}
|
|
139
|
+
const jitterRange = delay * this.#options.jitter;
|
|
140
|
+
// Random value between -jitterRange and +jitterRange
|
|
141
|
+
const jitter = (Math.random() * 2 - 1) * jitterRange;
|
|
142
|
+
return Math.max(0, Math.round(delay + jitter));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parses the Retry-After header and returns the delay in milliseconds.
|
|
146
|
+
* Supports both delta-seconds and HTTP-date formats.
|
|
147
|
+
*/
|
|
148
|
+
#parseRetryAfter(response) {
|
|
149
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
150
|
+
if (!retryAfter) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
// Try parsing as seconds (integer)
|
|
154
|
+
const seconds = parseInt(retryAfter, 10);
|
|
155
|
+
if (!isNaN(seconds)) {
|
|
156
|
+
return seconds * 1000;
|
|
157
|
+
}
|
|
158
|
+
// Try parsing as HTTP-date
|
|
159
|
+
const date = Date.parse(retryAfter);
|
|
160
|
+
if (!isNaN(date)) {
|
|
161
|
+
return Math.max(0, date - Date.now());
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sleep for the specified number of milliseconds.
|
|
167
|
+
*/
|
|
168
|
+
#sleep(ms) {
|
|
169
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
exports.RetryMiddleware = RetryMiddleware;
|
|
173
|
+
/**
|
|
174
|
+
* Creates a retry middleware with the given options.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* const client = new FetchClient();
|
|
179
|
+
* client.use(createRetryMiddleware({ limit: 3 }));
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
function createRetryMiddleware(options) {
|
|
183
|
+
return new RetryMiddleware(options).middleware();
|
|
184
|
+
}
|
|
@@ -6,6 +6,7 @@ exports.MockHistoryImpl = void 0;
|
|
|
6
6
|
*/
|
|
7
7
|
class MockHistoryImpl {
|
|
8
8
|
#get = [];
|
|
9
|
+
#head = [];
|
|
9
10
|
#post = [];
|
|
10
11
|
#put = [];
|
|
11
12
|
#patch = [];
|
|
@@ -14,6 +15,9 @@ class MockHistoryImpl {
|
|
|
14
15
|
get get() {
|
|
15
16
|
return [...this.#get];
|
|
16
17
|
}
|
|
18
|
+
get head() {
|
|
19
|
+
return [...this.#head];
|
|
20
|
+
}
|
|
17
21
|
get post() {
|
|
18
22
|
return [...this.#post];
|
|
19
23
|
}
|
|
@@ -38,6 +42,9 @@ class MockHistoryImpl {
|
|
|
38
42
|
case "GET":
|
|
39
43
|
this.#get.push(request);
|
|
40
44
|
break;
|
|
45
|
+
case "HEAD":
|
|
46
|
+
this.#head.push(request);
|
|
47
|
+
break;
|
|
41
48
|
case "POST":
|
|
42
49
|
this.#post.push(request);
|
|
43
50
|
break;
|
|
@@ -57,6 +64,7 @@ class MockHistoryImpl {
|
|
|
57
64
|
*/
|
|
58
65
|
clear() {
|
|
59
66
|
this.#get = [];
|
|
67
|
+
this.#head = [];
|
|
60
68
|
this.#post = [];
|
|
61
69
|
this.#put = [];
|
|
62
70
|
this.#patch = [];
|
|
@@ -43,6 +43,13 @@ class MockRegistry {
|
|
|
43
43
|
onGet(url) {
|
|
44
44
|
return this.#addMock("GET", url);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a mock for HEAD requests matching the given URL.
|
|
48
|
+
* @param url - URL string or RegExp to match
|
|
49
|
+
*/
|
|
50
|
+
onHead(url) {
|
|
51
|
+
return this.#addMock("HEAD", url);
|
|
52
|
+
}
|
|
46
53
|
/**
|
|
47
54
|
* Creates a mock for POST requests matching the given URL.
|
|
48
55
|
* @param url - URL string or RegExp to match
|
package/types/mod.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { FetchClient } from "./src/FetchClient.js";
|
|
2
2
|
export type { FetchClientOptions } from "./src/FetchClientOptions.js";
|
|
3
3
|
export type { FetchClientResponse } from "./src/FetchClientResponse.js";
|
|
4
|
+
export { FetchClientError } from "./src/FetchClientError.js";
|
|
5
|
+
export { ResponsePromise } from "./src/ResponsePromise.js";
|
|
4
6
|
export { ProblemDetails } from "./src/ProblemDetails.js";
|
|
5
7
|
export { type CacheKey, type CacheTag, FetchClientCache, } from "./src/FetchClientCache.js";
|
|
6
8
|
export type { RequestOptions } from "./src/RequestOptions.js";
|
|
@@ -10,4 +12,98 @@ export { defaultInstance as defaultProviderInstance, FetchClientProvider, } from
|
|
|
10
12
|
export * from "./src/DefaultHelpers.js";
|
|
11
13
|
export { CircuitBreaker, type CircuitBreakerOptions, type CircuitState, groupByDomain as circuitBreakerGroupByDomain, type GroupCircuitBreakerOptions, } from "./src/CircuitBreaker.js";
|
|
12
14
|
export { CircuitBreakerMiddleware, type CircuitBreakerMiddlewareOptions, CircuitOpenError, createCircuitBreakerMiddleware, createPerDomainCircuitBreakerMiddleware, } from "./src/CircuitBreakerMiddleware.js";
|
|
15
|
+
export { createRetryMiddleware, RetryMiddleware, type RetryMiddlewareOptions, } from "./src/RetryMiddleware.js";
|
|
16
|
+
export { createPerDomainRateLimitMiddleware, createRateLimitMiddleware, RateLimitError, RateLimitMiddleware, type RateLimitMiddlewareOptions, } from "./src/RateLimitMiddleware.js";
|
|
17
|
+
export { groupByDomain as rateLimiterGroupByDomain, RateLimiter, type RateLimiterOptions, } from "./src/RateLimiter.js";
|
|
18
|
+
import { createRetryMiddleware } from "./src/RetryMiddleware.js";
|
|
19
|
+
import { createPerDomainRateLimitMiddleware, createRateLimitMiddleware } from "./src/RateLimitMiddleware.js";
|
|
20
|
+
import { createCircuitBreakerMiddleware, createPerDomainCircuitBreakerMiddleware } from "./src/CircuitBreakerMiddleware.js";
|
|
21
|
+
import { deleteJSON, getJSON, patchJSON, postJSON, putJSON, useMiddleware } from "./src/DefaultHelpers.js";
|
|
22
|
+
import type { GetRequestOptions, RequestOptions } from "./src/RequestOptions.js";
|
|
23
|
+
import type { ResponsePromise } from "./src/ResponsePromise.js";
|
|
24
|
+
/**
|
|
25
|
+
* Convenience middleware factory functions for use with FetchClient.use()
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { FetchClient, middleware } from "@foundatiofx/fetchclient";
|
|
30
|
+
*
|
|
31
|
+
* const client = new FetchClient();
|
|
32
|
+
* client.use(
|
|
33
|
+
* middleware.retry({ limit: 3 }),
|
|
34
|
+
* middleware.rateLimit({ maxRequests: 100, windowSeconds: 60 }),
|
|
35
|
+
* middleware.circuitBreaker({ failureThreshold: 5 })
|
|
36
|
+
* );
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const middleware: {
|
|
40
|
+
/** Retry failed requests with exponential backoff and jitter */
|
|
41
|
+
retry: typeof createRetryMiddleware;
|
|
42
|
+
/** Rate limit requests to prevent overwhelming servers */
|
|
43
|
+
rateLimit: typeof createRateLimitMiddleware;
|
|
44
|
+
/** Per-domain rate limit (each domain tracked separately) */
|
|
45
|
+
perDomainRateLimit: typeof createPerDomainRateLimitMiddleware;
|
|
46
|
+
/** Circuit breaker for fault tolerance */
|
|
47
|
+
circuitBreaker: typeof createCircuitBreakerMiddleware;
|
|
48
|
+
/** Per-domain circuit breaker (each domain tracked separately) */
|
|
49
|
+
perDomainCircuitBreaker: typeof createPerDomainCircuitBreakerMiddleware;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Default export for convenient access to all HTTP methods.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import fc from "@foundatiofx/fetchclient";
|
|
57
|
+
*
|
|
58
|
+
* // Configure middleware
|
|
59
|
+
* fc.use(fc.middleware.retry({ limit: 3 }));
|
|
60
|
+
*
|
|
61
|
+
* // Use JSON methods (recommended)
|
|
62
|
+
* const { data: user } = await fc.getJSON<User>("/api/user/1");
|
|
63
|
+
* const { data: created } = await fc.postJSON<User>("/api/users", { name: "Alice" });
|
|
64
|
+
*
|
|
65
|
+
* // Or use fluent API for other response types
|
|
66
|
+
* const html = await fc.get("/page").text();
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
declare const fetchClient: {
|
|
70
|
+
/** Sends a GET request. Use `.json<T>()` for typed JSON response. */
|
|
71
|
+
get: (url: string, options?: GetRequestOptions) => ResponsePromise<unknown>;
|
|
72
|
+
/** Sends a POST request. Use `.json<T>()` for typed JSON response. */
|
|
73
|
+
post: (url: string, body?: object | string | FormData, options?: RequestOptions) => ResponsePromise<unknown>;
|
|
74
|
+
/** Sends a PUT request. Use `.json<T>()` for typed JSON response. */
|
|
75
|
+
put: (url: string, body?: object | string | FormData, options?: RequestOptions) => ResponsePromise<unknown>;
|
|
76
|
+
/** Sends a PATCH request. Use `.json<T>()` for typed JSON response. */
|
|
77
|
+
patch: (url: string, body?: object | string | FormData, options?: RequestOptions) => ResponsePromise<unknown>;
|
|
78
|
+
/** Sends a DELETE request. Use `.json<T>()` for typed JSON response. */
|
|
79
|
+
delete: (url: string, options?: RequestOptions) => ResponsePromise<unknown>;
|
|
80
|
+
/** Sends a HEAD request. */
|
|
81
|
+
head: (url: string, options?: GetRequestOptions) => ResponsePromise<void>;
|
|
82
|
+
/** Sends a GET request and returns parsed JSON in response.data */
|
|
83
|
+
getJSON: typeof getJSON;
|
|
84
|
+
/** Sends a POST request and returns parsed JSON in response.data */
|
|
85
|
+
postJSON: typeof postJSON;
|
|
86
|
+
/** Sends a PUT request and returns parsed JSON in response.data */
|
|
87
|
+
putJSON: typeof putJSON;
|
|
88
|
+
/** Sends a PATCH request and returns parsed JSON in response.data */
|
|
89
|
+
patchJSON: typeof patchJSON;
|
|
90
|
+
/** Sends a DELETE request and returns parsed JSON in response.data */
|
|
91
|
+
deleteJSON: typeof deleteJSON;
|
|
92
|
+
/** Adds middleware to the default provider */
|
|
93
|
+
use: typeof useMiddleware;
|
|
94
|
+
/** Middleware factory functions */
|
|
95
|
+
middleware: {
|
|
96
|
+
/** Retry failed requests with exponential backoff and jitter */
|
|
97
|
+
retry: typeof createRetryMiddleware;
|
|
98
|
+
/** Rate limit requests to prevent overwhelming servers */
|
|
99
|
+
rateLimit: typeof createRateLimitMiddleware;
|
|
100
|
+
/** Per-domain rate limit (each domain tracked separately) */
|
|
101
|
+
perDomainRateLimit: typeof createPerDomainRateLimitMiddleware;
|
|
102
|
+
/** Circuit breaker for fault tolerance */
|
|
103
|
+
circuitBreaker: typeof createCircuitBreakerMiddleware;
|
|
104
|
+
/** Per-domain circuit breaker (each domain tracked separately) */
|
|
105
|
+
perDomainCircuitBreaker: typeof createPerDomainCircuitBreakerMiddleware;
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
export default fetchClient;
|
|
13
109
|
//# sourceMappingURL=mod.d.ts.map
|
package/types/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,YAAY,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,YAAY,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,YAAY,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAC5E,YAAY,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EACL,eAAe,IAAI,uBAAuB,EAC1C,mBAAmB,GACpB,MAAM,8BAA8B,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,YAAY,EACjB,aAAa,IAAI,2BAA2B,EAC5C,KAAK,0BAA0B,GAChC,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,wBAAwB,EACxB,KAAK,+BAA+B,EACpC,gBAAgB,EAChB,8BAA8B,EAC9B,uCAAuC,GACxC,MAAM,mCAAmC,CAAC"}
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"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"}
|