@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
package/esm/mod.js
CHANGED
|
@@ -1,7 +1,90 @@
|
|
|
1
1
|
export { FetchClient } from "./src/FetchClient.js";
|
|
2
|
+
export { FetchClientError } from "./src/FetchClientError.js";
|
|
3
|
+
export { ResponsePromise } from "./src/ResponsePromise.js";
|
|
2
4
|
export { ProblemDetails } from "./src/ProblemDetails.js";
|
|
3
5
|
export { FetchClientCache, } from "./src/FetchClientCache.js";
|
|
4
6
|
export { defaultInstance as defaultProviderInstance, FetchClientProvider, } from "./src/FetchClientProvider.js";
|
|
5
7
|
export * from "./src/DefaultHelpers.js";
|
|
6
8
|
export { CircuitBreaker, groupByDomain as circuitBreakerGroupByDomain, } from "./src/CircuitBreaker.js";
|
|
7
9
|
export { CircuitBreakerMiddleware, CircuitOpenError, createCircuitBreakerMiddleware, createPerDomainCircuitBreakerMiddleware, } from "./src/CircuitBreakerMiddleware.js";
|
|
10
|
+
export { createRetryMiddleware, RetryMiddleware, } from "./src/RetryMiddleware.js";
|
|
11
|
+
export { createPerDomainRateLimitMiddleware, createRateLimitMiddleware, RateLimitError, RateLimitMiddleware, } from "./src/RateLimitMiddleware.js";
|
|
12
|
+
export { groupByDomain as rateLimiterGroupByDomain, RateLimiter, } from "./src/RateLimiter.js";
|
|
13
|
+
import { createRetryMiddleware } from "./src/RetryMiddleware.js";
|
|
14
|
+
import { createPerDomainRateLimitMiddleware, createRateLimitMiddleware, } from "./src/RateLimitMiddleware.js";
|
|
15
|
+
import { createCircuitBreakerMiddleware, createPerDomainCircuitBreakerMiddleware, } from "./src/CircuitBreakerMiddleware.js";
|
|
16
|
+
import { deleteJSON, getJSON, patchJSON, postJSON, putJSON, useFetchClient, useMiddleware, } from "./src/DefaultHelpers.js";
|
|
17
|
+
/**
|
|
18
|
+
* Convenience middleware factory functions for use with FetchClient.use()
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { FetchClient, middleware } from "@foundatiofx/fetchclient";
|
|
23
|
+
*
|
|
24
|
+
* const client = new FetchClient();
|
|
25
|
+
* client.use(
|
|
26
|
+
* middleware.retry({ limit: 3 }),
|
|
27
|
+
* middleware.rateLimit({ maxRequests: 100, windowSeconds: 60 }),
|
|
28
|
+
* middleware.circuitBreaker({ failureThreshold: 5 })
|
|
29
|
+
* );
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const middleware = {
|
|
33
|
+
/** Retry failed requests with exponential backoff and jitter */
|
|
34
|
+
retry: createRetryMiddleware,
|
|
35
|
+
/** Rate limit requests to prevent overwhelming servers */
|
|
36
|
+
rateLimit: createRateLimitMiddleware,
|
|
37
|
+
/** Per-domain rate limit (each domain tracked separately) */
|
|
38
|
+
perDomainRateLimit: createPerDomainRateLimitMiddleware,
|
|
39
|
+
/** Circuit breaker for fault tolerance */
|
|
40
|
+
circuitBreaker: createCircuitBreakerMiddleware,
|
|
41
|
+
/** Per-domain circuit breaker (each domain tracked separately) */
|
|
42
|
+
perDomainCircuitBreaker: createPerDomainCircuitBreakerMiddleware,
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Default export for convenient access to all HTTP methods.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import fc from "@foundatiofx/fetchclient";
|
|
50
|
+
*
|
|
51
|
+
* // Configure middleware
|
|
52
|
+
* fc.use(fc.middleware.retry({ limit: 3 }));
|
|
53
|
+
*
|
|
54
|
+
* // Use JSON methods (recommended)
|
|
55
|
+
* const { data: user } = await fc.getJSON<User>("/api/user/1");
|
|
56
|
+
* const { data: created } = await fc.postJSON<User>("/api/users", { name: "Alice" });
|
|
57
|
+
*
|
|
58
|
+
* // Or use fluent API for other response types
|
|
59
|
+
* const html = await fc.get("/page").text();
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
const fetchClient = {
|
|
63
|
+
/** Sends a GET request. Use `.json<T>()` for typed JSON response. */
|
|
64
|
+
get: (url, options) => useFetchClient().get(url, options),
|
|
65
|
+
/** Sends a POST request. Use `.json<T>()` for typed JSON response. */
|
|
66
|
+
post: (url, body, options) => useFetchClient().post(url, body, options),
|
|
67
|
+
/** Sends a PUT request. Use `.json<T>()` for typed JSON response. */
|
|
68
|
+
put: (url, body, options) => useFetchClient().put(url, body, options),
|
|
69
|
+
/** Sends a PATCH request. Use `.json<T>()` for typed JSON response. */
|
|
70
|
+
patch: (url, body, options) => useFetchClient().patch(url, body, options),
|
|
71
|
+
/** Sends a DELETE request. Use `.json<T>()` for typed JSON response. */
|
|
72
|
+
delete: (url, options) => useFetchClient().delete(url, options),
|
|
73
|
+
/** Sends a HEAD request. */
|
|
74
|
+
head: (url, options) => useFetchClient().head(url, options),
|
|
75
|
+
/** Sends a GET request and returns parsed JSON in response.data */
|
|
76
|
+
getJSON,
|
|
77
|
+
/** Sends a POST request and returns parsed JSON in response.data */
|
|
78
|
+
postJSON,
|
|
79
|
+
/** Sends a PUT request and returns parsed JSON in response.data */
|
|
80
|
+
putJSON,
|
|
81
|
+
/** Sends a PATCH request and returns parsed JSON in response.data */
|
|
82
|
+
patchJSON,
|
|
83
|
+
/** Sends a DELETE request and returns parsed JSON in response.data */
|
|
84
|
+
deleteJSON,
|
|
85
|
+
/** Adds middleware to the default provider */
|
|
86
|
+
use: useMiddleware,
|
|
87
|
+
/** Middleware factory functions */
|
|
88
|
+
middleware,
|
|
89
|
+
};
|
|
90
|
+
export default fetchClient;
|
|
@@ -11,7 +11,7 @@ export function useFetchClient(options) {
|
|
|
11
11
|
* Sends a GET request to the specified URL using the default client and provider and returns the response as JSON.
|
|
12
12
|
* @param url - The URL to send the GET request to.
|
|
13
13
|
* @param options - Optional request options.
|
|
14
|
-
* @returns A promise that resolves to the response
|
|
14
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
15
15
|
*/
|
|
16
16
|
export function getJSON(url, options) {
|
|
17
17
|
return useFetchClient().getJSON(url, options);
|
|
@@ -23,7 +23,7 @@ export function getJSON(url, options) {
|
|
|
23
23
|
* @param {string} url - The URL to send the request to.
|
|
24
24
|
* @param {object | string | FormData} [body] - The JSON payload or form data to send with the request.
|
|
25
25
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
26
|
-
* @returns
|
|
26
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
27
27
|
*/
|
|
28
28
|
export function postJSON(url, body, options) {
|
|
29
29
|
return useFetchClient().postJSON(url, body, options);
|
|
@@ -35,7 +35,7 @@ export function postJSON(url, body, options) {
|
|
|
35
35
|
* @param {string} url - The URL to send the request to.
|
|
36
36
|
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
37
37
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
38
|
-
* @returns
|
|
38
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
39
39
|
*/
|
|
40
40
|
export function putJSON(url, body, options) {
|
|
41
41
|
return useFetchClient().putJSON(url, body, options);
|
|
@@ -47,7 +47,7 @@ export function putJSON(url, body, options) {
|
|
|
47
47
|
* @param {string} url - The URL to send the request to.
|
|
48
48
|
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
49
49
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
50
|
-
* @returns
|
|
50
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
51
51
|
*/
|
|
52
52
|
export function patchJSON(url, body, options) {
|
|
53
53
|
return useFetchClient().patchJSON(url, body, options);
|
|
@@ -58,7 +58,7 @@ export function patchJSON(url, body, options) {
|
|
|
58
58
|
* @template T - The type of the response data.
|
|
59
59
|
* @param {string} url - The URL to send the request to.
|
|
60
60
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
61
|
-
* @returns
|
|
61
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
62
62
|
*/
|
|
63
63
|
export function deleteJSON(url, options) {
|
|
64
64
|
return useFetchClient().deleteJSON(url, options);
|
package/esm/src/FetchClient.js
CHANGED
|
@@ -4,6 +4,8 @@ import { parseLinkHeader } from "./LinkHeader.js";
|
|
|
4
4
|
import { FetchClientProvider } from "./FetchClientProvider.js";
|
|
5
5
|
import { getCurrentProvider } from "./DefaultHelpers.js";
|
|
6
6
|
import { ObjectEvent } from "./ObjectEvent.js";
|
|
7
|
+
import { ResponsePromise } from "./ResponsePromise.js";
|
|
8
|
+
import { FetchClientError } from "./FetchClientError.js";
|
|
7
9
|
/**
|
|
8
10
|
* Represents a client for making HTTP requests using the Fetch API.
|
|
9
11
|
*/
|
|
@@ -100,24 +102,27 @@ export class FetchClient {
|
|
|
100
102
|
*
|
|
101
103
|
* @param url - The URL to send the GET request to.
|
|
102
104
|
* @param options - The optional request options.
|
|
103
|
-
* @returns A
|
|
105
|
+
* @returns A ResponsePromise that resolves to the response. Can use `.json<T>()` for typed JSON.
|
|
104
106
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
get(url, options) {
|
|
108
|
+
const mergedOptions = {
|
|
107
109
|
...this.options.defaultRequestOptions,
|
|
108
110
|
...options,
|
|
109
111
|
};
|
|
110
|
-
const
|
|
111
|
-
return
|
|
112
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("GET", undefined, mergedOptions));
|
|
113
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
114
116
|
* Sends a GET request to the specified URL and returns the response as JSON.
|
|
117
|
+
* The response will have the parsed JSON in `response.data`.
|
|
118
|
+
*
|
|
115
119
|
* @param url - The URL to send the GET request to.
|
|
116
120
|
* @param options - Optional request options.
|
|
117
|
-
* @returns A promise that resolves to the response
|
|
121
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
118
122
|
*/
|
|
119
|
-
getJSON(url, options) {
|
|
120
|
-
|
|
123
|
+
async getJSON(url, options) {
|
|
124
|
+
const mergedOptions = this.buildJsonRequestOptions(options);
|
|
125
|
+
return await this.get(url, mergedOptions);
|
|
121
126
|
}
|
|
122
127
|
/**
|
|
123
128
|
* Sends a POST request to the specified URL.
|
|
@@ -125,107 +130,127 @@ export class FetchClient {
|
|
|
125
130
|
* @param url - The URL to send the request to.
|
|
126
131
|
* @param body - The request body, can be an object, a string, or FormData.
|
|
127
132
|
* @param options - Additional options for the request.
|
|
128
|
-
* @returns A
|
|
133
|
+
* @returns A ResponsePromise that resolves to the response. Can use `.json<T>()` for typed JSON.
|
|
129
134
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
post(url, body, options) {
|
|
136
|
+
const mergedOptions = {
|
|
132
137
|
...this.options.defaultRequestOptions,
|
|
133
138
|
...options,
|
|
134
139
|
};
|
|
135
|
-
const
|
|
136
|
-
return
|
|
140
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("POST", body, mergedOptions));
|
|
141
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
137
142
|
}
|
|
138
143
|
/**
|
|
139
144
|
* Sends a POST request with JSON payload to the specified URL.
|
|
145
|
+
* The response will have the parsed JSON in `response.data`.
|
|
140
146
|
*
|
|
141
147
|
* @template T - The type of the response data.
|
|
142
148
|
* @param {string} url - The URL to send the request to.
|
|
143
149
|
* @param {object | string | FormData} [body] - The JSON payload or form data to send with the request.
|
|
144
150
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
145
|
-
* @returns
|
|
151
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
146
152
|
*/
|
|
147
|
-
postJSON(url, body, options) {
|
|
148
|
-
return this.post(url, body, this.buildJsonRequestOptions(options));
|
|
153
|
+
async postJSON(url, body, options) {
|
|
154
|
+
return await this.post(url, body, this.buildJsonRequestOptions(options));
|
|
149
155
|
}
|
|
150
156
|
/**
|
|
151
157
|
* Sends a PUT request to the specified URL with the given body and options.
|
|
152
158
|
* @param url - The URL to send the request to.
|
|
153
159
|
* @param body - The request body, can be an object, a string, or FormData.
|
|
154
160
|
* @param options - The request options.
|
|
155
|
-
* @returns A
|
|
161
|
+
* @returns A ResponsePromise that resolves to the response. Can use `.json<T>()` for typed JSON.
|
|
156
162
|
*/
|
|
157
|
-
|
|
158
|
-
|
|
163
|
+
put(url, body, options) {
|
|
164
|
+
const mergedOptions = {
|
|
159
165
|
...this.options.defaultRequestOptions,
|
|
160
166
|
...options,
|
|
161
167
|
};
|
|
162
|
-
const
|
|
163
|
-
return
|
|
168
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("PUT", body, mergedOptions));
|
|
169
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
164
170
|
}
|
|
165
171
|
/**
|
|
166
172
|
* Sends a PUT request with JSON payload to the specified URL.
|
|
173
|
+
* The response will have the parsed JSON in `response.data`.
|
|
167
174
|
*
|
|
168
175
|
* @template T - The type of the response data.
|
|
169
176
|
* @param {string} url - The URL to send the request to.
|
|
170
177
|
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
171
178
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
172
|
-
* @returns
|
|
179
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
173
180
|
*/
|
|
174
|
-
putJSON(url, body, options) {
|
|
175
|
-
return this.put(url, body, this.buildJsonRequestOptions(options));
|
|
181
|
+
async putJSON(url, body, options) {
|
|
182
|
+
return await this.put(url, body, this.buildJsonRequestOptions(options));
|
|
176
183
|
}
|
|
177
184
|
/**
|
|
178
185
|
* Sends a PATCH request to the specified URL with the provided body and options.
|
|
179
186
|
* @param url - The URL to send the PATCH request to.
|
|
180
187
|
* @param body - The body of the request. It can be an object, a string, or FormData.
|
|
181
188
|
* @param options - The options for the request.
|
|
182
|
-
* @returns A
|
|
189
|
+
* @returns A ResponsePromise that resolves to the response. Can use `.json<T>()` for typed JSON.
|
|
183
190
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
patch(url, body, options) {
|
|
192
|
+
const mergedOptions = {
|
|
186
193
|
...this.options.defaultRequestOptions,
|
|
187
194
|
...options,
|
|
188
195
|
};
|
|
189
|
-
const
|
|
190
|
-
return
|
|
196
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("PATCH", body, mergedOptions));
|
|
197
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
191
198
|
}
|
|
192
199
|
/**
|
|
193
200
|
* Sends a PATCH request with JSON payload to the specified URL.
|
|
201
|
+
* The response will have the parsed JSON in `response.data`.
|
|
194
202
|
*
|
|
195
203
|
* @template T - The type of the response data.
|
|
196
204
|
* @param {string} url - The URL to send the request to.
|
|
197
205
|
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
198
206
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
199
|
-
* @returns
|
|
207
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
200
208
|
*/
|
|
201
|
-
patchJSON(url, body, options) {
|
|
202
|
-
return this.patch(url, body, this.buildJsonRequestOptions(options));
|
|
209
|
+
async patchJSON(url, body, options) {
|
|
210
|
+
return await this.patch(url, body, this.buildJsonRequestOptions(options));
|
|
203
211
|
}
|
|
204
212
|
/**
|
|
205
213
|
* Sends a DELETE request to the specified URL.
|
|
206
214
|
*
|
|
207
215
|
* @param url - The URL to send the DELETE request to.
|
|
208
216
|
* @param options - The options for the request.
|
|
209
|
-
* @returns A
|
|
217
|
+
* @returns A ResponsePromise that resolves to the response. Can use `.json<T>()` for typed JSON.
|
|
210
218
|
*/
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
delete(url, options) {
|
|
220
|
+
const mergedOptions = {
|
|
213
221
|
...this.options.defaultRequestOptions,
|
|
214
222
|
...options,
|
|
215
223
|
};
|
|
216
|
-
const
|
|
217
|
-
return
|
|
224
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("DELETE", undefined, mergedOptions));
|
|
225
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
218
226
|
}
|
|
219
227
|
/**
|
|
220
228
|
* Sends a DELETE request with JSON payload to the specified URL.
|
|
229
|
+
* The response will have the parsed JSON in `response.data`.
|
|
221
230
|
*
|
|
222
231
|
* @template T - The type of the response data.
|
|
223
232
|
* @param {string} url - The URL to send the request to.
|
|
224
233
|
* @param {RequestOptions} [options] - Additional options for the request.
|
|
225
|
-
* @returns
|
|
234
|
+
* @returns A promise that resolves to the response with parsed JSON in `data`.
|
|
226
235
|
*/
|
|
227
|
-
deleteJSON(url, options) {
|
|
228
|
-
return this.delete(url, this.buildJsonRequestOptions(options));
|
|
236
|
+
async deleteJSON(url, options) {
|
|
237
|
+
return await this.delete(url, this.buildJsonRequestOptions(options));
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Sends a HEAD request to the specified URL.
|
|
241
|
+
* HEAD requests are identical to GET requests but without the response body.
|
|
242
|
+
*
|
|
243
|
+
* @param url - The URL to send the HEAD request to.
|
|
244
|
+
* @param options - The optional request options.
|
|
245
|
+
* @returns A ResponsePromise that resolves to the response.
|
|
246
|
+
*/
|
|
247
|
+
head(url, options) {
|
|
248
|
+
const mergedOptions = {
|
|
249
|
+
...this.options.defaultRequestOptions,
|
|
250
|
+
...options,
|
|
251
|
+
};
|
|
252
|
+
const responsePromise = this.fetchInternal(url, mergedOptions, this.buildRequestInit("HEAD", undefined, mergedOptions));
|
|
253
|
+
return new ResponsePromise(responsePromise, mergedOptions);
|
|
229
254
|
}
|
|
230
255
|
async validate(data, options) {
|
|
231
256
|
if (typeof data !== "object" ||
|
|
@@ -540,6 +565,6 @@ export class FetchClient {
|
|
|
540
565
|
response.problem.status = response.status;
|
|
541
566
|
response.problem.title = `Unexpected status code: ${response.status}`;
|
|
542
567
|
response.problem.setErrorMessage(response.problem.title);
|
|
543
|
-
throw response;
|
|
568
|
+
throw new FetchClientError(response);
|
|
544
569
|
}
|
|
545
570
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error wrapper for non-2xx responses.
|
|
3
|
+
* Exposes the underlying response for compatibility and debugging.
|
|
4
|
+
*/
|
|
5
|
+
export class FetchClientError extends Error {
|
|
6
|
+
response;
|
|
7
|
+
constructor(response, message) {
|
|
8
|
+
super(message ??
|
|
9
|
+
response.problem?.title ??
|
|
10
|
+
`Unexpected status code: ${response.status}`);
|
|
11
|
+
this.name = "FetchClientError";
|
|
12
|
+
this.response = response;
|
|
13
|
+
}
|
|
14
|
+
get status() {
|
|
15
|
+
return this.response.status;
|
|
16
|
+
}
|
|
17
|
+
get statusText() {
|
|
18
|
+
return this.response.statusText;
|
|
19
|
+
}
|
|
20
|
+
get ok() {
|
|
21
|
+
return this.response.ok;
|
|
22
|
+
}
|
|
23
|
+
get headers() {
|
|
24
|
+
return this.response.headers;
|
|
25
|
+
}
|
|
26
|
+
get url() {
|
|
27
|
+
return this.response.url;
|
|
28
|
+
}
|
|
29
|
+
get redirected() {
|
|
30
|
+
return this.response.redirected;
|
|
31
|
+
}
|
|
32
|
+
get type() {
|
|
33
|
+
return this.response.type;
|
|
34
|
+
}
|
|
35
|
+
get body() {
|
|
36
|
+
return this.response.body;
|
|
37
|
+
}
|
|
38
|
+
get bodyUsed() {
|
|
39
|
+
return this.response.bodyUsed;
|
|
40
|
+
}
|
|
41
|
+
get data() {
|
|
42
|
+
return this.response.data;
|
|
43
|
+
}
|
|
44
|
+
get problem() {
|
|
45
|
+
return this.response.problem;
|
|
46
|
+
}
|
|
47
|
+
get meta() {
|
|
48
|
+
return this.response.meta;
|
|
49
|
+
}
|
|
50
|
+
json() {
|
|
51
|
+
return this.response.json();
|
|
52
|
+
}
|
|
53
|
+
text() {
|
|
54
|
+
return this.response.text();
|
|
55
|
+
}
|
|
56
|
+
arrayBuffer() {
|
|
57
|
+
return this.response.arrayBuffer();
|
|
58
|
+
}
|
|
59
|
+
blob() {
|
|
60
|
+
return this.response.blob();
|
|
61
|
+
}
|
|
62
|
+
formData() {
|
|
63
|
+
return this.response.formData();
|
|
64
|
+
}
|
|
65
|
+
// @ts-ignore: New in Deno 1.44
|
|
66
|
+
bytes() {
|
|
67
|
+
// @ts-ignore: New in Deno 1.44
|
|
68
|
+
return this.response.bytes();
|
|
69
|
+
}
|
|
70
|
+
clone() {
|
|
71
|
+
return this.response.clone();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -6,6 +6,7 @@ import { RateLimitMiddleware, } from "./RateLimitMiddleware.js";
|
|
|
6
6
|
import { groupByDomain } from "./RateLimiter.js";
|
|
7
7
|
import { CircuitBreakerMiddleware, } from "./CircuitBreakerMiddleware.js";
|
|
8
8
|
import { groupByDomain as circuitBreakerGroupByDomain, } from "./CircuitBreaker.js";
|
|
9
|
+
import { RetryMiddleware, } from "./RetryMiddleware.js";
|
|
9
10
|
/**
|
|
10
11
|
* Represents a provider for creating instances of the FetchClient class with shared default options and cache.
|
|
11
12
|
*/
|
|
@@ -17,6 +18,8 @@ export class FetchClientProvider {
|
|
|
17
18
|
#rateLimitMiddlewareFunc;
|
|
18
19
|
#circuitBreakerMiddleware;
|
|
19
20
|
#circuitBreakerMiddlewareFunc;
|
|
21
|
+
#retryMiddleware;
|
|
22
|
+
#retryMiddlewareFunc;
|
|
20
23
|
#counter = new Counter();
|
|
21
24
|
#onLoading = new ObjectEvent();
|
|
22
25
|
/**
|
|
@@ -250,6 +253,27 @@ export class FetchClientProvider {
|
|
|
250
253
|
this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
|
|
251
254
|
}
|
|
252
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Enables automatic retry for failed requests.
|
|
258
|
+
* Retries are performed with exponential backoff and jitter.
|
|
259
|
+
* @param options - The retry configuration options.
|
|
260
|
+
*/
|
|
261
|
+
useRetry(options) {
|
|
262
|
+
this.#retryMiddleware = new RetryMiddleware(options);
|
|
263
|
+
this.#retryMiddlewareFunc = this.#retryMiddleware.middleware();
|
|
264
|
+
this.useMiddleware(this.#retryMiddlewareFunc);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Removes the retry middleware from all FetchClient instances created by this provider.
|
|
268
|
+
*/
|
|
269
|
+
removeRetry() {
|
|
270
|
+
const middlewareFunc = this.#retryMiddlewareFunc;
|
|
271
|
+
this.#retryMiddleware = undefined;
|
|
272
|
+
this.#retryMiddlewareFunc = undefined;
|
|
273
|
+
if (middlewareFunc) {
|
|
274
|
+
this.#options.middleware = this.#options.middleware?.filter((m) => m !== middlewareFunc);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
253
277
|
}
|
|
254
278
|
const provider = new FetchClientProvider();
|
|
255
279
|
export const defaultInstance = provider;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProblemDetails } from "./ProblemDetails.js";
|
|
2
|
-
import { buildRateLimitHeader, buildRateLimitPolicyHeader, RateLimiter, } from "./RateLimiter.js";
|
|
2
|
+
import { buildRateLimitHeader, buildRateLimitPolicyHeader, groupByDomain, RateLimiter, } from "./RateLimiter.js";
|
|
3
3
|
/**
|
|
4
4
|
* Rate limiting error thrown when requests exceed the rate limit.
|
|
5
5
|
*/
|
|
@@ -113,3 +113,37 @@ export class RateLimitMiddleware {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Creates a rate limit middleware with the given options.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const client = new FetchClient();
|
|
122
|
+
* client.use(createRateLimitMiddleware({
|
|
123
|
+
* maxRequests: 100,
|
|
124
|
+
* windowSeconds: 60,
|
|
125
|
+
* }));
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export function createRateLimitMiddleware(options) {
|
|
129
|
+
return new RateLimitMiddleware(options).middleware();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates a per-domain rate limit middleware where each domain is tracked separately.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const client = new FetchClient();
|
|
137
|
+
* client.use(createPerDomainRateLimitMiddleware({
|
|
138
|
+
* maxRequests: 100,
|
|
139
|
+
* windowSeconds: 60,
|
|
140
|
+
* }));
|
|
141
|
+
* // api.example.com and api.other.com will have separate rate limits
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export function createPerDomainRateLimitMiddleware(options) {
|
|
145
|
+
return new RateLimitMiddleware({
|
|
146
|
+
...options,
|
|
147
|
+
getGroupFunc: groupByDomain,
|
|
148
|
+
}).middleware();
|
|
149
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A promise that resolves to a FetchClientResponse with additional helper methods
|
|
3
|
+
* for parsing the response body. This allows for a fluent API similar to ky:
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```typescript
|
|
7
|
+
* // Await to get the full response
|
|
8
|
+
* const response = await client.get("/api/users");
|
|
9
|
+
*
|
|
10
|
+
* // Or use helper methods for direct access to parsed body
|
|
11
|
+
* const users = await client.get("/api/users").json<User[]>();
|
|
12
|
+
* const html = await client.get("/page").text();
|
|
13
|
+
* const file = await client.get("/file").blob();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class ResponsePromise {
|
|
17
|
+
#responsePromise;
|
|
18
|
+
#options;
|
|
19
|
+
constructor(responsePromise, options) {
|
|
20
|
+
this.#responsePromise = responsePromise;
|
|
21
|
+
this.#options = options;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Implements PromiseLike interface so the ResponsePromise can be awaited.
|
|
25
|
+
*/
|
|
26
|
+
then(onfulfilled, onrejected) {
|
|
27
|
+
return this.#responsePromise.then(onfulfilled, onrejected);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Catches any errors from the response promise.
|
|
31
|
+
*/
|
|
32
|
+
catch(onrejected) {
|
|
33
|
+
return this.#responsePromise.catch(onrejected);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Executes a callback when the promise settles (fulfilled or rejected).
|
|
37
|
+
*/
|
|
38
|
+
finally(onfinally) {
|
|
39
|
+
return this.#responsePromise.finally(onfinally);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parses the response body as JSON.
|
|
43
|
+
*
|
|
44
|
+
* If the response was already parsed as JSON (via getJSON, postJSON, etc.),
|
|
45
|
+
* returns the parsed data directly. Otherwise, parses the response body.
|
|
46
|
+
*
|
|
47
|
+
* @template TJson - The expected type of the JSON response
|
|
48
|
+
* @returns A promise that resolves to the parsed JSON
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const user = await client.get("/api/user/1").json<User>();
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
async json() {
|
|
56
|
+
const response = await this.#responsePromise;
|
|
57
|
+
// If the response already has parsed data (from getJSON, etc.), return it
|
|
58
|
+
if (response.data !== null && response.data !== undefined) {
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
// Otherwise, parse the response body as JSON
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
// Apply reviver and date parsing if options are set
|
|
64
|
+
if (this.#options?.reviver || this.#options?.shouldParseDates) {
|
|
65
|
+
return this.#reviveJson(data);
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns the response body as text.
|
|
71
|
+
*
|
|
72
|
+
* @returns A promise that resolves to the response text
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const html = await client.get("/page").text();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
async text() {
|
|
80
|
+
const response = await this.#responsePromise;
|
|
81
|
+
return response.text();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Returns the response body as a Blob.
|
|
85
|
+
*
|
|
86
|
+
* @returns A promise that resolves to the response as a Blob
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const imageBlob = await client.get("/image.png").blob();
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
async blob() {
|
|
94
|
+
const response = await this.#responsePromise;
|
|
95
|
+
return response.blob();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns the response body as an ArrayBuffer.
|
|
99
|
+
*
|
|
100
|
+
* @returns A promise that resolves to the response as an ArrayBuffer
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const buffer = await client.get("/file").arrayBuffer();
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
async arrayBuffer() {
|
|
108
|
+
const response = await this.#responsePromise;
|
|
109
|
+
return response.arrayBuffer();
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Returns the response body as FormData.
|
|
113
|
+
*
|
|
114
|
+
* @returns A promise that resolves to the response as FormData
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* const formData = await client.get("/form").formData();
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
async formData() {
|
|
122
|
+
const response = await this.#responsePromise;
|
|
123
|
+
return response.formData();
|
|
124
|
+
}
|
|
125
|
+
#reviveJson(data) {
|
|
126
|
+
if (data === null || data === undefined) {
|
|
127
|
+
return data;
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(data)) {
|
|
130
|
+
return data.map((item) => this.#reviveJson(item));
|
|
131
|
+
}
|
|
132
|
+
if (typeof data === "object") {
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const [key, value] of Object.entries(data)) {
|
|
135
|
+
result[key] = this.#reviveValue(key, this.#reviveJson(value));
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
return this.#reviveValue("", data);
|
|
140
|
+
}
|
|
141
|
+
#reviveValue(key, value) {
|
|
142
|
+
let revivedValue = value;
|
|
143
|
+
if (this.#options?.reviver) {
|
|
144
|
+
revivedValue = this.#options.reviver.call(this, key, revivedValue);
|
|
145
|
+
}
|
|
146
|
+
if (this.#options?.shouldParseDates) {
|
|
147
|
+
revivedValue = this.#tryParseDate(revivedValue);
|
|
148
|
+
}
|
|
149
|
+
return revivedValue;
|
|
150
|
+
}
|
|
151
|
+
#tryParseDate(value) {
|
|
152
|
+
if (typeof value !== "string") {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
156
|
+
const date = new Date(value);
|
|
157
|
+
if (!isNaN(date.getTime())) {
|
|
158
|
+
return date;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
}
|