@foundatiofx/fetchclient 0.47.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 +5 -0
- package/esm/package.json +3 -0
- package/esm/src/Counter.js +36 -0
- package/esm/src/DefaultHelpers.js +132 -0
- package/esm/src/FetchClient.js +543 -0
- package/esm/src/FetchClientCache.js +88 -0
- package/esm/src/FetchClientContext.js +1 -0
- package/esm/src/FetchClientMiddleware.js +1 -0
- package/esm/src/FetchClientOptions.js +1 -0
- package/esm/src/FetchClientProvider.js +200 -0
- package/esm/src/FetchClientResponse.js +1 -0
- package/esm/src/LinkHeader.js +70 -0
- package/esm/src/ObjectEvent.js +15 -0
- package/esm/src/ProblemDetails.js +47 -0
- package/esm/src/RateLimitMiddleware.js +115 -0
- package/esm/src/RateLimiter.js +347 -0
- package/esm/src/RequestOptions.js +1 -0
- package/license +20 -0
- package/package.json +50 -0
- package/readme.md +303 -0
- package/script/mod.js +27 -0
- package/script/package.json +3 -0
- package/script/src/Counter.js +40 -0
- package/script/src/DefaultHelpers.js +149 -0
- package/script/src/FetchClient.js +547 -0
- package/script/src/FetchClientCache.js +92 -0
- package/script/src/FetchClientContext.js +2 -0
- package/script/src/FetchClientMiddleware.js +2 -0
- package/script/src/FetchClientOptions.js +2 -0
- package/script/src/FetchClientProvider.js +204 -0
- package/script/src/FetchClientResponse.js +2 -0
- package/script/src/LinkHeader.js +72 -0
- package/script/src/ObjectEvent.js +19 -0
- package/script/src/ProblemDetails.js +51 -0
- package/script/src/RateLimitMiddleware.js +120 -0
- package/script/src/RateLimiter.js +356 -0
- package/script/src/RequestOptions.js +2 -0
- package/types/_dnt.test_shims.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/almost_equals.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/array_includes.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/assert.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/assertion_error.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/equal.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/equals.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/exists.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/fail.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/false.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/greater.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/greater_or_equal.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/instance_of.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/is_error.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/less.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/less_or_equal.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/match.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/mod.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/not_equals.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/not_instance_of.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/not_match.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/not_strict_equals.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/object_match.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/rejects.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/strict_equals.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/string_includes.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/throws.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/unimplemented.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/assert/1.0.14/unreachable.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/build_message.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/diff.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/diff_str.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/format.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/styles.d.ts.map +1 -0
- package/types/deps/jsr.io/@std/internal/1.0.10/types.d.ts.map +1 -0
- package/types/mod.d.ts +11 -0
- package/types/mod.d.ts.map +1 -0
- package/types/src/Counter.d.ts +27 -0
- package/types/src/Counter.d.ts.map +1 -0
- package/types/src/DefaultHelpers.d.ts +106 -0
- package/types/src/DefaultHelpers.d.ts.map +1 -0
- package/types/src/FetchClient.d.ts +156 -0
- package/types/src/FetchClient.d.ts.map +1 -0
- package/types/src/FetchClient.test.d.ts.map +1 -0
- package/types/src/FetchClientCache.d.ts +62 -0
- package/types/src/FetchClientCache.d.ts.map +1 -0
- package/types/src/FetchClientContext.d.ts +8 -0
- package/types/src/FetchClientContext.d.ts.map +1 -0
- package/types/src/FetchClientMiddleware.d.ts +9 -0
- package/types/src/FetchClientMiddleware.d.ts.map +1 -0
- package/types/src/FetchClientOptions.d.ts +53 -0
- package/types/src/FetchClientOptions.d.ts.map +1 -0
- package/types/src/FetchClientProvider.d.ts +109 -0
- package/types/src/FetchClientProvider.d.ts.map +1 -0
- package/types/src/FetchClientResponse.d.ts +29 -0
- package/types/src/FetchClientResponse.d.ts.map +1 -0
- package/types/src/LinkHeader.d.ts +15 -0
- package/types/src/LinkHeader.d.ts.map +1 -0
- package/types/src/ObjectEvent.d.ts +20 -0
- package/types/src/ObjectEvent.d.ts.map +1 -0
- package/types/src/ProblemDetails.d.ts +43 -0
- package/types/src/ProblemDetails.d.ts.map +1 -0
- package/types/src/RateLimit.test.d.ts.map +1 -0
- package/types/src/RateLimitMiddleware.d.ts +50 -0
- package/types/src/RateLimitMiddleware.d.ts.map +1 -0
- package/types/src/RateLimiter.d.ts +179 -0
- package/types/src/RateLimiter.d.ts.map +1 -0
- package/types/src/RequestOptions.d.ts +64 -0
- package/types/src/RequestOptions.d.ts.map +1 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { Counter } from "./Counter.js";
|
|
2
|
+
import { ProblemDetails } from "./ProblemDetails.js";
|
|
3
|
+
import { parseLinkHeader } from "./LinkHeader.js";
|
|
4
|
+
import { FetchClientProvider } from "./FetchClientProvider.js";
|
|
5
|
+
import { getCurrentProvider } from "./DefaultHelpers.js";
|
|
6
|
+
import { ObjectEvent } from "./ObjectEvent.js";
|
|
7
|
+
/**
|
|
8
|
+
* Represents a client for making HTTP requests using the Fetch API.
|
|
9
|
+
*/
|
|
10
|
+
export class FetchClient {
|
|
11
|
+
#provider;
|
|
12
|
+
#options;
|
|
13
|
+
#counter = new Counter();
|
|
14
|
+
#middleware = [];
|
|
15
|
+
#onLoading = new ObjectEvent();
|
|
16
|
+
/**
|
|
17
|
+
* Represents a FetchClient that handles HTTP requests using the Fetch API.
|
|
18
|
+
* @param options - The options to use for the FetchClient.
|
|
19
|
+
*/
|
|
20
|
+
constructor(optionsOrProvider) {
|
|
21
|
+
if (optionsOrProvider instanceof FetchClientProvider) {
|
|
22
|
+
this.#provider = optionsOrProvider;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
this.#provider = optionsOrProvider?.provider ?? getCurrentProvider();
|
|
26
|
+
if (optionsOrProvider) {
|
|
27
|
+
this.#options = {
|
|
28
|
+
...this.#provider.options,
|
|
29
|
+
...optionsOrProvider,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.#counter.changed.on((e) => {
|
|
34
|
+
if (!e) {
|
|
35
|
+
throw new Error("Event data is required.");
|
|
36
|
+
}
|
|
37
|
+
if (e.value > 0 && e.previous == 0) {
|
|
38
|
+
this.#onLoading.trigger(true);
|
|
39
|
+
}
|
|
40
|
+
else if (e.value == 0 && e.previous > 0) {
|
|
41
|
+
this.#onLoading.trigger(false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets the provider used by this FetchClient instance. The provider contains shared options that can be used by multiple FetchClient instances.
|
|
47
|
+
*/
|
|
48
|
+
get provider() {
|
|
49
|
+
return this.#provider;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets the options used by this FetchClient instance.
|
|
53
|
+
*/
|
|
54
|
+
get options() {
|
|
55
|
+
return this.#options ?? this.#provider.options;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets the cache used for storing HTTP responses.
|
|
59
|
+
*/
|
|
60
|
+
get cache() {
|
|
61
|
+
return this.#options?.cache ?? this.#provider.cache;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Gets the fetch implementation used for making HTTP requests.
|
|
65
|
+
*/
|
|
66
|
+
get fetch() {
|
|
67
|
+
return this.#options?.fetch ?? this.#provider.fetch;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Gets the number of inflight requests for this FetchClient instance.
|
|
71
|
+
*/
|
|
72
|
+
get requestCount() {
|
|
73
|
+
return this.#counter.count;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Gets a value indicating whether the client is currently loading.
|
|
77
|
+
* @returns {boolean} A boolean value indicating whether the client is loading.
|
|
78
|
+
*/
|
|
79
|
+
get isLoading() {
|
|
80
|
+
return this.requestCount > 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Gets an event that is triggered when the loading state changes.
|
|
84
|
+
*/
|
|
85
|
+
get loading() {
|
|
86
|
+
return this.#onLoading.expose();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Adds one or more middleware functions to the FetchClient's middleware pipeline.
|
|
90
|
+
* Middleware functions are executed in the order they are added.
|
|
91
|
+
*
|
|
92
|
+
* @param mw - The middleware functions to add.
|
|
93
|
+
*/
|
|
94
|
+
use(...mw) {
|
|
95
|
+
this.#middleware.push(...mw);
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Sends a GET request to the specified URL.
|
|
100
|
+
*
|
|
101
|
+
* @param url - The URL to send the GET request to.
|
|
102
|
+
* @param options - The optional request options.
|
|
103
|
+
* @returns A promise that resolves to the response of the GET request.
|
|
104
|
+
*/
|
|
105
|
+
async get(url, options) {
|
|
106
|
+
options = {
|
|
107
|
+
...this.options.defaultRequestOptions,
|
|
108
|
+
...options,
|
|
109
|
+
};
|
|
110
|
+
const response = await this.fetchInternal(url, options, this.buildRequestInit("GET", undefined, options));
|
|
111
|
+
return response;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sends a GET request to the specified URL and returns the response as JSON.
|
|
115
|
+
* @param url - The URL to send the GET request to.
|
|
116
|
+
* @param options - Optional request options.
|
|
117
|
+
* @returns A promise that resolves to the response as JSON.
|
|
118
|
+
*/
|
|
119
|
+
getJSON(url, options) {
|
|
120
|
+
return this.get(url, this.buildJsonRequestOptions(options));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Sends a POST request to the specified URL.
|
|
124
|
+
*
|
|
125
|
+
* @param url - The URL to send the request to.
|
|
126
|
+
* @param body - The request body, can be an object, a string, or FormData.
|
|
127
|
+
* @param options - Additional options for the request.
|
|
128
|
+
* @returns A promise that resolves to a FetchClientResponse object.
|
|
129
|
+
*/
|
|
130
|
+
async post(url, body, options) {
|
|
131
|
+
options = {
|
|
132
|
+
...this.options.defaultRequestOptions,
|
|
133
|
+
...options,
|
|
134
|
+
};
|
|
135
|
+
const response = await this.fetchInternal(url, options, this.buildRequestInit("POST", body, options));
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Sends a POST request with JSON payload to the specified URL.
|
|
140
|
+
*
|
|
141
|
+
* @template T - The type of the response data.
|
|
142
|
+
* @param {string} url - The URL to send the request to.
|
|
143
|
+
* @param {object | string | FormData} [body] - The JSON payload or form data to send with the request.
|
|
144
|
+
* @param {RequestOptions} [options] - Additional options for the request.
|
|
145
|
+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
|
|
146
|
+
*/
|
|
147
|
+
postJSON(url, body, options) {
|
|
148
|
+
return this.post(url, body, this.buildJsonRequestOptions(options));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Sends a PUT request to the specified URL with the given body and options.
|
|
152
|
+
* @param url - The URL to send the request to.
|
|
153
|
+
* @param body - The request body, can be an object, a string, or FormData.
|
|
154
|
+
* @param options - The request options.
|
|
155
|
+
* @returns A promise that resolves to a FetchClientResponse object.
|
|
156
|
+
*/
|
|
157
|
+
async put(url, body, options) {
|
|
158
|
+
options = {
|
|
159
|
+
...this.options.defaultRequestOptions,
|
|
160
|
+
...options,
|
|
161
|
+
};
|
|
162
|
+
const response = await this.fetchInternal(url, options, this.buildRequestInit("PUT", body, options));
|
|
163
|
+
return response;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sends a PUT request with JSON payload to the specified URL.
|
|
167
|
+
*
|
|
168
|
+
* @template T - The type of the response data.
|
|
169
|
+
* @param {string} url - The URL to send the request to.
|
|
170
|
+
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
171
|
+
* @param {RequestOptions} [options] - Additional options for the request.
|
|
172
|
+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
|
|
173
|
+
*/
|
|
174
|
+
putJSON(url, body, options) {
|
|
175
|
+
return this.put(url, body, this.buildJsonRequestOptions(options));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Sends a PATCH request to the specified URL with the provided body and options.
|
|
179
|
+
* @param url - The URL to send the PATCH request to.
|
|
180
|
+
* @param body - The body of the request. It can be an object, a string, or FormData.
|
|
181
|
+
* @param options - The options for the request.
|
|
182
|
+
* @returns A Promise that resolves to the response of the PATCH request.
|
|
183
|
+
*/
|
|
184
|
+
async patch(url, body, options) {
|
|
185
|
+
options = {
|
|
186
|
+
...this.options.defaultRequestOptions,
|
|
187
|
+
...options,
|
|
188
|
+
};
|
|
189
|
+
const response = await this.fetchInternal(url, options, this.buildRequestInit("PATCH", body, options));
|
|
190
|
+
return response;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Sends a PATCH request with JSON payload to the specified URL.
|
|
194
|
+
*
|
|
195
|
+
* @template T - The type of the response data.
|
|
196
|
+
* @param {string} url - The URL to send the request to.
|
|
197
|
+
* @param {object | string} [body] - The JSON payload to send with the request.
|
|
198
|
+
* @param {RequestOptions} [options] - Additional options for the request.
|
|
199
|
+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
|
|
200
|
+
*/
|
|
201
|
+
patchJSON(url, body, options) {
|
|
202
|
+
return this.patch(url, body, this.buildJsonRequestOptions(options));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Sends a DELETE request to the specified URL.
|
|
206
|
+
*
|
|
207
|
+
* @param url - The URL to send the DELETE request to.
|
|
208
|
+
* @param options - The options for the request.
|
|
209
|
+
* @returns A promise that resolves to a `FetchClientResponse` object.
|
|
210
|
+
*/
|
|
211
|
+
async delete(url, options) {
|
|
212
|
+
options = {
|
|
213
|
+
...this.options.defaultRequestOptions,
|
|
214
|
+
...options,
|
|
215
|
+
};
|
|
216
|
+
const response = await this.fetchInternal(url, options, this.buildRequestInit("DELETE", undefined, options));
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Sends a DELETE request with JSON payload to the specified URL.
|
|
221
|
+
*
|
|
222
|
+
* @template T - The type of the response data.
|
|
223
|
+
* @param {string} url - The URL to send the request to.
|
|
224
|
+
* @param {RequestOptions} [options] - Additional options for the request.
|
|
225
|
+
* @returns {Promise<FetchClientResponse<T>>} - A promise that resolves to the response data.
|
|
226
|
+
*/
|
|
227
|
+
deleteJSON(url, options) {
|
|
228
|
+
return this.delete(url, this.buildJsonRequestOptions(options));
|
|
229
|
+
}
|
|
230
|
+
async validate(data, options) {
|
|
231
|
+
if (typeof data !== "object" ||
|
|
232
|
+
(options && options.shouldValidateModel === false))
|
|
233
|
+
return null;
|
|
234
|
+
if (this.options?.modelValidator === undefined) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const problem = await this.options.modelValidator(data);
|
|
238
|
+
if (!problem)
|
|
239
|
+
return null;
|
|
240
|
+
return problem;
|
|
241
|
+
}
|
|
242
|
+
async fetchInternal(url, options, init) {
|
|
243
|
+
const { builtUrl, absoluteUrl } = this.buildUrl(url, options);
|
|
244
|
+
// if we have a body and it's not FormData, validate it before proceeding
|
|
245
|
+
if (init?.body && !(init?.body instanceof FormData)) {
|
|
246
|
+
const problem = await this.validate(init?.body, options);
|
|
247
|
+
if (problem) {
|
|
248
|
+
return this.problemToResponse(problem, url);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (init?.body && typeof init.body === "object") {
|
|
252
|
+
init.body = JSON.stringify(init.body);
|
|
253
|
+
}
|
|
254
|
+
const accessToken = this.options.accessTokenFunc?.() ?? null;
|
|
255
|
+
if (accessToken !== null) {
|
|
256
|
+
init = {
|
|
257
|
+
...init,
|
|
258
|
+
...{
|
|
259
|
+
headers: { ...init?.headers, Authorization: `Bearer ${accessToken}` },
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (options?.signal) {
|
|
264
|
+
init = { ...init, signal: options.signal };
|
|
265
|
+
}
|
|
266
|
+
if (options?.timeout) {
|
|
267
|
+
let signal = AbortSignal.timeout(options.timeout);
|
|
268
|
+
if (init?.signal) {
|
|
269
|
+
signal = this.mergeAbortSignals(signal, init.signal);
|
|
270
|
+
}
|
|
271
|
+
init = { ...init, signal: signal };
|
|
272
|
+
}
|
|
273
|
+
const fetchMiddleware = async (ctx, next) => {
|
|
274
|
+
const getOptions = ctx.options;
|
|
275
|
+
if (getOptions?.cacheKey) {
|
|
276
|
+
const cachedResponse = this.cache.get(getOptions.cacheKey);
|
|
277
|
+
if (cachedResponse) {
|
|
278
|
+
ctx.response = cachedResponse;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const response = await (this.fetch ? this.fetch(ctx.request) : fetch(ctx.request));
|
|
284
|
+
if (ctx.request.headers.get("Accept")?.startsWith("application/json") ||
|
|
285
|
+
response?.headers.get("Content-Type")?.startsWith("application/problem+json")) {
|
|
286
|
+
ctx.response = await this.getJSONResponse(response, ctx.options);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
ctx.response = response;
|
|
290
|
+
ctx.response.data = null;
|
|
291
|
+
ctx.response.problem = new ProblemDetails();
|
|
292
|
+
}
|
|
293
|
+
ctx.response.meta = {
|
|
294
|
+
links: parseLinkHeader(response.headers.get("Link")) || {},
|
|
295
|
+
};
|
|
296
|
+
if (getOptions?.cacheKey) {
|
|
297
|
+
this.cache.set(getOptions.cacheKey, ctx.response, getOptions.cacheDuration);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error instanceof Error && error.name === "TimeoutError") {
|
|
302
|
+
ctx.response = this.problemToResponse(Object.assign(new ProblemDetails(), {
|
|
303
|
+
status: 408,
|
|
304
|
+
title: "Request Timeout",
|
|
305
|
+
}), ctx.request.url);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
await next();
|
|
312
|
+
};
|
|
313
|
+
const middleware = [
|
|
314
|
+
...this.options.middleware ?? [],
|
|
315
|
+
...this.#middleware,
|
|
316
|
+
fetchMiddleware,
|
|
317
|
+
];
|
|
318
|
+
this.#counter.increment();
|
|
319
|
+
this.#provider.counter.increment();
|
|
320
|
+
let request = null;
|
|
321
|
+
try {
|
|
322
|
+
request = new Request(builtUrl, init);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// try using absolute URL
|
|
326
|
+
request = new Request(absoluteUrl, init);
|
|
327
|
+
}
|
|
328
|
+
const context = {
|
|
329
|
+
options,
|
|
330
|
+
request: request,
|
|
331
|
+
response: null,
|
|
332
|
+
meta: {},
|
|
333
|
+
};
|
|
334
|
+
await this.invokeMiddleware(context, middleware);
|
|
335
|
+
this.#counter.decrement();
|
|
336
|
+
this.#provider.counter.decrement();
|
|
337
|
+
this.validateResponse(context.response, options);
|
|
338
|
+
return context.response;
|
|
339
|
+
}
|
|
340
|
+
async invokeMiddleware(context, middleware) {
|
|
341
|
+
if (!middleware.length)
|
|
342
|
+
return;
|
|
343
|
+
const mw = middleware[0];
|
|
344
|
+
return await mw(context, async () => {
|
|
345
|
+
await this.invokeMiddleware(context, middleware.slice(1));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
mergeAbortSignals(...signals) {
|
|
349
|
+
const controller = new AbortController();
|
|
350
|
+
const onAbort = (event) => {
|
|
351
|
+
const originalSignal = event.target;
|
|
352
|
+
try {
|
|
353
|
+
controller.abort(originalSignal.reason);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Just in case multiple signals abort nearly simultaneously
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
for (const signal of signals) {
|
|
360
|
+
if (signal.aborted) {
|
|
361
|
+
controller.abort(signal.reason);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
signal.addEventListener("abort", onAbort);
|
|
365
|
+
}
|
|
366
|
+
return controller.signal;
|
|
367
|
+
}
|
|
368
|
+
async getJSONResponse(response, options) {
|
|
369
|
+
let data = null;
|
|
370
|
+
let bodyText = "";
|
|
371
|
+
try {
|
|
372
|
+
bodyText = await response.text();
|
|
373
|
+
if (options.reviver || options.shouldParseDates) {
|
|
374
|
+
data = JSON.parse(bodyText, (key, value) => {
|
|
375
|
+
return this.reviveJsonValue(options, key, value);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
data = JSON.parse(bodyText);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
data = new ProblemDetails();
|
|
384
|
+
data.detail = bodyText;
|
|
385
|
+
data.title = `Unable to deserialize response data: ${error instanceof Error ? error.message : String(error)}`;
|
|
386
|
+
data.setErrorMessage(data.title);
|
|
387
|
+
}
|
|
388
|
+
const jsonResponse = response;
|
|
389
|
+
if (!response.ok ||
|
|
390
|
+
response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
|
|
391
|
+
jsonResponse.problem = Object.assign(new ProblemDetails(), data);
|
|
392
|
+
jsonResponse.data = null;
|
|
393
|
+
return jsonResponse;
|
|
394
|
+
}
|
|
395
|
+
jsonResponse.problem = new ProblemDetails();
|
|
396
|
+
jsonResponse.data = data;
|
|
397
|
+
return jsonResponse;
|
|
398
|
+
}
|
|
399
|
+
reviveJsonValue(options, key, value) {
|
|
400
|
+
let revivedValued = value;
|
|
401
|
+
if (options.reviver) {
|
|
402
|
+
revivedValued = options.reviver.call(this, key, revivedValued);
|
|
403
|
+
}
|
|
404
|
+
if (options.shouldParseDates) {
|
|
405
|
+
revivedValued = this.tryParseDate(key, revivedValued);
|
|
406
|
+
}
|
|
407
|
+
return revivedValued;
|
|
408
|
+
}
|
|
409
|
+
tryParseDate(_key, value) {
|
|
410
|
+
if (typeof value !== "string") {
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
|
414
|
+
const date = new Date(value);
|
|
415
|
+
if (!isNaN(date.getTime())) {
|
|
416
|
+
return date;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
buildRequestInit(method, body, options) {
|
|
422
|
+
const isDefinitelyJsonBody = body !== undefined &&
|
|
423
|
+
body !== null &&
|
|
424
|
+
typeof body === "object";
|
|
425
|
+
const headers = {};
|
|
426
|
+
if (isDefinitelyJsonBody) {
|
|
427
|
+
headers["Content-Type"] = "application/json";
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
method,
|
|
431
|
+
headers: {
|
|
432
|
+
...headers,
|
|
433
|
+
...options?.headers,
|
|
434
|
+
},
|
|
435
|
+
body,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
buildJsonRequestOptions(options) {
|
|
439
|
+
return {
|
|
440
|
+
headers: {
|
|
441
|
+
"Accept": "application/json, application/problem+json",
|
|
442
|
+
...options?.headers,
|
|
443
|
+
},
|
|
444
|
+
...options,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
problemToResponse(problem, url) {
|
|
448
|
+
const headers = new Headers();
|
|
449
|
+
headers.set("Content-Type", "application/problem+json");
|
|
450
|
+
return {
|
|
451
|
+
url,
|
|
452
|
+
status: problem.status ?? 422,
|
|
453
|
+
statusText: problem.title ?? "Unprocessable Entity",
|
|
454
|
+
body: null,
|
|
455
|
+
bodyUsed: true,
|
|
456
|
+
ok: false,
|
|
457
|
+
headers: headers,
|
|
458
|
+
redirected: false,
|
|
459
|
+
problem: problem,
|
|
460
|
+
data: null,
|
|
461
|
+
meta: { links: {} },
|
|
462
|
+
type: "basic",
|
|
463
|
+
json: () => new Promise((resolve) => resolve(problem)),
|
|
464
|
+
text: () => new Promise((resolve) => resolve(JSON.stringify(problem))),
|
|
465
|
+
arrayBuffer: () => new Promise((resolve) => resolve(new ArrayBuffer(0))),
|
|
466
|
+
// @ts-ignore: New in Deno 1.44
|
|
467
|
+
bytes: () => new Promise((resolve) => resolve(new Uint8Array())),
|
|
468
|
+
blob: () => new Promise((resolve) => resolve(new Blob())),
|
|
469
|
+
formData: () => new Promise((resolve) => resolve(new FormData())),
|
|
470
|
+
clone: () => {
|
|
471
|
+
throw new Error("Not implemented");
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
buildUrl(url, options) {
|
|
476
|
+
let builtUrl = url;
|
|
477
|
+
if (!builtUrl.startsWith("http") && this.options?.baseUrl) {
|
|
478
|
+
if (this.options.baseUrl.endsWith("/") || builtUrl.startsWith("/")) {
|
|
479
|
+
builtUrl = this.options.baseUrl + builtUrl;
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
builtUrl = this.options.baseUrl + "/" + builtUrl;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const isAbsoluteUrl = builtUrl.startsWith("http");
|
|
486
|
+
let parsed = undefined;
|
|
487
|
+
if (isAbsoluteUrl) {
|
|
488
|
+
parsed = new URL(builtUrl);
|
|
489
|
+
}
|
|
490
|
+
else if (globalThis.location?.origin &&
|
|
491
|
+
globalThis.location?.origin.startsWith("http")) {
|
|
492
|
+
if (builtUrl.startsWith("/")) {
|
|
493
|
+
parsed = new URL(builtUrl, globalThis.location.origin);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
parsed = new URL(builtUrl, globalThis.location.origin + "/");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
if (builtUrl.startsWith("/")) {
|
|
501
|
+
parsed = new URL(builtUrl, "http://localhost");
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
parsed = new URL(builtUrl, "http://localhost/");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (options?.params) {
|
|
508
|
+
for (const [key, value] of Object.entries(options?.params)) {
|
|
509
|
+
if (value !== undefined && value !== null && !parsed.searchParams.has(key)) {
|
|
510
|
+
parsed.searchParams.set(key, value);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
builtUrl = parsed.toString();
|
|
515
|
+
const result = isAbsoluteUrl
|
|
516
|
+
? builtUrl
|
|
517
|
+
: `${parsed.pathname}${parsed.search}`;
|
|
518
|
+
return { builtUrl: result, absoluteUrl: builtUrl };
|
|
519
|
+
}
|
|
520
|
+
validateResponse(response, options) {
|
|
521
|
+
if (!response) {
|
|
522
|
+
throw new Error("Response is null");
|
|
523
|
+
}
|
|
524
|
+
if (response.ok || options?.shouldThrowOnUnexpectedStatusCodes === false) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (options?.expectedStatusCodes &&
|
|
528
|
+
options.expectedStatusCodes.includes(response.status)) {
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (options?.errorCallback) {
|
|
532
|
+
const result = options.errorCallback(response);
|
|
533
|
+
if (result === true) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
response.problem ??= new ProblemDetails();
|
|
538
|
+
response.problem.status = response.status;
|
|
539
|
+
response.problem.title = `Unexpected status code: ${response.status}`;
|
|
540
|
+
response.problem.setErrorMessage(response.problem.title);
|
|
541
|
+
throw response;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a cache for storing responses from the FetchClient.
|
|
3
|
+
*/
|
|
4
|
+
export class FetchClientCache {
|
|
5
|
+
cache = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* Sets a response in the cache with the specified key.
|
|
8
|
+
* @param key - The cache key.
|
|
9
|
+
* @param response - The response to be cached.
|
|
10
|
+
* @param cacheDuration - The duration for which the response should be cached (in milliseconds).
|
|
11
|
+
*/
|
|
12
|
+
set(key, response, cacheDuration) {
|
|
13
|
+
this.cache.set(this.getHash(key), {
|
|
14
|
+
key,
|
|
15
|
+
lastAccess: new Date(),
|
|
16
|
+
expires: new Date(Date.now() + (cacheDuration ?? 60000)),
|
|
17
|
+
response,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Retrieves a response from the cache with the specified key.
|
|
22
|
+
* @param key - The cache key.
|
|
23
|
+
* @returns The cached response, or null if the response is not found or has expired.
|
|
24
|
+
*/
|
|
25
|
+
get(key) {
|
|
26
|
+
const cacheEntry = this.cache.get(this.getHash(key));
|
|
27
|
+
if (!cacheEntry) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (cacheEntry.expires < new Date()) {
|
|
31
|
+
this.cache.delete(this.getHash(key));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
cacheEntry.lastAccess = new Date();
|
|
35
|
+
return cacheEntry.response;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Deletes a response from the cache with the specified key.
|
|
39
|
+
* @param key - The cache key.
|
|
40
|
+
* @returns True if the response was successfully deleted, false otherwise.
|
|
41
|
+
*/
|
|
42
|
+
delete(key) {
|
|
43
|
+
return this.cache.delete(this.getHash(key));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deletes all responses from the cache that have keys beginning with the specified key.
|
|
47
|
+
* @param prefix - The cache key prefix.
|
|
48
|
+
* @returns The number of responses that were deleted.
|
|
49
|
+
*/
|
|
50
|
+
deleteAll(prefix) {
|
|
51
|
+
let count = 0;
|
|
52
|
+
for (const key of this.cache.keys()) {
|
|
53
|
+
if (key.startsWith(this.getHash(prefix))) {
|
|
54
|
+
if (this.cache.delete(key)) {
|
|
55
|
+
count++;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks if a response exists in the cache with the specified key.
|
|
63
|
+
* @param key - The cache key.
|
|
64
|
+
* @returns True if the response exists in the cache, false otherwise.
|
|
65
|
+
*/
|
|
66
|
+
has(key) {
|
|
67
|
+
return this.cache.has(this.getHash(key));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns an iterator for the cache entries.
|
|
71
|
+
* @returns An iterator for the cache entries.
|
|
72
|
+
*/
|
|
73
|
+
values() {
|
|
74
|
+
return this.cache.values();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Clears all entries from the cache.
|
|
78
|
+
*/
|
|
79
|
+
clear() {
|
|
80
|
+
this.cache.clear();
|
|
81
|
+
}
|
|
82
|
+
getHash(key) {
|
|
83
|
+
if (key instanceof Array) {
|
|
84
|
+
return key.join(":");
|
|
85
|
+
}
|
|
86
|
+
return key;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|