create-request 0.0.1-beta.10

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.
@@ -0,0 +1,1930 @@
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
+ if (error instanceof Error) {
1446
+ // If selector fails, enhance the error with the original data
1447
+ const enhancedError = new Error(`Data selector failed: ${error.message}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
1448
+ enhancedError.stack = error.stack;
1449
+ throw enhancedError;
1450
+ }
1451
+ throw error;
1452
+ }
1453
+ };
1454
+ return responsePromise;
1455
+ }
1456
+ /**
1457
+ * Formats the URL with any query parameters
1458
+ * @param url The base URL
1459
+ * @returns The URL with query parameters appended
1460
+ */
1461
+ formatUrlWithQueryParams(url) {
1462
+ if (!this.queryParams.toString()) {
1463
+ return url;
1464
+ }
1465
+ try {
1466
+ // Try to use the URL constructor (works for absolute URLs)
1467
+ const urlObj = new URL(url);
1468
+ // Merge our query params with any that might be in the URL already
1469
+ this.queryParams.forEach((value, key) => {
1470
+ urlObj.searchParams.append(key, value);
1471
+ });
1472
+ return urlObj.toString();
1473
+ }
1474
+ catch (error) {
1475
+ // Handle relative URLs
1476
+ const hasExistingParams = url.includes("?");
1477
+ const separator = hasExistingParams ? "&" : "?";
1478
+ return `${url}${separator}${this.queryParams.toString()}`;
1479
+ }
1480
+ }
1481
+ /**
1482
+ * Executes a request with configured retry logic
1483
+ * @param url The formatted URL to send the request to
1484
+ * @param fetchOptions The fetch options to use
1485
+ * @returns A wrapped response object
1486
+ */
1487
+ async executeWithRetries(url, fetchOptions) {
1488
+ let attempt = 0;
1489
+ const maxRetries = this.requestOptions.retries || 0;
1490
+ do {
1491
+ try {
1492
+ return await this.executeRequest(url, fetchOptions);
1493
+ }
1494
+ catch (error) {
1495
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
1496
+ // Don't retry on timeout errors or if we've reached the max retries
1497
+ if (attempt >= maxRetries || requestError.timeoutError)
1498
+ throw requestError;
1499
+ attempt++;
1500
+ if (this.requestOptions.onRetry) {
1501
+ await this.requestOptions.onRetry({ attempt, error: requestError });
1502
+ }
1503
+ }
1504
+ } while (attempt <= maxRetries);
1505
+ // If we somehow get here, we throw an error to avoid undefined behavior
1506
+ throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
1507
+ }
1508
+ async executeRequest(url, fetchOptions) {
1509
+ // Create a request controller for timeout if needed, or use the provided one
1510
+ const controller = this.abortController || new AbortController();
1511
+ const { signal } = controller;
1512
+ // Use the controller signal for the fetch request, but don't overwrite an existing signal
1513
+ fetchOptions.signal = fetchOptions.signal || signal;
1514
+ let timeoutId;
1515
+ // Flag to track if abort was caused by our timeout
1516
+ let abortedByTimeout = false;
1517
+ // Handle timeout regardless of whether an external controller is provided
1518
+ if (this.requestOptions.timeout) {
1519
+ if (this.abortController) {
1520
+ // If we have an external controller, create a separate timeout controller
1521
+ // that won't interfere with the external one
1522
+ // Use AbortSignal.timeout if available (modern browsers)
1523
+ if (typeof AbortSignal.timeout === "function") {
1524
+ const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
1525
+ // Add event listener to propagate the abort to the main controller
1526
+ timeoutSignal.addEventListener("abort", () => {
1527
+ if (!controller.signal.aborted) {
1528
+ abortedByTimeout = true;
1529
+ controller.abort();
1530
+ }
1531
+ });
1532
+ }
1533
+ else {
1534
+ // Fallback for browsers without AbortSignal.timeout
1535
+ timeoutId = setTimeout(() => {
1536
+ if (!controller.signal.aborted) {
1537
+ abortedByTimeout = true;
1538
+ controller.abort();
1539
+ }
1540
+ }, this.requestOptions.timeout);
1541
+ }
1542
+ }
1543
+ else {
1544
+ timeoutId = setTimeout(() => {
1545
+ abortedByTimeout = true;
1546
+ controller.abort();
1547
+ }, this.requestOptions.timeout);
1548
+ }
1549
+ }
1550
+ try {
1551
+ let response;
1552
+ try {
1553
+ response = await fetch(url, fetchOptions);
1554
+ }
1555
+ catch (error) {
1556
+ // Check if this is an abort error that was specifically caused by our timeout
1557
+ if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
1558
+ throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
1559
+ }
1560
+ // Otherwise it's either a user-triggered abort or another network error
1561
+ throw RequestError.networkError(url, fetchOptions.method, error);
1562
+ }
1563
+ if (!response.ok) {
1564
+ throw RequestError.fromResponse(response, url, fetchOptions.method);
1565
+ }
1566
+ // Return a wrapped response instead of parsing it
1567
+ return new ResponseWrapper(response);
1568
+ }
1569
+ finally {
1570
+ if (timeoutId) {
1571
+ clearTimeout(timeoutId);
1572
+ }
1573
+ }
1574
+ }
1575
+ /**
1576
+ * Executes a request using cache when possible
1577
+ * @param url The formatted URL to send the request to
1578
+ * @param fetchOptions The fetch options to use
1579
+ * @param cacheKey The cache key for this request
1580
+ */
1581
+ async executeWithCache(url, fetchOptions, cacheKey) {
1582
+ // Try to get from cache first
1583
+ if (this.cacheManager) {
1584
+ const cachedEntry = await this.cacheManager.get(cacheKey);
1585
+ if (cachedEntry) {
1586
+ // Create a synthetic Response object from cached data
1587
+ const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
1588
+ status: 200,
1589
+ headers: new Headers(cachedEntry.headers || {}),
1590
+ });
1591
+ return new ResponseWrapper(cachedResponse);
1592
+ }
1593
+ }
1594
+ // Not in cache, make the actual request
1595
+ const response = await (!this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions));
1596
+ // Cache the successful response if caching is enabled
1597
+ // Only cache successful responses
1598
+ if (response.ok && this.cacheManager) {
1599
+ // Clone the response so we don't consume it
1600
+ try {
1601
+ const responseData = await response.getJson();
1602
+ // Extract headers to store in cache
1603
+ const headers = {};
1604
+ response.headers.forEach((value, key) => {
1605
+ headers[key] = value;
1606
+ });
1607
+ // Store in cache
1608
+ await this.cacheManager.set(cacheKey, responseData, headers);
1609
+ }
1610
+ catch (error) {
1611
+ // Failed to cache, but we can still return the response
1612
+ console.warn("Failed to cache response:", error);
1613
+ }
1614
+ }
1615
+ return response;
1616
+ }
1617
+ }
1618
+
1619
+ /**
1620
+ * Base class for requests that can have a body (POST, PUT, PATCH)
1621
+ */
1622
+ class BodyRequest extends BaseRequest {
1623
+ /**
1624
+ * Sets the body of the request
1625
+ * @param body - The request body
1626
+ */
1627
+ withBody(body) {
1628
+ this.body = body;
1629
+ // Set body type and validate
1630
+ if (typeof body === "string") {
1631
+ this.bodyType = BodyType.STRING;
1632
+ this.setContentTypeIfNeeded("text/plain");
1633
+ }
1634
+ else if (body !== null &&
1635
+ typeof body === "object" &&
1636
+ !(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
1637
+ this.bodyType = BodyType.JSON;
1638
+ this.setContentTypeIfNeeded("application/json");
1639
+ // Validate JSON is stringifiable early
1640
+ try {
1641
+ JSON.stringify(body);
1642
+ }
1643
+ catch (error) {
1644
+ throw new Error(`Failed to stringify request body: ${String(error)}`);
1645
+ }
1646
+ }
1647
+ else {
1648
+ this.bodyType = BodyType.BINARY;
1649
+ }
1650
+ return this;
1651
+ }
1652
+ setContentTypeIfNeeded(contentType) {
1653
+ // Only set if not already set
1654
+ const headers = this.requestOptions.headers;
1655
+ if (!Object.keys(headers).some(header => header.toLowerCase() === "content-type")) {
1656
+ this.withContentType(contentType);
1657
+ }
1658
+ }
1659
+ /**
1660
+ * Send the request to the specified URL
1661
+ * Overrides the base implementation to add body handling
1662
+ */
1663
+ sendTo(url) {
1664
+ if (this.body !== undefined) {
1665
+ // Remove previous body if it exists
1666
+ if (this.requestOptions.body)
1667
+ delete this.requestOptions.body;
1668
+ // Process the body based on its type
1669
+ if (this.bodyType === BodyType.JSON) {
1670
+ this.requestOptions.body = JSON.stringify(this.body);
1671
+ }
1672
+ else {
1673
+ this.requestOptions.body = this.body;
1674
+ }
1675
+ }
1676
+ return super.sendTo(url);
1677
+ }
1678
+ }
1679
+
1680
+ /**
1681
+ * HTTP GET request implementation
1682
+ * Used for retrieving data from an API endpoint.
1683
+ *
1684
+ * @example
1685
+ * const request = new GetRequest()
1686
+ * .withQueryParams({ id: '123' })
1687
+ * .sendTo('/api/users');
1688
+ */
1689
+ class GetRequest extends BaseRequest {
1690
+ constructor() {
1691
+ super(...arguments);
1692
+ this.method = exports.HttpMethod.GET;
1693
+ }
1694
+ }
1695
+ /**
1696
+ * HTTP HEAD request implementation
1697
+ * Similar to GET but returns only headers without a response body.
1698
+ *
1699
+ * @example
1700
+ * const request = new HeadRequest()
1701
+ * .sendTo('/api/users/123');
1702
+ */
1703
+ class HeadRequest extends BaseRequest {
1704
+ constructor() {
1705
+ super(...arguments);
1706
+ this.method = exports.HttpMethod.HEAD;
1707
+ }
1708
+ }
1709
+ /**
1710
+ * HTTP OPTIONS request implementation
1711
+ * Used to describe the communication options for the target resource.
1712
+ *
1713
+ * @example
1714
+ * const request = new OptionsRequest()
1715
+ * .sendTo('/api/users');
1716
+ */
1717
+ class OptionsRequest extends BaseRequest {
1718
+ constructor() {
1719
+ super(...arguments);
1720
+ this.method = exports.HttpMethod.OPTIONS;
1721
+ }
1722
+ }
1723
+ /**
1724
+ * HTTP DELETE request implementation
1725
+ * Used to delete a resource on the server.
1726
+ *
1727
+ * @example
1728
+ * const request = new DeleteRequest()
1729
+ * .sendTo('/api/users/123');
1730
+ */
1731
+ class DeleteRequest extends BaseRequest {
1732
+ constructor() {
1733
+ super(...arguments);
1734
+ this.method = exports.HttpMethod.DELETE;
1735
+ }
1736
+ }
1737
+ /**
1738
+ * HTTP POST request implementation
1739
+ * Used to create a new resource or submit data for processing.
1740
+ *
1741
+ * @example
1742
+ * const request = new PostRequest()
1743
+ * .withJson({ name: 'John', email: 'john@example.com' })
1744
+ * .sendTo('/api/users');
1745
+ */
1746
+ class PostRequest extends BodyRequest {
1747
+ constructor() {
1748
+ super(...arguments);
1749
+ this.method = exports.HttpMethod.POST;
1750
+ }
1751
+ }
1752
+ /**
1753
+ * HTTP PUT request implementation
1754
+ * Used to replace or update an existing resource.
1755
+ *
1756
+ * @example
1757
+ * const request = new PutRequest()
1758
+ * .withJson({ id: '123', name: 'John', email: 'john@example.com' })
1759
+ * .sendTo('/api/users/123');
1760
+ */
1761
+ class PutRequest extends BodyRequest {
1762
+ constructor() {
1763
+ super(...arguments);
1764
+ this.method = exports.HttpMethod.PUT;
1765
+ }
1766
+ }
1767
+ /**
1768
+ * HTTP PATCH request implementation
1769
+ * Used to apply partial modifications to a resource.
1770
+ *
1771
+ * @example
1772
+ * const request = new PatchRequest()
1773
+ * .withJson({ email: 'new.email@example.com' })
1774
+ * .sendTo('/api/users/123');
1775
+ */
1776
+ class PatchRequest extends BodyRequest {
1777
+ constructor() {
1778
+ super(...arguments);
1779
+ this.method = exports.HttpMethod.PATCH;
1780
+ }
1781
+ }
1782
+
1783
+ /**
1784
+ * Create a GET request
1785
+ * Used for retrieving resources from the server.
1786
+ *
1787
+ * @returns A new GET request instance
1788
+ *
1789
+ * @example
1790
+ * const users = await get()
1791
+ * .withQueryParams({ limit: 10 })
1792
+ * .sendTo('/api/users')
1793
+ * .getJson();
1794
+ */
1795
+ function get() {
1796
+ return new GetRequest();
1797
+ }
1798
+ /**
1799
+ * Create a POST request
1800
+ * Used for creating new resources or submitting data.
1801
+ *
1802
+ * @returns A new POST request instance
1803
+ *
1804
+ * @example
1805
+ * const newUser = await post()
1806
+ * .withJson({ name: 'John', email: 'john@example.com' })
1807
+ * .sendTo('/api/users')
1808
+ * .getJson();
1809
+ */
1810
+ function post() {
1811
+ return new PostRequest();
1812
+ }
1813
+ /**
1814
+ * Create a PUT request
1815
+ * Used for replacing or updating an existing resource.
1816
+ *
1817
+ * @returns A new PUT request instance
1818
+ *
1819
+ * @example
1820
+ * const updatedUser = await put()
1821
+ * .withJson({ name: 'John Updated', email: 'john@example.com' })
1822
+ * .sendTo('/api/users/123')
1823
+ * .getJson();
1824
+ */
1825
+ function put() {
1826
+ return new PutRequest();
1827
+ }
1828
+ /**
1829
+ * Create a DELETE request
1830
+ * Used for removing resources from the server.
1831
+ *
1832
+ * @returns A new DELETE request instance
1833
+ *
1834
+ * @example
1835
+ * await del()
1836
+ * .sendTo('/api/users/123')
1837
+ * .getJson();
1838
+ */
1839
+ function del() {
1840
+ return new DeleteRequest();
1841
+ }
1842
+ /**
1843
+ * Create a PATCH request
1844
+ * Used for applying partial modifications to a resource.
1845
+ *
1846
+ * @returns A new PATCH request instance
1847
+ *
1848
+ * @example
1849
+ * const patchedUser = await patch()
1850
+ * .withJson({ status: 'active' })
1851
+ * .sendTo('/api/users/123')
1852
+ * .getJson();
1853
+ */
1854
+ function patch() {
1855
+ return new PatchRequest();
1856
+ }
1857
+ /**
1858
+ * Create a HEAD request
1859
+ * Similar to GET but returns only headers without a body.
1860
+ *
1861
+ * @returns A new HEAD request instance
1862
+ *
1863
+ * @example
1864
+ * const response = await head()
1865
+ * .sendTo('/api/users/123');
1866
+ * console.log(response.headers.get('Last-Modified'));
1867
+ */
1868
+ function head() {
1869
+ return new HeadRequest();
1870
+ }
1871
+ /**
1872
+ * Create an OPTIONS request
1873
+ * Used to describe the communication options for a resource.
1874
+ *
1875
+ * @returns A new OPTIONS request instance
1876
+ *
1877
+ * @example
1878
+ * const response = await options()
1879
+ * .sendTo('/api/users');
1880
+ * console.log(response.headers.get('Allow'));
1881
+ */
1882
+ function options() {
1883
+ return new OptionsRequest();
1884
+ }
1885
+
1886
+ /**
1887
+ * Main API object for creating HTTP requests
1888
+ * Provides factory methods for all HTTP methods and access to global configuration.
1889
+ *
1890
+ * @example
1891
+ * // Making a GET request
1892
+ * const users = await create.get()
1893
+ * .withQueryParams({ limit: 10 })
1894
+ * .sendTo('/api/users')
1895
+ * .getJson();
1896
+ *
1897
+ * // Configure global settings
1898
+ * create.config.setCsrfToken('token123');
1899
+ */
1900
+ const create = {
1901
+ get,
1902
+ post,
1903
+ put,
1904
+ del,
1905
+ patch,
1906
+ head,
1907
+ options,
1908
+ // Add configuration access
1909
+ config: Config.getInstance(),
1910
+ };
1911
+
1912
+ exports.CacheManager = CacheManager;
1913
+ exports.CookieUtils = CookieUtils;
1914
+ exports.DeleteRequest = DeleteRequest;
1915
+ exports.GetRequest = GetRequest;
1916
+ exports.HeadRequest = HeadRequest;
1917
+ exports.OptionsRequest = OptionsRequest;
1918
+ exports.PatchRequest = PatchRequest;
1919
+ exports.PostRequest = PostRequest;
1920
+ exports.PutRequest = PutRequest;
1921
+ exports.RequestError = RequestError;
1922
+ exports.ResponseWrapper = ResponseWrapper;
1923
+ exports.createLocalStorageStorage = createLocalStorageStorage;
1924
+ exports.createMemoryStorage = createMemoryStorage;
1925
+ exports.createSessionStorageStorage = createSessionStorageStorage;
1926
+ exports["default"] = create;
1927
+
1928
+ Object.defineProperty(exports, '__esModule', { value: true });
1929
+
1930
+ }));