create-request 0.0.1-beta.11 → 0.0.1-beta.12
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/dist/library/index.d.ts +4 -14
- package/dist/library/index.esm.js +1 -1907
- package/dist/library/index.esm.js.map +1 -0
- package/dist/library/index.js +1 -1931
- package/dist/library/index.js.map +1 -0
- package/dist/library/utils/sizeUtils.d.ts +8 -0
- package/package.json +34 -12
package/dist/library/index.js
CHANGED
|
@@ -1,1931 +1 @@
|
|
|
1
|
-
(function (global, factory) {
|
|
2
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
-
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.createRequest = {}));
|
|
5
|
-
})(this, (function (exports) { 'use strict';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Enum for HTTP methods
|
|
9
|
-
*/
|
|
10
|
-
exports.HttpMethod = void 0;
|
|
11
|
-
(function (HttpMethod) {
|
|
12
|
-
HttpMethod["GET"] = "GET";
|
|
13
|
-
HttpMethod["PUT"] = "PUT";
|
|
14
|
-
HttpMethod["POST"] = "POST";
|
|
15
|
-
HttpMethod["HEAD"] = "HEAD";
|
|
16
|
-
HttpMethod["PATCH"] = "PATCH";
|
|
17
|
-
HttpMethod["DELETE"] = "DELETE";
|
|
18
|
-
HttpMethod["OPTIONS"] = "OPTIONS";
|
|
19
|
-
})(exports.HttpMethod || (exports.HttpMethod = {}));
|
|
20
|
-
/**
|
|
21
|
-
* Enum for request priorities
|
|
22
|
-
*/
|
|
23
|
-
exports.RequestPriority = void 0;
|
|
24
|
-
(function (RequestPriority) {
|
|
25
|
-
RequestPriority["LOW"] = "low";
|
|
26
|
-
RequestPriority["HIGH"] = "high";
|
|
27
|
-
RequestPriority["AUTO"] = "auto";
|
|
28
|
-
})(exports.RequestPriority || (exports.RequestPriority = {}));
|
|
29
|
-
/**
|
|
30
|
-
* Enum for credentials policies
|
|
31
|
-
*/
|
|
32
|
-
exports.CredentialsPolicy = void 0;
|
|
33
|
-
(function (CredentialsPolicy) {
|
|
34
|
-
CredentialsPolicy["OMIT"] = "omit";
|
|
35
|
-
CredentialsPolicy["INCLUDE"] = "include";
|
|
36
|
-
CredentialsPolicy["SAME_ORIGIN"] = "same-origin";
|
|
37
|
-
})(exports.CredentialsPolicy || (exports.CredentialsPolicy = {}));
|
|
38
|
-
/**
|
|
39
|
-
* Enum for request modes
|
|
40
|
-
*/
|
|
41
|
-
exports.RequestMode = void 0;
|
|
42
|
-
(function (RequestMode) {
|
|
43
|
-
RequestMode["CORS"] = "cors";
|
|
44
|
-
RequestMode["NO_CORS"] = "no-cors";
|
|
45
|
-
RequestMode["SAME_ORIGIN"] = "same-origin";
|
|
46
|
-
RequestMode["NAVIGATE"] = "navigate";
|
|
47
|
-
})(exports.RequestMode || (exports.RequestMode = {}));
|
|
48
|
-
/**
|
|
49
|
-
* Enum for redirect modes
|
|
50
|
-
*/
|
|
51
|
-
exports.RedirectMode = void 0;
|
|
52
|
-
(function (RedirectMode) {
|
|
53
|
-
RedirectMode["ERROR"] = "error";
|
|
54
|
-
RedirectMode["FOLLOW"] = "follow";
|
|
55
|
-
RedirectMode["MANUAL"] = "manual";
|
|
56
|
-
})(exports.RedirectMode || (exports.RedirectMode = {}));
|
|
57
|
-
/**
|
|
58
|
-
* Enum for cache modes
|
|
59
|
-
*/
|
|
60
|
-
exports.CacheMode = void 0;
|
|
61
|
-
(function (CacheMode) {
|
|
62
|
-
CacheMode["DEFAULT"] = "default";
|
|
63
|
-
CacheMode["RELOAD"] = "reload";
|
|
64
|
-
CacheMode["NO_STORE"] = "no-store";
|
|
65
|
-
CacheMode["NO_CACHE"] = "no-cache";
|
|
66
|
-
CacheMode["FORCE_CACHE"] = "force-cache";
|
|
67
|
-
CacheMode["ONLY_IF_CACHED"] = "only-if-cached";
|
|
68
|
-
})(exports.CacheMode || (exports.CacheMode = {}));
|
|
69
|
-
/**
|
|
70
|
-
* Enum for cookie SameSite policies
|
|
71
|
-
*/
|
|
72
|
-
exports.SameSitePolicy = void 0;
|
|
73
|
-
(function (SameSitePolicy) {
|
|
74
|
-
SameSitePolicy["LAX"] = "Lax";
|
|
75
|
-
SameSitePolicy["NONE"] = "None";
|
|
76
|
-
SameSitePolicy["STRICT"] = "Strict";
|
|
77
|
-
})(exports.SameSitePolicy || (exports.SameSitePolicy = {}));
|
|
78
|
-
/**
|
|
79
|
-
* Enum for body types
|
|
80
|
-
*/
|
|
81
|
-
var BodyType;
|
|
82
|
-
(function (BodyType) {
|
|
83
|
-
BodyType["JSON"] = "json";
|
|
84
|
-
BodyType["STRING"] = "string";
|
|
85
|
-
BodyType["BINARY"] = "binary";
|
|
86
|
-
})(BodyType || (BodyType = {}));
|
|
87
|
-
/**
|
|
88
|
-
* Referrer policies for fetch requests
|
|
89
|
-
*/
|
|
90
|
-
exports.ReferrerPolicy = void 0;
|
|
91
|
-
(function (ReferrerPolicy) {
|
|
92
|
-
ReferrerPolicy["ORIGIN"] = "origin";
|
|
93
|
-
ReferrerPolicy["UNSAFE_URL"] = "unsafe-url";
|
|
94
|
-
ReferrerPolicy["SAME_ORIGIN"] = "same-origin";
|
|
95
|
-
ReferrerPolicy["NO_REFERRER"] = "no-referrer";
|
|
96
|
-
ReferrerPolicy["STRICT_ORIGIN"] = "strict-origin";
|
|
97
|
-
ReferrerPolicy["ORIGIN_WHEN_CROSS_ORIGIN"] = "origin-when-cross-origin";
|
|
98
|
-
ReferrerPolicy["NO_REFERRER_WHEN_DOWNGRADE"] = "no-referrer-when-downgrade";
|
|
99
|
-
ReferrerPolicy["STRICT_ORIGIN_WHEN_CROSS_ORIGIN"] = "strict-origin-when-cross-origin";
|
|
100
|
-
})(exports.ReferrerPolicy || (exports.ReferrerPolicy = {}));
|
|
101
|
-
|
|
102
|
-
class RequestError extends Error {
|
|
103
|
-
constructor(message, url, method, options = {}) {
|
|
104
|
-
super(message);
|
|
105
|
-
this.name = "RequestError";
|
|
106
|
-
this.url = url;
|
|
107
|
-
this.method = method;
|
|
108
|
-
this.status = options.status;
|
|
109
|
-
this.response = options.response;
|
|
110
|
-
this.timeoutError = options.timeoutError;
|
|
111
|
-
// For better stack traces in modern environments
|
|
112
|
-
if (Error.captureStackTrace) {
|
|
113
|
-
Error.captureStackTrace(this, RequestError);
|
|
114
|
-
}
|
|
115
|
-
// Maintains proper prototype chain for instanceof checks
|
|
116
|
-
Object.setPrototypeOf(this, RequestError.prototype);
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Static methods for creating specific types of RequestError
|
|
120
|
-
*/
|
|
121
|
-
static timeout(url, method, timeoutMs) {
|
|
122
|
-
return new RequestError(`Request timed out after ${timeoutMs}ms`, url, method, {
|
|
123
|
-
timeoutError: true,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
static fromResponse(response, url, method) {
|
|
127
|
-
return new RequestError(`Request failed with status ${response.status}`, url, method, {
|
|
128
|
-
status: response.status,
|
|
129
|
-
response,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
static networkError(url, method, originalError) {
|
|
133
|
-
const error = new RequestError(`Network error: ${originalError.message}`, url, method);
|
|
134
|
-
error.stack = originalError.stack;
|
|
135
|
-
return error;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Wrapper for HTTP responses with methods to transform the response data
|
|
141
|
-
* Provides caching and conversion between different response formats.
|
|
142
|
-
*/
|
|
143
|
-
class ResponseWrapper {
|
|
144
|
-
constructor(response) {
|
|
145
|
-
this.bodyUsed = false;
|
|
146
|
-
this.response = response;
|
|
147
|
-
}
|
|
148
|
-
// Wrapper properties
|
|
149
|
-
get status() {
|
|
150
|
-
return this.response.status;
|
|
151
|
-
}
|
|
152
|
-
get statusText() {
|
|
153
|
-
return this.response.statusText;
|
|
154
|
-
}
|
|
155
|
-
get headers() {
|
|
156
|
-
return this.response.headers;
|
|
157
|
-
}
|
|
158
|
-
get ok() {
|
|
159
|
-
return this.response.ok;
|
|
160
|
-
}
|
|
161
|
-
get raw() {
|
|
162
|
-
return this.response;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Parse the response body as JSON
|
|
166
|
-
* Caches the result for subsequent calls.
|
|
167
|
-
*
|
|
168
|
-
* @returns The parsed JSON data
|
|
169
|
-
* @throws Error if the response has already been consumed in a different format
|
|
170
|
-
*
|
|
171
|
-
* @example
|
|
172
|
-
* const data = await response.getJson();
|
|
173
|
-
* console.log(data.items);
|
|
174
|
-
*/
|
|
175
|
-
async getJson() {
|
|
176
|
-
if (this.jsonCache !== undefined) {
|
|
177
|
-
return this.jsonCache;
|
|
178
|
-
}
|
|
179
|
-
if (this.bodyUsed) {
|
|
180
|
-
// If we already have text, try to parse it as JSON
|
|
181
|
-
if (this.textCache !== undefined) {
|
|
182
|
-
try {
|
|
183
|
-
// Fix unsafe assignment by explicitly casting to unknown first
|
|
184
|
-
this.jsonCache = JSON.parse(this.textCache);
|
|
185
|
-
return this.jsonCache;
|
|
186
|
-
}
|
|
187
|
-
catch (e) {
|
|
188
|
-
throw new Error("Cannot parse cached text as JSON");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
throw new Error("Response body has already been consumed in a different format");
|
|
192
|
-
}
|
|
193
|
-
this.bodyUsed = true;
|
|
194
|
-
this.jsonCache = (await this.response.json());
|
|
195
|
-
return this.jsonCache;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Get the response body as text
|
|
199
|
-
* Caches the result for subsequent calls. Attempts to convert from JSON or Blob
|
|
200
|
-
* if the body has already been consumed in those formats.
|
|
201
|
-
*
|
|
202
|
-
* @returns The response text
|
|
203
|
-
* @throws Error if the response has already been consumed in an incompatible format
|
|
204
|
-
*
|
|
205
|
-
* @example
|
|
206
|
-
* const text = await response.getText();
|
|
207
|
-
*/
|
|
208
|
-
async getText() {
|
|
209
|
-
if (this.textCache !== undefined) {
|
|
210
|
-
return this.textCache;
|
|
211
|
-
}
|
|
212
|
-
if (this.bodyUsed) {
|
|
213
|
-
// If we already have JSON, convert it to text
|
|
214
|
-
if (this.jsonCache !== undefined) {
|
|
215
|
-
this.textCache = JSON.stringify(this.jsonCache);
|
|
216
|
-
return this.textCache;
|
|
217
|
-
}
|
|
218
|
-
// If we have blob, convert to text
|
|
219
|
-
if (this.blobCache !== undefined) {
|
|
220
|
-
this.textCache = await this.blobCache.text();
|
|
221
|
-
return this.textCache;
|
|
222
|
-
}
|
|
223
|
-
throw new Error("Response body has already been consumed in a format that cannot be converted to text");
|
|
224
|
-
}
|
|
225
|
-
this.bodyUsed = true;
|
|
226
|
-
this.textCache = await this.response.text();
|
|
227
|
-
return this.textCache;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Get the response body as a Blob
|
|
231
|
-
* Caches the result for subsequent calls. Attempts to convert from text
|
|
232
|
-
* if the body has already been consumed as text.
|
|
233
|
-
*
|
|
234
|
-
* @returns The response as a Blob
|
|
235
|
-
* @throws Error if the response has already been consumed in an incompatible format
|
|
236
|
-
*
|
|
237
|
-
* @example
|
|
238
|
-
* const blob = await response.getBlob();
|
|
239
|
-
* const url = URL.createObjectURL(blob);
|
|
240
|
-
*/
|
|
241
|
-
async getBlob() {
|
|
242
|
-
if (this.blobCache !== undefined) {
|
|
243
|
-
return this.blobCache;
|
|
244
|
-
}
|
|
245
|
-
if (this.bodyUsed) {
|
|
246
|
-
// If we have text, convert to Blob
|
|
247
|
-
if (this.textCache !== undefined) {
|
|
248
|
-
this.blobCache = new Blob([this.textCache], { type: "text/plain" });
|
|
249
|
-
return this.blobCache;
|
|
250
|
-
}
|
|
251
|
-
throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
|
|
252
|
-
}
|
|
253
|
-
this.bodyUsed = true;
|
|
254
|
-
this.blobCache = await this.response.blob();
|
|
255
|
-
return this.blobCache;
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Get the response body as an ArrayBuffer
|
|
259
|
-
* Caches the result for subsequent calls. Attempts to convert from text or Blob
|
|
260
|
-
* if the body has already been consumed in those formats.
|
|
261
|
-
*
|
|
262
|
-
* @returns The response as an ArrayBuffer
|
|
263
|
-
* @throws Error if the response has already been consumed in an incompatible format
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* const buffer = await response.getArrayBuffer();
|
|
267
|
-
*/
|
|
268
|
-
async getArrayBuffer() {
|
|
269
|
-
if (this.arrayBufferCache !== undefined) {
|
|
270
|
-
return this.arrayBufferCache;
|
|
271
|
-
}
|
|
272
|
-
if (this.bodyUsed) {
|
|
273
|
-
// If we have text, convert to ArrayBuffer
|
|
274
|
-
if (this.textCache !== undefined) {
|
|
275
|
-
const encoder = new TextEncoder();
|
|
276
|
-
// Fix ArrayBufferLike by explicitly casting to ArrayBuffer
|
|
277
|
-
const uint8Array = encoder.encode(this.textCache);
|
|
278
|
-
this.arrayBufferCache = uint8Array.buffer;
|
|
279
|
-
return this.arrayBufferCache;
|
|
280
|
-
}
|
|
281
|
-
// If we have blob, convert to ArrayBuffer
|
|
282
|
-
if (this.blobCache !== undefined) {
|
|
283
|
-
this.arrayBufferCache = await this.blobCache.arrayBuffer();
|
|
284
|
-
return this.arrayBufferCache;
|
|
285
|
-
}
|
|
286
|
-
throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
|
|
287
|
-
}
|
|
288
|
-
this.bodyUsed = true;
|
|
289
|
-
this.arrayBufferCache = await this.response.arrayBuffer();
|
|
290
|
-
return this.arrayBufferCache;
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Get the raw response body as a ReadableStream
|
|
294
|
-
* This method consumes the body and should only be called once.
|
|
295
|
-
*
|
|
296
|
-
* @returns The response body as a ReadableStream or null
|
|
297
|
-
* @throws Error if the response body has already been consumed
|
|
298
|
-
*
|
|
299
|
-
* @example
|
|
300
|
-
* const stream = response.getBody();
|
|
301
|
-
* if (stream) {
|
|
302
|
-
* const reader = stream.getReader();
|
|
303
|
-
* // Process the stream
|
|
304
|
-
* }
|
|
305
|
-
*/
|
|
306
|
-
getBody() {
|
|
307
|
-
if (this.bodyUsed) {
|
|
308
|
-
throw new Error("Response body has already been consumed");
|
|
309
|
-
}
|
|
310
|
-
this.bodyUsed = true;
|
|
311
|
-
return this.response.body;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Parse size strings like "10MB" into bytes
|
|
317
|
-
* Supports KB, MB, and GB units. If no unit is specified, assumes bytes.
|
|
318
|
-
*
|
|
319
|
-
* @param size - Size as number (bytes) or string with units (e.g., "5MB")
|
|
320
|
-
* @returns Size in bytes
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* parseSize("10MB") // returns 10485760
|
|
324
|
-
* parseSize(1024) // returns 1024
|
|
325
|
-
*/
|
|
326
|
-
function parseSize(size) {
|
|
327
|
-
var _a;
|
|
328
|
-
if (typeof size === "number")
|
|
329
|
-
return size;
|
|
330
|
-
const match = size.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/i);
|
|
331
|
-
if (!match)
|
|
332
|
-
return 0;
|
|
333
|
-
const num = parseFloat(match[1]);
|
|
334
|
-
const unit = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase();
|
|
335
|
-
switch (unit) {
|
|
336
|
-
case "KB":
|
|
337
|
-
return num * 1024;
|
|
338
|
-
case "MB":
|
|
339
|
-
return num * 1024 * 1024;
|
|
340
|
-
case "GB":
|
|
341
|
-
return num * 1024 * 1024 * 1024;
|
|
342
|
-
default:
|
|
343
|
-
return num;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
/**
|
|
347
|
-
* Create a memory storage provider using Map
|
|
348
|
-
* This provider stores data in memory and is cleared when the application restarts.
|
|
349
|
-
*
|
|
350
|
-
* @returns A storage provider using an in-memory Map
|
|
351
|
-
*
|
|
352
|
-
* @example
|
|
353
|
-
* const storage = createMemoryStorage();
|
|
354
|
-
* storage.set("key", "value");
|
|
355
|
-
*/
|
|
356
|
-
function createMemoryStorage() {
|
|
357
|
-
const store = new Map();
|
|
358
|
-
return {
|
|
359
|
-
get(key) {
|
|
360
|
-
return store.get(key) || null;
|
|
361
|
-
},
|
|
362
|
-
set(key, value) {
|
|
363
|
-
store.set(key, value);
|
|
364
|
-
},
|
|
365
|
-
has(key) {
|
|
366
|
-
return store.has(key);
|
|
367
|
-
},
|
|
368
|
-
delete(key) {
|
|
369
|
-
store.delete(key);
|
|
370
|
-
},
|
|
371
|
-
clear() {
|
|
372
|
-
store.clear();
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Create a localStorage-based storage provider
|
|
378
|
-
* This provider persists data between browser sessions.
|
|
379
|
-
*
|
|
380
|
-
* @returns A storage provider using localStorage
|
|
381
|
-
* @throws Console warnings if localStorage is not available or storage quota is exceeded
|
|
382
|
-
*
|
|
383
|
-
* @example
|
|
384
|
-
* const storage = createLocalStorageStorage();
|
|
385
|
-
* storage.set("key", "value");
|
|
386
|
-
*/
|
|
387
|
-
function createLocalStorageStorage() {
|
|
388
|
-
return {
|
|
389
|
-
get(key) {
|
|
390
|
-
if (typeof localStorage === "undefined")
|
|
391
|
-
return null;
|
|
392
|
-
return localStorage.getItem(key);
|
|
393
|
-
},
|
|
394
|
-
set(key, value) {
|
|
395
|
-
if (typeof localStorage === "undefined")
|
|
396
|
-
return;
|
|
397
|
-
try {
|
|
398
|
-
localStorage.setItem(key, value);
|
|
399
|
-
}
|
|
400
|
-
catch (e) {
|
|
401
|
-
// Handle quota exceeded errors
|
|
402
|
-
console.warn("Cache storage quota exceeded, clearing cache");
|
|
403
|
-
void this.clear();
|
|
404
|
-
try {
|
|
405
|
-
localStorage.setItem(key, value);
|
|
406
|
-
}
|
|
407
|
-
catch (e) {
|
|
408
|
-
console.error("Failed to set cache item even after clearing cache");
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
},
|
|
412
|
-
has(key) {
|
|
413
|
-
if (typeof localStorage === "undefined")
|
|
414
|
-
return false;
|
|
415
|
-
return localStorage.getItem(key) !== null;
|
|
416
|
-
},
|
|
417
|
-
delete(key) {
|
|
418
|
-
if (typeof localStorage === "undefined")
|
|
419
|
-
return;
|
|
420
|
-
localStorage.removeItem(key);
|
|
421
|
-
},
|
|
422
|
-
clear() {
|
|
423
|
-
if (typeof localStorage === "undefined")
|
|
424
|
-
return;
|
|
425
|
-
localStorage.clear();
|
|
426
|
-
},
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Create a sessionStorage-based storage provider
|
|
431
|
-
* This provider persists data only for the current browser session.
|
|
432
|
-
*
|
|
433
|
-
* @returns A storage provider using sessionStorage
|
|
434
|
-
* @throws Console warnings if sessionStorage is not available or storage quota is exceeded
|
|
435
|
-
*
|
|
436
|
-
* @example
|
|
437
|
-
* const storage = createSessionStorageStorage();
|
|
438
|
-
* storage.set("key", "value");
|
|
439
|
-
*/
|
|
440
|
-
function createSessionStorageStorage() {
|
|
441
|
-
return {
|
|
442
|
-
get(key) {
|
|
443
|
-
if (typeof sessionStorage === "undefined")
|
|
444
|
-
return null;
|
|
445
|
-
return sessionStorage.getItem(key);
|
|
446
|
-
},
|
|
447
|
-
set(key, value) {
|
|
448
|
-
if (typeof sessionStorage === "undefined")
|
|
449
|
-
return;
|
|
450
|
-
try {
|
|
451
|
-
sessionStorage.setItem(key, value);
|
|
452
|
-
}
|
|
453
|
-
catch (e) {
|
|
454
|
-
// Handle quota exceeded errors
|
|
455
|
-
console.warn("Cache storage quota exceeded, clearing cache");
|
|
456
|
-
void this.clear();
|
|
457
|
-
try {
|
|
458
|
-
sessionStorage.setItem(key, value);
|
|
459
|
-
}
|
|
460
|
-
catch (e) {
|
|
461
|
-
console.error("Failed to set cache item even after clearing cache");
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
has(key) {
|
|
466
|
-
if (typeof sessionStorage === "undefined")
|
|
467
|
-
return false;
|
|
468
|
-
return sessionStorage.getItem(key) !== null;
|
|
469
|
-
},
|
|
470
|
-
delete(key) {
|
|
471
|
-
if (typeof sessionStorage === "undefined")
|
|
472
|
-
return;
|
|
473
|
-
sessionStorage.removeItem(key);
|
|
474
|
-
},
|
|
475
|
-
clear() {
|
|
476
|
-
if (typeof sessionStorage === "undefined")
|
|
477
|
-
return;
|
|
478
|
-
sessionStorage.clear();
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
/**
|
|
483
|
-
* Create a storage provider from cache options
|
|
484
|
-
* Selects the appropriate storage implementation based on the provided options.
|
|
485
|
-
* Falls back to memory storage if no suitable storage is found.
|
|
486
|
-
*
|
|
487
|
-
* @param options - Cache configuration options
|
|
488
|
-
* @returns A storage provider based on the options
|
|
489
|
-
*
|
|
490
|
-
* @example
|
|
491
|
-
* const storage = createStorageProvider({ storage: localStorage });
|
|
492
|
-
*/
|
|
493
|
-
function createStorageProvider(options) {
|
|
494
|
-
const { storage } = options || {};
|
|
495
|
-
if (!storage) {
|
|
496
|
-
// Default to memory storage
|
|
497
|
-
return createMemoryStorage();
|
|
498
|
-
}
|
|
499
|
-
// Check if storage is already a StorageProvider
|
|
500
|
-
if (typeof storage.get === "function" && typeof storage.set === "function") {
|
|
501
|
-
return storage;
|
|
502
|
-
}
|
|
503
|
-
// Check if it's localStorage or sessionStorage
|
|
504
|
-
if (storage === localStorage) {
|
|
505
|
-
return createLocalStorageStorage();
|
|
506
|
-
}
|
|
507
|
-
if (storage === sessionStorage) {
|
|
508
|
-
return createSessionStorageStorage();
|
|
509
|
-
}
|
|
510
|
-
// Check if it's a Map
|
|
511
|
-
if (storage instanceof Map) {
|
|
512
|
-
return {
|
|
513
|
-
get(key) {
|
|
514
|
-
return storage.get(key) || null;
|
|
515
|
-
},
|
|
516
|
-
set(key, value, _ttl) {
|
|
517
|
-
storage.set(key, value);
|
|
518
|
-
},
|
|
519
|
-
has(key) {
|
|
520
|
-
return storage.has(key);
|
|
521
|
-
},
|
|
522
|
-
delete(key) {
|
|
523
|
-
storage.delete(key);
|
|
524
|
-
},
|
|
525
|
-
clear() {
|
|
526
|
-
storage.clear();
|
|
527
|
-
},
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
// Default to memory storage
|
|
531
|
-
console.warn("Unsupported storage type, falling back to memory storage");
|
|
532
|
-
return createMemoryStorage();
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Cache manager that handles storage operations
|
|
537
|
-
* Provides methods for storing, retrieving, and managing cached API responses.
|
|
538
|
-
*/
|
|
539
|
-
class CacheManager {
|
|
540
|
-
constructor(options = {}) {
|
|
541
|
-
this.entryCount = 0;
|
|
542
|
-
this.totalSize = 0;
|
|
543
|
-
this.options = options;
|
|
544
|
-
this.storage = createStorageProvider(options);
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Generate a cache key based on request details
|
|
548
|
-
* Keys can vary based on URL, method, headers, and request body.
|
|
549
|
-
*
|
|
550
|
-
* @param url - The request URL
|
|
551
|
-
* @param method - The HTTP method
|
|
552
|
-
* @param headers - Request headers that might affect caching
|
|
553
|
-
* @param body - Request body for non-GET requests
|
|
554
|
-
* @returns A unique cache key string
|
|
555
|
-
*
|
|
556
|
-
* @example
|
|
557
|
-
* const key = cacheManager.generateKey("/api/users", "GET", { "Accept": "application/json" });
|
|
558
|
-
*/
|
|
559
|
-
generateKey(url, method, headers, body) {
|
|
560
|
-
// Use custom key generator if provided
|
|
561
|
-
if (this.options.keyGenerator) {
|
|
562
|
-
const customKey = this.options.keyGenerator(url, method, headers, body);
|
|
563
|
-
return this.options.keyPrefix ? `${this.options.keyPrefix}:${customKey}` : customKey;
|
|
564
|
-
}
|
|
565
|
-
// Default key generation
|
|
566
|
-
let key = `${method}:${url}`;
|
|
567
|
-
// Add vary headers to key if specified
|
|
568
|
-
if (this.options.varyByHeaders && headers) {
|
|
569
|
-
const varyHeaders = {};
|
|
570
|
-
this.options.varyByHeaders.forEach(header => {
|
|
571
|
-
const normalizedHeader = header.toLowerCase();
|
|
572
|
-
// Find the header in case-insensitive way
|
|
573
|
-
Object.keys(headers).forEach(h => {
|
|
574
|
-
if (h.toLowerCase() === normalizedHeader) {
|
|
575
|
-
varyHeaders[normalizedHeader] = headers[h];
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
});
|
|
579
|
-
if (Object.keys(varyHeaders).length > 0) {
|
|
580
|
-
key += `:${JSON.stringify(varyHeaders)}`;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// Include body in cache key for non-GET requests if it's a simple value
|
|
584
|
-
if (method !== "GET" && body !== undefined && body !== null) {
|
|
585
|
-
try {
|
|
586
|
-
if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") {
|
|
587
|
-
key += `:${String(body)}`;
|
|
588
|
-
}
|
|
589
|
-
else if (typeof body === "object") {
|
|
590
|
-
key += `:${JSON.stringify(body)}`;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
catch (e) {
|
|
594
|
-
console.warn("Failed to stringify request body for cache key");
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
// Add prefix if specified
|
|
598
|
-
return this.options.keyPrefix ? `${this.options.keyPrefix}:${key}` : key;
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Get a cached value by key
|
|
602
|
-
* Checks if the entry exists and isn't expired before returning it.
|
|
603
|
-
*
|
|
604
|
-
* @param key - The cache key to retrieve
|
|
605
|
-
* @returns The cached entry or null if not found or expired
|
|
606
|
-
*
|
|
607
|
-
* @example
|
|
608
|
-
* const entry = await cacheManager.get("GET:/api/users");
|
|
609
|
-
*/
|
|
610
|
-
async get(key) {
|
|
611
|
-
const value = await Promise.resolve(this.storage.get(key));
|
|
612
|
-
if (!value)
|
|
613
|
-
return null;
|
|
614
|
-
try {
|
|
615
|
-
const entry = JSON.parse(value);
|
|
616
|
-
// Check if the entry has expired
|
|
617
|
-
if (entry.expiry && entry.expiry < Date.now()) {
|
|
618
|
-
await Promise.resolve(this.storage.delete(key));
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
return entry;
|
|
622
|
-
}
|
|
623
|
-
catch (e) {
|
|
624
|
-
// If we can't parse the cached value, remove it
|
|
625
|
-
await Promise.resolve(this.storage.delete(key));
|
|
626
|
-
return null;
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Store a value in the cache
|
|
631
|
-
* Enforces size and entry count limits if configured.
|
|
632
|
-
*
|
|
633
|
-
* @param key - The cache key
|
|
634
|
-
* @param value - The value to cache
|
|
635
|
-
* @param headers - Response headers to store with the cached value
|
|
636
|
-
* @returns Promise that resolves when caching is complete
|
|
637
|
-
*
|
|
638
|
-
* @example
|
|
639
|
-
* await cacheManager.set("GET:/api/users", userData, { "Content-Type": "application/json" });
|
|
640
|
-
*/
|
|
641
|
-
async set(key, value, headers) {
|
|
642
|
-
const now = Date.now();
|
|
643
|
-
const ttl = this.options.ttl;
|
|
644
|
-
const entry = {
|
|
645
|
-
value,
|
|
646
|
-
timestamp: now,
|
|
647
|
-
expiry: ttl ? now + ttl : null,
|
|
648
|
-
headers,
|
|
649
|
-
};
|
|
650
|
-
const serialized = JSON.stringify(entry);
|
|
651
|
-
// Check size constraints if applicable
|
|
652
|
-
if (this.options.maxSize) {
|
|
653
|
-
const maxBytes = parseSize(this.options.maxSize);
|
|
654
|
-
const entrySize = new TextEncoder().encode(serialized).length;
|
|
655
|
-
// If a single entry exceeds the max size, reject it without clearing the cache
|
|
656
|
-
if (entrySize > maxBytes) {
|
|
657
|
-
console.warn(`Cache entry size (${entrySize} bytes) exceeds maximum cache size (${maxBytes} bytes)`);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
// Only clear if adding this entry would exceed the total limit
|
|
661
|
-
if (this.totalSize + entrySize > maxBytes) {
|
|
662
|
-
// Simple strategy: clear everything
|
|
663
|
-
// A more sophisticated strategy would remove oldest entries first
|
|
664
|
-
await Promise.resolve(this.storage.clear());
|
|
665
|
-
this.totalSize = 0;
|
|
666
|
-
this.entryCount = 0;
|
|
667
|
-
}
|
|
668
|
-
this.totalSize += entrySize;
|
|
669
|
-
}
|
|
670
|
-
// Check max entries constraint
|
|
671
|
-
if (this.options.maxEntries) {
|
|
672
|
-
this.entryCount++;
|
|
673
|
-
if (this.entryCount > this.options.maxEntries) {
|
|
674
|
-
// Simple strategy: clear everything
|
|
675
|
-
// A more sophisticated strategy would use LRU cache
|
|
676
|
-
await Promise.resolve(this.storage.clear());
|
|
677
|
-
this.entryCount = 1;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
// Pass ttl to storage provider if available
|
|
681
|
-
await Promise.resolve(this.storage.set(key, serialized, ttl));
|
|
682
|
-
}
|
|
683
|
-
/**
|
|
684
|
-
* Check if a key exists in the cache
|
|
685
|
-
*
|
|
686
|
-
* @param key - The cache key to check
|
|
687
|
-
* @returns Promise that resolves to true if the key exists, false otherwise
|
|
688
|
-
*
|
|
689
|
-
* @example
|
|
690
|
-
* if (await cacheManager.has("GET:/api/users")) {
|
|
691
|
-
* // Use cached value
|
|
692
|
-
* }
|
|
693
|
-
*/
|
|
694
|
-
async has(key) {
|
|
695
|
-
return Promise.resolve(this.storage.has(key));
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Delete a specific key from the cache
|
|
699
|
-
*
|
|
700
|
-
* @param key - The cache key to delete
|
|
701
|
-
* @returns Promise that resolves when deletion is complete
|
|
702
|
-
*
|
|
703
|
-
* @example
|
|
704
|
-
* await cacheManager.delete("GET:/api/users");
|
|
705
|
-
*/
|
|
706
|
-
async delete(key) {
|
|
707
|
-
return Promise.resolve(this.storage.delete(key));
|
|
708
|
-
}
|
|
709
|
-
/**
|
|
710
|
-
* Clear the entire cache
|
|
711
|
-
* Resets internal counters for entry count and size.
|
|
712
|
-
*
|
|
713
|
-
* @returns Promise that resolves when the cache is cleared
|
|
714
|
-
*
|
|
715
|
-
* @example
|
|
716
|
-
* await cacheManager.clear();
|
|
717
|
-
*/
|
|
718
|
-
async clear() {
|
|
719
|
-
this.entryCount = 0;
|
|
720
|
-
this.totalSize = 0;
|
|
721
|
-
return Promise.resolve(this.storage.clear());
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Global configuration for create-request
|
|
727
|
-
*/
|
|
728
|
-
class Config {
|
|
729
|
-
constructor() {
|
|
730
|
-
// CSRF configuration
|
|
731
|
-
this.csrfHeaderName = "X-CSRF-Token";
|
|
732
|
-
this.xsrfCookieName = "XSRF-TOKEN";
|
|
733
|
-
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
734
|
-
this.csrfToken = null;
|
|
735
|
-
this.enableAutoXsrf = true;
|
|
736
|
-
this.enableAntiCsrf = true; // X-Requested-With header
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Get the singleton instance of the Config class
|
|
740
|
-
*
|
|
741
|
-
* @returns The global configuration instance
|
|
742
|
-
*
|
|
743
|
-
* @example
|
|
744
|
-
* const config = Config.getInstance();
|
|
745
|
-
* config.setCsrfToken('token123');
|
|
746
|
-
*/
|
|
747
|
-
static getInstance() {
|
|
748
|
-
if (!Config.instance) {
|
|
749
|
-
Config.instance = new Config();
|
|
750
|
-
}
|
|
751
|
-
return Config.instance;
|
|
752
|
-
}
|
|
753
|
-
/**
|
|
754
|
-
* Set a global CSRF token to be used for all requests
|
|
755
|
-
*
|
|
756
|
-
* @param token - The CSRF token value
|
|
757
|
-
* @returns The config instance for chaining
|
|
758
|
-
*
|
|
759
|
-
* @example
|
|
760
|
-
* Config.getInstance().setCsrfToken('myToken123');
|
|
761
|
-
*/
|
|
762
|
-
setCsrfToken(token) {
|
|
763
|
-
this.csrfToken = token;
|
|
764
|
-
return this;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Get the global CSRF token that will be automatically applied to requests
|
|
768
|
-
*
|
|
769
|
-
* @returns The current CSRF token or null if not set
|
|
770
|
-
*/
|
|
771
|
-
getCsrfToken() {
|
|
772
|
-
return this.csrfToken;
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Set the CSRF header name used when sending the token
|
|
776
|
-
*
|
|
777
|
-
* @param name - The header name to use
|
|
778
|
-
* @returns The config instance for chaining
|
|
779
|
-
*
|
|
780
|
-
* @example
|
|
781
|
-
* Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
|
|
782
|
-
*/
|
|
783
|
-
setCsrfHeaderName(name) {
|
|
784
|
-
this.csrfHeaderName = name;
|
|
785
|
-
return this;
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Get the configured CSRF header name
|
|
789
|
-
*
|
|
790
|
-
* @returns The current CSRF header name
|
|
791
|
-
*/
|
|
792
|
-
getCsrfHeaderName() {
|
|
793
|
-
return this.csrfHeaderName;
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Set the XSRF cookie name to look for when extracting tokens from cookies
|
|
797
|
-
*
|
|
798
|
-
* @param name - The cookie name to look for
|
|
799
|
-
* @returns The config instance for chaining
|
|
800
|
-
*
|
|
801
|
-
* @example
|
|
802
|
-
* Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
|
|
803
|
-
*/
|
|
804
|
-
setXsrfCookieName(name) {
|
|
805
|
-
this.xsrfCookieName = name;
|
|
806
|
-
return this;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Get the configured XSRF cookie name
|
|
810
|
-
*
|
|
811
|
-
* @returns The current XSRF cookie name
|
|
812
|
-
*/
|
|
813
|
-
getXsrfCookieName() {
|
|
814
|
-
return this.xsrfCookieName;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Set the XSRF header name for sending tokens extracted from cookies
|
|
818
|
-
*
|
|
819
|
-
* @param name - The header name to use
|
|
820
|
-
* @returns The config instance for chaining
|
|
821
|
-
*/
|
|
822
|
-
setXsrfHeaderName(name) {
|
|
823
|
-
this.xsrfHeaderName = name;
|
|
824
|
-
return this;
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Get the configured XSRF header name
|
|
828
|
-
*
|
|
829
|
-
* @returns The current XSRF header name
|
|
830
|
-
*/
|
|
831
|
-
getXsrfHeaderName() {
|
|
832
|
-
return this.xsrfHeaderName;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Enable or disable automatic extraction of XSRF tokens from cookies
|
|
836
|
-
* When enabled, the library will look for XSRF tokens in cookies and
|
|
837
|
-
* automatically add them to request headers.
|
|
838
|
-
*
|
|
839
|
-
* @param enable - Whether to enable this feature
|
|
840
|
-
* @returns The config instance for chaining
|
|
841
|
-
*
|
|
842
|
-
* @example
|
|
843
|
-
* Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
|
|
844
|
-
*/
|
|
845
|
-
setEnableAutoXsrf(enable) {
|
|
846
|
-
this.enableAutoXsrf = enable;
|
|
847
|
-
return this;
|
|
848
|
-
}
|
|
849
|
-
/**
|
|
850
|
-
* Check if automatic XSRF token extraction is enabled
|
|
851
|
-
*
|
|
852
|
-
* @returns True if automatic XSRF is enabled
|
|
853
|
-
*/
|
|
854
|
-
isAutoXsrfEnabled() {
|
|
855
|
-
return this.enableAutoXsrf;
|
|
856
|
-
}
|
|
857
|
-
/**
|
|
858
|
-
* Enable or disable automatic addition of anti-CSRF headers
|
|
859
|
-
* When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
|
|
860
|
-
*
|
|
861
|
-
* @param enable - Whether to enable this feature
|
|
862
|
-
* @returns The config instance for chaining
|
|
863
|
-
*/
|
|
864
|
-
setEnableAntiCsrf(enable) {
|
|
865
|
-
this.enableAntiCsrf = enable;
|
|
866
|
-
return this;
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Check if anti-CSRF protection is enabled
|
|
870
|
-
*
|
|
871
|
-
* @returns True if anti-CSRF protection is enabled
|
|
872
|
-
*/
|
|
873
|
-
isAntiCsrfEnabled() {
|
|
874
|
-
return this.enableAntiCsrf;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Reset all configuration options to their default values
|
|
878
|
-
*
|
|
879
|
-
* @returns The config instance for chaining
|
|
880
|
-
*
|
|
881
|
-
* @example
|
|
882
|
-
* Config.getInstance().reset();
|
|
883
|
-
*/
|
|
884
|
-
reset() {
|
|
885
|
-
this.csrfToken = null;
|
|
886
|
-
this.csrfHeaderName = "X-CSRF-Token";
|
|
887
|
-
this.enableAntiCsrf = true;
|
|
888
|
-
this.xsrfCookieName = "XSRF-TOKEN";
|
|
889
|
-
this.xsrfHeaderName = "X-XSRF-TOKEN";
|
|
890
|
-
this.enableAutoXsrf = true;
|
|
891
|
-
return this;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
class CookieUtils {
|
|
896
|
-
/**
|
|
897
|
-
* Formats cookies for a request
|
|
898
|
-
* @param cookies Object containing cookie name-value pairs or cookie options
|
|
899
|
-
* @returns Formatted cookie string for the Cookie header
|
|
900
|
-
*/
|
|
901
|
-
static formatRequestCookies(cookies) {
|
|
902
|
-
const cookiePairs = [];
|
|
903
|
-
Object.entries(cookies).forEach(([name, valueOrOptions]) => {
|
|
904
|
-
let value;
|
|
905
|
-
if (typeof valueOrOptions === "string") {
|
|
906
|
-
value = valueOrOptions;
|
|
907
|
-
}
|
|
908
|
-
else {
|
|
909
|
-
// Extract value from options object without validation
|
|
910
|
-
value = valueOrOptions.value;
|
|
911
|
-
}
|
|
912
|
-
// Add the cookie to the request
|
|
913
|
-
cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
|
|
914
|
-
});
|
|
915
|
-
return cookiePairs.join("; ");
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Utility class for CSRF token management
|
|
921
|
-
*/
|
|
922
|
-
class CsrfUtils {
|
|
923
|
-
/**
|
|
924
|
-
* Extracts CSRF token from a meta tag in the document head
|
|
925
|
-
* @param metaName The name attribute of the meta tag (default: "csrf-token")
|
|
926
|
-
* @returns The CSRF token or null if not found
|
|
927
|
-
*/
|
|
928
|
-
static getTokenFromMeta(metaName = "csrf-token") {
|
|
929
|
-
if (typeof document === "undefined") {
|
|
930
|
-
return null;
|
|
931
|
-
}
|
|
932
|
-
const meta = document.querySelector(`meta[name="${metaName}"]`);
|
|
933
|
-
return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Extracts CSRF token from a cookie
|
|
937
|
-
* @param cookieName The name of the cookie containing the CSRF token
|
|
938
|
-
* @returns The CSRF token or null if not found
|
|
939
|
-
*/
|
|
940
|
-
static getTokenFromCookie(cookieName = "csrf-token") {
|
|
941
|
-
if (typeof document === "undefined") {
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
const cookies = document.cookie.split(";");
|
|
945
|
-
for (const cookie of cookies) {
|
|
946
|
-
const [name, value] = cookie.trim().split("=");
|
|
947
|
-
if (name === cookieName) {
|
|
948
|
-
return decodeURIComponent(value);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
return null;
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Validates if the provided string is a potential CSRF token
|
|
955
|
-
* Checks if the token meets security requirements
|
|
956
|
-
* @param token The token to validate
|
|
957
|
-
* @returns Whether the token is valid
|
|
958
|
-
*/
|
|
959
|
-
static isValidToken(token) {
|
|
960
|
-
// Basic checks for null/undefined and type
|
|
961
|
-
if (typeof token !== "string") {
|
|
962
|
-
return false;
|
|
963
|
-
}
|
|
964
|
-
if (token.length < 8)
|
|
965
|
-
return false;
|
|
966
|
-
// If token is longer than 10 chars, perform additional security checks
|
|
967
|
-
if (token.length > 10) {
|
|
968
|
-
// Check for valid character set (alphanumeric & common token symbols)
|
|
969
|
-
const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
|
|
970
|
-
if (!validTokenRegex.test(token)) {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
// For longer tokens, check for sufficient entropy (at least 2 character types)
|
|
974
|
-
const hasUpperCase = /[A-Z]/.test(token);
|
|
975
|
-
const hasLowerCase = /[a-z]/.test(token);
|
|
976
|
-
const hasNumbers = /[0-9]/.test(token);
|
|
977
|
-
const hasSpecials = /[-_=+/.]/.test(token);
|
|
978
|
-
const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
|
|
979
|
-
return characterTypesCount >= 2;
|
|
980
|
-
}
|
|
981
|
-
// For shorter tokens (8-10 chars), just return true if we reached here
|
|
982
|
-
return true;
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Base class with common functionality for all request types
|
|
988
|
-
* Provides the core request building and execution capabilities.
|
|
989
|
-
*/
|
|
990
|
-
class BaseRequest {
|
|
991
|
-
constructor() {
|
|
992
|
-
this.requestOptions = {
|
|
993
|
-
headers: {},
|
|
994
|
-
};
|
|
995
|
-
this.queryParams = new URLSearchParams();
|
|
996
|
-
this.autoApplyCsrfProtection = true;
|
|
997
|
-
this.cacheEnabled = false;
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Add multiple HTTP headers to the request
|
|
1001
|
-
*
|
|
1002
|
-
* @param headers - Key-value pairs of header names and values
|
|
1003
|
-
* @returns The request instance for chaining
|
|
1004
|
-
*
|
|
1005
|
-
* @example
|
|
1006
|
-
* request.withHeaders({
|
|
1007
|
-
* 'Accept': 'application/json',
|
|
1008
|
-
* 'X-Custom-Header': 'value'
|
|
1009
|
-
* });
|
|
1010
|
-
*/
|
|
1011
|
-
withHeaders(headers) {
|
|
1012
|
-
this.requestOptions.headers = {
|
|
1013
|
-
...this.requestOptions.headers,
|
|
1014
|
-
...headers,
|
|
1015
|
-
};
|
|
1016
|
-
return this;
|
|
1017
|
-
}
|
|
1018
|
-
/**
|
|
1019
|
-
* Add a single HTTP header to the request
|
|
1020
|
-
*
|
|
1021
|
-
* @param key - The header name
|
|
1022
|
-
* @param value - The header value
|
|
1023
|
-
* @returns The request instance for chaining
|
|
1024
|
-
*
|
|
1025
|
-
* @example
|
|
1026
|
-
* request.withHeader('Accept', 'application/json');
|
|
1027
|
-
*/
|
|
1028
|
-
withHeader(key, value) {
|
|
1029
|
-
return this.withHeaders({ [key]: value });
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* Set a timeout for the request
|
|
1033
|
-
* If the request takes longer than the specified timeout, it will be aborted.
|
|
1034
|
-
*
|
|
1035
|
-
* @param timeout - The timeout in milliseconds
|
|
1036
|
-
* @returns The request instance for chaining
|
|
1037
|
-
* @throws Error if timeout is not a positive number
|
|
1038
|
-
*
|
|
1039
|
-
* @example
|
|
1040
|
-
* request.withTimeout(5000); // 5 seconds timeout
|
|
1041
|
-
*/
|
|
1042
|
-
withTimeout(timeout) {
|
|
1043
|
-
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
1044
|
-
throw new Error("Timeout must be a positive number");
|
|
1045
|
-
this.requestOptions.timeout = timeout;
|
|
1046
|
-
return this;
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Configure automatic retry behavior for failed requests
|
|
1050
|
-
*
|
|
1051
|
-
* @param retries - Number of retry attempts before failing
|
|
1052
|
-
* @returns The request instance for chaining
|
|
1053
|
-
* @throws Error if retries is not a non-negative integer
|
|
1054
|
-
*
|
|
1055
|
-
* @example
|
|
1056
|
-
* request.withRetries(3); // Retry up to 3 times
|
|
1057
|
-
*/
|
|
1058
|
-
withRetries(retries) {
|
|
1059
|
-
if (!Number.isInteger(retries) || retries < 0)
|
|
1060
|
-
throw new Error("Retry count must be a non-negative integer");
|
|
1061
|
-
this.requestOptions.retries = retries;
|
|
1062
|
-
return this;
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Set a callback to be invoked before each retry attempt
|
|
1066
|
-
* Useful for implementing backoff strategies or logging retry attempts.
|
|
1067
|
-
*
|
|
1068
|
-
* @param callback - Function to call before retrying
|
|
1069
|
-
* @returns The request instance for chaining
|
|
1070
|
-
*
|
|
1071
|
-
* @example
|
|
1072
|
-
* request.onRetry(({ attempt, error }) => {
|
|
1073
|
-
* console.log(`Retry attempt ${attempt} after error: ${error.message}`);
|
|
1074
|
-
* return new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
1075
|
-
* });
|
|
1076
|
-
*/
|
|
1077
|
-
onRetry(callback) {
|
|
1078
|
-
this.requestOptions.onRetry = callback;
|
|
1079
|
-
return this;
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Sets the credentials policy for the request
|
|
1083
|
-
* @param credentialsPolicy - Can be 'omit', 'same-origin', or 'include'
|
|
1084
|
-
* @returns The instance for chaining
|
|
1085
|
-
*/
|
|
1086
|
-
withCredentials(credentialsPolicy = exports.CredentialsPolicy.SAME_ORIGIN) {
|
|
1087
|
-
this.requestOptions.credentials = credentialsPolicy;
|
|
1088
|
-
return this;
|
|
1089
|
-
}
|
|
1090
|
-
/**
|
|
1091
|
-
* Allows providing an external AbortController to cancel the request
|
|
1092
|
-
* @param controller - The AbortController to use for this request
|
|
1093
|
-
* @returns The instance for chaining
|
|
1094
|
-
*/
|
|
1095
|
-
withAbortController(controller) {
|
|
1096
|
-
this.abortController = controller;
|
|
1097
|
-
return this;
|
|
1098
|
-
}
|
|
1099
|
-
/**
|
|
1100
|
-
* Sets the referrer for the request
|
|
1101
|
-
* @param referrer - The referrer URL or policy
|
|
1102
|
-
* @returns The instance for chaining
|
|
1103
|
-
*/
|
|
1104
|
-
withReferrer(referrer) {
|
|
1105
|
-
this.requestOptions.referrer = referrer;
|
|
1106
|
-
return this;
|
|
1107
|
-
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Sets the referrer policy for the request
|
|
1110
|
-
* @param policy - The referrer policy to use
|
|
1111
|
-
* @returns The instance for chaining
|
|
1112
|
-
*/
|
|
1113
|
-
withReferrerPolicy(policy) {
|
|
1114
|
-
this.requestOptions.referrerPolicy = policy;
|
|
1115
|
-
return this;
|
|
1116
|
-
}
|
|
1117
|
-
/**
|
|
1118
|
-
* Sets the redirect policy for the request
|
|
1119
|
-
* @param redirect - Can be 'follow', 'error', or 'manual'
|
|
1120
|
-
* @returns The instance for chaining
|
|
1121
|
-
*/
|
|
1122
|
-
withRedirect(redirect) {
|
|
1123
|
-
this.requestOptions.redirect = redirect;
|
|
1124
|
-
return this;
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Sets the keepalive flag for the request
|
|
1128
|
-
* @param keepalive - Whether to allow the request to outlive the page
|
|
1129
|
-
* @returns The instance for chaining
|
|
1130
|
-
*/
|
|
1131
|
-
withKeepAlive(keepalive) {
|
|
1132
|
-
this.requestOptions.keepalive = keepalive;
|
|
1133
|
-
return this;
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* Sets the priority of the request
|
|
1137
|
-
* @param priority - Can be 'high', 'low', or 'auto'
|
|
1138
|
-
* @returns The instance for chaining
|
|
1139
|
-
*/
|
|
1140
|
-
withPriority(priority) {
|
|
1141
|
-
this.requestOptions.priority = priority;
|
|
1142
|
-
return this;
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Adds query parameters to the request URL
|
|
1146
|
-
* @param params - An object containing the query parameters
|
|
1147
|
-
* @returns The instance for chaining
|
|
1148
|
-
*/
|
|
1149
|
-
withQueryParams(params) {
|
|
1150
|
-
Object.entries(params).forEach(([key, value]) => {
|
|
1151
|
-
if (value === null || value === undefined) {
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
if (Array.isArray(value)) {
|
|
1155
|
-
// Handle array values - add multiple entries with the same key
|
|
1156
|
-
value.forEach(v => this.queryParams.append(key, String(v)));
|
|
1157
|
-
}
|
|
1158
|
-
else {
|
|
1159
|
-
this.queryParams.append(key, String(value));
|
|
1160
|
-
}
|
|
1161
|
-
});
|
|
1162
|
-
return this;
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* Adds a single query parameter
|
|
1166
|
-
* @param key - The parameter name
|
|
1167
|
-
* @param value - The parameter value, can be a single value or array of values
|
|
1168
|
-
* @returns The instance for chaining
|
|
1169
|
-
*/
|
|
1170
|
-
withQueryParam(key, value) {
|
|
1171
|
-
if (value === null || value === undefined)
|
|
1172
|
-
return this;
|
|
1173
|
-
if (Array.isArray(value))
|
|
1174
|
-
value.forEach(v => this.queryParams.append(key, String(v)));
|
|
1175
|
-
else
|
|
1176
|
-
this.queryParams.append(key, String(value));
|
|
1177
|
-
return this;
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* Sets the mode of the request
|
|
1181
|
-
* @param mode - The mode for the request (cors, no-cors, same-origin, navigate)
|
|
1182
|
-
* @returns The instance for chaining
|
|
1183
|
-
*/
|
|
1184
|
-
withMode(mode) {
|
|
1185
|
-
this.requestOptions.mode = mode;
|
|
1186
|
-
return this;
|
|
1187
|
-
}
|
|
1188
|
-
/**
|
|
1189
|
-
* Shorthand for setting the Content-Type header
|
|
1190
|
-
* @param contentType - The content type
|
|
1191
|
-
* @returns The instance for chaining
|
|
1192
|
-
*/
|
|
1193
|
-
withContentType(contentType) {
|
|
1194
|
-
return this.withHeaders({
|
|
1195
|
-
"Content-Type": contentType,
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
/**
|
|
1199
|
-
* Shorthand for setting the Authorization header
|
|
1200
|
-
* @param authValue - The authorization value
|
|
1201
|
-
* @returns The instance for chaining
|
|
1202
|
-
*/
|
|
1203
|
-
withAuthorization(authValue) {
|
|
1204
|
-
return this.withHeaders({
|
|
1205
|
-
Authorization: authValue,
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
/**
|
|
1209
|
-
* Shorthand for setting up Basic authentication
|
|
1210
|
-
* @param username - The username
|
|
1211
|
-
* @param password - The password
|
|
1212
|
-
* @returns The instance for chaining
|
|
1213
|
-
*/
|
|
1214
|
-
withBasicAuth(username, password) {
|
|
1215
|
-
const credentials = this.encodeBase64(`${username}:${password}`);
|
|
1216
|
-
return this.withAuthorization(`Basic ${credentials}`);
|
|
1217
|
-
}
|
|
1218
|
-
/**
|
|
1219
|
-
* Cross-environment base64 encoding
|
|
1220
|
-
* Works in both browser and Node.js environments
|
|
1221
|
-
*/
|
|
1222
|
-
encodeBase64(str) {
|
|
1223
|
-
// Modern approach using TextEncoder (available in both modern browsers and Node.js)
|
|
1224
|
-
if (typeof TextEncoder !== "undefined" && typeof btoa === "function") {
|
|
1225
|
-
const encoder = new TextEncoder();
|
|
1226
|
-
const bytes = encoder.encode(str);
|
|
1227
|
-
return btoa(String.fromCharCode.apply(null, [...new Uint8Array(bytes)]));
|
|
1228
|
-
}
|
|
1229
|
-
// Browser environment
|
|
1230
|
-
if (typeof btoa === "function")
|
|
1231
|
-
return btoa(str);
|
|
1232
|
-
// Node.js environment
|
|
1233
|
-
if (typeof Buffer !== "undefined")
|
|
1234
|
-
return Buffer.from(str).toString("base64");
|
|
1235
|
-
// Fallback (should never happen in modern environments)
|
|
1236
|
-
throw new Error("Base64 encoding is not supported in this environment");
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* Sets the bearer token for authentication
|
|
1240
|
-
* @param token - The bearer token
|
|
1241
|
-
* @returns The instance for chaining
|
|
1242
|
-
*/
|
|
1243
|
-
withBearerToken(token) {
|
|
1244
|
-
return this.withAuthorization(`Bearer ${token}`);
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* Helper function to check for header presence in a case-insensitive way
|
|
1248
|
-
* @param headers Headers object
|
|
1249
|
-
* @param headerName Header name to check
|
|
1250
|
-
* @returns Boolean indicating if the header exists (case-insensitive)
|
|
1251
|
-
*/
|
|
1252
|
-
hasHeader(headers, headerName) {
|
|
1253
|
-
return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Sets cookies for the request
|
|
1257
|
-
* @param cookies Object containing cookie name-value pairs or cookie options
|
|
1258
|
-
* @returns The instance for chaining
|
|
1259
|
-
*/
|
|
1260
|
-
withCookies(cookies) {
|
|
1261
|
-
const cookieEntries = Object.entries(cookies);
|
|
1262
|
-
if (cookieEntries.length === 0) {
|
|
1263
|
-
return this;
|
|
1264
|
-
}
|
|
1265
|
-
// Get current headers or initialize empty object
|
|
1266
|
-
const currentHeaders = this.requestOptions.headers || {};
|
|
1267
|
-
// Get all existing cookie headers with different cases
|
|
1268
|
-
const cookieValues = [];
|
|
1269
|
-
const cookieHeaderKeys = [];
|
|
1270
|
-
Object.keys(currentHeaders).forEach(key => {
|
|
1271
|
-
if (key.toLowerCase() === "cookie") {
|
|
1272
|
-
// Don't add empty cookie values
|
|
1273
|
-
if (currentHeaders[key])
|
|
1274
|
-
cookieValues.push(currentHeaders[key]);
|
|
1275
|
-
cookieHeaderKeys.push(key);
|
|
1276
|
-
}
|
|
1277
|
-
});
|
|
1278
|
-
// Format the new cookies
|
|
1279
|
-
const cookieString = CookieUtils.formatRequestCookies(cookies);
|
|
1280
|
-
// Choose which header name to use - preserve existing case if possible
|
|
1281
|
-
const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
|
|
1282
|
-
// Combine all existing cookie values with the new ones
|
|
1283
|
-
const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
|
|
1284
|
-
// Create a new headers object without any cookie headers
|
|
1285
|
-
const newHeaders = { ...currentHeaders };
|
|
1286
|
-
cookieHeaderKeys.forEach(key => {
|
|
1287
|
-
delete newHeaders[key];
|
|
1288
|
-
});
|
|
1289
|
-
// Set the new combined cookie header
|
|
1290
|
-
this.requestOptions.headers = {
|
|
1291
|
-
...newHeaders,
|
|
1292
|
-
[headerName]: combinedCookieValue,
|
|
1293
|
-
};
|
|
1294
|
-
return this;
|
|
1295
|
-
}
|
|
1296
|
-
/**
|
|
1297
|
-
* Set a single cookie
|
|
1298
|
-
* @param name Cookie name
|
|
1299
|
-
* @param value Cookie value or options object
|
|
1300
|
-
* @returns The instance for chaining
|
|
1301
|
-
*/
|
|
1302
|
-
withCookie(name, value) {
|
|
1303
|
-
return this.withCookies({ [name]: value });
|
|
1304
|
-
}
|
|
1305
|
-
/**
|
|
1306
|
-
* Sets a CSRF token in the request headers
|
|
1307
|
-
* @param token The CSRF token
|
|
1308
|
-
* @param headerName The name of the header to use (default: X-CSRF-Token)
|
|
1309
|
-
* @returns The instance for chaining
|
|
1310
|
-
*/
|
|
1311
|
-
withCsrfToken(token, headerName = "X-CSRF-Token") {
|
|
1312
|
-
return this.withHeaders({
|
|
1313
|
-
[headerName]: token,
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Disables automatic anti-CSRF protection.
|
|
1318
|
-
* By default, X-Requested-With: XMLHttpRequest header is sent with all requests.
|
|
1319
|
-
* @returns The instance for chaining
|
|
1320
|
-
*/
|
|
1321
|
-
withoutCsrfProtection() {
|
|
1322
|
-
this.autoApplyCsrfProtection = false;
|
|
1323
|
-
return this;
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Sets common security headers to help prevent CSRF attacks
|
|
1327
|
-
* @returns The instance for chaining
|
|
1328
|
-
*/
|
|
1329
|
-
withAntiCsrfHeaders() {
|
|
1330
|
-
return this.withHeaders({
|
|
1331
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Configure request caching behavior
|
|
1336
|
-
* When enabled, responses will be cached and reused for subsequent identical requests.
|
|
1337
|
-
*
|
|
1338
|
-
* @param options - Cache configuration options
|
|
1339
|
-
* @returns The request instance for chaining
|
|
1340
|
-
*
|
|
1341
|
-
* @example
|
|
1342
|
-
* request.withCache({
|
|
1343
|
-
* ttl: 60000, // Cache for 1 minute
|
|
1344
|
-
* maxEntries: 100,
|
|
1345
|
-
* storage: localStorage
|
|
1346
|
-
* });
|
|
1347
|
-
*/
|
|
1348
|
-
withCache(options = {}) {
|
|
1349
|
-
this.cacheEnabled = true;
|
|
1350
|
-
this.cacheManager = new CacheManager(options);
|
|
1351
|
-
return this;
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Send the request to the specified URL
|
|
1355
|
-
* This is the final method in the chain that executes the request.
|
|
1356
|
-
*
|
|
1357
|
-
* @param url - The URL to send the request to
|
|
1358
|
-
* @returns A promise with additional methods for processing the response
|
|
1359
|
-
*
|
|
1360
|
-
* @example
|
|
1361
|
-
* const response = await request.sendTo('/api/users');
|
|
1362
|
-
* const data = await response.getJson();
|
|
1363
|
-
*/
|
|
1364
|
-
sendTo(url) {
|
|
1365
|
-
// Format the URL with query parameters
|
|
1366
|
-
url = this.formatUrlWithQueryParams(url);
|
|
1367
|
-
// Apply automatic CSRF protection if enabled
|
|
1368
|
-
if (this.autoApplyCsrfProtection) {
|
|
1369
|
-
const config = Config.getInstance();
|
|
1370
|
-
// Apply anti-CSRF headers if enabled
|
|
1371
|
-
if (config.isAntiCsrfEnabled()) {
|
|
1372
|
-
this.withHeaders({
|
|
1373
|
-
"X-Requested-With": "XMLHttpRequest",
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
// Apply global CSRF token if set
|
|
1377
|
-
const globalToken = config.getCsrfToken();
|
|
1378
|
-
if (globalToken) {
|
|
1379
|
-
// Check if local token exists using case-insensitive comparison
|
|
1380
|
-
const headers = this.requestOptions.headers;
|
|
1381
|
-
const csrfHeaderName = config.getCsrfHeaderName();
|
|
1382
|
-
const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
|
|
1383
|
-
if (!hasLocalToken) {
|
|
1384
|
-
this.withHeaders({
|
|
1385
|
-
[csrfHeaderName]: globalToken,
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
// check for XSRF token in cookies and send it as a header
|
|
1390
|
-
if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
|
|
1391
|
-
const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
|
|
1392
|
-
if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
|
|
1393
|
-
const headers = this.requestOptions.headers;
|
|
1394
|
-
const xsrfHeaderName = config.getXsrfHeaderName();
|
|
1395
|
-
const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
|
|
1396
|
-
if (!hasLocalToken) {
|
|
1397
|
-
this.withHeaders({
|
|
1398
|
-
[xsrfHeaderName]: xsrfToken,
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
// Cast the request options to RequestInit to ensure compatibility
|
|
1405
|
-
const fetchOptions = {
|
|
1406
|
-
...this.requestOptions,
|
|
1407
|
-
method: this.method,
|
|
1408
|
-
};
|
|
1409
|
-
// Create the base promise - handle caching if enabled
|
|
1410
|
-
let basePromise;
|
|
1411
|
-
if (this.cacheEnabled && this.cacheManager) {
|
|
1412
|
-
const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
|
|
1413
|
-
basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
|
|
1414
|
-
}
|
|
1415
|
-
else {
|
|
1416
|
-
basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
|
|
1417
|
-
}
|
|
1418
|
-
const responsePromise = basePromise;
|
|
1419
|
-
// Attach convenience methods that begin processing immediately
|
|
1420
|
-
responsePromise.getJson = async () => {
|
|
1421
|
-
return basePromise.then(response => response.getJson());
|
|
1422
|
-
};
|
|
1423
|
-
responsePromise.getText = async () => {
|
|
1424
|
-
return basePromise.then(response => response.getText());
|
|
1425
|
-
};
|
|
1426
|
-
responsePromise.getBlob = async () => {
|
|
1427
|
-
return basePromise.then(response => response.getBlob());
|
|
1428
|
-
};
|
|
1429
|
-
responsePromise.getArrayBuffer = async () => {
|
|
1430
|
-
return basePromise.then(response => response.getArrayBuffer());
|
|
1431
|
-
};
|
|
1432
|
-
responsePromise.getBody = async () => {
|
|
1433
|
-
return basePromise.then(response => response.getBody());
|
|
1434
|
-
};
|
|
1435
|
-
responsePromise.getData = async (selector) => {
|
|
1436
|
-
try {
|
|
1437
|
-
const data = await basePromise.then(response => response.getJson());
|
|
1438
|
-
// If no selector is provided, return the raw JSON data
|
|
1439
|
-
if (!selector)
|
|
1440
|
-
return data;
|
|
1441
|
-
// Apply the selector if provided
|
|
1442
|
-
return selector(data);
|
|
1443
|
-
}
|
|
1444
|
-
catch (error) {
|
|
1445
|
-
// Always enhance the error with the original data
|
|
1446
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1447
|
-
const enhancedError = new Error(`Data selector failed: ${errorMessage}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
|
|
1448
|
-
// Preserve the stack trace if available
|
|
1449
|
-
if (error instanceof Error && error.stack) {
|
|
1450
|
-
enhancedError.stack = error.stack;
|
|
1451
|
-
}
|
|
1452
|
-
throw enhancedError;
|
|
1453
|
-
}
|
|
1454
|
-
};
|
|
1455
|
-
return responsePromise;
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Formats the URL with any query parameters
|
|
1459
|
-
* @param url The base URL
|
|
1460
|
-
* @returns The URL with query parameters appended
|
|
1461
|
-
*/
|
|
1462
|
-
formatUrlWithQueryParams(url) {
|
|
1463
|
-
if (!this.queryParams.toString()) {
|
|
1464
|
-
return url;
|
|
1465
|
-
}
|
|
1466
|
-
try {
|
|
1467
|
-
// Try to use the URL constructor (works for absolute URLs)
|
|
1468
|
-
const urlObj = new URL(url);
|
|
1469
|
-
// Merge our query params with any that might be in the URL already
|
|
1470
|
-
this.queryParams.forEach((value, key) => {
|
|
1471
|
-
urlObj.searchParams.append(key, value);
|
|
1472
|
-
});
|
|
1473
|
-
return urlObj.toString();
|
|
1474
|
-
}
|
|
1475
|
-
catch (error) {
|
|
1476
|
-
// Handle relative URLs
|
|
1477
|
-
const hasExistingParams = url.includes("?");
|
|
1478
|
-
const separator = hasExistingParams ? "&" : "?";
|
|
1479
|
-
return `${url}${separator}${this.queryParams.toString()}`;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
/**
|
|
1483
|
-
* Executes a request with configured retry logic
|
|
1484
|
-
* @param url The formatted URL to send the request to
|
|
1485
|
-
* @param fetchOptions The fetch options to use
|
|
1486
|
-
* @returns A wrapped response object
|
|
1487
|
-
*/
|
|
1488
|
-
async executeWithRetries(url, fetchOptions) {
|
|
1489
|
-
let attempt = 0;
|
|
1490
|
-
const maxRetries = this.requestOptions.retries || 0;
|
|
1491
|
-
do {
|
|
1492
|
-
try {
|
|
1493
|
-
return await this.executeRequest(url, fetchOptions);
|
|
1494
|
-
}
|
|
1495
|
-
catch (error) {
|
|
1496
|
-
const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
|
|
1497
|
-
// Don't retry on timeout errors or if we've reached the max retries
|
|
1498
|
-
if (attempt >= maxRetries || requestError.timeoutError)
|
|
1499
|
-
throw requestError;
|
|
1500
|
-
attempt++;
|
|
1501
|
-
if (this.requestOptions.onRetry) {
|
|
1502
|
-
await this.requestOptions.onRetry({ attempt, error: requestError });
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
} while (attempt <= maxRetries);
|
|
1506
|
-
// If we somehow get here, we throw an error to avoid undefined behavior
|
|
1507
|
-
throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
|
|
1508
|
-
}
|
|
1509
|
-
async executeRequest(url, fetchOptions) {
|
|
1510
|
-
// Create a request controller for timeout if needed, or use the provided one
|
|
1511
|
-
const controller = this.abortController || new AbortController();
|
|
1512
|
-
const { signal } = controller;
|
|
1513
|
-
// Use the controller signal for the fetch request, but don't overwrite an existing signal
|
|
1514
|
-
fetchOptions.signal = fetchOptions.signal || signal;
|
|
1515
|
-
let timeoutId;
|
|
1516
|
-
// Flag to track if abort was caused by our timeout
|
|
1517
|
-
let abortedByTimeout = false;
|
|
1518
|
-
// Handle timeout regardless of whether an external controller is provided
|
|
1519
|
-
if (this.requestOptions.timeout) {
|
|
1520
|
-
if (this.abortController) {
|
|
1521
|
-
// If we have an external controller, create a separate timeout controller
|
|
1522
|
-
// that won't interfere with the external one
|
|
1523
|
-
// Use AbortSignal.timeout if available (modern browsers)
|
|
1524
|
-
if (typeof AbortSignal.timeout === "function") {
|
|
1525
|
-
const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
|
|
1526
|
-
// Add event listener to propagate the abort to the main controller
|
|
1527
|
-
timeoutSignal.addEventListener("abort", () => {
|
|
1528
|
-
if (!controller.signal.aborted) {
|
|
1529
|
-
abortedByTimeout = true;
|
|
1530
|
-
controller.abort();
|
|
1531
|
-
}
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
else {
|
|
1535
|
-
// Fallback for browsers without AbortSignal.timeout
|
|
1536
|
-
timeoutId = setTimeout(() => {
|
|
1537
|
-
if (!controller.signal.aborted) {
|
|
1538
|
-
abortedByTimeout = true;
|
|
1539
|
-
controller.abort();
|
|
1540
|
-
}
|
|
1541
|
-
}, this.requestOptions.timeout);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
else {
|
|
1545
|
-
timeoutId = setTimeout(() => {
|
|
1546
|
-
abortedByTimeout = true;
|
|
1547
|
-
controller.abort();
|
|
1548
|
-
}, this.requestOptions.timeout);
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
try {
|
|
1552
|
-
let response;
|
|
1553
|
-
try {
|
|
1554
|
-
response = await fetch(url, fetchOptions);
|
|
1555
|
-
}
|
|
1556
|
-
catch (error) {
|
|
1557
|
-
// Check if this is an abort error that was specifically caused by our timeout
|
|
1558
|
-
if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
|
|
1559
|
-
throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
|
|
1560
|
-
}
|
|
1561
|
-
// Otherwise it's either a user-triggered abort or another network error
|
|
1562
|
-
throw RequestError.networkError(url, fetchOptions.method, error);
|
|
1563
|
-
}
|
|
1564
|
-
if (!response.ok) {
|
|
1565
|
-
throw RequestError.fromResponse(response, url, fetchOptions.method);
|
|
1566
|
-
}
|
|
1567
|
-
// Return a wrapped response instead of parsing it
|
|
1568
|
-
return new ResponseWrapper(response);
|
|
1569
|
-
}
|
|
1570
|
-
finally {
|
|
1571
|
-
if (timeoutId) {
|
|
1572
|
-
clearTimeout(timeoutId);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Executes a request using cache when possible
|
|
1578
|
-
* @param url The formatted URL to send the request to
|
|
1579
|
-
* @param fetchOptions The fetch options to use
|
|
1580
|
-
* @param cacheKey The cache key for this request
|
|
1581
|
-
*/
|
|
1582
|
-
async executeWithCache(url, fetchOptions, cacheKey) {
|
|
1583
|
-
// Try to get from cache first
|
|
1584
|
-
if (this.cacheManager) {
|
|
1585
|
-
const cachedEntry = await this.cacheManager.get(cacheKey);
|
|
1586
|
-
if (cachedEntry) {
|
|
1587
|
-
// Create a synthetic Response object from cached data
|
|
1588
|
-
const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
|
|
1589
|
-
status: 200,
|
|
1590
|
-
headers: new Headers(cachedEntry.headers || {}),
|
|
1591
|
-
});
|
|
1592
|
-
return new ResponseWrapper(cachedResponse);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
// Not in cache, make the actual request
|
|
1596
|
-
const response = await (!this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions));
|
|
1597
|
-
// Cache the successful response if caching is enabled
|
|
1598
|
-
// Only cache successful responses
|
|
1599
|
-
if (response.ok && this.cacheManager) {
|
|
1600
|
-
// Clone the response so we don't consume it
|
|
1601
|
-
try {
|
|
1602
|
-
const responseData = await response.getJson();
|
|
1603
|
-
// Extract headers to store in cache
|
|
1604
|
-
const headers = {};
|
|
1605
|
-
response.headers.forEach((value, key) => {
|
|
1606
|
-
headers[key] = value;
|
|
1607
|
-
});
|
|
1608
|
-
// Store in cache
|
|
1609
|
-
await this.cacheManager.set(cacheKey, responseData, headers);
|
|
1610
|
-
}
|
|
1611
|
-
catch (error) {
|
|
1612
|
-
// Failed to cache, but we can still return the response
|
|
1613
|
-
console.warn("Failed to cache response:", error);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
return response;
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
/**
|
|
1621
|
-
* Base class for requests that can have a body (POST, PUT, PATCH)
|
|
1622
|
-
*/
|
|
1623
|
-
class BodyRequest extends BaseRequest {
|
|
1624
|
-
/**
|
|
1625
|
-
* Sets the body of the request
|
|
1626
|
-
* @param body - The request body
|
|
1627
|
-
*/
|
|
1628
|
-
withBody(body) {
|
|
1629
|
-
this.body = body;
|
|
1630
|
-
// Set body type and validate
|
|
1631
|
-
if (typeof body === "string") {
|
|
1632
|
-
this.bodyType = BodyType.STRING;
|
|
1633
|
-
this.setContentTypeIfNeeded("text/plain");
|
|
1634
|
-
}
|
|
1635
|
-
else if (body !== null &&
|
|
1636
|
-
typeof body === "object" &&
|
|
1637
|
-
!(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
|
|
1638
|
-
this.bodyType = BodyType.JSON;
|
|
1639
|
-
this.setContentTypeIfNeeded("application/json");
|
|
1640
|
-
// Validate JSON is stringifiable early
|
|
1641
|
-
try {
|
|
1642
|
-
JSON.stringify(body);
|
|
1643
|
-
}
|
|
1644
|
-
catch (error) {
|
|
1645
|
-
throw new Error(`Failed to stringify request body: ${String(error)}`);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
else {
|
|
1649
|
-
this.bodyType = BodyType.BINARY;
|
|
1650
|
-
}
|
|
1651
|
-
return this;
|
|
1652
|
-
}
|
|
1653
|
-
setContentTypeIfNeeded(contentType) {
|
|
1654
|
-
// Only set if not already set
|
|
1655
|
-
const headers = this.requestOptions.headers;
|
|
1656
|
-
if (!Object.keys(headers).some(header => header.toLowerCase() === "content-type")) {
|
|
1657
|
-
this.withContentType(contentType);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* Send the request to the specified URL
|
|
1662
|
-
* Overrides the base implementation to add body handling
|
|
1663
|
-
*/
|
|
1664
|
-
sendTo(url) {
|
|
1665
|
-
if (this.body !== undefined) {
|
|
1666
|
-
// Remove previous body if it exists
|
|
1667
|
-
if (this.requestOptions.body)
|
|
1668
|
-
delete this.requestOptions.body;
|
|
1669
|
-
// Process the body based on its type
|
|
1670
|
-
if (this.bodyType === BodyType.JSON) {
|
|
1671
|
-
this.requestOptions.body = JSON.stringify(this.body);
|
|
1672
|
-
}
|
|
1673
|
-
else {
|
|
1674
|
-
this.requestOptions.body = this.body;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
return super.sendTo(url);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
/**
|
|
1682
|
-
* HTTP GET request implementation
|
|
1683
|
-
* Used for retrieving data from an API endpoint.
|
|
1684
|
-
*
|
|
1685
|
-
* @example
|
|
1686
|
-
* const request = new GetRequest()
|
|
1687
|
-
* .withQueryParams({ id: '123' })
|
|
1688
|
-
* .sendTo('/api/users');
|
|
1689
|
-
*/
|
|
1690
|
-
class GetRequest extends BaseRequest {
|
|
1691
|
-
constructor() {
|
|
1692
|
-
super(...arguments);
|
|
1693
|
-
this.method = exports.HttpMethod.GET;
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
/**
|
|
1697
|
-
* HTTP HEAD request implementation
|
|
1698
|
-
* Similar to GET but returns only headers without a response body.
|
|
1699
|
-
*
|
|
1700
|
-
* @example
|
|
1701
|
-
* const request = new HeadRequest()
|
|
1702
|
-
* .sendTo('/api/users/123');
|
|
1703
|
-
*/
|
|
1704
|
-
class HeadRequest extends BaseRequest {
|
|
1705
|
-
constructor() {
|
|
1706
|
-
super(...arguments);
|
|
1707
|
-
this.method = exports.HttpMethod.HEAD;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
/**
|
|
1711
|
-
* HTTP OPTIONS request implementation
|
|
1712
|
-
* Used to describe the communication options for the target resource.
|
|
1713
|
-
*
|
|
1714
|
-
* @example
|
|
1715
|
-
* const request = new OptionsRequest()
|
|
1716
|
-
* .sendTo('/api/users');
|
|
1717
|
-
*/
|
|
1718
|
-
class OptionsRequest extends BaseRequest {
|
|
1719
|
-
constructor() {
|
|
1720
|
-
super(...arguments);
|
|
1721
|
-
this.method = exports.HttpMethod.OPTIONS;
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
/**
|
|
1725
|
-
* HTTP DELETE request implementation
|
|
1726
|
-
* Used to delete a resource on the server.
|
|
1727
|
-
*
|
|
1728
|
-
* @example
|
|
1729
|
-
* const request = new DeleteRequest()
|
|
1730
|
-
* .sendTo('/api/users/123');
|
|
1731
|
-
*/
|
|
1732
|
-
class DeleteRequest extends BaseRequest {
|
|
1733
|
-
constructor() {
|
|
1734
|
-
super(...arguments);
|
|
1735
|
-
this.method = exports.HttpMethod.DELETE;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
/**
|
|
1739
|
-
* HTTP POST request implementation
|
|
1740
|
-
* Used to create a new resource or submit data for processing.
|
|
1741
|
-
*
|
|
1742
|
-
* @example
|
|
1743
|
-
* const request = new PostRequest()
|
|
1744
|
-
* .withJson({ name: 'John', email: 'john@example.com' })
|
|
1745
|
-
* .sendTo('/api/users');
|
|
1746
|
-
*/
|
|
1747
|
-
class PostRequest extends BodyRequest {
|
|
1748
|
-
constructor() {
|
|
1749
|
-
super(...arguments);
|
|
1750
|
-
this.method = exports.HttpMethod.POST;
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
/**
|
|
1754
|
-
* HTTP PUT request implementation
|
|
1755
|
-
* Used to replace or update an existing resource.
|
|
1756
|
-
*
|
|
1757
|
-
* @example
|
|
1758
|
-
* const request = new PutRequest()
|
|
1759
|
-
* .withJson({ id: '123', name: 'John', email: 'john@example.com' })
|
|
1760
|
-
* .sendTo('/api/users/123');
|
|
1761
|
-
*/
|
|
1762
|
-
class PutRequest extends BodyRequest {
|
|
1763
|
-
constructor() {
|
|
1764
|
-
super(...arguments);
|
|
1765
|
-
this.method = exports.HttpMethod.PUT;
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
/**
|
|
1769
|
-
* HTTP PATCH request implementation
|
|
1770
|
-
* Used to apply partial modifications to a resource.
|
|
1771
|
-
*
|
|
1772
|
-
* @example
|
|
1773
|
-
* const request = new PatchRequest()
|
|
1774
|
-
* .withJson({ email: 'new.email@example.com' })
|
|
1775
|
-
* .sendTo('/api/users/123');
|
|
1776
|
-
*/
|
|
1777
|
-
class PatchRequest extends BodyRequest {
|
|
1778
|
-
constructor() {
|
|
1779
|
-
super(...arguments);
|
|
1780
|
-
this.method = exports.HttpMethod.PATCH;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
/**
|
|
1785
|
-
* Create a GET request
|
|
1786
|
-
* Used for retrieving resources from the server.
|
|
1787
|
-
*
|
|
1788
|
-
* @returns A new GET request instance
|
|
1789
|
-
*
|
|
1790
|
-
* @example
|
|
1791
|
-
* const users = await get()
|
|
1792
|
-
* .withQueryParams({ limit: 10 })
|
|
1793
|
-
* .sendTo('/api/users')
|
|
1794
|
-
* .getJson();
|
|
1795
|
-
*/
|
|
1796
|
-
function get() {
|
|
1797
|
-
return new GetRequest();
|
|
1798
|
-
}
|
|
1799
|
-
/**
|
|
1800
|
-
* Create a POST request
|
|
1801
|
-
* Used for creating new resources or submitting data.
|
|
1802
|
-
*
|
|
1803
|
-
* @returns A new POST request instance
|
|
1804
|
-
*
|
|
1805
|
-
* @example
|
|
1806
|
-
* const newUser = await post()
|
|
1807
|
-
* .withJson({ name: 'John', email: 'john@example.com' })
|
|
1808
|
-
* .sendTo('/api/users')
|
|
1809
|
-
* .getJson();
|
|
1810
|
-
*/
|
|
1811
|
-
function post() {
|
|
1812
|
-
return new PostRequest();
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Create a PUT request
|
|
1816
|
-
* Used for replacing or updating an existing resource.
|
|
1817
|
-
*
|
|
1818
|
-
* @returns A new PUT request instance
|
|
1819
|
-
*
|
|
1820
|
-
* @example
|
|
1821
|
-
* const updatedUser = await put()
|
|
1822
|
-
* .withJson({ name: 'John Updated', email: 'john@example.com' })
|
|
1823
|
-
* .sendTo('/api/users/123')
|
|
1824
|
-
* .getJson();
|
|
1825
|
-
*/
|
|
1826
|
-
function put() {
|
|
1827
|
-
return new PutRequest();
|
|
1828
|
-
}
|
|
1829
|
-
/**
|
|
1830
|
-
* Create a DELETE request
|
|
1831
|
-
* Used for removing resources from the server.
|
|
1832
|
-
*
|
|
1833
|
-
* @returns A new DELETE request instance
|
|
1834
|
-
*
|
|
1835
|
-
* @example
|
|
1836
|
-
* await del()
|
|
1837
|
-
* .sendTo('/api/users/123')
|
|
1838
|
-
* .getJson();
|
|
1839
|
-
*/
|
|
1840
|
-
function del() {
|
|
1841
|
-
return new DeleteRequest();
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Create a PATCH request
|
|
1845
|
-
* Used for applying partial modifications to a resource.
|
|
1846
|
-
*
|
|
1847
|
-
* @returns A new PATCH request instance
|
|
1848
|
-
*
|
|
1849
|
-
* @example
|
|
1850
|
-
* const patchedUser = await patch()
|
|
1851
|
-
* .withJson({ status: 'active' })
|
|
1852
|
-
* .sendTo('/api/users/123')
|
|
1853
|
-
* .getJson();
|
|
1854
|
-
*/
|
|
1855
|
-
function patch() {
|
|
1856
|
-
return new PatchRequest();
|
|
1857
|
-
}
|
|
1858
|
-
/**
|
|
1859
|
-
* Create a HEAD request
|
|
1860
|
-
* Similar to GET but returns only headers without a body.
|
|
1861
|
-
*
|
|
1862
|
-
* @returns A new HEAD request instance
|
|
1863
|
-
*
|
|
1864
|
-
* @example
|
|
1865
|
-
* const response = await head()
|
|
1866
|
-
* .sendTo('/api/users/123');
|
|
1867
|
-
* console.log(response.headers.get('Last-Modified'));
|
|
1868
|
-
*/
|
|
1869
|
-
function head() {
|
|
1870
|
-
return new HeadRequest();
|
|
1871
|
-
}
|
|
1872
|
-
/**
|
|
1873
|
-
* Create an OPTIONS request
|
|
1874
|
-
* Used to describe the communication options for a resource.
|
|
1875
|
-
*
|
|
1876
|
-
* @returns A new OPTIONS request instance
|
|
1877
|
-
*
|
|
1878
|
-
* @example
|
|
1879
|
-
* const response = await options()
|
|
1880
|
-
* .sendTo('/api/users');
|
|
1881
|
-
* console.log(response.headers.get('Allow'));
|
|
1882
|
-
*/
|
|
1883
|
-
function options() {
|
|
1884
|
-
return new OptionsRequest();
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
/**
|
|
1888
|
-
* Main API object for creating HTTP requests
|
|
1889
|
-
* Provides factory methods for all HTTP methods and access to global configuration.
|
|
1890
|
-
*
|
|
1891
|
-
* @example
|
|
1892
|
-
* // Making a GET request
|
|
1893
|
-
* const users = await create.get()
|
|
1894
|
-
* .withQueryParams({ limit: 10 })
|
|
1895
|
-
* .sendTo('/api/users')
|
|
1896
|
-
* .getJson();
|
|
1897
|
-
*
|
|
1898
|
-
* // Configure global settings
|
|
1899
|
-
* create.config.setCsrfToken('token123');
|
|
1900
|
-
*/
|
|
1901
|
-
const create = {
|
|
1902
|
-
get,
|
|
1903
|
-
post,
|
|
1904
|
-
put,
|
|
1905
|
-
del,
|
|
1906
|
-
patch,
|
|
1907
|
-
head,
|
|
1908
|
-
options,
|
|
1909
|
-
// Add configuration access
|
|
1910
|
-
config: Config.getInstance(),
|
|
1911
|
-
};
|
|
1912
|
-
|
|
1913
|
-
exports.CacheManager = CacheManager;
|
|
1914
|
-
exports.CookieUtils = CookieUtils;
|
|
1915
|
-
exports.DeleteRequest = DeleteRequest;
|
|
1916
|
-
exports.GetRequest = GetRequest;
|
|
1917
|
-
exports.HeadRequest = HeadRequest;
|
|
1918
|
-
exports.OptionsRequest = OptionsRequest;
|
|
1919
|
-
exports.PatchRequest = PatchRequest;
|
|
1920
|
-
exports.PostRequest = PostRequest;
|
|
1921
|
-
exports.PutRequest = PutRequest;
|
|
1922
|
-
exports.RequestError = RequestError;
|
|
1923
|
-
exports.ResponseWrapper = ResponseWrapper;
|
|
1924
|
-
exports.createLocalStorageStorage = createLocalStorageStorage;
|
|
1925
|
-
exports.createMemoryStorage = createMemoryStorage;
|
|
1926
|
-
exports.createSessionStorageStorage = createSessionStorageStorage;
|
|
1927
|
-
exports["default"] = create;
|
|
1928
|
-
|
|
1929
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
1930
|
-
|
|
1931
|
-
}));
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).createRequest={})}(this,(function(t){"use strict";var e,s,i,r,n,o,h,a,c;t.HttpMethod=void 0,(e=t.HttpMethod||(t.HttpMethod={})).GET="GET",e.PUT="PUT",e.POST="POST",e.HEAD="HEAD",e.PATCH="PATCH",e.DELETE="DELETE",e.OPTIONS="OPTIONS",t.RequestPriority=void 0,(s=t.RequestPriority||(t.RequestPriority={})).LOW="low",s.HIGH="high",s.AUTO="auto",t.CredentialsPolicy=void 0,(i=t.CredentialsPolicy||(t.CredentialsPolicy={})).OMIT="omit",i.INCLUDE="include",i.SAME_ORIGIN="same-origin",t.RequestMode=void 0,(r=t.RequestMode||(t.RequestMode={})).CORS="cors",r.NO_CORS="no-cors",r.SAME_ORIGIN="same-origin",r.NAVIGATE="navigate",t.RedirectMode=void 0,(n=t.RedirectMode||(t.RedirectMode={})).ERROR="error",n.FOLLOW="follow",n.MANUAL="manual",t.CacheMode=void 0,(o=t.CacheMode||(t.CacheMode={})).DEFAULT="default",o.RELOAD="reload",o.NO_STORE="no-store",o.NO_CACHE="no-cache",o.FORCE_CACHE="force-cache",o.ONLY_IF_CACHED="only-if-cached",t.SameSitePolicy=void 0,(h=t.SameSitePolicy||(t.SameSitePolicy={})).LAX="Lax",h.NONE="None",h.STRICT="Strict",function(t){t.JSON="json",t.STRING="string",t.BINARY="binary"}(a||(a={})),t.ReferrerPolicy=void 0,(c=t.ReferrerPolicy||(t.ReferrerPolicy={})).ORIGIN="origin",c.UNSAFE_URL="unsafe-url",c.SAME_ORIGIN="same-origin",c.NO_REFERRER="no-referrer",c.STRICT_ORIGIN="strict-origin",c.ORIGIN_WHEN_CROSS_ORIGIN="origin-when-cross-origin",c.NO_REFERRER_WHEN_DOWNGRADE="no-referrer-when-downgrade",c.STRICT_ORIGIN_WHEN_CROSS_ORIGIN="strict-origin-when-cross-origin";class u extends Error{constructor(t,e,s,i={}){super(t),this.name="RequestError",this.url=e,this.method=s,this.status=i.status,this.response=i.response,this.timeoutError=i.timeoutError,Error.captureStackTrace&&Error.captureStackTrace(this,u),Object.setPrototypeOf(this,u.prototype)}static timeout(t,e,s){return new u(`Request timed out after ${s}ms`,t,e,{timeoutError:!0})}static fromResponse(t,e,s){return new u("Request failed with status "+t.status,e,s,{status:t.status,response:t})}static networkError(t,e,s){const i=new u("Network error: "+s.message,t,e);return i.stack=s.stack,i}}class f{constructor(t){this.bodyUsed=!1,this.response=t}get status(){return this.response.status}get statusText(){return this.response.statusText}get headers(){return this.response.headers}get ok(){return this.response.ok}get raw(){return this.response}async getJson(){if(void 0!==this.jsonCache)return this.jsonCache;if(this.bodyUsed){if(void 0!==this.textCache)try{return this.jsonCache=JSON.parse(this.textCache),this.jsonCache}catch(t){throw Error("Cannot parse cached text as JSON")}throw Error("Response body has already been consumed in a different format")}return this.bodyUsed=!0,this.jsonCache=await this.response.json(),this.jsonCache}async getText(){if(void 0!==this.textCache)return this.textCache;if(this.bodyUsed){if(void 0!==this.jsonCache)return this.textCache=JSON.stringify(this.jsonCache),this.textCache;if(void 0!==this.blobCache)return this.textCache=await this.blobCache.text(),this.textCache;throw Error("Response body has already been consumed in a format that cannot be converted to text")}return this.bodyUsed=!0,this.textCache=await this.response.text(),this.textCache}async getBlob(){if(void 0!==this.blobCache)return this.blobCache;if(this.bodyUsed){if(void 0!==this.textCache)return this.blobCache=new Blob([this.textCache],{type:"text/plain"}),this.blobCache;throw Error("Response body has already been consumed in a format that cannot be converted to Blob")}return this.bodyUsed=!0,this.blobCache=await this.response.blob(),this.blobCache}async getArrayBuffer(){if(void 0!==this.arrayBufferCache)return this.arrayBufferCache;if(this.bodyUsed){if(void 0!==this.textCache){const t=(new TextEncoder).encode(this.textCache);return this.arrayBufferCache=t.buffer,this.arrayBufferCache}if(void 0!==this.blobCache)return this.arrayBufferCache=await this.blobCache.arrayBuffer(),this.arrayBufferCache;throw Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer")}return this.bodyUsed=!0,this.arrayBufferCache=await this.response.arrayBuffer(),this.arrayBufferCache}getBody(){if(this.bodyUsed)throw Error("Response body has already been consumed");return this.bodyUsed=!0,this.response.body}}function l(){const t=new Map;return{get:e=>t.get(e)||null,set(e,s){t.set(e,s)},has:e=>t.has(e),delete(e){t.delete(e)},clear(){t.clear()}}}function d(){return{get:t=>"undefined"==typeof localStorage?null:localStorage.getItem(t),set(t,e){if("undefined"!=typeof localStorage)try{localStorage.setItem(t,e)}catch(s){console.warn("Cache storage quota exceeded, clearing cache"),this.clear();try{localStorage.setItem(t,e)}catch(t){console.error("Failed to set cache item even after clearing cache")}}},has:t=>"undefined"!=typeof localStorage&&null!==localStorage.getItem(t),delete(t){"undefined"!=typeof localStorage&&localStorage.removeItem(t)},clear(){"undefined"!=typeof localStorage&&localStorage.clear()}}}function y(){return{get:t=>"undefined"==typeof sessionStorage?null:sessionStorage.getItem(t),set(t,e){if("undefined"!=typeof sessionStorage)try{sessionStorage.setItem(t,e)}catch(s){console.warn("Cache storage quota exceeded, clearing cache"),this.clear();try{sessionStorage.setItem(t,e)}catch(t){console.error("Failed to set cache item even after clearing cache")}}},has:t=>"undefined"!=typeof sessionStorage&&null!==sessionStorage.getItem(t),delete(t){"undefined"!=typeof sessionStorage&&sessionStorage.removeItem(t)},clear(){"undefined"!=typeof sessionStorage&&sessionStorage.clear()}}}class w{constructor(t={}){this.entryCount=0,this.totalSize=0,this.options=t,this.storage=function(t){const{storage:e}=t||{};return e?"function"==typeof e.get&&"function"==typeof e.set?e:e===localStorage?d():e===sessionStorage?y():e instanceof Map?{get:t=>e.get(t)||null,set(t,s,i){e.set(t,s)},has:t=>e.has(t),delete(t){e.delete(t)},clear(){e.clear()}}:(console.warn("Unsupported storage type, falling back to memory storage"),l()):l()}(t)}generateKey(t,e,s,i){if(this.options.keyGenerator){const r=this.options.keyGenerator(t,e,s,i);return this.options.keyPrefix?`${this.options.keyPrefix}:${r}`:r}let r=`${e}:${t}`;if(this.options.varyByHeaders&&s){const t={};this.options.varyByHeaders.forEach((e=>{const i=e.toLowerCase();Object.keys(s).forEach((e=>{e.toLowerCase()===i&&(t[i]=s[e])}))})),Object.keys(t).length>0&&(r+=":"+JSON.stringify(t))}if("GET"!==e&&null!=i)try{"string"==typeof i||"number"==typeof i||"boolean"==typeof i?r+=":"+i:"object"==typeof i&&(r+=":"+JSON.stringify(i))}catch(t){console.warn("Failed to stringify request body for cache key")}return this.options.keyPrefix?`${this.options.keyPrefix}:${r}`:r}async get(t){const e=await Promise.resolve(this.storage.get(t));if(!e)return null;try{const s=JSON.parse(e);return s.expiry&&s.expiry<Date.now()?(await Promise.resolve(this.storage.delete(t)),null):s}catch(e){return await Promise.resolve(this.storage.delete(t)),null}}async set(t,e,s){const i=Date.now(),r=this.options.ttl,n=JSON.stringify({value:e,timestamp:i,expiry:r?i+r:null,headers:s});if(this.options.maxSize){const t=function(t){var e;if("number"==typeof t)return t;const s=t.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/i);if(!s)return 0;const i=parseFloat(s[1]);switch(null===(e=s[2])||void 0===e?void 0:e.toUpperCase()){case"KB":return 1024*i;case"MB":return 1048576*i;case"GB":return 1073741824*i;default:return i}}(this.options.maxSize),e=(new TextEncoder).encode(n).length;if(e>t)return void console.warn(`Cache entry size (${e} bytes) exceeds maximum cache size (${t} bytes)`);this.totalSize+e>t&&(await Promise.resolve(this.storage.clear()),this.totalSize=0,this.entryCount=0),this.totalSize+=e}this.options.maxEntries&&(this.entryCount++,this.entryCount>this.options.maxEntries&&(await Promise.resolve(this.storage.clear()),this.entryCount=1)),await Promise.resolve(this.storage.set(t,n,r))}async has(t){return Promise.resolve(this.storage.has(t))}async delete(t){return Promise.resolve(this.storage.delete(t))}async clear(){return this.entryCount=0,this.totalSize=0,Promise.resolve(this.storage.clear())}}class g{constructor(){this.csrfHeaderName="X-CSRF-Token",this.xsrfCookieName="XSRF-TOKEN",this.xsrfHeaderName="X-XSRF-TOKEN",this.csrfToken=null,this.enableAutoXsrf=!0,this.enableAntiCsrf=!0}static getInstance(){return g.instance||(g.instance=new g),g.instance}setCsrfToken(t){return this.csrfToken=t,this}getCsrfToken(){return this.csrfToken}setCsrfHeaderName(t){return this.csrfHeaderName=t,this}getCsrfHeaderName(){return this.csrfHeaderName}setXsrfCookieName(t){return this.xsrfCookieName=t,this}getXsrfCookieName(){return this.xsrfCookieName}setXsrfHeaderName(t){return this.xsrfHeaderName=t,this}getXsrfHeaderName(){return this.xsrfHeaderName}setEnableAutoXsrf(t){return this.enableAutoXsrf=t,this}isAutoXsrfEnabled(){return this.enableAutoXsrf}setEnableAntiCsrf(t){return this.enableAntiCsrf=t,this}isAntiCsrfEnabled(){return this.enableAntiCsrf}reset(){return this.csrfToken=null,this.csrfHeaderName="X-CSRF-Token",this.enableAntiCsrf=!0,this.xsrfCookieName="XSRF-TOKEN",this.xsrfHeaderName="X-XSRF-TOKEN",this.enableAutoXsrf=!0,this}}class p{static formatRequestCookies(t){const e=[];return Object.entries(t).forEach((([t,s])=>{let i;i="string"==typeof s?s:s.value,e.push(`${encodeURIComponent(t)}=${encodeURIComponent(i)}`)})),e.join("; ")}}class m{static getTokenFromMeta(t="csrf-token"){if("undefined"==typeof document)return null;const e=document.querySelector(`meta[name="${t}"]`);return(null==e?void 0:e.getAttribute("content"))||null}static getTokenFromCookie(t="csrf-token"){if("undefined"==typeof document)return null;const e=document.cookie.split(";");for(const s of e){const[e,i]=s.trim().split("=");if(e===t)return decodeURIComponent(i)}return null}static isValidToken(t){return!("string"!=typeof t||8>t.length||t.length>10&&(!/^[A-Za-z0-9\-_=+/.]+$/.test(t)||2>[/[A-Z]/.test(t),/[a-z]/.test(t),/[0-9]/.test(t),/[-_=+/.]/.test(t)].filter(Boolean).length))}}class b{constructor(){this.requestOptions={headers:{}},this.queryParams=new URLSearchParams,this.autoApplyCsrfProtection=!0,this.cacheEnabled=!1}withHeaders(t){return this.requestOptions.headers={...this.requestOptions.headers,...t},this}withHeader(t,e){return this.withHeaders({[t]:e})}withTimeout(t){if(!Number.isFinite(t)||0>=t)throw Error("Timeout must be a positive number");return this.requestOptions.timeout=t,this}withRetries(t){if(!Number.isInteger(t)||0>t)throw Error("Retry count must be a non-negative integer");return this.requestOptions.retries=t,this}onRetry(t){return this.requestOptions.onRetry=t,this}withCredentials(e=t.CredentialsPolicy.SAME_ORIGIN){return this.requestOptions.credentials=e,this}withAbortController(t){return this.abortController=t,this}withReferrer(t){return this.requestOptions.referrer=t,this}withReferrerPolicy(t){return this.requestOptions.referrerPolicy=t,this}withRedirect(t){return this.requestOptions.redirect=t,this}withKeepAlive(t){return this.requestOptions.keepalive=t,this}withPriority(t){return this.requestOptions.priority=t,this}withQueryParams(t){return Object.entries(t).forEach((([t,e])=>{null!=e&&(Array.isArray(e)?e.forEach((e=>this.queryParams.append(t,e+""))):this.queryParams.append(t,e+""))})),this}withQueryParam(t,e){return null==e||(Array.isArray(e)?e.forEach((e=>this.queryParams.append(t,e+""))):this.queryParams.append(t,e+"")),this}withMode(t){return this.requestOptions.mode=t,this}withContentType(t){return this.withHeaders({"Content-Type":t})}withAuthorization(t){return this.withHeaders({Authorization:t})}withBasicAuth(t,e){const s=this.encodeBase64(`${t}:${e}`);return this.withAuthorization("Basic "+s)}encodeBase64(t){if("undefined"!=typeof TextEncoder&&"function"==typeof btoa){const e=(new TextEncoder).encode(t);return btoa(String.fromCharCode.call(null,...new Uint8Array(e)))}if("function"==typeof btoa)return btoa(t);if("undefined"!=typeof Buffer)return Buffer.from(t).toString("base64");throw Error("Base64 encoding is not supported in this environment")}withBearerToken(t){return this.withAuthorization("Bearer "+t)}hasHeader(t,e){return Object.keys(t).some((t=>t.toLowerCase()===e.toLowerCase()))}withCookies(t){if(0===Object.entries(t).length)return this;const e=this.requestOptions.headers||{},s=[],i=[];Object.keys(e).forEach((t=>{"cookie"===t.toLowerCase()&&(e[t]&&s.push(e[t]),i.push(t))}));const r=p.formatRequestCookies(t),n=i.length>0?i[0]:"Cookie",o=[...s,r].filter(Boolean).join("; "),h={...e};return i.forEach((t=>{delete h[t]})),this.requestOptions.headers={...h,[n]:o},this}withCookie(t,e){return this.withCookies({[t]:e})}withCsrfToken(t,e="X-CSRF-Token"){return this.withHeaders({[e]:t})}withoutCsrfProtection(){return this.autoApplyCsrfProtection=!1,this}withAntiCsrfHeaders(){return this.withHeaders({"X-Requested-With":"XMLHttpRequest"})}withCache(t={}){return this.cacheEnabled=!0,this.cacheManager=new w(t),this}sendTo(t){if(t=this.formatUrlWithQueryParams(t),this.autoApplyCsrfProtection){const t=g.getInstance();t.isAntiCsrfEnabled()&&this.withHeaders({"X-Requested-With":"XMLHttpRequest"});const e=t.getCsrfToken();if(e){const s=this.requestOptions.headers,i=t.getCsrfHeaderName();this.hasHeader(s,"X-CSRF-Token")||this.hasHeader(s,i)||this.withHeaders({[i]:e})}if(t.isAutoXsrfEnabled()&&"undefined"!=typeof document){const e=m.getTokenFromCookie(t.getXsrfCookieName());if(e&&m.isValidToken(e)){const s=this.requestOptions.headers,i=t.getXsrfHeaderName();this.hasHeader(s,"X-XSRF-TOKEN")||this.hasHeader(s,i)||this.withHeaders({[i]:e})}}}const e={...this.requestOptions,method:this.method};let s;if(this.cacheEnabled&&this.cacheManager){const i=this.cacheManager.generateKey(t,this.method,this.requestOptions.headers,this.requestOptions.body);s=this.executeWithCache(t,e,i)}else s=this.requestOptions.retries?this.executeWithRetries(t,e):this.executeRequest(t,e);const i=s;return i.getJson=async()=>s.then((t=>t.getJson())),i.getText=async()=>s.then((t=>t.getText())),i.getBlob=async()=>s.then((t=>t.getBlob())),i.getArrayBuffer=async()=>s.then((t=>t.getArrayBuffer())),i.getBody=async()=>s.then((t=>t.getBody())),i.getData=async t=>{try{const e=await s.then((t=>t.getJson()));return t?t(e):e}catch(t){const e=Error(`Data selector failed: ${t instanceof Error?t.message:t+""}. Original data: ${JSON.stringify(await s.then((t=>t.getJson())),null,2)}`);throw t instanceof Error&&t.stack&&(e.stack=t.stack),e}},i}formatUrlWithQueryParams(t){if(!(""+this.queryParams))return t;try{const e=new URL(t);return this.queryParams.forEach(((t,s)=>{e.searchParams.append(s,t)})),""+e}catch(e){const s=t.includes("?");return`${t}${s?"&":"?"}${""+this.queryParams}`}}async executeWithRetries(t,e){let s=0;const i=this.requestOptions.retries||0;do{try{return await this.executeRequest(t,e)}catch(r){const n=r instanceof u?r:u.networkError(t,e.method,r);if(s>=i||n.timeoutError)throw n;s++,this.requestOptions.onRetry&&await this.requestOptions.onRetry({attempt:s,error:n})}}while(i>=s);throw new u(`Max retries reached (${i}) but request still failed`,t,e.method)}async executeRequest(t,e){const s=this.abortController||new AbortController,{signal:i}=s;let r;e.signal=e.signal||i;let n=!1;this.requestOptions.timeout&&(this.abortController?"function"==typeof AbortSignal.timeout?AbortSignal.timeout(this.requestOptions.timeout).addEventListener("abort",(()=>{s.signal.aborted||(n=!0,s.abort())})):r=setTimeout((()=>{s.signal.aborted||(n=!0,s.abort())}),this.requestOptions.timeout):r=setTimeout((()=>{n=!0,s.abort()}),this.requestOptions.timeout));try{let s;try{s=await fetch(t,e)}catch(s){if(s instanceof DOMException&&"AbortError"===s.name&&n)throw u.timeout(t,e.method,this.requestOptions.timeout);throw u.networkError(t,e.method,s)}if(!s.ok)throw u.fromResponse(s,t,e.method);return new f(s)}finally{r&&clearTimeout(r)}}async executeWithCache(t,e,s){if(this.cacheManager){const t=await this.cacheManager.get(s);if(t){const e=new Response(JSON.stringify(t.value),{status:200,headers:new Headers(t.headers||{})});return new f(e)}}const i=await(this.requestOptions.retries?this.executeWithRetries(t,e):this.executeRequest(t,e));if(i.ok&&this.cacheManager)try{const t=await i.getJson(),e={};i.headers.forEach(((t,s)=>{e[s]=t})),await this.cacheManager.set(s,t,e)}catch(t){console.warn("Failed to cache response:",t)}return i}}class S extends b{withBody(t){if(this.body=t,"string"==typeof t)this.bodyType=a.STRING,this.setContentTypeIfNeeded("text/plain");else if(null===t||"object"!=typeof t||t instanceof FormData||t instanceof Blob||t instanceof ArrayBuffer||t instanceof URLSearchParams||t instanceof ReadableStream)this.bodyType=a.BINARY;else{this.bodyType=a.JSON,this.setContentTypeIfNeeded("application/json");try{JSON.stringify(t)}catch(t){throw Error("Failed to stringify request body: "+t)}}return this}setContentTypeIfNeeded(t){Object.keys(this.requestOptions.headers).some((t=>"content-type"===t.toLowerCase()))||this.withContentType(t)}sendTo(t){return void 0!==this.body&&(this.requestOptions.body&&delete this.requestOptions.body,this.requestOptions.body=this.bodyType===a.JSON?JSON.stringify(this.body):this.body),super.sendTo(t)}}class T extends b{constructor(){super(...arguments),this.method=t.HttpMethod.GET}}class E extends b{constructor(){super(...arguments),this.method=t.HttpMethod.HEAD}}class R extends b{constructor(){super(...arguments),this.method=t.HttpMethod.OPTIONS}}class v extends b{constructor(){super(...arguments),this.method=t.HttpMethod.DELETE}}class C extends S{constructor(){super(...arguments),this.method=t.HttpMethod.POST}}class x extends S{constructor(){super(...arguments),this.method=t.HttpMethod.PUT}}class O extends S{constructor(){super(...arguments),this.method=t.HttpMethod.PATCH}}const N={get:function(){return new T},post:function(){return new C},put:function(){return new x},del:function(){return new v},patch:function(){return new O},head:function(){return new E},options:function(){return new R},config:g.getInstance()};t.CacheManager=w,t.CookieUtils=p,t.DeleteRequest=v,t.GetRequest=T,t.HeadRequest=E,t.OptionsRequest=R,t.PatchRequest=O,t.PostRequest=C,t.PutRequest=x,t.RequestError=u,t.ResponseWrapper=f,t.createLocalStorageStorage=d,t.createMemoryStorage=l,t.createSessionStorageStorage=y,t.default=N,Object.defineProperty(t,"t",{value:!0})}));//# sourceMappingURL=index.js.map
|