create-request 0.0.1-beta.5 → 0.0.1-beta.7

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.
@@ -127,73 +127,440 @@
127
127
  */
128
128
  class ResponseWrapper {
129
129
  constructor(response) {
130
+ this.bodyUsed = false;
130
131
  this.response = response;
131
- this.ok = response.status >= 200 && response.status < 300; // Standard Fetch API behavior
132
132
  }
133
- /**
134
- * Get the raw Response object
135
- */
136
- get raw() {
137
- return this.response;
138
- }
139
- /**
140
- * Get the HTTP status code
141
- */
133
+ // Wrapper properties
142
134
  get status() {
143
135
  return this.response.status;
144
136
  }
145
- /**
146
- * Get the HTTP status text
147
- */
148
137
  get statusText() {
149
138
  return this.response.statusText;
150
139
  }
151
- /**
152
- * Get response headers
153
- */
154
140
  get headers() {
155
141
  return this.response.headers;
156
142
  }
143
+ get ok() {
144
+ return this.response.ok;
145
+ }
146
+ get raw() {
147
+ return this.response;
148
+ }
149
+ // Response body methods with caching
150
+ async getJson() {
151
+ if (this.jsonCache !== undefined) {
152
+ return this.jsonCache;
153
+ }
154
+ if (this.bodyUsed) {
155
+ // If we already have text, try to parse it as JSON
156
+ if (this.textCache !== undefined) {
157
+ try {
158
+ // Fix unsafe assignment by explicitly casting to unknown first
159
+ this.jsonCache = JSON.parse(this.textCache);
160
+ return this.jsonCache;
161
+ }
162
+ catch (e) {
163
+ throw new Error("Cannot parse cached text as JSON");
164
+ }
165
+ }
166
+ throw new Error("Response body has already been consumed in a different format");
167
+ }
168
+ this.bodyUsed = true;
169
+ this.jsonCache = (await this.response.json());
170
+ return this.jsonCache;
171
+ }
172
+ async getText() {
173
+ if (this.textCache !== undefined) {
174
+ return this.textCache;
175
+ }
176
+ if (this.bodyUsed) {
177
+ // If we already have JSON, convert it to text
178
+ if (this.jsonCache !== undefined) {
179
+ this.textCache = JSON.stringify(this.jsonCache);
180
+ return this.textCache;
181
+ }
182
+ // If we have blob, convert to text
183
+ if (this.blobCache !== undefined) {
184
+ this.textCache = await this.blobCache.text();
185
+ return this.textCache;
186
+ }
187
+ throw new Error("Response body has already been consumed in a format that cannot be converted to text");
188
+ }
189
+ this.bodyUsed = true;
190
+ this.textCache = await this.response.text();
191
+ return this.textCache;
192
+ }
193
+ async getBlob() {
194
+ if (this.blobCache !== undefined) {
195
+ return this.blobCache;
196
+ }
197
+ if (this.bodyUsed) {
198
+ // If we have text, convert to Blob
199
+ if (this.textCache !== undefined) {
200
+ this.blobCache = new Blob([this.textCache], { type: "text/plain" });
201
+ return this.blobCache;
202
+ }
203
+ throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
204
+ }
205
+ this.bodyUsed = true;
206
+ this.blobCache = await this.response.blob();
207
+ return this.blobCache;
208
+ }
209
+ async getArrayBuffer() {
210
+ if (this.arrayBufferCache !== undefined) {
211
+ return this.arrayBufferCache;
212
+ }
213
+ if (this.bodyUsed) {
214
+ // If we have text, convert to ArrayBuffer
215
+ if (this.textCache !== undefined) {
216
+ const encoder = new TextEncoder();
217
+ // Fix ArrayBufferLike by explicitly casting to ArrayBuffer
218
+ const uint8Array = encoder.encode(this.textCache);
219
+ this.arrayBufferCache = uint8Array.buffer;
220
+ return this.arrayBufferCache;
221
+ }
222
+ // If we have blob, convert to ArrayBuffer
223
+ if (this.blobCache !== undefined) {
224
+ this.arrayBufferCache = await this.blobCache.arrayBuffer();
225
+ return this.arrayBufferCache;
226
+ }
227
+ throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
228
+ }
229
+ this.bodyUsed = true;
230
+ this.arrayBufferCache = await this.response.arrayBuffer();
231
+ return this.arrayBufferCache;
232
+ }
233
+ getBody() {
234
+ if (this.bodyUsed) {
235
+ throw new Error("Response body has already been consumed");
236
+ }
237
+ this.bodyUsed = true;
238
+ return this.response.body;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Parse size strings like "10MB" into bytes
244
+ * @param size Size as number or string with units
245
+ */
246
+ function parseSize(size) {
247
+ var _a;
248
+ if (typeof size === "number")
249
+ return size;
250
+ const match = size.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/i);
251
+ if (!match)
252
+ return 0;
253
+ const num = parseFloat(match[1]);
254
+ const unit = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase();
255
+ switch (unit) {
256
+ case "KB":
257
+ return num * 1024;
258
+ case "MB":
259
+ return num * 1024 * 1024;
260
+ case "GB":
261
+ return num * 1024 * 1024 * 1024;
262
+ default:
263
+ return num;
264
+ }
265
+ }
266
+ /**
267
+ * Create a memory storage provider using Map
268
+ */
269
+ function createMemoryStorage() {
270
+ const store = new Map();
271
+ return {
272
+ get(key) {
273
+ return store.get(key) || null;
274
+ },
275
+ set(key, value) {
276
+ store.set(key, value);
277
+ },
278
+ has(key) {
279
+ return store.has(key);
280
+ },
281
+ delete(key) {
282
+ store.delete(key);
283
+ },
284
+ clear() {
285
+ store.clear();
286
+ },
287
+ };
288
+ }
289
+ /**
290
+ * Create a localStorage-based storage provider
291
+ */
292
+ function createLocalStorageStorage() {
293
+ return {
294
+ get(key) {
295
+ if (typeof localStorage === "undefined")
296
+ return null;
297
+ return localStorage.getItem(key);
298
+ },
299
+ set(key, value) {
300
+ if (typeof localStorage === "undefined")
301
+ return;
302
+ try {
303
+ localStorage.setItem(key, value);
304
+ }
305
+ catch (e) {
306
+ // Handle quota exceeded errors
307
+ console.warn("Cache storage quota exceeded, clearing cache");
308
+ void this.clear();
309
+ try {
310
+ localStorage.setItem(key, value);
311
+ }
312
+ catch (e) {
313
+ console.error("Failed to set cache item even after clearing cache");
314
+ }
315
+ }
316
+ },
317
+ has(key) {
318
+ if (typeof localStorage === "undefined")
319
+ return false;
320
+ return localStorage.getItem(key) !== null;
321
+ },
322
+ delete(key) {
323
+ if (typeof localStorage === "undefined")
324
+ return;
325
+ localStorage.removeItem(key);
326
+ },
327
+ clear() {
328
+ if (typeof localStorage === "undefined")
329
+ return;
330
+ localStorage.clear();
331
+ },
332
+ };
333
+ }
334
+ /**
335
+ * Create a sessionStorage-based storage provider
336
+ */
337
+ function createSessionStorageStorage() {
338
+ return {
339
+ get(key) {
340
+ if (typeof sessionStorage === "undefined")
341
+ return null;
342
+ return sessionStorage.getItem(key);
343
+ },
344
+ set(key, value) {
345
+ if (typeof sessionStorage === "undefined")
346
+ return;
347
+ try {
348
+ sessionStorage.setItem(key, value);
349
+ }
350
+ catch (e) {
351
+ // Handle quota exceeded errors
352
+ console.warn("Cache storage quota exceeded, clearing cache");
353
+ void this.clear();
354
+ try {
355
+ sessionStorage.setItem(key, value);
356
+ }
357
+ catch (e) {
358
+ console.error("Failed to set cache item even after clearing cache");
359
+ }
360
+ }
361
+ },
362
+ has(key) {
363
+ if (typeof sessionStorage === "undefined")
364
+ return false;
365
+ return sessionStorage.getItem(key) !== null;
366
+ },
367
+ delete(key) {
368
+ if (typeof sessionStorage === "undefined")
369
+ return;
370
+ sessionStorage.removeItem(key);
371
+ },
372
+ clear() {
373
+ if (typeof sessionStorage === "undefined")
374
+ return;
375
+ sessionStorage.clear();
376
+ },
377
+ };
378
+ }
379
+ /**
380
+ * Create a storage provider from a cache options object
381
+ */
382
+ function createStorageProvider(options) {
383
+ const { storage } = options || {};
384
+ if (!storage) {
385
+ // Default to memory storage
386
+ return createMemoryStorage();
387
+ }
388
+ // Check if storage is already a StorageProvider
389
+ if (typeof storage.get === "function" && typeof storage.set === "function") {
390
+ return storage;
391
+ }
392
+ // Check if it's localStorage or sessionStorage
393
+ if (storage === localStorage) {
394
+ return createLocalStorageStorage();
395
+ }
396
+ if (storage === sessionStorage) {
397
+ return createSessionStorageStorage();
398
+ }
399
+ // Check if it's a Map
400
+ if (storage instanceof Map) {
401
+ return {
402
+ get(key) {
403
+ return storage.get(key) || null;
404
+ },
405
+ set(key, value, _ttl) {
406
+ storage.set(key, value);
407
+ },
408
+ has(key) {
409
+ return storage.has(key);
410
+ },
411
+ delete(key) {
412
+ storage.delete(key);
413
+ },
414
+ clear() {
415
+ storage.clear();
416
+ },
417
+ };
418
+ }
419
+ // Default to memory storage
420
+ console.warn("Unsupported storage type, falling back to memory storage");
421
+ return createMemoryStorage();
422
+ }
423
+
424
+ /**
425
+ * Cache manager that handles storage operations
426
+ */
427
+ class CacheManager {
428
+ constructor(options = {}) {
429
+ this.entryCount = 0;
430
+ this.totalSize = 0;
431
+ this.options = options;
432
+ this.storage = createStorageProvider(options);
433
+ }
157
434
  /**
158
- * Parse response as JSON
435
+ * Generate a cache key based on request details
159
436
  */
160
- async getJson() {
161
- return this.response.json();
437
+ generateKey(url, method, headers, body) {
438
+ // Use custom key generator if provided
439
+ if (this.options.keyGenerator) {
440
+ const customKey = this.options.keyGenerator(url, method, headers, body);
441
+ return this.options.keyPrefix ? `${this.options.keyPrefix}:${customKey}` : customKey;
442
+ }
443
+ // Default key generation
444
+ let key = `${method}:${url}`;
445
+ // Add vary headers to key if specified
446
+ if (this.options.varyByHeaders && headers) {
447
+ const varyHeaders = {};
448
+ this.options.varyByHeaders.forEach(header => {
449
+ const normalizedHeader = header.toLowerCase();
450
+ // Find the header in case-insensitive way
451
+ Object.keys(headers).forEach(h => {
452
+ if (h.toLowerCase() === normalizedHeader) {
453
+ varyHeaders[normalizedHeader] = headers[h];
454
+ }
455
+ });
456
+ });
457
+ if (Object.keys(varyHeaders).length > 0) {
458
+ key += `:${JSON.stringify(varyHeaders)}`;
459
+ }
460
+ }
461
+ // Include body in cache key for non-GET requests if it's a simple value
462
+ if (method !== "GET" && body !== undefined && body !== null) {
463
+ try {
464
+ if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") {
465
+ key += `:${String(body)}`;
466
+ }
467
+ else if (typeof body === "object") {
468
+ key += `:${JSON.stringify(body)}`;
469
+ }
470
+ }
471
+ catch (e) {
472
+ console.warn("Failed to stringify request body for cache key");
473
+ }
474
+ }
475
+ // Add prefix if specified
476
+ return this.options.keyPrefix ? `${this.options.keyPrefix}:${key}` : key;
162
477
  }
163
478
  /**
164
- * Get response as text
479
+ * Get a value from cache
165
480
  */
166
- async getText() {
167
- return this.response.text();
481
+ async get(key) {
482
+ const value = await Promise.resolve(this.storage.get(key));
483
+ if (!value)
484
+ return null;
485
+ try {
486
+ const entry = JSON.parse(value);
487
+ // Check if the entry has expired
488
+ if (entry.expiry && entry.expiry < Date.now()) {
489
+ await Promise.resolve(this.storage.delete(key));
490
+ return null;
491
+ }
492
+ return entry;
493
+ }
494
+ catch (e) {
495
+ // If we can't parse the cached value, remove it
496
+ await Promise.resolve(this.storage.delete(key));
497
+ return null;
498
+ }
168
499
  }
169
500
  /**
170
- * Get response as Blob
501
+ * Set a value in cache
171
502
  */
172
- async getBlob() {
173
- return this.response.blob();
503
+ async set(key, value, headers) {
504
+ const now = Date.now();
505
+ const ttl = this.options.ttl;
506
+ const entry = {
507
+ value,
508
+ timestamp: now,
509
+ expiry: ttl ? now + ttl : null,
510
+ headers,
511
+ };
512
+ const serialized = JSON.stringify(entry);
513
+ // Check size constraints if applicable
514
+ if (this.options.maxSize) {
515
+ const maxBytes = parseSize(this.options.maxSize);
516
+ const entrySize = new TextEncoder().encode(serialized).length;
517
+ // If a single entry exceeds the max size, reject it without clearing the cache
518
+ if (entrySize > maxBytes) {
519
+ console.warn(`Cache entry size (${entrySize} bytes) exceeds maximum cache size (${maxBytes} bytes)`);
520
+ return;
521
+ }
522
+ // Only clear if adding this entry would exceed the total limit
523
+ if (this.totalSize + entrySize > maxBytes) {
524
+ // Simple strategy: clear everything
525
+ // A more sophisticated strategy would remove oldest entries first
526
+ await Promise.resolve(this.storage.clear());
527
+ this.totalSize = 0;
528
+ this.entryCount = 0;
529
+ }
530
+ this.totalSize += entrySize;
531
+ }
532
+ // Check max entries constraint
533
+ if (this.options.maxEntries) {
534
+ this.entryCount++;
535
+ if (this.entryCount > this.options.maxEntries) {
536
+ // Simple strategy: clear everything
537
+ // A more sophisticated strategy would use LRU cache
538
+ await Promise.resolve(this.storage.clear());
539
+ this.entryCount = 1;
540
+ }
541
+ }
542
+ // Pass ttl to storage provider if available
543
+ await Promise.resolve(this.storage.set(key, serialized, ttl));
174
544
  }
175
545
  /**
176
- * Get response as ArrayBuffer
546
+ * Check if a key exists in the cache
177
547
  */
178
- async getArrayBuffer() {
179
- return this.response.arrayBuffer();
180
- }
181
- /**
182
- * Get response as ReadableStream for true streaming consumption.
183
- * This allows you to process data incrementally as it arrives.
184
- *
185
- * @example
186
- * const stream = await request.sendTo('https://example.com/large-file').getBody();
187
- * const reader = stream.getReader();
188
- *
189
- * while (true) {
190
- * const { done, value } = await reader.read();
191
- * if (done) break;
192
- * // Process chunk of data in 'value'
193
- * }
548
+ async has(key) {
549
+ return Promise.resolve(this.storage.has(key));
550
+ }
551
+ /**
552
+ * Delete a key from the cache
194
553
  */
195
- getBody() {
196
- return this.response.body;
554
+ async delete(key) {
555
+ return Promise.resolve(this.storage.delete(key));
556
+ }
557
+ /**
558
+ * Clear the entire cache
559
+ */
560
+ async clear() {
561
+ this.entryCount = 0;
562
+ this.totalSize = 0;
563
+ return Promise.resolve(this.storage.clear());
197
564
  }
198
565
  }
199
566
 
@@ -298,15 +665,15 @@
298
665
  return this.enableAntiCsrf;
299
666
  }
300
667
  /**
301
- * Reset configuration to defaults
668
+ * Reset all configuration options to their defaults
302
669
  */
303
670
  reset() {
304
671
  this.csrfToken = null;
305
672
  this.csrfHeaderName = "X-CSRF-Token";
673
+ this.enableAntiCsrf = true;
306
674
  this.xsrfCookieName = "XSRF-TOKEN";
307
675
  this.xsrfHeaderName = "X-XSRF-TOKEN";
308
676
  this.enableAutoXsrf = true;
309
- this.enableAntiCsrf = true;
310
677
  return this;
311
678
  }
312
679
  }
@@ -371,12 +738,34 @@
371
738
  }
372
739
  /**
373
740
  * Validates if the provided string is a potential CSRF token
374
- * Checks if the token meets minimum security requirements (non-empty, not too short)
741
+ * Checks if the token meets security requirements
375
742
  * @param token The token to validate
376
- * @returns Whether the token is potentially valid
743
+ * @returns Whether the token is valid
377
744
  */
378
745
  static isValidToken(token) {
379
- return typeof token === "string" && token.length >= 8;
746
+ // Basic checks for null/undefined and type
747
+ if (typeof token !== "string") {
748
+ return false;
749
+ }
750
+ if (token.length < 8)
751
+ return false;
752
+ // If token is longer than 10 chars, perform additional security checks
753
+ if (token.length > 10) {
754
+ // Check for valid character set (alphanumeric & common token symbols)
755
+ const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
756
+ if (!validTokenRegex.test(token)) {
757
+ return false;
758
+ }
759
+ // For longer tokens, check for sufficient entropy (at least 2 character types)
760
+ const hasUpperCase = /[A-Z]/.test(token);
761
+ const hasLowerCase = /[a-z]/.test(token);
762
+ const hasNumbers = /[0-9]/.test(token);
763
+ const hasSpecials = /[-_=+/.]/.test(token);
764
+ const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
765
+ return characterTypesCount >= 2;
766
+ }
767
+ // For shorter tokens (8-10 chars), just return true if we reached here
768
+ return true;
380
769
  }
381
770
  }
382
771
 
@@ -388,6 +777,7 @@
388
777
  };
389
778
  this.queryParams = new URLSearchParams();
390
779
  this.autoApplyCsrfProtection = true;
780
+ this.cacheEnabled = false;
391
781
  }
392
782
  withHeaders(headers) {
393
783
  this.requestOptions.headers = {
@@ -397,10 +787,14 @@
397
787
  return this;
398
788
  }
399
789
  withTimeout(timeout) {
790
+ if (!Number.isFinite(timeout) || timeout <= 0)
791
+ throw new Error("Timeout must be a positive number");
400
792
  this.requestOptions.timeout = timeout;
401
793
  return this;
402
794
  }
403
795
  withRetries(retries) {
796
+ if (!Number.isInteger(retries) || retries < 0)
797
+ throw new Error("Retry count must be a non-negative integer");
404
798
  this.requestOptions.retries = retries;
405
799
  return this;
406
800
  }
@@ -485,13 +879,16 @@
485
879
  /**
486
880
  * Adds a single query parameter
487
881
  * @param key - The parameter name
488
- * @param value - The parameter value
882
+ * @param value - The parameter value, can be a single value or array of values
489
883
  * @returns The instance for chaining
490
884
  */
491
885
  withQueryParam(key, value) {
492
- if (value !== null && value !== undefined) {
886
+ if (value === null || value === undefined)
887
+ return this;
888
+ if (Array.isArray(value))
889
+ value.forEach(v => this.queryParams.append(key, String(v)));
890
+ else
493
891
  this.queryParams.append(key, String(value));
494
- }
495
892
  return this;
496
893
  }
497
894
  /**
@@ -530,9 +927,23 @@
530
927
  * @returns The instance for chaining
531
928
  */
532
929
  withBasicAuth(username, password) {
533
- const credentials = btoa(`${username}:${password}`);
930
+ const credentials = this.encodeBase64(`${username}:${password}`);
534
931
  return this.withAuthorization(`Basic ${credentials}`);
535
932
  }
933
+ /**
934
+ * Cross-environment base64 encoding
935
+ * Works in both browser and Node.js environments
936
+ */
937
+ encodeBase64(str) {
938
+ // Browser environment
939
+ if (typeof btoa === "function")
940
+ return btoa(str);
941
+ // Node.js environment
942
+ if (typeof Buffer !== "undefined")
943
+ return Buffer.from(str).toString("base64");
944
+ // Fallback (should never happen in modern environments)
945
+ throw new Error("Base64 encoding is not supported in this environment");
946
+ }
536
947
  /**
537
948
  * Sets the bearer token for authentication
538
949
  * @param token - The bearer token
@@ -541,6 +952,15 @@
541
952
  withBearerToken(token) {
542
953
  return this.withAuthorization(`Bearer ${token}`);
543
954
  }
955
+ /**
956
+ * Helper function to check for header presence in a case-insensitive way
957
+ * @param headers Headers object
958
+ * @param headerName Header name to check
959
+ * @returns Boolean indicating if the header exists (case-insensitive)
960
+ */
961
+ hasHeader(headers, headerName) {
962
+ return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
963
+ }
544
964
  /**
545
965
  * Sets cookies for the request
546
966
  * @param cookies Object containing cookie name-value pairs or cookie options
@@ -553,19 +973,34 @@
553
973
  }
554
974
  // Get current headers or initialize empty object
555
975
  const currentHeaders = this.requestOptions.headers || {};
556
- // Get the existing cookie header in a case-insensitive way
557
- let existingCookies = "";
558
- const cookieHeaderName = Object.keys(currentHeaders).find(header => header.toLowerCase() === "cookie");
559
- if (cookieHeaderName)
560
- existingCookies = currentHeaders[cookieHeaderName];
976
+ // Get all existing cookie headers with different cases
977
+ const cookieValues = [];
978
+ const cookieHeaderKeys = [];
979
+ Object.keys(currentHeaders).forEach(key => {
980
+ if (key.toLowerCase() === "cookie") {
981
+ // Don't add empty cookie values
982
+ if (currentHeaders[key])
983
+ cookieValues.push(currentHeaders[key]);
984
+ cookieHeaderKeys.push(key);
985
+ }
986
+ });
987
+ // Format the new cookies
561
988
  const cookieString = CookieUtils.formatRequestCookies(cookies);
562
- // Combine with existing cookies if present
563
- const newCookieValue = existingCookies ? `${existingCookies}; ${cookieString}` : cookieString;
564
- // Set the Cookie header, preserving original case if it exists
565
- const headerName = cookieHeaderName || "Cookie";
566
- return this.withHeaders({
567
- [headerName]: newCookieValue,
989
+ // Choose which header name to use - preserve existing case if possible
990
+ const headerName = cookieHeaderKeys.length > 0 ? cookieHeaderKeys[0] : "Cookie";
991
+ // Combine all existing cookie values with the new ones
992
+ const combinedCookieValue = [...cookieValues, cookieString].filter(Boolean).join("; ");
993
+ // Create a new headers object without any cookie headers
994
+ const newHeaders = { ...currentHeaders };
995
+ cookieHeaderKeys.forEach(key => {
996
+ delete newHeaders[key];
568
997
  });
998
+ // Set the new combined cookie header
999
+ this.requestOptions.headers = {
1000
+ ...newHeaders,
1001
+ [headerName]: combinedCookieValue,
1002
+ };
1003
+ return this;
569
1004
  }
570
1005
  /**
571
1006
  * Set a single cookie
@@ -605,6 +1040,16 @@
605
1040
  "X-Requested-With": "XMLHttpRequest",
606
1041
  });
607
1042
  }
1043
+ /**
1044
+ * Enables caching for the request with the specified options
1045
+ * @param options The cache configuration options
1046
+ * @returns The instance for chaining
1047
+ */
1048
+ withCache(options = {}) {
1049
+ this.cacheEnabled = true;
1050
+ this.cacheManager = new CacheManager(options);
1051
+ return this;
1052
+ }
608
1053
  /**
609
1054
  * Send the request to the specified URL
610
1055
  * @param url The URL to send the request to
@@ -624,12 +1069,13 @@
624
1069
  // Apply global CSRF token if set
625
1070
  const globalToken = config.getCsrfToken();
626
1071
  if (globalToken) {
627
- // Check if local token exists
1072
+ // Check if local token exists using case-insensitive comparison
628
1073
  const headers = this.requestOptions.headers;
629
- const hasLocalToken = Object.keys(headers).some(key => key === "X-CSRF-Token" || key === config.getCsrfHeaderName());
1074
+ const csrfHeaderName = config.getCsrfHeaderName();
1075
+ const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
630
1076
  if (!hasLocalToken) {
631
1077
  this.withHeaders({
632
- [config.getCsrfHeaderName()]: globalToken,
1078
+ [csrfHeaderName]: globalToken,
633
1079
  });
634
1080
  }
635
1081
  }
@@ -638,10 +1084,11 @@
638
1084
  const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
639
1085
  if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
640
1086
  const headers = this.requestOptions.headers;
641
- const hasLocalToken = Object.keys(headers).some(key => key === "X-XSRF-TOKEN" || key === config.getXsrfHeaderName());
1087
+ const xsrfHeaderName = config.getXsrfHeaderName();
1088
+ const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
642
1089
  if (!hasLocalToken) {
643
1090
  this.withHeaders({
644
- [config.getXsrfHeaderName()]: xsrfToken,
1091
+ [xsrfHeaderName]: xsrfToken,
645
1092
  });
646
1093
  }
647
1094
  }
@@ -652,10 +1099,15 @@
652
1099
  ...this.requestOptions,
653
1100
  method: this.method,
654
1101
  };
655
- // Create the base promise
656
- const basePromise = !this.requestOptions.retries
657
- ? this.executeRequest(url, fetchOptions)
658
- : this.executeWithRetries(url, fetchOptions);
1102
+ // Create the base promise - handle caching if enabled
1103
+ let basePromise;
1104
+ if (this.cacheEnabled && this.cacheManager) {
1105
+ const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
1106
+ basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
1107
+ }
1108
+ else {
1109
+ basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1110
+ }
659
1111
  const responsePromise = basePromise;
660
1112
  // Attach convenience methods that begin processing immediately
661
1113
  responsePromise.getJson = async () => {
@@ -684,12 +1136,21 @@
684
1136
  if (!this.queryParams.toString()) {
685
1137
  return url;
686
1138
  }
687
- const urlObj = new URL(url);
688
- // Merge our query params with any that might be in the URL already
689
- this.queryParams.forEach((value, key) => {
690
- urlObj.searchParams.append(key, value);
691
- });
692
- return urlObj.toString();
1139
+ try {
1140
+ // Try to use the URL constructor (works for absolute URLs)
1141
+ const urlObj = new URL(url);
1142
+ // Merge our query params with any that might be in the URL already
1143
+ this.queryParams.forEach((value, key) => {
1144
+ urlObj.searchParams.append(key, value);
1145
+ });
1146
+ return urlObj.toString();
1147
+ }
1148
+ catch (error) {
1149
+ // Handle relative URLs
1150
+ const hasExistingParams = url.includes("?");
1151
+ const separator = hasExistingParams ? "&" : "?";
1152
+ return `${url}${separator}${this.queryParams.toString()}`;
1153
+ }
693
1154
  }
694
1155
  /**
695
1156
  * Executes a request with configured retry logic
@@ -700,38 +1161,65 @@
700
1161
  async executeWithRetries(url, fetchOptions) {
701
1162
  let attempt = 0;
702
1163
  const maxRetries = this.requestOptions.retries || 0;
703
- // eslint-disable-next-line no-constant-condition
704
- while (true) {
1164
+ do {
705
1165
  try {
706
1166
  return await this.executeRequest(url, fetchOptions);
707
1167
  }
708
1168
  catch (error) {
709
- const requestError = error instanceof RequestError
710
- ? error
711
- : RequestError.networkError(url, fetchOptions.method, error);
712
- if (attempt >= maxRetries || requestError.timeoutError) {
1169
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
1170
+ // Don't retry on timeout errors or if we've reached the max retries
1171
+ if (attempt >= maxRetries || requestError.timeoutError)
713
1172
  throw requestError;
714
- }
715
1173
  attempt++;
716
1174
  if (this.requestOptions.onRetry) {
717
1175
  await this.requestOptions.onRetry({ attempt, error: requestError });
718
1176
  }
719
1177
  }
720
- }
1178
+ } while (attempt <= maxRetries);
1179
+ // If we somehow get here, we throw an error to avoid undefined behavior
1180
+ throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
721
1181
  }
722
1182
  async executeRequest(url, fetchOptions) {
723
1183
  // Create a request controller for timeout if needed, or use the provided one
724
1184
  const controller = this.abortController || new AbortController();
725
1185
  const { signal } = controller;
726
1186
  // Use the controller signal for the fetch request, but don't overwrite an existing signal
727
- // that might have been set in requestOptions if it exists
728
1187
  fetchOptions.signal = fetchOptions.signal || signal;
729
1188
  let timeoutId;
730
- // If we have a timeout and we're using our own controller (not provided externally)
731
- if (this.requestOptions.timeout && !this.abortController) {
732
- timeoutId = setTimeout(() => {
733
- controller.abort();
734
- }, this.requestOptions.timeout);
1189
+ // Flag to track if abort was caused by our timeout
1190
+ let abortedByTimeout = false;
1191
+ // Handle timeout regardless of whether an external controller is provided
1192
+ if (this.requestOptions.timeout) {
1193
+ if (this.abortController) {
1194
+ // If we have an external controller, create a separate timeout controller
1195
+ // that won't interfere with the external one
1196
+ // Use AbortSignal.timeout if available (modern browsers)
1197
+ if (typeof AbortSignal.timeout === "function") {
1198
+ const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
1199
+ // Add event listener to propagate the abort to the main controller
1200
+ timeoutSignal.addEventListener("abort", () => {
1201
+ if (!controller.signal.aborted) {
1202
+ abortedByTimeout = true;
1203
+ controller.abort();
1204
+ }
1205
+ });
1206
+ }
1207
+ else {
1208
+ // Fallback for browsers without AbortSignal.timeout
1209
+ timeoutId = setTimeout(() => {
1210
+ if (!controller.signal.aborted) {
1211
+ abortedByTimeout = true;
1212
+ controller.abort();
1213
+ }
1214
+ }, this.requestOptions.timeout);
1215
+ }
1216
+ }
1217
+ else {
1218
+ timeoutId = setTimeout(() => {
1219
+ abortedByTimeout = true;
1220
+ controller.abort();
1221
+ }, this.requestOptions.timeout);
1222
+ }
735
1223
  }
736
1224
  try {
737
1225
  let response;
@@ -739,11 +1227,11 @@
739
1227
  response = await fetch(url, fetchOptions);
740
1228
  }
741
1229
  catch (error) {
742
- // Check if this is an abort error from our timeout
743
- if (error instanceof DOMException && error.name === "AbortError" && timeoutId) {
1230
+ // Check if this is an abort error that was specifically caused by our timeout
1231
+ if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
744
1232
  throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
745
1233
  }
746
- // Otherwise it's a network error
1234
+ // Otherwise it's either a user-triggered abort or another network error
747
1235
  throw RequestError.networkError(url, fetchOptions.method, error);
748
1236
  }
749
1237
  if (!response.ok) {
@@ -758,23 +1246,60 @@
758
1246
  }
759
1247
  }
760
1248
  }
1249
+ /**
1250
+ * Executes a request using cache when possible
1251
+ * @param url The formatted URL to send the request to
1252
+ * @param fetchOptions The fetch options to use
1253
+ * @param cacheKey The cache key for this request
1254
+ */
1255
+ async executeWithCache(url, fetchOptions, cacheKey) {
1256
+ // Try to get from cache first
1257
+ if (this.cacheManager) {
1258
+ const cachedEntry = await this.cacheManager.get(cacheKey);
1259
+ if (cachedEntry) {
1260
+ // Create a synthetic Response object from cached data
1261
+ const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
1262
+ status: 200,
1263
+ headers: new Headers(cachedEntry.headers || {}),
1264
+ });
1265
+ return new ResponseWrapper(cachedResponse);
1266
+ }
1267
+ }
1268
+ // Not in cache, make the actual request
1269
+ const response = await (!this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions));
1270
+ // Cache the successful response if caching is enabled
1271
+ // Only cache successful responses
1272
+ if (response.ok && this.cacheManager) {
1273
+ // Clone the response so we don't consume it
1274
+ try {
1275
+ const responseData = await response.getJson();
1276
+ // Extract headers to store in cache
1277
+ const headers = {};
1278
+ response.headers.forEach((value, key) => {
1279
+ headers[key] = value;
1280
+ });
1281
+ // Store in cache
1282
+ await this.cacheManager.set(cacheKey, responseData, headers);
1283
+ }
1284
+ catch (error) {
1285
+ // Failed to cache, but we can still return the response
1286
+ console.warn("Failed to cache response:", error);
1287
+ }
1288
+ }
1289
+ return response;
1290
+ }
761
1291
  }
762
1292
 
763
1293
  /**
764
1294
  * Base class for requests that can have a body (POST, PUT, PATCH)
765
1295
  */
766
1296
  class BodyRequest extends BaseRequest {
767
- constructor() {
768
- super(...arguments);
769
- this.bodyProcessed = false;
770
- }
771
1297
  /**
772
1298
  * Sets the body of the request
773
1299
  * @param body - The request body
774
1300
  */
775
1301
  withBody(body) {
776
1302
  this.body = body;
777
- this.bodyProcessed = false;
778
1303
  // Set body type and validate
779
1304
  if (typeof body === "string") {
780
1305
  this.bodyType = BodyType.STRING;
@@ -782,11 +1307,7 @@
782
1307
  }
783
1308
  else if (body !== null &&
784
1309
  typeof body === "object" &&
785
- !(body instanceof FormData ||
786
- body instanceof Blob ||
787
- body instanceof ArrayBuffer ||
788
- body instanceof URLSearchParams ||
789
- body instanceof ReadableStream)) {
1310
+ !(body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || body instanceof ReadableStream)) {
790
1311
  this.bodyType = BodyType.JSON;
791
1312
  this.setContentTypeIfNeeded("application/json");
792
1313
  // Validate JSON is stringifiable early
@@ -814,15 +1335,17 @@
814
1335
  * Overrides the base implementation to add body handling
815
1336
  */
816
1337
  sendTo(url) {
817
- // Process body only once
818
- if (!this.bodyProcessed && this.body !== undefined && !this.requestOptions.body) {
1338
+ if (this.body !== undefined) {
1339
+ // Remove previous body if it exists
1340
+ if (this.requestOptions.body)
1341
+ delete this.requestOptions.body;
1342
+ // Process the body based on its type
819
1343
  if (this.bodyType === BodyType.JSON) {
820
1344
  this.requestOptions.body = JSON.stringify(this.body);
821
1345
  }
822
1346
  else {
823
1347
  this.requestOptions.body = this.body;
824
1348
  }
825
- this.bodyProcessed = true;
826
1349
  }
827
1350
  return super.sendTo(url);
828
1351
  }
@@ -930,6 +1453,7 @@
930
1453
  config: Config.getInstance(),
931
1454
  };
932
1455
 
1456
+ exports.CacheManager = CacheManager;
933
1457
  exports.CookieUtils = CookieUtils;
934
1458
  exports.DeleteRequest = DeleteRequest;
935
1459
  exports.GetRequest = GetRequest;
@@ -940,6 +1464,9 @@
940
1464
  exports.PutRequest = PutRequest;
941
1465
  exports.RequestError = RequestError;
942
1466
  exports.ResponseWrapper = ResponseWrapper;
1467
+ exports.createLocalStorageStorage = createLocalStorageStorage;
1468
+ exports.createMemoryStorage = createMemoryStorage;
1469
+ exports.createSessionStorageStorage = createSessionStorageStorage;
943
1470
  exports["default"] = create;
944
1471
 
945
1472
  Object.defineProperty(exports, '__esModule', { value: true });