create-request 0.0.1-beta.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,18 +48,6 @@ var RedirectMode;
48
48
  RedirectMode["FOLLOW"] = "follow";
49
49
  RedirectMode["MANUAL"] = "manual";
50
50
  })(RedirectMode || (RedirectMode = {}));
51
- /**
52
- * Enum for cache modes
53
- */
54
- var CacheMode;
55
- (function (CacheMode) {
56
- CacheMode["DEFAULT"] = "default";
57
- CacheMode["RELOAD"] = "reload";
58
- CacheMode["NO_STORE"] = "no-store";
59
- CacheMode["NO_CACHE"] = "no-cache";
60
- CacheMode["FORCE_CACHE"] = "force-cache";
61
- CacheMode["ONLY_IF_CACHED"] = "only-if-cached";
62
- })(CacheMode || (CacheMode = {}));
63
51
  /**
64
52
  * Enum for cookie SameSite policies
65
53
  */
@@ -101,7 +89,8 @@ class RequestError extends Error {
101
89
  this.method = method;
102
90
  this.status = options.status;
103
91
  this.response = options.response;
104
- this.timeoutError = options.timeoutError;
92
+ this.isTimeout = options.isTimeout;
93
+ this.isAborted = options.isAborted;
105
94
  // For better stack traces in modern environments
106
95
  if (Error.captureStackTrace) {
107
96
  Error.captureStackTrace(this, RequestError);
@@ -113,30 +102,90 @@ class RequestError extends Error {
113
102
  * Static methods for creating specific types of RequestError
114
103
  */
115
104
  static timeout(url, method, timeoutMs) {
116
- return new RequestError(`Request timed out after ${timeoutMs}ms`, url, method, {
117
- timeoutError: true,
105
+ return new RequestError(`Timeout: ${timeoutMs}ms`, url, method, {
106
+ isTimeout: true,
118
107
  });
119
108
  }
120
109
  static fromResponse(response, url, method) {
121
- return new RequestError(`Request failed with status ${response.status}`, url, method, {
110
+ return new RequestError(`HTTP ${response.status}`, url, method, {
122
111
  status: response.status,
123
112
  response,
124
113
  });
125
114
  }
126
115
  static networkError(url, method, originalError) {
127
- const error = new RequestError(`Network error: ${originalError.message}`, url, method);
128
- error.stack = originalError.stack;
116
+ // Provide more descriptive error messages for common network errors
117
+ let message = originalError.message;
118
+ // Check for Node.js error codes (e.g., from undici/dns errors)
119
+ const errorCode = originalError.code;
120
+ const errorName = originalError.name;
121
+ const stack = originalError.stack || "";
122
+ const errorMessageLower = message.toLowerCase();
123
+ // Check for timeout errors (Node.js/undici TimeoutError)
124
+ const isTimeoutError = errorName === "TimeoutError" ||
125
+ errorMessageLower.includes("timeout") ||
126
+ errorMessageLower.includes("aborted due to timeout") ||
127
+ errorCode === "ETIMEDOUT" ||
128
+ stack.includes("TimeoutError") ||
129
+ stack.includes("timeout");
130
+ // If the error message is generic "fetch failed", provide more context
131
+ if (message === "fetch failed" || message === "Failed to fetch") {
132
+ // Check for DNS resolution errors
133
+ const isDnsError = errorCode === "ENOTFOUND" ||
134
+ errorCode === "EAI_AGAIN" ||
135
+ errorCode === "EAI_NODATA" ||
136
+ stack.includes("getaddrinfo") ||
137
+ stack.includes("ENOTFOUND") ||
138
+ stack.includes("EAI_AGAIN");
139
+ // Check for connection errors (but not timeout errors)
140
+ const isConnectionError = !isTimeoutError && (errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || stack.includes("ECONNREFUSED") || stack.includes("connect"));
141
+ if (isTimeoutError) {
142
+ message = `Network error: Request timeout for ${url}`;
143
+ }
144
+ else if (isDnsError) {
145
+ message = `Network error: Unable to resolve hostname for ${url}`;
146
+ }
147
+ else if (isConnectionError) {
148
+ message = `Network error: Connection refused for ${url}`;
149
+ }
150
+ else {
151
+ message = `Network error: Failed to fetch ${url}`;
152
+ }
153
+ }
154
+ const error = new RequestError(message, url, method, {
155
+ ...(isTimeoutError ? { isTimeout: true } : {}),
156
+ });
157
+ // Create a proper RequestError stack trace, but append the original stack for debugging
158
+ // This way Node.js will show "RequestError: ..." instead of "TypeError: ..."
159
+ if (originalError.stack) {
160
+ // Get the current stack (which will start with RequestError)
161
+ const currentStack = error.stack || "";
162
+ // Append the original error's stack as "Caused by:" for debugging context
163
+ error.stack = `${currentStack}\n\nCaused by: ${originalError.stack}`;
164
+ }
129
165
  return error;
130
166
  }
167
+ static abortError(url, method) {
168
+ return new RequestError("Aborted", url, method, {
169
+ isAborted: true,
170
+ });
171
+ }
131
172
  }
132
173
 
133
174
  /**
134
175
  * Wrapper for HTTP responses with methods to transform the response data
176
+ * Provides caching and conversion between different response formats.
135
177
  */
136
178
  class ResponseWrapper {
137
- constructor(response) {
179
+ constructor(response, url, method, graphQLOptions) {
138
180
  this.bodyUsed = false;
139
181
  this.response = response;
182
+ this.url = url;
183
+ this.method = method;
184
+ if (graphQLOptions) {
185
+ this.graphQLOptions = {
186
+ throwOnError: graphQLOptions.throwOnError,
187
+ };
188
+ }
140
189
  }
141
190
  // Wrapper properties
142
191
  get status() {
@@ -154,29 +203,109 @@ class ResponseWrapper {
154
203
  get raw() {
155
204
  return this.response;
156
205
  }
157
- // Response body methods with caching
206
+ /**
207
+ * Check for GraphQL errors and throw if throwOnError is enabled
208
+ * @param data - The parsed JSON data
209
+ * @throws RequestError if GraphQL response contains errors and throwOnError is enabled
210
+ */
211
+ checkGraphQLErrors(data) {
212
+ var _a;
213
+ if (!((_a = this.graphQLOptions) === null || _a === void 0 ? void 0 : _a.throwOnError) || typeof data !== "object" || data === null)
214
+ return;
215
+ const responseData = data;
216
+ if (!Array.isArray(responseData.errors) || responseData.errors.length === 0)
217
+ return;
218
+ const errors = responseData.errors;
219
+ const errorMessages = errors.map(x => typeof x === "string" ? x : x && typeof x === "object" && "message" in x ? String(x.message || "Unknown error") : String(x));
220
+ const errorMessage = errorMessages.join(", ");
221
+ throw new RequestError(`GraphQL request failed with errors: ${errorMessage}`, this.url || "", this.method || "", {
222
+ status: this.response.status,
223
+ response: this.response,
224
+ });
225
+ }
226
+ /**
227
+ * Parse the response body as JSON
228
+ * Caches the result for subsequent calls.
229
+ * If GraphQL options are set with throwOnError=true, will check for GraphQL errors and throw.
230
+ *
231
+ * @returns The parsed JSON data
232
+ * @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
233
+ *
234
+ * @example
235
+ * const data = await response.getJson();
236
+ * console.log(data.items);
237
+ *
238
+ * @example
239
+ * // Error handling - errors are always RequestError
240
+ * try {
241
+ * const data = await response.getJson();
242
+ * } catch (error) {
243
+ * if (error instanceof RequestError) {
244
+ * console.log(error.status, error.url, error.method);
245
+ * }
246
+ * }
247
+ */
158
248
  async getJson() {
159
249
  if (this.jsonCache !== undefined) {
250
+ this.checkGraphQLErrors(this.jsonCache);
160
251
  return this.jsonCache;
161
252
  }
162
253
  if (this.bodyUsed) {
163
254
  // If we already have text, try to parse it as JSON
164
255
  if (this.textCache !== undefined) {
165
256
  try {
166
- // Fix unsafe assignment by explicitly casting to unknown first
167
- this.jsonCache = JSON.parse(this.textCache);
168
- return this.jsonCache;
257
+ const parsed = JSON.parse(this.textCache);
258
+ this.jsonCache = parsed;
259
+ this.checkGraphQLErrors(parsed);
260
+ return parsed;
169
261
  }
170
- catch (e) {
171
- throw new Error("Cannot parse cached text as JSON");
262
+ catch (error) {
263
+ // Re-throw RequestErrors from checkGraphQLErrors without wrapping
264
+ if (error instanceof RequestError) {
265
+ throw error;
266
+ }
267
+ const errorMessage = error instanceof Error ? error.message : String(error);
268
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
269
+ status: this.response.status,
270
+ response: this.response,
271
+ });
172
272
  }
173
273
  }
174
- throw new Error("Response body has already been consumed in a different format");
274
+ throw new RequestError("Body already consumed", this.url || "", this.method || "", {
275
+ status: this.response.status,
276
+ response: this.response,
277
+ });
175
278
  }
176
279
  this.bodyUsed = true;
177
- this.jsonCache = (await this.response.json());
178
- return this.jsonCache;
280
+ try {
281
+ const parsed = await this.response.json();
282
+ this.jsonCache = parsed;
283
+ this.checkGraphQLErrors(parsed);
284
+ return parsed;
285
+ }
286
+ catch (error) {
287
+ // Re-throw RequestErrors from checkGraphQLErrors without wrapping
288
+ if (error instanceof RequestError) {
289
+ throw error;
290
+ }
291
+ const errorMessage = error instanceof Error ? error.message : String(error);
292
+ throw new RequestError(`Invalid JSON: ${errorMessage}`, this.url || "", this.method || "", {
293
+ status: this.response.status,
294
+ response: this.response,
295
+ });
296
+ }
179
297
  }
298
+ /**
299
+ * Get the response body as text
300
+ * Caches the result for subsequent calls. Attempts to convert from JSON or Blob
301
+ * if the body has already been consumed in those formats.
302
+ *
303
+ * @returns The response text
304
+ * @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
305
+ *
306
+ * @example
307
+ * const text = await response.getText();
308
+ */
180
309
  async getText() {
181
310
  if (this.textCache !== undefined) {
182
311
  return this.textCache;
@@ -189,15 +318,48 @@ class ResponseWrapper {
189
318
  }
190
319
  // If we have blob, convert to text
191
320
  if (this.blobCache !== undefined) {
192
- this.textCache = await this.blobCache.text();
193
- return this.textCache;
321
+ try {
322
+ this.textCache = await this.blobCache.text();
323
+ return this.textCache;
324
+ }
325
+ catch (e) {
326
+ const errorMessage = e instanceof Error ? e.message : String(e);
327
+ throw new RequestError(`Cannot convert to text: ${errorMessage}`, this.url || "", this.method || "", {
328
+ status: this.response.status,
329
+ response: this.response,
330
+ });
331
+ }
194
332
  }
195
- throw new Error("Response body has already been consumed in a format that cannot be converted to text");
333
+ throw new RequestError("Cannot convert to text", this.url || "", this.method || "", {
334
+ status: this.response.status,
335
+ response: this.response,
336
+ });
196
337
  }
197
338
  this.bodyUsed = true;
198
- this.textCache = await this.response.text();
199
- return this.textCache;
339
+ try {
340
+ this.textCache = await this.response.text();
341
+ return this.textCache;
342
+ }
343
+ catch (e) {
344
+ const errorMessage = e instanceof Error ? e.message : String(e);
345
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
346
+ status: this.response.status,
347
+ response: this.response,
348
+ });
349
+ }
200
350
  }
351
+ /**
352
+ * Get the response body as a Blob
353
+ * Caches the result for subsequent calls. Attempts to convert from text
354
+ * if the body has already been consumed as text.
355
+ *
356
+ * @returns The response as a Blob
357
+ * @throws {RequestError} When reading fails or the response has already been consumed in an incompatible format
358
+ *
359
+ * @example
360
+ * const blob = await response.getBlob();
361
+ * const url = URL.createObjectURL(blob);
362
+ */
201
363
  async getBlob() {
202
364
  if (this.blobCache !== undefined) {
203
365
  return this.blobCache;
@@ -208,367 +370,191 @@ class ResponseWrapper {
208
370
  this.blobCache = new Blob([this.textCache], { type: "text/plain" });
209
371
  return this.blobCache;
210
372
  }
211
- throw new Error("Response body has already been consumed in a format that cannot be converted to Blob");
373
+ throw new RequestError("Cannot convert to Blob", this.url || "", this.method || "", {
374
+ status: this.response.status,
375
+ response: this.response,
376
+ });
212
377
  }
213
378
  this.bodyUsed = true;
214
- this.blobCache = await this.response.blob();
215
- return this.blobCache;
216
- }
217
- async getArrayBuffer() {
218
- if (this.arrayBufferCache !== undefined) {
219
- return this.arrayBufferCache;
379
+ try {
380
+ this.blobCache = await this.response.blob();
381
+ return this.blobCache;
220
382
  }
221
- if (this.bodyUsed) {
222
- // If we have text, convert to ArrayBuffer
223
- if (this.textCache !== undefined) {
224
- const encoder = new TextEncoder();
225
- // Fix ArrayBufferLike by explicitly casting to ArrayBuffer
226
- const uint8Array = encoder.encode(this.textCache);
227
- this.arrayBufferCache = uint8Array.buffer;
228
- return this.arrayBufferCache;
229
- }
230
- // If we have blob, convert to ArrayBuffer
231
- if (this.blobCache !== undefined) {
232
- this.arrayBufferCache = await this.blobCache.arrayBuffer();
233
- return this.arrayBufferCache;
234
- }
235
- throw new Error("Response body has already been consumed in a format that cannot be converted to ArrayBuffer");
383
+ catch (e) {
384
+ const errorMessage = e instanceof Error ? e.message : String(e);
385
+ throw new RequestError(`Read failed: ${errorMessage}`, this.url || "", this.method || "", {
386
+ status: this.response.status,
387
+ response: this.response,
388
+ });
236
389
  }
237
- this.bodyUsed = true;
238
- this.arrayBufferCache = await this.response.arrayBuffer();
239
- return this.arrayBufferCache;
240
390
  }
391
+ /**
392
+ * Get the raw response body as a ReadableStream
393
+ * This method consumes the body and should only be called once.
394
+ *
395
+ * @returns The response body as a ReadableStream or null
396
+ * @throws {RequestError} When the response body has already been consumed
397
+ *
398
+ * @example
399
+ * const stream = response.getBody();
400
+ * if (stream) {
401
+ * const reader = stream.getReader();
402
+ * // Process the stream
403
+ * }
404
+ */
241
405
  getBody() {
242
406
  if (this.bodyUsed) {
243
- throw new Error("Response body has already been consumed");
407
+ throw new RequestError("Body already consumed", this.url || "", this.method || "", {
408
+ status: this.response.status,
409
+ response: this.response,
410
+ });
244
411
  }
245
412
  this.bodyUsed = true;
246
413
  return this.response.body;
247
414
  }
248
- }
249
-
250
- /**
251
- * Parse size strings like "10MB" into bytes
252
- * @param size Size as number or string with units
253
- */
254
- function parseSize(size) {
255
- var _a;
256
- if (typeof size === "number")
257
- return size;
258
- const match = size.match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/i);
259
- if (!match)
260
- return 0;
261
- const num = parseFloat(match[1]);
262
- const unit = (_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase();
263
- switch (unit) {
264
- case "KB":
265
- return num * 1024;
266
- case "MB":
267
- return num * 1024 * 1024;
268
- case "GB":
269
- return num * 1024 * 1024 * 1024;
270
- default:
271
- return num;
272
- }
273
- }
274
- /**
275
- * Create a memory storage provider using Map
276
- */
277
- function createMemoryStorage() {
278
- const store = new Map();
279
- return {
280
- get(key) {
281
- return store.get(key) || null;
282
- },
283
- set(key, value) {
284
- store.set(key, value);
285
- },
286
- has(key) {
287
- return store.has(key);
288
- },
289
- delete(key) {
290
- store.delete(key);
291
- },
292
- clear() {
293
- store.clear();
294
- },
295
- };
296
- }
297
- /**
298
- * Create a localStorage-based storage provider
299
- */
300
- function createLocalStorageStorage() {
301
- return {
302
- get(key) {
303
- if (typeof localStorage === "undefined")
304
- return null;
305
- return localStorage.getItem(key);
306
- },
307
- set(key, value) {
308
- if (typeof localStorage === "undefined")
309
- return;
310
- try {
311
- localStorage.setItem(key, value);
415
+ /**
416
+ * Extract specific data using a selector function
417
+ * If no selector is provided, returns the full JSON response.
418
+ *
419
+ * @param selector - Optional function to extract and transform data
420
+ * @returns A promise that resolves to the selected data
421
+ * @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
422
+ *
423
+ * @example
424
+ * // Get full response
425
+ * const data = await response.getData();
426
+ *
427
+ * // Extract specific data
428
+ * const users = await response.getData(data => data.results.users);
429
+ *
430
+ * @example
431
+ * // Error handling - errors are always RequestError
432
+ * try {
433
+ * const data = await response.getData();
434
+ * } catch (error) {
435
+ * if (error instanceof RequestError) {
436
+ * console.log(error.status, error.url, error.method);
437
+ * }
438
+ * }
439
+ */
440
+ async getData(selector) {
441
+ try {
442
+ const data = await this.getJson();
443
+ // If no selector is provided, return the raw JSON data
444
+ if (!selector)
445
+ return data;
446
+ // Apply the selector if provided
447
+ return selector(data);
448
+ }
449
+ catch (error) {
450
+ // If it's already a RequestError, re-throw it
451
+ if (error instanceof RequestError) {
452
+ throw error;
312
453
  }
313
- catch (e) {
314
- // Handle quota exceeded errors
315
- console.warn("Cache storage quota exceeded, clearing cache");
316
- void this.clear();
317
- try {
318
- localStorage.setItem(key, value);
319
- }
320
- catch (e) {
321
- console.error("Failed to set cache item even after clearing cache");
322
- }
454
+ // Enhance selector errors with context
455
+ if (selector) {
456
+ const errorMessage = error instanceof Error ? error.message : String(error);
457
+ throw new RequestError(`Data selector failed: ${errorMessage}`, this.url || "", this.method || "", {
458
+ status: this.response.status,
459
+ response: this.response,
460
+ });
323
461
  }
324
- },
325
- has(key) {
326
- if (typeof localStorage === "undefined")
327
- return false;
328
- return localStorage.getItem(key) !== null;
329
- },
330
- delete(key) {
331
- if (typeof localStorage === "undefined")
332
- return;
333
- localStorage.removeItem(key);
334
- },
335
- clear() {
336
- if (typeof localStorage === "undefined")
337
- return;
338
- localStorage.clear();
339
- },
340
- };
462
+ // If we get here and it's not a RequestError, wrap it
463
+ // This should rarely happen as getJson() should throw RequestError
464
+ const errorObj = error instanceof Error ? error : new Error(String(error));
465
+ throw RequestError.networkError(this.url || "", this.method || "", errorObj);
466
+ }
467
+ }
341
468
  }
342
- /**
343
- * Create a sessionStorage-based storage provider
344
- */
345
- function createSessionStorageStorage() {
346
- return {
347
- get(key) {
348
- if (typeof sessionStorage === "undefined")
349
- return null;
350
- return sessionStorage.getItem(key);
351
- },
352
- set(key, value) {
353
- if (typeof sessionStorage === "undefined")
354
- return;
355
- try {
356
- sessionStorage.setItem(key, value);
469
+
470
+ class CookieUtils {
471
+ /**
472
+ * Formats cookies for a request
473
+ * @param cookies Object containing cookie name-value pairs or cookie options
474
+ * @returns Formatted cookie string for the Cookie header
475
+ */
476
+ static formatRequestCookies(cookies) {
477
+ const cookiePairs = [];
478
+ Object.entries(cookies).forEach(([name, valueOrOptions]) => {
479
+ let value;
480
+ if (typeof valueOrOptions === "string") {
481
+ value = valueOrOptions;
357
482
  }
358
- catch (e) {
359
- // Handle quota exceeded errors
360
- console.warn("Cache storage quota exceeded, clearing cache");
361
- void this.clear();
362
- try {
363
- sessionStorage.setItem(key, value);
364
- }
365
- catch (e) {
366
- console.error("Failed to set cache item even after clearing cache");
367
- }
483
+ else {
484
+ // Extract value from options object without validation
485
+ value = valueOrOptions.value;
368
486
  }
369
- },
370
- has(key) {
371
- if (typeof sessionStorage === "undefined")
372
- return false;
373
- return sessionStorage.getItem(key) !== null;
374
- },
375
- delete(key) {
376
- if (typeof sessionStorage === "undefined")
377
- return;
378
- sessionStorage.removeItem(key);
379
- },
380
- clear() {
381
- if (typeof sessionStorage === "undefined")
382
- return;
383
- sessionStorage.clear();
384
- },
385
- };
386
- }
387
- /**
388
- * Create a storage provider from a cache options object
389
- */
390
- function createStorageProvider(options) {
391
- const { storage } = options || {};
392
- if (!storage) {
393
- // Default to memory storage
394
- return createMemoryStorage();
395
- }
396
- // Check if storage is already a StorageProvider
397
- if (typeof storage.get === "function" && typeof storage.set === "function") {
398
- return storage;
399
- }
400
- // Check if it's localStorage or sessionStorage
401
- if (storage === localStorage) {
402
- return createLocalStorageStorage();
403
- }
404
- if (storage === sessionStorage) {
405
- return createSessionStorageStorage();
406
- }
407
- // Check if it's a Map
408
- if (storage instanceof Map) {
409
- return {
410
- get(key) {
411
- return storage.get(key) || null;
412
- },
413
- set(key, value, _ttl) {
414
- storage.set(key, value);
415
- },
416
- has(key) {
417
- return storage.has(key);
418
- },
419
- delete(key) {
420
- storage.delete(key);
421
- },
422
- clear() {
423
- storage.clear();
424
- },
425
- };
487
+ // Add the cookie to the request
488
+ cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
489
+ });
490
+ return cookiePairs.join("; ");
426
491
  }
427
- // Default to memory storage
428
- console.warn("Unsupported storage type, falling back to memory storage");
429
- return createMemoryStorage();
430
492
  }
431
493
 
432
494
  /**
433
- * Cache manager that handles storage operations
495
+ * Utility class for CSRF token management
434
496
  */
435
- class CacheManager {
436
- constructor(options = {}) {
437
- this.entryCount = 0;
438
- this.totalSize = 0;
439
- this.options = options;
440
- this.storage = createStorageProvider(options);
441
- }
497
+ class CsrfUtils {
442
498
  /**
443
- * Generate a cache key based on request details
499
+ * Extracts CSRF token from a meta tag in the document head
500
+ * @param metaName The name attribute of the meta tag (default: "csrf-token")
501
+ * @returns The CSRF token or null if not found
444
502
  */
445
- generateKey(url, method, headers, body) {
446
- // Use custom key generator if provided
447
- if (this.options.keyGenerator) {
448
- const customKey = this.options.keyGenerator(url, method, headers, body);
449
- return this.options.keyPrefix ? `${this.options.keyPrefix}:${customKey}` : customKey;
450
- }
451
- // Default key generation
452
- let key = `${method}:${url}`;
453
- // Add vary headers to key if specified
454
- if (this.options.varyByHeaders && headers) {
455
- const varyHeaders = {};
456
- this.options.varyByHeaders.forEach(header => {
457
- const normalizedHeader = header.toLowerCase();
458
- // Find the header in case-insensitive way
459
- Object.keys(headers).forEach(h => {
460
- if (h.toLowerCase() === normalizedHeader) {
461
- varyHeaders[normalizedHeader] = headers[h];
462
- }
463
- });
464
- });
465
- if (Object.keys(varyHeaders).length > 0) {
466
- key += `:${JSON.stringify(varyHeaders)}`;
467
- }
468
- }
469
- // Include body in cache key for non-GET requests if it's a simple value
470
- if (method !== "GET" && body !== undefined && body !== null) {
471
- try {
472
- if (typeof body === "string" || typeof body === "number" || typeof body === "boolean") {
473
- key += `:${String(body)}`;
474
- }
475
- else if (typeof body === "object") {
476
- key += `:${JSON.stringify(body)}`;
477
- }
478
- }
479
- catch (e) {
480
- console.warn("Failed to stringify request body for cache key");
481
- }
503
+ static getTokenFromMeta(metaName = "csrf-token") {
504
+ if (typeof document === "undefined") {
505
+ return null;
482
506
  }
483
- // Add prefix if specified
484
- return this.options.keyPrefix ? `${this.options.keyPrefix}:${key}` : key;
507
+ const meta = document.querySelector(`meta[name="${metaName}"]`);
508
+ return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
485
509
  }
486
510
  /**
487
- * Get a value from cache
511
+ * Extracts CSRF token from a cookie
512
+ * @param cookieName The name of the cookie containing the CSRF token
513
+ * @returns The CSRF token or null if not found
488
514
  */
489
- async get(key) {
490
- const value = await Promise.resolve(this.storage.get(key));
491
- if (!value)
515
+ static getTokenFromCookie(cookieName = "csrf-token") {
516
+ if (typeof document === "undefined") {
492
517
  return null;
493
- try {
494
- const entry = JSON.parse(value);
495
- // Check if the entry has expired
496
- if (entry.expiry && entry.expiry < Date.now()) {
497
- await Promise.resolve(this.storage.delete(key));
498
- return null;
499
- }
500
- return entry;
501
518
  }
502
- catch (e) {
503
- // If we can't parse the cached value, remove it
504
- await Promise.resolve(this.storage.delete(key));
505
- return null;
519
+ const cookies = document.cookie.split(";");
520
+ for (const cookie of cookies) {
521
+ const [name, value] = cookie.trim().split("=");
522
+ if (name === cookieName) {
523
+ return decodeURIComponent(value);
524
+ }
506
525
  }
526
+ return null;
507
527
  }
508
528
  /**
509
- * Set a value in cache
529
+ * Validates if the provided string is a potential CSRF token
530
+ * Checks if the token meets security requirements
531
+ * @param token The token to validate
532
+ * @returns Whether the token is valid
510
533
  */
511
- async set(key, value, headers) {
512
- const now = Date.now();
513
- const ttl = this.options.ttl;
514
- const entry = {
515
- value,
516
- timestamp: now,
517
- expiry: ttl ? now + ttl : null,
518
- headers,
519
- };
520
- const serialized = JSON.stringify(entry);
521
- // Check size constraints if applicable
522
- if (this.options.maxSize) {
523
- const maxBytes = parseSize(this.options.maxSize);
524
- const entrySize = new TextEncoder().encode(serialized).length;
525
- // If a single entry exceeds the max size, reject it without clearing the cache
526
- if (entrySize > maxBytes) {
527
- console.warn(`Cache entry size (${entrySize} bytes) exceeds maximum cache size (${maxBytes} bytes)`);
528
- return;
529
- }
530
- // Only clear if adding this entry would exceed the total limit
531
- if (this.totalSize + entrySize > maxBytes) {
532
- // Simple strategy: clear everything
533
- // A more sophisticated strategy would remove oldest entries first
534
- await Promise.resolve(this.storage.clear());
535
- this.totalSize = 0;
536
- this.entryCount = 0;
537
- }
538
- this.totalSize += entrySize;
534
+ static isValidToken(token) {
535
+ // Basic checks for null/undefined and type
536
+ if (typeof token !== "string") {
537
+ return false;
539
538
  }
540
- // Check max entries constraint
541
- if (this.options.maxEntries) {
542
- this.entryCount++;
543
- if (this.entryCount > this.options.maxEntries) {
544
- // Simple strategy: clear everything
545
- // A more sophisticated strategy would use LRU cache
546
- await Promise.resolve(this.storage.clear());
547
- this.entryCount = 1;
539
+ if (token.length < 8)
540
+ return false;
541
+ // If token is longer than 10 chars, perform additional security checks
542
+ if (token.length > 10) {
543
+ // Check for valid character set (alphanumeric & common token symbols)
544
+ const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
545
+ if (!validTokenRegex.test(token)) {
546
+ return false;
548
547
  }
548
+ // For longer tokens, check for sufficient entropy (at least 2 character types)
549
+ const hasUpperCase = /[A-Z]/.test(token);
550
+ const hasLowerCase = /[a-z]/.test(token);
551
+ const hasNumbers = /[0-9]/.test(token);
552
+ const hasSpecials = /[-_=+/.]/.test(token);
553
+ const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
554
+ return characterTypesCount >= 2;
549
555
  }
550
- // Pass ttl to storage provider if available
551
- await Promise.resolve(this.storage.set(key, serialized, ttl));
552
- }
553
- /**
554
- * Check if a key exists in the cache
555
- */
556
- async has(key) {
557
- return Promise.resolve(this.storage.has(key));
558
- }
559
- /**
560
- * Delete a key from the cache
561
- */
562
- async delete(key) {
563
- return Promise.resolve(this.storage.delete(key));
564
- }
565
- /**
566
- * Clear the entire cache
567
- */
568
- async clear() {
569
- this.entryCount = 0;
570
- this.totalSize = 0;
571
- return Promise.resolve(this.storage.clear());
556
+ // For shorter tokens (8-10 chars), just return true if we reached here
557
+ return true;
572
558
  }
573
559
  }
574
560
 
@@ -584,9 +570,20 @@ class Config {
584
570
  this.csrfToken = null;
585
571
  this.enableAutoXsrf = true;
586
572
  this.enableAntiCsrf = true; // X-Requested-With header
573
+ // Interceptor configuration
574
+ this.requestInterceptors = [];
575
+ this.responseInterceptors = [];
576
+ this.errorInterceptors = [];
577
+ this.nextInterceptorId = 1;
587
578
  }
588
579
  /**
589
- * Get the singleton instance
580
+ * Get the singleton instance of the Config class
581
+ *
582
+ * @returns The global configuration instance
583
+ *
584
+ * @example
585
+ * const config = Config.getInstance();
586
+ * config.setCsrfToken('token123');
590
587
  */
591
588
  static getInstance() {
592
589
  if (!Config.instance) {
@@ -596,231 +593,432 @@ class Config {
596
593
  }
597
594
  /**
598
595
  * Set a global CSRF token to be used for all requests
596
+ *
597
+ * @param token - The CSRF token value
598
+ * @returns The config instance for chaining
599
+ *
600
+ * @example
601
+ * Config.getInstance().setCsrfToken('myToken123');
599
602
  */
600
603
  setCsrfToken(token) {
601
604
  this.csrfToken = token;
602
605
  return this;
603
606
  }
604
607
  /**
605
- * Get the global CSRF token
608
+ * Get the global CSRF token that will be automatically applied to requests
609
+ *
610
+ * @returns The current CSRF token or null if not set
606
611
  */
607
612
  getCsrfToken() {
608
613
  return this.csrfToken;
609
614
  }
610
615
  /**
611
- * Set the CSRF header name (default is X-CSRF-Token)
616
+ * Set the CSRF header name used when sending the token
617
+ *
618
+ * @param name - The header name to use
619
+ * @returns The config instance for chaining
620
+ *
621
+ * @example
622
+ * Config.getInstance().setCsrfHeaderName('X-My-CSRF-Token');
612
623
  */
613
624
  setCsrfHeaderName(name) {
614
625
  this.csrfHeaderName = name;
615
626
  return this;
616
627
  }
617
628
  /**
618
- * Get the CSRF header name
629
+ * Get the configured CSRF header name
630
+ *
631
+ * @returns The current CSRF header name
619
632
  */
620
633
  getCsrfHeaderName() {
621
634
  return this.csrfHeaderName;
622
635
  }
623
636
  /**
624
- * Set the XSRF cookie name to look for (default is XSRF-TOKEN)
637
+ * Set the XSRF cookie name to look for when extracting tokens from cookies
638
+ *
639
+ * @param name - The cookie name to look for
640
+ * @returns The config instance for chaining
641
+ *
642
+ * @example
643
+ * Config.getInstance().setXsrfCookieName('MY-XSRF-COOKIE');
625
644
  */
626
645
  setXsrfCookieName(name) {
627
646
  this.xsrfCookieName = name;
628
647
  return this;
629
648
  }
630
649
  /**
631
- * Get the XSRF cookie name
650
+ * Get the configured XSRF cookie name
651
+ *
652
+ * @returns The current XSRF cookie name
632
653
  */
633
654
  getXsrfCookieName() {
634
655
  return this.xsrfCookieName;
635
656
  }
636
657
  /**
637
- * Set the XSRF header name to send the token in (default is X-XSRF-TOKEN)
658
+ * Set the XSRF header name for sending tokens extracted from cookies
659
+ *
660
+ * @param name - The header name to use
661
+ * @returns The config instance for chaining
638
662
  */
639
663
  setXsrfHeaderName(name) {
640
664
  this.xsrfHeaderName = name;
641
665
  return this;
642
666
  }
643
667
  /**
644
- * Get the XSRF header name
668
+ * Get the configured XSRF header name
669
+ *
670
+ * @returns The current XSRF header name
645
671
  */
646
672
  getXsrfHeaderName() {
647
673
  return this.xsrfHeaderName;
648
674
  }
649
675
  /**
650
- * Enable or disable automatic XSRF token extraction from cookies
676
+ * Enable or disable automatic extraction of XSRF tokens from cookies
677
+ * When enabled, the library will look for XSRF tokens in cookies and
678
+ * automatically add them to request headers.
679
+ *
680
+ * @param enable - Whether to enable this feature
681
+ * @returns The config instance for chaining
682
+ *
683
+ * @example
684
+ * Config.getInstance().setEnableAutoXsrf(false); // Disable XSRF extraction
651
685
  */
652
686
  setEnableAutoXsrf(enable) {
653
687
  this.enableAutoXsrf = enable;
654
688
  return this;
655
689
  }
656
690
  /**
657
- * Check if automatic XSRF is enabled
691
+ * Check if automatic XSRF token extraction is enabled
692
+ *
693
+ * @returns True if automatic XSRF is enabled
658
694
  */
659
695
  isAutoXsrfEnabled() {
660
696
  return this.enableAutoXsrf;
661
697
  }
662
698
  /**
663
699
  * Enable or disable automatic addition of anti-CSRF headers
700
+ * When enabled, X-Requested-With: XMLHttpRequest will be added to all requests.
701
+ *
702
+ * @param enable - Whether to enable this feature
703
+ * @returns The config instance for chaining
664
704
  */
665
705
  setEnableAntiCsrf(enable) {
666
706
  this.enableAntiCsrf = enable;
667
707
  return this;
668
708
  }
669
709
  /**
670
- * Check if anti-CSRF is enabled
710
+ * Check if anti-CSRF protection is enabled
711
+ *
712
+ * @returns True if anti-CSRF protection is enabled
671
713
  */
672
714
  isAntiCsrfEnabled() {
673
715
  return this.enableAntiCsrf;
674
716
  }
675
717
  /**
676
- * Reset all configuration options to their defaults
718
+ * Add a global request interceptor
719
+ * Request interceptors can modify the request configuration or return an early response
720
+ *
721
+ * @param interceptor - The request interceptor function
722
+ * @returns The interceptor ID for later removal
723
+ *
724
+ * @example
725
+ * const id = Config.getInstance().addRequestInterceptor((config) => {
726
+ * config.headers['X-Custom'] = 'value';
727
+ * return config;
728
+ * });
677
729
  */
678
- reset() {
679
- this.csrfToken = null;
680
- this.csrfHeaderName = "X-CSRF-Token";
681
- this.enableAntiCsrf = true;
682
- this.xsrfCookieName = "XSRF-TOKEN";
683
- this.xsrfHeaderName = "X-XSRF-TOKEN";
684
- this.enableAutoXsrf = true;
685
- return this;
730
+ addRequestInterceptor(interceptor) {
731
+ const id = this.nextInterceptorId++;
732
+ this.requestInterceptors.push({ id, interceptor });
733
+ return id;
686
734
  }
687
- }
688
-
689
- class CookieUtils {
690
735
  /**
691
- * Formats cookies for a request
692
- * @param cookies Object containing cookie name-value pairs or cookie options
693
- * @returns Formatted cookie string for the Cookie header
736
+ * Add a global response interceptor
737
+ * Response interceptors can transform the response
738
+ *
739
+ * @param interceptor - The response interceptor function
740
+ * @returns The interceptor ID for later removal
741
+ *
742
+ * @example
743
+ * const id = Config.getInstance().addResponseInterceptor((response) => {
744
+ * console.log('Response received:', response.status);
745
+ * return response;
746
+ * });
694
747
  */
695
- static formatRequestCookies(cookies) {
696
- const cookiePairs = [];
697
- Object.entries(cookies).forEach(([name, valueOrOptions]) => {
698
- let value;
699
- if (typeof valueOrOptions === "string") {
700
- value = valueOrOptions;
701
- }
702
- else {
703
- // Extract value from options object without validation
704
- value = valueOrOptions.value;
705
- }
706
- // Add the cookie to the request
707
- cookiePairs.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
708
- });
709
- return cookiePairs.join("; ");
748
+ addResponseInterceptor(interceptor) {
749
+ const id = this.nextInterceptorId++;
750
+ this.responseInterceptors.push({ id, interceptor });
751
+ return id;
710
752
  }
711
- }
712
-
713
- /**
714
- * Utility class for CSRF token management
715
- */
716
- class CsrfUtils {
717
753
  /**
718
- * Extracts CSRF token from a meta tag in the document head
719
- * @param metaName The name attribute of the meta tag (default: "csrf-token")
720
- * @returns The CSRF token or null if not found
754
+ * Add a global error interceptor
755
+ * Error interceptors can handle or transform errors
756
+ *
757
+ * @param interceptor - The error interceptor function
758
+ * @returns The interceptor ID for later removal
759
+ *
760
+ * @example
761
+ * const id = Config.getInstance().addErrorInterceptor((error) => {
762
+ * console.error('Request failed:', error);
763
+ * throw error;
764
+ * });
721
765
  */
722
- static getTokenFromMeta(metaName = "csrf-token") {
723
- if (typeof document === "undefined") {
724
- return null;
725
- }
726
- const meta = document.querySelector(`meta[name="${metaName}"]`);
727
- return (meta === null || meta === void 0 ? void 0 : meta.getAttribute("content")) || null;
766
+ addErrorInterceptor(interceptor) {
767
+ const id = this.nextInterceptorId++;
768
+ this.errorInterceptors.push({ id, interceptor });
769
+ return id;
728
770
  }
729
771
  /**
730
- * Extracts CSRF token from a cookie
731
- * @param cookieName The name of the cookie containing the CSRF token
732
- * @returns The CSRF token or null if not found
772
+ * Remove a request interceptor by its ID
773
+ *
774
+ * @param id - The interceptor ID returned from addRequestInterceptor
775
+ *
776
+ * @example
777
+ * Config.getInstance().removeRequestInterceptor(id);
733
778
  */
734
- static getTokenFromCookie(cookieName = "csrf-token") {
735
- if (typeof document === "undefined") {
736
- return null;
737
- }
738
- const cookies = document.cookie.split(";");
739
- for (const cookie of cookies) {
740
- const [name, value] = cookie.trim().split("=");
741
- if (name === cookieName) {
742
- return decodeURIComponent(value);
743
- }
744
- }
745
- return null;
779
+ removeRequestInterceptor(id) {
780
+ this.requestInterceptors = this.requestInterceptors.filter(item => item.id !== id);
746
781
  }
747
782
  /**
748
- * Validates if the provided string is a potential CSRF token
749
- * Checks if the token meets security requirements
750
- * @param token The token to validate
751
- * @returns Whether the token is valid
783
+ * Remove a response interceptor by its ID
784
+ *
785
+ * @param id - The interceptor ID returned from addResponseInterceptor
786
+ *
787
+ * @example
788
+ * Config.getInstance().removeResponseInterceptor(id);
752
789
  */
753
- static isValidToken(token) {
754
- // Basic checks for null/undefined and type
755
- if (typeof token !== "string") {
756
- return false;
757
- }
758
- if (token.length < 8)
759
- return false;
760
- // If token is longer than 10 chars, perform additional security checks
761
- if (token.length > 10) {
762
- // Check for valid character set (alphanumeric & common token symbols)
763
- const validTokenRegex = /^[A-Za-z0-9\-_=+/.]+$/;
764
- if (!validTokenRegex.test(token)) {
765
- return false;
766
- }
767
- // For longer tokens, check for sufficient entropy (at least 2 character types)
768
- const hasUpperCase = /[A-Z]/.test(token);
769
- const hasLowerCase = /[a-z]/.test(token);
770
- const hasNumbers = /[0-9]/.test(token);
771
- const hasSpecials = /[-_=+/.]/.test(token);
772
- const characterTypesCount = [hasUpperCase, hasLowerCase, hasNumbers, hasSpecials].filter(Boolean).length;
773
- return characterTypesCount >= 2;
774
- }
775
- // For shorter tokens (8-10 chars), just return true if we reached here
776
- return true;
790
+ removeResponseInterceptor(id) {
791
+ this.responseInterceptors = this.responseInterceptors.filter(item => item.id !== id);
792
+ }
793
+ /**
794
+ * Remove an error interceptor by its ID
795
+ *
796
+ * @param id - The interceptor ID returned from addErrorInterceptor
797
+ *
798
+ * @example
799
+ * Config.getInstance().removeErrorInterceptor(id);
800
+ */
801
+ removeErrorInterceptor(id) {
802
+ this.errorInterceptors = this.errorInterceptors.filter(item => item.id !== id);
803
+ }
804
+ /**
805
+ * Clear all interceptors (request, response, and error)
806
+ *
807
+ * @example
808
+ * Config.getInstance().clearInterceptors();
809
+ */
810
+ clearInterceptors() {
811
+ this.requestInterceptors = [];
812
+ this.responseInterceptors = [];
813
+ this.errorInterceptors = [];
814
+ }
815
+ /**
816
+ * Get all global request interceptors (in registration order)
817
+ * @internal
818
+ */
819
+ getRequestInterceptors() {
820
+ return this.requestInterceptors.map(item => item.interceptor);
821
+ }
822
+ /**
823
+ * Get all global response interceptors (in registration order)
824
+ * @internal
825
+ */
826
+ getResponseInterceptors() {
827
+ return this.responseInterceptors.map(item => item.interceptor);
828
+ }
829
+ /**
830
+ * Get all global error interceptors (in registration order)
831
+ * @internal
832
+ */
833
+ getErrorInterceptors() {
834
+ return this.errorInterceptors.map(item => item.interceptor);
835
+ }
836
+ /**
837
+ * Reset all configuration options to their default values
838
+ *
839
+ * @returns The config instance for chaining
840
+ *
841
+ * @example
842
+ * Config.getInstance().reset();
843
+ */
844
+ reset() {
845
+ this.csrfToken = null;
846
+ this.csrfHeaderName = "X-CSRF-Token";
847
+ this.enableAntiCsrf = true;
848
+ this.xsrfCookieName = "XSRF-TOKEN";
849
+ this.xsrfHeaderName = "X-XSRF-TOKEN";
850
+ this.enableAutoXsrf = true;
851
+ this.clearInterceptors();
852
+ return this;
777
853
  }
778
854
  }
779
855
 
780
- // Base class with common functionality for all request types
856
+ /**
857
+ * Base class with common functionality for all request types
858
+ * Provides the core request building and execution capabilities.
859
+ */
781
860
  class BaseRequest {
782
- constructor() {
861
+ constructor(url) {
783
862
  this.requestOptions = {
784
863
  headers: {},
785
864
  };
786
865
  this.queryParams = new URLSearchParams();
787
866
  this.autoApplyCsrfProtection = true;
788
- this.cacheEnabled = false;
867
+ // Per-request interceptors
868
+ this.requestInterceptors = [];
869
+ this.responseInterceptors = [];
870
+ this.errorInterceptors = [];
871
+ this.url = url;
872
+ }
873
+ /**
874
+ * Get GraphQL options if set (only for BodyRequest subclasses)
875
+ * @returns GraphQL options or undefined
876
+ */
877
+ getGraphQLOptions() {
878
+ return undefined;
879
+ }
880
+ /**
881
+ * Creates a fluent API for setting enum-based options
882
+ * Combines direct setter with convenience methods
883
+ */
884
+ createFluentSetter(optionName, options) {
885
+ const fluent = {};
886
+ // Create convenience methods for each enum value
887
+ Object.entries(options).forEach(([key, value]) => {
888
+ fluent[key] = () => {
889
+ this.requestOptions[optionName] = value;
890
+ return this;
891
+ };
892
+ });
893
+ // Create the callable setter
894
+ const callable = (value) => {
895
+ this.requestOptions[optionName] = value;
896
+ return this;
897
+ };
898
+ return Object.assign(callable, fluent);
899
+ }
900
+ validateUrl(url) {
901
+ if (!(url === null || url === void 0 ? void 0 : url.trim()))
902
+ throw new RequestError("URL cannot be empty", url, this.method);
903
+ if (url.includes("\0") || url.includes("\r") || url.includes("\n")) {
904
+ throw new RequestError("Invalid URL (control chars)", url, this.method);
905
+ }
906
+ const trimmed = url.trim();
907
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
908
+ try {
909
+ new URL(trimmed);
910
+ }
911
+ catch (_a) {
912
+ throw new RequestError(`Invalid URL: ${trimmed}`, trimmed, this.method);
913
+ }
914
+ }
789
915
  }
916
+ /**
917
+ * Add multiple HTTP headers to the request
918
+ *
919
+ * @param headers - Key-value pairs of header names and values
920
+ * @returns The request instance for chaining
921
+ *
922
+ * @example
923
+ * request.withHeaders({
924
+ * 'Accept': 'application/json',
925
+ * 'X-Custom-Header': 'value'
926
+ * });
927
+ */
790
928
  withHeaders(headers) {
929
+ // Filter out null and undefined values
930
+ const filteredHeaders = {};
931
+ Object.entries(headers).forEach(([key, value]) => {
932
+ if (value !== null && value !== undefined) {
933
+ filteredHeaders[key] = value;
934
+ }
935
+ });
791
936
  this.requestOptions.headers = {
792
- ...this.requestOptions.headers,
793
- ...headers,
937
+ ...this.getHeadersRecord(),
938
+ ...filteredHeaders,
794
939
  };
795
940
  return this;
796
941
  }
942
+ /**
943
+ * Add a single HTTP header to the request
944
+ *
945
+ * @param key - The header name
946
+ * @param value - The header value
947
+ * @returns The request instance for chaining
948
+ *
949
+ * @example
950
+ * request.withHeader('Accept', 'application/json');
951
+ */
797
952
  withHeader(key, value) {
798
- return this.withHeaders({ [key]: value });
953
+ // Ignore null and undefined values
954
+ if (value !== null && value !== undefined) {
955
+ return this.withHeaders({ [key]: value });
956
+ }
957
+ return this;
799
958
  }
959
+ /**
960
+ * Set a timeout for the request
961
+ * If the request takes longer than the specified timeout, it will be aborted.
962
+ *
963
+ * @param timeout - The timeout in milliseconds
964
+ * @returns The request instance for chaining
965
+ * @throws RequestError if timeout is not a positive number
966
+ *
967
+ * @example
968
+ * request.withTimeout(5000); // 5 seconds timeout
969
+ */
800
970
  withTimeout(timeout) {
801
971
  if (!Number.isFinite(timeout) || timeout <= 0)
802
- throw new Error("Timeout must be a positive number");
972
+ throw new RequestError("Timeout must be a positive number", this.url, this.method);
803
973
  this.requestOptions.timeout = timeout;
804
974
  return this;
805
975
  }
976
+ /**
977
+ * Configure automatic retry behavior for failed requests
978
+ *
979
+ * @param retries - Number of retry attempts before failing
980
+ * @returns The request instance for chaining
981
+ * @throws RequestError if retries is not a non-negative integer
982
+ *
983
+ * @example
984
+ * request.withRetries(3); // Retry up to 3 times
985
+ */
806
986
  withRetries(retries) {
807
987
  if (!Number.isInteger(retries) || retries < 0)
808
- throw new Error("Retry count must be a non-negative integer");
988
+ throw new RequestError("Retry count must be a non-negative integer", this.url, this.method);
809
989
  this.requestOptions.retries = retries;
810
990
  return this;
811
991
  }
992
+ /**
993
+ * Set a callback to be invoked before each retry attempt
994
+ * Useful for implementing backoff strategies or logging retry attempts.
995
+ *
996
+ * @param callback - Function to call before retrying
997
+ * @returns The request instance for chaining
998
+ *
999
+ * @example
1000
+ * request.onRetry(({ attempt, error }) => {
1001
+ * console.log(`Retry attempt ${attempt} after error: ${error.message}`);
1002
+ * return new Promise(resolve => setTimeout(resolve, attempt * 1000));
1003
+ * });
1004
+ */
812
1005
  onRetry(callback) {
813
1006
  this.requestOptions.onRetry = callback;
814
1007
  return this;
815
1008
  }
816
1009
  /**
817
- * Sets the credentials policy for the request
818
- * @param credentialsPolicy - Can be 'omit', 'same-origin', or 'include'
819
- * @returns The instance for chaining
1010
+ * Sets credentials policy - supports both direct call and fluent API
1011
+ * @example
1012
+ * request.withCredentials("include")
1013
+ * request.withCredentials(CredentialsPolicy.INCLUDE)
1014
+ * request.withCredentials.INCLUDE()
820
1015
  */
821
- withCredentials(credentialsPolicy = CredentialsPolicy.SAME_ORIGIN) {
822
- this.requestOptions.credentials = credentialsPolicy;
823
- return this;
1016
+ get withCredentials() {
1017
+ return this.createFluentSetter("credentials", {
1018
+ INCLUDE: CredentialsPolicy.INCLUDE,
1019
+ OMIT: CredentialsPolicy.OMIT,
1020
+ SAME_ORIGIN: CredentialsPolicy.SAME_ORIGIN,
1021
+ });
824
1022
  }
825
1023
  /**
826
1024
  * Allows providing an external AbortController to cancel the request
@@ -841,22 +1039,37 @@ class BaseRequest {
841
1039
  return this;
842
1040
  }
843
1041
  /**
844
- * Sets the referrer policy for the request
845
- * @param policy - The referrer policy to use
846
- * @returns The instance for chaining
1042
+ * Sets referrer policy - supports both direct call and fluent API
1043
+ * @example
1044
+ * request.withReferrerPolicy("no-referrer")
1045
+ * request.withReferrerPolicy(ReferrerPolicy.NO_REFERRER)
1046
+ * request.withReferrerPolicy.NO_REFERRER()
847
1047
  */
848
- withReferrerPolicy(policy) {
849
- this.requestOptions.referrerPolicy = policy;
850
- return this;
1048
+ get withReferrerPolicy() {
1049
+ return this.createFluentSetter("referrerPolicy", {
1050
+ ORIGIN: ReferrerPolicy.ORIGIN,
1051
+ UNSAFE_URL: ReferrerPolicy.UNSAFE_URL,
1052
+ SAME_ORIGIN: ReferrerPolicy.SAME_ORIGIN,
1053
+ NO_REFERRER: ReferrerPolicy.NO_REFERRER,
1054
+ STRICT_ORIGIN: ReferrerPolicy.STRICT_ORIGIN,
1055
+ ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.ORIGIN_WHEN_CROSS_ORIGIN,
1056
+ NO_REFERRER_WHEN_DOWNGRADE: ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE,
1057
+ STRICT_ORIGIN_WHEN_CROSS_ORIGIN: ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
1058
+ });
851
1059
  }
852
1060
  /**
853
- * Sets the redirect policy for the request
854
- * @param redirect - Can be 'follow', 'error', or 'manual'
855
- * @returns The instance for chaining
1061
+ * Sets redirect mode - supports both direct call and fluent API
1062
+ * @example
1063
+ * request.withRedirect("follow")
1064
+ * request.withRedirect(RedirectMode.FOLLOW)
1065
+ * request.withRedirect.FOLLOW()
856
1066
  */
857
- withRedirect(redirect) {
858
- this.requestOptions.redirect = redirect;
859
- return this;
1067
+ get withRedirect() {
1068
+ return this.createFluentSetter("redirect", {
1069
+ FOLLOW: RedirectMode.FOLLOW,
1070
+ ERROR: RedirectMode.ERROR,
1071
+ MANUAL: RedirectMode.MANUAL,
1072
+ });
860
1073
  }
861
1074
  /**
862
1075
  * Sets the keepalive flag for the request
@@ -868,13 +1081,18 @@ class BaseRequest {
868
1081
  return this;
869
1082
  }
870
1083
  /**
871
- * Sets the priority of the request
872
- * @param priority - Can be 'high', 'low', or 'auto'
873
- * @returns The instance for chaining
1084
+ * Sets request priority - supports both direct call and fluent API
1085
+ * @example
1086
+ * request.withPriority("high")
1087
+ * request.withPriority(RequestPriority.HIGH)
1088
+ * request.withPriority.HIGH()
874
1089
  */
875
- withPriority(priority) {
876
- this.requestOptions.priority = priority;
877
- return this;
1090
+ get withPriority() {
1091
+ return this.createFluentSetter("priority", {
1092
+ HIGH: RequestPriority.HIGH,
1093
+ LOW: RequestPriority.LOW,
1094
+ AUTO: RequestPriority.AUTO,
1095
+ });
878
1096
  }
879
1097
  /**
880
1098
  * Adds query parameters to the request URL
@@ -912,13 +1130,19 @@ class BaseRequest {
912
1130
  return this;
913
1131
  }
914
1132
  /**
915
- * Sets the mode of the request
916
- * @param mode - The mode for the request (cors, no-cors, same-origin, navigate)
917
- * @returns The instance for chaining
1133
+ * Sets request mode - supports both direct call and fluent API
1134
+ * @example
1135
+ * request.withMode("cors")
1136
+ * request.withMode(RequestMode.CORS)
1137
+ * request.withMode.CORS()
918
1138
  */
919
- withMode(mode) {
920
- this.requestOptions.mode = mode;
921
- return this;
1139
+ get withMode() {
1140
+ return this.createFluentSetter("mode", {
1141
+ CORS: RequestMode.CORS,
1142
+ NO_CORS: RequestMode.NO_CORS,
1143
+ SAME_ORIGIN: RequestMode.SAME_ORIGIN,
1144
+ NAVIGATE: RequestMode.NAVIGATE,
1145
+ });
922
1146
  }
923
1147
  /**
924
1148
  * Shorthand for setting the Content-Type header
@@ -968,7 +1192,7 @@ class BaseRequest {
968
1192
  if (typeof Buffer !== "undefined")
969
1193
  return Buffer.from(str).toString("base64");
970
1194
  // Fallback (should never happen in modern environments)
971
- throw new Error("Base64 encoding is not supported in this environment");
1195
+ throw new RequestError("Base64 encoding is not supported", this.url, this.method);
972
1196
  }
973
1197
  /**
974
1198
  * Sets the bearer token for authentication
@@ -978,13 +1202,23 @@ class BaseRequest {
978
1202
  withBearerToken(token) {
979
1203
  return this.withAuthorization(`Bearer ${token}`);
980
1204
  }
1205
+ /**
1206
+ * Safely get headers as a Record<string, string>
1207
+ * @returns The headers object
1208
+ */
1209
+ getHeadersRecord() {
1210
+ if (typeof this.requestOptions.headers === "object" && this.requestOptions.headers !== null) {
1211
+ return this.requestOptions.headers;
1212
+ }
1213
+ return {};
1214
+ }
981
1215
  /**
982
1216
  * Helper function to check for header presence in a case-insensitive way
983
- * @param headers Headers object
984
1217
  * @param headerName Header name to check
985
1218
  * @returns Boolean indicating if the header exists (case-insensitive)
986
1219
  */
987
- hasHeader(headers, headerName) {
1220
+ hasHeader(headerName) {
1221
+ const headers = this.getHeadersRecord();
988
1222
  return Object.keys(headers).some(key => key.toLowerCase() === headerName.toLowerCase());
989
1223
  }
990
1224
  /**
@@ -993,12 +1227,11 @@ class BaseRequest {
993
1227
  * @returns The instance for chaining
994
1228
  */
995
1229
  withCookies(cookies) {
996
- const cookieEntries = Object.entries(cookies);
1230
+ const cookieEntries = Object.entries(cookies || {});
997
1231
  if (cookieEntries.length === 0) {
998
1232
  return this;
999
1233
  }
1000
- // Get current headers or initialize empty object
1001
- const currentHeaders = this.requestOptions.headers || {};
1234
+ const currentHeaders = this.getHeadersRecord();
1002
1235
  // Get all existing cookie headers with different cases
1003
1236
  const cookieValues = [];
1004
1237
  const cookieHeaderKeys = [];
@@ -1067,110 +1300,200 @@ class BaseRequest {
1067
1300
  });
1068
1301
  }
1069
1302
  /**
1070
- * Enables caching for the request with the specified options
1071
- * @param options The cache configuration options
1303
+ * Add a request interceptor for this specific request
1304
+ * Request interceptors can modify the request configuration or return an early response
1305
+ *
1306
+ * @param interceptor - The request interceptor function
1307
+ * @returns The instance for chaining
1308
+ *
1309
+ * @example
1310
+ * request.withRequestInterceptor((config) => {
1311
+ * config.headers['X-Custom'] = 'value';
1312
+ * return config;
1313
+ * });
1314
+ */
1315
+ withRequestInterceptor(interceptor) {
1316
+ this.requestInterceptors.push(interceptor);
1317
+ return this;
1318
+ }
1319
+ /**
1320
+ * Add a response interceptor for this specific request
1321
+ * Response interceptors can transform the response
1322
+ *
1323
+ * @param interceptor - The response interceptor function
1324
+ * @returns The instance for chaining
1325
+ *
1326
+ * @example
1327
+ * request.withResponseInterceptor((response) => {
1328
+ * console.log('Status:', response.status);
1329
+ * return response;
1330
+ * });
1331
+ */
1332
+ withResponseInterceptor(interceptor) {
1333
+ this.responseInterceptors.push(interceptor);
1334
+ return this;
1335
+ }
1336
+ /**
1337
+ * Add an error interceptor for this specific request
1338
+ * Error interceptors can handle or transform errors
1339
+ *
1340
+ * @param interceptor - The error interceptor function
1072
1341
  * @returns The instance for chaining
1342
+ *
1343
+ * @example
1344
+ * request.withErrorInterceptor((error) => {
1345
+ * console.error('Request failed:', error);
1346
+ * throw error;
1347
+ * });
1073
1348
  */
1074
- withCache(options = {}) {
1075
- this.cacheEnabled = true;
1076
- this.cacheManager = new CacheManager(options);
1349
+ withErrorInterceptor(interceptor) {
1350
+ this.errorInterceptors.push(interceptor);
1077
1351
  return this;
1078
1352
  }
1079
1353
  /**
1080
- * Send the request to the specified URL
1081
- * @param url The URL to send the request to
1354
+ * Execute the request and return the ResponseWrapper
1355
+ * This is the base method for getting the full response.
1356
+ *
1357
+ * @returns A promise that resolves to the ResponseWrapper
1358
+ *
1359
+ * @example
1360
+ * const response = await request.getResponse();
1361
+ * console.log(response.status);
1362
+ */
1363
+ async getResponse() {
1364
+ const url = this.formatUrlWithQueryParams(this.url);
1365
+ this.applyCsrfProtection();
1366
+ const fetchOptions = {
1367
+ ...this.requestOptions,
1368
+ method: this.method,
1369
+ };
1370
+ return !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1371
+ }
1372
+ /**
1373
+ * Execute the request and parse the response as JSON
1374
+ *
1375
+ * @returns A promise that resolves to the parsed JSON data
1376
+ * @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
1377
+ *
1378
+ * @example
1379
+ * const users = await request.getJson<User[]>();
1380
+ *
1381
+ * @example
1382
+ * // Error handling - errors are always RequestError
1383
+ * try {
1384
+ * const data = await request.getJson();
1385
+ * } catch (error) {
1386
+ * if (error instanceof RequestError) {
1387
+ * console.log(error.status, error.url, error.method);
1388
+ * }
1389
+ * }
1390
+ */
1391
+ async getJson() {
1392
+ const response = await this.getResponse();
1393
+ return response.getJson();
1394
+ }
1395
+ /**
1396
+ * Execute the request and get the response as text
1397
+ *
1398
+ * @returns A promise that resolves to the response text
1399
+ *
1400
+ * @example
1401
+ * const text = await request.getText();
1402
+ */
1403
+ async getText() {
1404
+ const response = await this.getResponse();
1405
+ return response.getText();
1406
+ }
1407
+ /**
1408
+ * Execute the request and get the response as a Blob
1409
+ *
1410
+ * @returns A promise that resolves to the response Blob
1411
+ *
1412
+ * @example
1413
+ * const blob = await request.getBlob();
1414
+ */
1415
+ async getBlob() {
1416
+ const response = await this.getResponse();
1417
+ return response.getBlob();
1418
+ }
1419
+ /**
1420
+ * Execute the request and get the response body as a ReadableStream
1421
+ *
1422
+ * @returns A promise that resolves to the response body stream
1423
+ *
1424
+ * @example
1425
+ * const stream = await request.getBody();
1426
+ */
1427
+ async getBody() {
1428
+ const response = await this.getResponse();
1429
+ return response.getBody();
1430
+ }
1431
+ /**
1432
+ * Execute the request and extract specific data using a selector function
1433
+ * If no selector is provided, returns the full JSON response.
1434
+ *
1435
+ * @param selector - Optional function to extract and transform data
1436
+ * @returns A promise that resolves to the selected data
1437
+ * @throws {RequestError} When the request fails, JSON parsing fails, or the selector throws an error
1438
+ *
1439
+ * @example
1440
+ * // Get full response
1441
+ * const data = await request.getData();
1442
+ *
1443
+ * // Extract specific data
1444
+ * const users = await request.getData(data => data.results.users);
1445
+ *
1446
+ * @example
1447
+ * // Error handling - errors are always RequestError
1448
+ * try {
1449
+ * const data = await request.getData();
1450
+ * } catch (error) {
1451
+ * if (error instanceof RequestError) {
1452
+ * console.log(error.status, error.url, error.method);
1453
+ * }
1454
+ * }
1455
+ */
1456
+ async getData(selector) {
1457
+ const response = await this.getResponse();
1458
+ return response.getData(selector);
1459
+ }
1460
+ /**
1461
+ * Apply CSRF protection headers based on configuration
1082
1462
  */
1083
- sendTo(url) {
1084
- // Format the URL with query parameters
1085
- url = this.formatUrlWithQueryParams(url);
1086
- // Apply automatic CSRF protection if enabled
1087
- if (this.autoApplyCsrfProtection) {
1088
- const config = Config.getInstance();
1089
- // Apply anti-CSRF headers if enabled
1090
- if (config.isAntiCsrfEnabled()) {
1463
+ applyCsrfProtection() {
1464
+ if (!this.autoApplyCsrfProtection)
1465
+ return;
1466
+ const config = Config.getInstance();
1467
+ // Apply anti-CSRF headers if enabled
1468
+ if (config.isAntiCsrfEnabled()) {
1469
+ this.withHeaders({
1470
+ "X-Requested-With": "XMLHttpRequest",
1471
+ });
1472
+ }
1473
+ // Apply global CSRF token if set
1474
+ const globalToken = config.getCsrfToken();
1475
+ if (globalToken) {
1476
+ const csrfHeaderName = config.getCsrfHeaderName();
1477
+ const hasLocalToken = this.hasHeader("X-CSRF-Token") || this.hasHeader(csrfHeaderName);
1478
+ if (!hasLocalToken) {
1091
1479
  this.withHeaders({
1092
- "X-Requested-With": "XMLHttpRequest",
1480
+ [csrfHeaderName]: globalToken,
1093
1481
  });
1094
1482
  }
1095
- // Apply global CSRF token if set
1096
- const globalToken = config.getCsrfToken();
1097
- if (globalToken) {
1098
- // Check if local token exists using case-insensitive comparison
1099
- const headers = this.requestOptions.headers;
1100
- const csrfHeaderName = config.getCsrfHeaderName();
1101
- const hasLocalToken = this.hasHeader(headers, "X-CSRF-Token") || this.hasHeader(headers, csrfHeaderName);
1483
+ }
1484
+ // Check for XSRF token in cookies and send it as a header
1485
+ if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
1486
+ const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
1487
+ if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
1488
+ const xsrfHeaderName = config.getXsrfHeaderName();
1489
+ const hasLocalToken = this.hasHeader("X-XSRF-TOKEN") || this.hasHeader(xsrfHeaderName);
1102
1490
  if (!hasLocalToken) {
1103
1491
  this.withHeaders({
1104
- [csrfHeaderName]: globalToken,
1492
+ [xsrfHeaderName]: xsrfToken,
1105
1493
  });
1106
1494
  }
1107
1495
  }
1108
- // check for XSRF token in cookies and send it as a header
1109
- if (config.isAutoXsrfEnabled() && typeof document !== "undefined") {
1110
- const xsrfToken = CsrfUtils.getTokenFromCookie(config.getXsrfCookieName());
1111
- if (xsrfToken && CsrfUtils.isValidToken(xsrfToken)) {
1112
- const headers = this.requestOptions.headers;
1113
- const xsrfHeaderName = config.getXsrfHeaderName();
1114
- const hasLocalToken = this.hasHeader(headers, "X-XSRF-TOKEN") || this.hasHeader(headers, xsrfHeaderName);
1115
- if (!hasLocalToken) {
1116
- this.withHeaders({
1117
- [xsrfHeaderName]: xsrfToken,
1118
- });
1119
- }
1120
- }
1121
- }
1122
- }
1123
- // Cast the request options to RequestInit to ensure compatibility
1124
- const fetchOptions = {
1125
- ...this.requestOptions,
1126
- method: this.method,
1127
- };
1128
- // Create the base promise - handle caching if enabled
1129
- let basePromise;
1130
- if (this.cacheEnabled && this.cacheManager) {
1131
- const cacheKey = this.cacheManager.generateKey(url, this.method, this.requestOptions.headers, this.requestOptions.body);
1132
- basePromise = this.executeWithCache(url, fetchOptions, cacheKey);
1133
1496
  }
1134
- else {
1135
- basePromise = !this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions);
1136
- }
1137
- const responsePromise = basePromise;
1138
- // Attach convenience methods that begin processing immediately
1139
- responsePromise.getJson = async () => {
1140
- return basePromise.then(response => response.getJson());
1141
- };
1142
- responsePromise.getText = async () => {
1143
- return basePromise.then(response => response.getText());
1144
- };
1145
- responsePromise.getBlob = async () => {
1146
- return basePromise.then(response => response.getBlob());
1147
- };
1148
- responsePromise.getArrayBuffer = async () => {
1149
- return basePromise.then(response => response.getArrayBuffer());
1150
- };
1151
- responsePromise.getBody = async () => {
1152
- return basePromise.then(response => response.getBody());
1153
- };
1154
- responsePromise.getData = async (selector) => {
1155
- try {
1156
- const data = await basePromise.then(response => response.getJson());
1157
- // If no selector is provided, return the raw JSON data
1158
- if (!selector)
1159
- return data;
1160
- // Apply the selector if provided
1161
- return selector(data);
1162
- }
1163
- catch (error) {
1164
- if (error instanceof Error) {
1165
- // If selector fails, enhance the error with the original data
1166
- const enhancedError = new Error(`Data selector failed: ${error.message}. Original data: ${JSON.stringify(await basePromise.then(response => response.getJson()), null, 2)}`);
1167
- enhancedError.stack = error.stack;
1168
- throw enhancedError;
1169
- }
1170
- throw error;
1171
- }
1172
- };
1173
- return responsePromise;
1174
1497
  }
1175
1498
  /**
1176
1499
  * Formats the URL with any query parameters
@@ -1178,7 +1501,8 @@ class BaseRequest {
1178
1501
  * @returns The URL with query parameters appended
1179
1502
  */
1180
1503
  formatUrlWithQueryParams(url) {
1181
- if (!this.queryParams.toString()) {
1504
+ const queryString = this.queryParams.toString();
1505
+ if (!queryString) {
1182
1506
  return url;
1183
1507
  }
1184
1508
  try {
@@ -1194,7 +1518,7 @@ class BaseRequest {
1194
1518
  // Handle relative URLs
1195
1519
  const hasExistingParams = url.includes("?");
1196
1520
  const separator = hasExistingParams ? "&" : "?";
1197
- return `${url}${separator}${this.queryParams.toString()}`;
1521
+ return `${url}${separator}${queryString}`;
1198
1522
  }
1199
1523
  }
1200
1524
  /**
@@ -1202,136 +1526,298 @@ class BaseRequest {
1202
1526
  * @param url The formatted URL to send the request to
1203
1527
  * @param fetchOptions The fetch options to use
1204
1528
  * @returns A wrapped response object
1529
+ * @throws RequestError if the request fails after all retries
1205
1530
  */
1206
1531
  async executeWithRetries(url, fetchOptions) {
1207
- let attempt = 0;
1208
1532
  const maxRetries = this.requestOptions.retries || 0;
1209
- do {
1533
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1534
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1210
1535
  try {
1211
1536
  return await this.executeRequest(url, fetchOptions);
1212
1537
  }
1213
1538
  catch (error) {
1214
- const requestError = error instanceof RequestError ? error : RequestError.networkError(url, fetchOptions.method, error);
1215
- // Don't retry on timeout errors or if we've reached the max retries
1216
- if (attempt >= maxRetries || requestError.timeoutError)
1539
+ const requestError = error instanceof RequestError ? error : RequestError.networkError(url, method, error instanceof Error ? error : new Error(String(error)));
1540
+ if (attempt >= maxRetries)
1217
1541
  throw requestError;
1218
- attempt++;
1219
1542
  if (this.requestOptions.onRetry) {
1220
- await this.requestOptions.onRetry({ attempt, error: requestError });
1543
+ await this.requestOptions.onRetry({ attempt: attempt + 1, error: requestError });
1221
1544
  }
1222
1545
  }
1223
- } while (attempt <= maxRetries);
1224
- // If we somehow get here, we throw an error to avoid undefined behavior
1225
- throw new RequestError(`Max retries reached (${maxRetries}) but request still failed`, url, fetchOptions.method);
1546
+ }
1547
+ // This should never happen but is needed for type safety
1548
+ throw new RequestError(`Max retries reached`, url, method);
1226
1549
  }
1227
- async executeRequest(url, fetchOptions) {
1228
- // Create a request controller for timeout if needed, or use the provided one
1229
- const controller = this.abortController || new AbortController();
1230
- const { signal } = controller;
1231
- // Use the controller signal for the fetch request, but don't overwrite an existing signal
1232
- fetchOptions.signal = fetchOptions.signal || signal;
1233
- let timeoutId;
1234
- // Flag to track if abort was caused by our timeout
1235
- let abortedByTimeout = false;
1236
- // Handle timeout regardless of whether an external controller is provided
1237
- if (this.requestOptions.timeout) {
1238
- if (this.abortController) {
1239
- // If we have an external controller, create a separate timeout controller
1240
- // that won't interfere with the external one
1241
- // Use AbortSignal.timeout if available (modern browsers)
1242
- if (typeof AbortSignal.timeout === "function") {
1243
- const timeoutSignal = AbortSignal.timeout(this.requestOptions.timeout);
1244
- // Add event listener to propagate the abort to the main controller
1245
- timeoutSignal.addEventListener("abort", () => {
1246
- if (!controller.signal.aborted) {
1247
- abortedByTimeout = true;
1248
- controller.abort();
1249
- }
1250
- });
1550
+ /**
1551
+ * Run request interceptors in order: global interceptors first, then per-request
1552
+ * @param configParam - The request configuration
1553
+ * @returns Modified config or a Response to short-circuit
1554
+ */
1555
+ async runRequestInterceptors(configParam) {
1556
+ const globalConfig = Config.getInstance();
1557
+ const allInterceptors = [...globalConfig.getRequestInterceptors(), ...this.requestInterceptors];
1558
+ let currentConfig = configParam;
1559
+ for (let i = 0; i < allInterceptors.length; i++) {
1560
+ try {
1561
+ const result = await allInterceptors[i](currentConfig);
1562
+ // If interceptor returns a Response, short-circuit
1563
+ if (result instanceof Response) {
1564
+ return result;
1565
+ }
1566
+ currentConfig = result;
1567
+ }
1568
+ catch (error) {
1569
+ const errorMessage = error instanceof Error ? error.message : String(error);
1570
+ throw new RequestError(`Request interceptor failed: ${errorMessage}`, currentConfig.url, currentConfig.method);
1571
+ }
1572
+ }
1573
+ return currentConfig;
1574
+ }
1575
+ /**
1576
+ * Run response interceptors in reverse order: per-request interceptors first, then global in reverse
1577
+ * @param response - The response wrapper
1578
+ * @returns Modified response wrapper
1579
+ */
1580
+ async runResponseInterceptors(response) {
1581
+ const globalConfig = Config.getInstance();
1582
+ const globalInterceptors = globalConfig.getResponseInterceptors();
1583
+ // Per-request in order, then global in reverse
1584
+ const allInterceptors = [...this.responseInterceptors, ...globalInterceptors.reverse()];
1585
+ let currentResponse = response;
1586
+ for (let i = 0; i < allInterceptors.length; i++) {
1587
+ try {
1588
+ currentResponse = await allInterceptors[i](currentResponse);
1589
+ }
1590
+ catch (error) {
1591
+ const errorMessage = error instanceof Error ? error.message : String(error);
1592
+ const url = currentResponse.url || "";
1593
+ const method = currentResponse.method || "";
1594
+ throw new RequestError(`Response interceptor failed: ${errorMessage}`, url, method);
1595
+ }
1596
+ }
1597
+ return currentResponse;
1598
+ }
1599
+ /**
1600
+ * Run error interceptors in reverse order: per-request interceptors first, then global in reverse
1601
+ * @param error - The error that occurred
1602
+ * @returns Modified error or a ResponseWrapper to recover
1603
+ */
1604
+ async runErrorInterceptors(error) {
1605
+ const globalConfig = Config.getInstance();
1606
+ const globalInterceptors = globalConfig.getErrorInterceptors();
1607
+ // Per-request in order, then global in reverse
1608
+ const allInterceptors = [...this.errorInterceptors, ...globalInterceptors.reverse()];
1609
+ let currentError = error;
1610
+ for (let i = 0; i < allInterceptors.length; i++) {
1611
+ try {
1612
+ // If previous interceptor recovered with a response, stop processing
1613
+ if (currentError instanceof ResponseWrapper) {
1614
+ return currentError;
1615
+ }
1616
+ const result = await allInterceptors[i](currentError);
1617
+ currentError = result;
1618
+ }
1619
+ catch (interceptorError) {
1620
+ // If an error interceptor throws, that becomes the new error
1621
+ if (interceptorError instanceof RequestError) {
1622
+ currentError = interceptorError;
1251
1623
  }
1252
1624
  else {
1253
- // Fallback for browsers without AbortSignal.timeout
1254
- timeoutId = setTimeout(() => {
1255
- if (!controller.signal.aborted) {
1256
- abortedByTimeout = true;
1257
- controller.abort();
1258
- }
1259
- }, this.requestOptions.timeout);
1625
+ const errorMessage = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
1626
+ // Always wrap in RequestError when we have context
1627
+ if (currentError instanceof RequestError) {
1628
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
1629
+ status: currentError.status,
1630
+ response: currentError.response,
1631
+ });
1632
+ }
1633
+ else if (currentError instanceof ResponseWrapper && currentError.url && currentError.method) {
1634
+ // If it's a ResponseWrapper, we can still create a proper RequestError from its context
1635
+ currentError = new RequestError(`Error interceptor ${i + 1} failed: ${errorMessage}`, currentError.url, currentError.method, {
1636
+ status: currentError.status,
1637
+ });
1638
+ }
1639
+ else {
1640
+ // Last resort: if we have no context at all, use the original error's context
1641
+ // This shouldn't happen in practice, but handle it gracefully
1642
+ const errorObj = interceptorError instanceof Error ? interceptorError : new Error(String(interceptorError));
1643
+ currentError = RequestError.networkError(error.url, error.method, errorObj);
1644
+ }
1260
1645
  }
1261
1646
  }
1647
+ }
1648
+ return currentError;
1649
+ }
1650
+ /**
1651
+ * Helper to create an abort signal with timeout support
1652
+ * Handles various AbortSignal API levels gracefully
1653
+ */
1654
+ createAbortSignal(timeoutMs, externalController) {
1655
+ let timeoutId;
1656
+ let timeoutController;
1657
+ let isTimeout = false;
1658
+ // No timeout - just use external controller
1659
+ if (!timeoutMs) {
1660
+ return {
1661
+ signal: externalController === null || externalController === void 0 ? void 0 : externalController.signal,
1662
+ cleanup: () => { },
1663
+ wasTimeout: () => false,
1664
+ };
1665
+ }
1666
+ // Check if AbortSignal.any() is available (modern browsers)
1667
+ const hasAbortSignalAny = typeof AbortSignal.any === "function";
1668
+ const hasAbortSignalTimeout = typeof AbortSignal.timeout === "function";
1669
+ // Create timeout signal
1670
+ const createTimeoutSignal = () => {
1671
+ if (hasAbortSignalTimeout) {
1672
+ const signal = AbortSignal.timeout(timeoutMs);
1673
+ signal.addEventListener("abort", () => (isTimeout = true), { once: true });
1674
+ return signal;
1675
+ }
1262
1676
  else {
1677
+ timeoutController = new AbortController();
1263
1678
  timeoutId = setTimeout(() => {
1264
- abortedByTimeout = true;
1265
- controller.abort();
1266
- }, this.requestOptions.timeout);
1679
+ isTimeout = true;
1680
+ timeoutController.abort();
1681
+ }, timeoutMs);
1682
+ return timeoutController.signal;
1267
1683
  }
1684
+ };
1685
+ const timeoutSignal = createTimeoutSignal();
1686
+ // Combine signals if we have both timeout and external controller
1687
+ const finalSignal = externalController && hasAbortSignalAny
1688
+ ? AbortSignal.any([externalController.signal, timeoutSignal])
1689
+ : externalController
1690
+ ? this.combineSignalsManually(externalController.signal, timeoutSignal)
1691
+ : timeoutSignal;
1692
+ return {
1693
+ signal: finalSignal,
1694
+ cleanup: () => {
1695
+ if (timeoutId !== undefined)
1696
+ clearTimeout(timeoutId);
1697
+ },
1698
+ wasTimeout: () => isTimeout,
1699
+ };
1700
+ }
1701
+ /**
1702
+ * Manually combine two abort signals for older environments
1703
+ * Returns the first signal and listens to the second
1704
+ */
1705
+ combineSignalsManually(signal1, signal2) {
1706
+ // If either is already aborted, use that one
1707
+ if (signal1.aborted)
1708
+ return signal1;
1709
+ if (signal2.aborted)
1710
+ return signal2;
1711
+ // Use a controller to create a combined signal
1712
+ const controller = new AbortController();
1713
+ const abort = () => controller.abort();
1714
+ signal1.addEventListener("abort", abort, { once: true });
1715
+ signal2.addEventListener("abort", abort, { once: true });
1716
+ return controller.signal;
1717
+ }
1718
+ /**
1719
+ * Convert fetchOptions to RequestConfig with proper typing
1720
+ */
1721
+ createRequestConfig(url, fetchOptions) {
1722
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1723
+ const headers = this.getHeadersRecord();
1724
+ const extendedOptions = fetchOptions;
1725
+ return {
1726
+ url,
1727
+ method,
1728
+ headers,
1729
+ body: fetchOptions.body,
1730
+ signal: fetchOptions.signal || undefined,
1731
+ credentials: fetchOptions.credentials,
1732
+ mode: fetchOptions.mode,
1733
+ redirect: fetchOptions.redirect,
1734
+ referrer: fetchOptions.referrer,
1735
+ referrerPolicy: fetchOptions.referrerPolicy,
1736
+ keepalive: fetchOptions.keepalive,
1737
+ priority: extendedOptions.priority,
1738
+ };
1739
+ }
1740
+ /**
1741
+ * Apply interceptor results back to fetchOptions
1742
+ */
1743
+ applyRequestConfig(config, fetchOptions) {
1744
+ fetchOptions.headers = config.headers;
1745
+ if (config.body !== undefined) {
1746
+ fetchOptions.body = config.body;
1268
1747
  }
1748
+ }
1749
+ async executeRequest(url, fetchOptions) {
1750
+ const method = typeof fetchOptions.method === "string" ? fetchOptions.method : "GET";
1751
+ // Setup abort signal with timeout
1752
+ const abortSignal = this.createAbortSignal(this.requestOptions.timeout, this.abortController);
1269
1753
  try {
1754
+ // Run request interceptors before making the request
1755
+ const requestConfig = this.createRequestConfig(url, fetchOptions);
1756
+ const interceptorResult = await this.runRequestInterceptors(requestConfig);
1757
+ // If interceptor returned a Response, short-circuit and wrap it
1758
+ if (interceptorResult instanceof Response) {
1759
+ const graphQLOptions = this.getGraphQLOptions();
1760
+ const wrappedResponse = new ResponseWrapper(interceptorResult, this.url, this.method, graphQLOptions);
1761
+ return await this.runResponseInterceptors(wrappedResponse);
1762
+ }
1763
+ // Update fetchOptions with interceptor modifications
1764
+ url = interceptorResult.url;
1765
+ this.validateUrl(url);
1766
+ this.applyRequestConfig(interceptorResult, fetchOptions);
1767
+ // Set the combined abort signal
1768
+ fetchOptions.signal = abortSignal.signal;
1769
+ // Execute fetch
1270
1770
  let response;
1271
1771
  try {
1272
1772
  response = await fetch(url, fetchOptions);
1273
1773
  }
1274
1774
  catch (error) {
1275
- // Check if this is an abort error that was specifically caused by our timeout
1276
- if (error instanceof DOMException && error.name === "AbortError" && abortedByTimeout) {
1277
- throw RequestError.timeout(url, fetchOptions.method, this.requestOptions.timeout);
1775
+ // Check if it's an abort error (DOMException in browsers, or AbortSignal abort)
1776
+ if (error instanceof DOMException && error.name === "AbortError") {
1777
+ if (abortSignal.wasTimeout()) {
1778
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1779
+ }
1780
+ throw RequestError.abortError(url, method);
1278
1781
  }
1279
- // Otherwise it's either a user-triggered abort or another network error
1280
- throw RequestError.networkError(url, fetchOptions.method, error);
1782
+ // Check for Node.js/undici TimeoutError
1783
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1784
+ const errorName = errorObj.name;
1785
+ const errorMessage = errorObj.message.toLowerCase();
1786
+ // Detect timeout errors from Node.js/undici (TimeoutError)
1787
+ const isTimeoutError = errorName === "TimeoutError" || errorMessage.includes("timeout") || errorMessage.includes("aborted due to timeout") || abortSignal.wasTimeout();
1788
+ if (isTimeoutError && this.requestOptions.timeout) {
1789
+ throw RequestError.timeout(url, method, this.requestOptions.timeout);
1790
+ }
1791
+ throw RequestError.networkError(url, method, errorObj);
1281
1792
  }
1282
1793
  if (!response.ok) {
1283
- throw RequestError.fromResponse(response, url, fetchOptions.method);
1284
- }
1285
- // Return a wrapped response instead of parsing it
1286
- return new ResponseWrapper(response);
1287
- }
1288
- finally {
1289
- if (timeoutId) {
1290
- clearTimeout(timeoutId);
1794
+ throw RequestError.fromResponse(response, url, method);
1291
1795
  }
1796
+ const graphQLOptions = this.getGraphQLOptions();
1797
+ const wrappedResponse = new ResponseWrapper(response, url, method, graphQLOptions);
1798
+ return await this.runResponseInterceptors(wrappedResponse);
1292
1799
  }
1293
- }
1294
- /**
1295
- * Executes a request using cache when possible
1296
- * @param url The formatted URL to send the request to
1297
- * @param fetchOptions The fetch options to use
1298
- * @param cacheKey The cache key for this request
1299
- */
1300
- async executeWithCache(url, fetchOptions, cacheKey) {
1301
- // Try to get from cache first
1302
- if (this.cacheManager) {
1303
- const cachedEntry = await this.cacheManager.get(cacheKey);
1304
- if (cachedEntry) {
1305
- // Create a synthetic Response object from cached data
1306
- const cachedResponse = new Response(JSON.stringify(cachedEntry.value), {
1307
- status: 200,
1308
- headers: new Headers(cachedEntry.headers || {}),
1309
- });
1310
- return new ResponseWrapper(cachedResponse);
1800
+ catch (error) {
1801
+ // Convert to RequestError if needed
1802
+ let requestError;
1803
+ if (error instanceof RequestError) {
1804
+ requestError = error;
1311
1805
  }
1312
- }
1313
- // Not in cache, make the actual request
1314
- const response = await (!this.requestOptions.retries ? this.executeRequest(url, fetchOptions) : this.executeWithRetries(url, fetchOptions));
1315
- // Cache the successful response if caching is enabled
1316
- // Only cache successful responses
1317
- if (response.ok && this.cacheManager) {
1318
- // Clone the response so we don't consume it
1319
- try {
1320
- const responseData = await response.getJson();
1321
- // Extract headers to store in cache
1322
- const headers = {};
1323
- response.headers.forEach((value, key) => {
1324
- headers[key] = value;
1325
- });
1326
- // Store in cache
1327
- await this.cacheManager.set(cacheKey, responseData, headers);
1806
+ else {
1807
+ const errorObj = error instanceof Error ? error : new Error(String(error));
1808
+ requestError = RequestError.networkError(url, method, errorObj);
1328
1809
  }
1329
- catch (error) {
1330
- // Failed to cache, but we can still return the response
1331
- console.warn("Failed to cache response:", error);
1810
+ // Run error interceptors
1811
+ const interceptorResult = await this.runErrorInterceptors(requestError);
1812
+ // If error interceptor returned a ResponseWrapper, recover from error
1813
+ if (interceptorResult instanceof ResponseWrapper) {
1814
+ return interceptorResult;
1332
1815
  }
1816
+ throw interceptorResult;
1817
+ }
1818
+ finally {
1819
+ abortSignal.cleanup();
1333
1820
  }
1334
- return response;
1335
1821
  }
1336
1822
  }
1337
1823
 
@@ -1339,6 +1825,10 @@ class BaseRequest {
1339
1825
  * Base class for requests that can have a body (POST, PUT, PATCH)
1340
1826
  */
1341
1827
  class BodyRequest extends BaseRequest {
1828
+ constructor(url) {
1829
+ super(url);
1830
+ this.graphQLOptions = undefined;
1831
+ }
1342
1832
  /**
1343
1833
  * Sets the body of the request
1344
1834
  * @param body - The request body
@@ -1360,7 +1850,8 @@ class BodyRequest extends BaseRequest {
1360
1850
  JSON.stringify(body);
1361
1851
  }
1362
1852
  catch (error) {
1363
- throw new Error(`Failed to stringify request body: ${String(error)}`);
1853
+ const errorMessage = error instanceof Error ? error.message : String(error);
1854
+ throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
1364
1855
  }
1365
1856
  }
1366
1857
  else {
@@ -1368,18 +1859,93 @@ class BodyRequest extends BaseRequest {
1368
1859
  }
1369
1860
  return this;
1370
1861
  }
1371
- setContentTypeIfNeeded(contentType) {
1372
- // Only set if not already set
1862
+ /**
1863
+ * Sets a GraphQL query or mutation as the request body
1864
+ * Automatically formats the body as JSON and sets Content-Type to application/json
1865
+ *
1866
+ * @param query - The GraphQL query or mutation string
1867
+ * @param variables - Optional variables object to pass with the query
1868
+ * @param options - Optional GraphQL-specific options
1869
+ * @returns The request instance for chaining
1870
+ *
1871
+ * @example
1872
+ * const request = new PostRequest('/graphql')
1873
+ * .withGraphQL('query { user(id: $id) { name email } }', { id: '123' });
1874
+ *
1875
+ * @example
1876
+ * const request = new PostRequest('/graphql')
1877
+ * .withGraphQL('mutation { createUser(name: $name) { id } }', { name: 'John' });
1878
+ *
1879
+ * @example
1880
+ * // Throw an error if the GraphQL response contains errors
1881
+ * const request = new PostRequest('/graphql')
1882
+ * .withGraphQL('query { user { id } }', undefined, { throwOnError: true });
1883
+ */
1884
+ withGraphQL(query, variables, options) {
1885
+ if (typeof query !== "string" || query.length === 0) {
1886
+ throw new RequestError("Invalid GraphQL query", this.url, this.method);
1887
+ }
1888
+ const graphQLBody = {
1889
+ query: query,
1890
+ };
1891
+ if (variables !== undefined) {
1892
+ if (typeof variables !== "object" || variables === null || Array.isArray(variables)) {
1893
+ throw new RequestError("Invalid GraphQL variables", this.url, this.method);
1894
+ }
1895
+ graphQLBody.variables = variables;
1896
+ }
1897
+ // Store GraphQL options if provided
1898
+ if (options !== undefined) {
1899
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1900
+ throw new RequestError("Invalid GraphQL options", this.url, this.method);
1901
+ }
1902
+ // Store only the known GraphQL options properties
1903
+ const opts = options;
1904
+ this.graphQLOptions = {
1905
+ throwOnError: typeof opts.throwOnError === "boolean" ? opts.throwOnError : undefined,
1906
+ };
1907
+ }
1908
+ // Validate JSON is stringifiable early
1909
+ try {
1910
+ JSON.stringify(graphQLBody);
1911
+ }
1912
+ catch (error) {
1913
+ const errorMessage = error instanceof Error ? error.message : String(error);
1914
+ throw new RequestError(`JSON stringify failed: ${errorMessage}`, this.url, this.method);
1915
+ }
1916
+ this.body = graphQLBody;
1917
+ this.bodyType = BodyType.JSON;
1918
+ this.setContentTypeIfNeeded("application/json");
1919
+ return this;
1920
+ }
1921
+ /**
1922
+ * Check if Content-Type header is already set (case-insensitive)
1923
+ */
1924
+ hasContentType() {
1373
1925
  const headers = this.requestOptions.headers;
1374
- if (!Object.keys(headers).some(header => header.toLowerCase() === "content-type")) {
1926
+ if (typeof headers === "object" && headers !== null) {
1927
+ const headersObj = headers;
1928
+ return Object.keys(headersObj).some(header => header.toLowerCase() === "content-type");
1929
+ }
1930
+ return false;
1931
+ }
1932
+ setContentTypeIfNeeded(contentType) {
1933
+ if (!this.hasContentType()) {
1375
1934
  this.withContentType(contentType);
1376
1935
  }
1377
1936
  }
1378
1937
  /**
1379
- * Send the request to the specified URL
1938
+ * Get the GraphQL options if set
1939
+ * @returns The GraphQL options or undefined
1940
+ */
1941
+ getGraphQLOptions() {
1942
+ return this.graphQLOptions;
1943
+ }
1944
+ /**
1945
+ * Execute the request and return the ResponseWrapper
1380
1946
  * Overrides the base implementation to add body handling
1381
1947
  */
1382
- sendTo(url) {
1948
+ async getResponse() {
1383
1949
  if (this.body !== undefined) {
1384
1950
  // Remove previous body if it exists
1385
1951
  if (this.requestOptions.body)
@@ -1392,100 +1958,222 @@ class BodyRequest extends BaseRequest {
1392
1958
  this.requestOptions.body = this.body;
1393
1959
  }
1394
1960
  }
1395
- return super.sendTo(url);
1961
+ return super.getResponse();
1396
1962
  }
1397
1963
  }
1398
1964
 
1399
- // Methods without body
1965
+ /**
1966
+ * HTTP GET request implementation
1967
+ * Used for retrieving data from an API endpoint.
1968
+ *
1969
+ * @example
1970
+ * const request = new GetRequest('/api/users')
1971
+ * .withQueryParams({ id: '123' });
1972
+ * const data = await request.getData();
1973
+ */
1400
1974
  class GetRequest extends BaseRequest {
1401
- constructor() {
1402
- super(...arguments);
1975
+ constructor(url) {
1976
+ super(url);
1403
1977
  this.method = HttpMethod.GET;
1404
1978
  }
1405
1979
  }
1980
+ /**
1981
+ * HTTP HEAD request implementation
1982
+ * Similar to GET but returns only headers without a response body.
1983
+ *
1984
+ * @example
1985
+ * const request = new HeadRequest('/api/users/123');
1986
+ * const response = await request.getResponse();
1987
+ */
1406
1988
  class HeadRequest extends BaseRequest {
1407
- constructor() {
1408
- super(...arguments);
1989
+ constructor(url) {
1990
+ super(url);
1409
1991
  this.method = HttpMethod.HEAD;
1410
1992
  }
1411
1993
  }
1994
+ /**
1995
+ * HTTP OPTIONS request implementation
1996
+ * Used to describe the communication options for the target resource.
1997
+ *
1998
+ * @example
1999
+ * const request = new OptionsRequest('/api/users');
2000
+ * const response = await request.getResponse();
2001
+ */
1412
2002
  class OptionsRequest extends BaseRequest {
1413
- constructor() {
1414
- super(...arguments);
2003
+ constructor(url) {
2004
+ super(url);
1415
2005
  this.method = HttpMethod.OPTIONS;
1416
2006
  }
1417
2007
  }
2008
+ /**
2009
+ * HTTP DELETE request implementation
2010
+ * Used to delete a resource on the server.
2011
+ *
2012
+ * @example
2013
+ * const request = new DeleteRequest('/api/users/123');
2014
+ * await request.getData();
2015
+ */
1418
2016
  class DeleteRequest extends BaseRequest {
1419
- constructor() {
1420
- super(...arguments);
2017
+ constructor(url) {
2018
+ super(url);
1421
2019
  this.method = HttpMethod.DELETE;
1422
2020
  }
1423
2021
  }
1424
- // Methods with body
2022
+ /**
2023
+ * HTTP POST request implementation
2024
+ * Used to create a new resource or submit data for processing.
2025
+ *
2026
+ * @example
2027
+ * const request = new PostRequest('/api/users')
2028
+ * .withJson({ name: 'John', email: 'john@example.com' });
2029
+ * const data = await request.getData();
2030
+ */
1425
2031
  class PostRequest extends BodyRequest {
1426
- constructor() {
1427
- super(...arguments);
2032
+ constructor(url) {
2033
+ super(url);
1428
2034
  this.method = HttpMethod.POST;
1429
2035
  }
1430
2036
  }
2037
+ /**
2038
+ * HTTP PUT request implementation
2039
+ * Used to replace or update an existing resource.
2040
+ *
2041
+ * @example
2042
+ * const request = new PutRequest('/api/users/123')
2043
+ * .withJson({ id: '123', name: 'John', email: 'john@example.com' });
2044
+ * const data = await request.getData();
2045
+ */
1431
2046
  class PutRequest extends BodyRequest {
1432
- constructor() {
1433
- super(...arguments);
2047
+ constructor(url) {
2048
+ super(url);
1434
2049
  this.method = HttpMethod.PUT;
1435
2050
  }
1436
2051
  }
2052
+ /**
2053
+ * HTTP PATCH request implementation
2054
+ * Used to apply partial modifications to a resource.
2055
+ *
2056
+ * @example
2057
+ * const request = new PatchRequest('/api/users/123')
2058
+ * .withJson({ email: 'new.email@example.com' });
2059
+ * const data = await request.getData();
2060
+ */
1437
2061
  class PatchRequest extends BodyRequest {
1438
- constructor() {
1439
- super(...arguments);
2062
+ constructor(url) {
2063
+ super(url);
1440
2064
  this.method = HttpMethod.PATCH;
1441
2065
  }
1442
2066
  }
1443
2067
 
1444
2068
  /**
1445
2069
  * Create a GET request
2070
+ * Used for retrieving resources from the server.
2071
+ *
2072
+ * @param url - The URL to send the request to
2073
+ * @returns A new GET request instance
2074
+ *
2075
+ * @example
2076
+ * const request = get('/api/users')
2077
+ * .withQueryParams({ limit: 10 });
2078
+ * const users = await request.getData();
1446
2079
  */
1447
- function get() {
1448
- return new GetRequest();
2080
+ function get(url) {
2081
+ return new GetRequest(url);
1449
2082
  }
1450
2083
  /**
1451
2084
  * Create a POST request
2085
+ * Used for creating new resources or submitting data.
2086
+ *
2087
+ * @param url - The URL to send the request to
2088
+ * @returns A new POST request instance
2089
+ *
2090
+ * @example
2091
+ * const request = post('/api/users')
2092
+ * .withJson({ name: 'John', email: 'john@example.com' });
2093
+ * const newUser = await request.getData();
1452
2094
  */
1453
- function post() {
1454
- return new PostRequest();
2095
+ function post(url) {
2096
+ return new PostRequest(url);
1455
2097
  }
1456
2098
  /**
1457
2099
  * Create a PUT request
2100
+ * Used for replacing or updating an existing resource.
2101
+ *
2102
+ * @param url - The URL to send the request to
2103
+ * @returns A new PUT request instance
2104
+ *
2105
+ * @example
2106
+ * const request = put('/api/users/123')
2107
+ * .withJson({ name: 'John Updated', email: 'john@example.com' });
2108
+ * const updatedUser = await request.getData();
1458
2109
  */
1459
- function put() {
1460
- return new PutRequest();
2110
+ function put(url) {
2111
+ return new PutRequest(url);
1461
2112
  }
1462
2113
  /**
1463
2114
  * Create a DELETE request
1464
- * Note: We use 'del' to avoid JS keyword conflict
2115
+ * Used for removing resources from the server.
2116
+ *
2117
+ * @param url - The URL to send the request to
2118
+ * @returns A new DELETE request instance
2119
+ *
2120
+ * @example
2121
+ * const request = del('/api/users/123');
2122
+ * await request.getData();
1465
2123
  */
1466
- function del() {
1467
- return new DeleteRequest();
2124
+ function del(url) {
2125
+ return new DeleteRequest(url);
1468
2126
  }
1469
2127
  /**
1470
2128
  * Create a PATCH request
2129
+ * Used for applying partial modifications to a resource.
2130
+ *
2131
+ * @param url - The URL to send the request to
2132
+ * @returns A new PATCH request instance
2133
+ *
2134
+ * @example
2135
+ * const request = patch('/api/users/123')
2136
+ * .withJson({ status: 'active' });
2137
+ * const patchedUser = await request.getData();
1471
2138
  */
1472
- function patch() {
1473
- return new PatchRequest();
2139
+ function patch(url) {
2140
+ return new PatchRequest(url);
1474
2141
  }
1475
2142
  /**
1476
2143
  * Create a HEAD request
2144
+ * Similar to GET but returns only headers without a body.
2145
+ *
2146
+ * @param url - The URL to send the request to
2147
+ * @returns A new HEAD request instance
2148
+ *
2149
+ * @example
2150
+ * const request = head('/api/users/123');
2151
+ * const response = await request.getResponse();
2152
+ * console.log(response.headers.get('Last-Modified'));
1477
2153
  */
1478
- function head() {
1479
- return new HeadRequest();
2154
+ function head(url) {
2155
+ return new HeadRequest(url);
1480
2156
  }
1481
2157
  /**
1482
2158
  * Create an OPTIONS request
2159
+ * Used to describe the communication options for a resource.
2160
+ *
2161
+ * @param url - The URL to send the request to
2162
+ * @returns A new OPTIONS request instance
2163
+ *
2164
+ * @example
2165
+ * const request = options('/api/users');
2166
+ * const response = await request.getResponse();
2167
+ * console.log(response.headers.get('Allow'));
1483
2168
  */
1484
- function options() {
1485
- return new OptionsRequest();
2169
+ function options(url) {
2170
+ return new OptionsRequest(url);
1486
2171
  }
1487
2172
 
1488
- // Create the main API object with configuration
2173
+ /**
2174
+ * Main API object for creating HTTP requests
2175
+ * Provides factory methods for all HTTP methods and access to global configuration.
2176
+ */
1489
2177
  const create = {
1490
2178
  get,
1491
2179
  post,
@@ -1494,8 +2182,8 @@ const create = {
1494
2182
  patch,
1495
2183
  head,
1496
2184
  options,
1497
- // Add configuration access
1498
2185
  config: Config.getInstance(),
1499
2186
  };
1500
2187
 
1501
- export { CacheManager, CacheMode, CookieUtils, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, ReferrerPolicy, RequestError, RequestMode, RequestPriority, ResponseWrapper, SameSitePolicy, createLocalStorageStorage, createMemoryStorage, createSessionStorageStorage, create as default };
2188
+ export { CookieUtils, CredentialsPolicy, DeleteRequest, GetRequest, HeadRequest, HttpMethod, OptionsRequest, PatchRequest, PostRequest, PutRequest, RedirectMode, ReferrerPolicy, RequestError, RequestMode, RequestPriority, ResponseWrapper, SameSitePolicy, create as default };
2189
+ //# sourceMappingURL=index.esm.js.map