UNPKG

@fgrzl/fetch

Version:

A modern, type-safe HTTP client with middleware support for CSRF protection and authentication

1,219 lines (1,199 loc) 36.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { FetchClient: () => FetchClient, FetchError: () => FetchError, HttpError: () => HttpError, NetworkError: () => NetworkError, appendQueryParams: () => appendQueryParams, buildQueryParams: () => buildQueryParams, createAuthenticationMiddleware: () => createAuthenticationMiddleware, createAuthorizationMiddleware: () => createAuthorizationMiddleware, createCacheMiddleware: () => createCacheMiddleware, createLoggingMiddleware: () => createLoggingMiddleware, createRateLimitMiddleware: () => createRateLimitMiddleware, createRetryMiddleware: () => createRetryMiddleware, default: () => index_default, useAuthentication: () => useAuthentication, useAuthorization: () => useAuthorization, useBasicStack: () => useBasicStack, useCSRF: () => useCSRF, useCache: () => useCache, useDevelopmentStack: () => useDevelopmentStack, useLogging: () => useLogging, useProductionStack: () => useProductionStack, useRateLimit: () => useRateLimit, useRetry: () => useRetry }); module.exports = __toCommonJS(index_exports); // src/client/fetch-client.ts var FetchClient = class { constructor(config = {}) { this.middlewares = []; this.credentials = config.credentials ?? "same-origin"; this.baseUrl = config.baseUrl; } use(middleware) { this.middlewares.push(middleware); return this; } /** * Set or update the base URL for this client instance. * * When a base URL is set, relative URLs will be resolved against it. * Absolute URLs will continue to work unchanged. * * @param baseUrl - The base URL to set, or undefined to clear it * @returns The client instance for method chaining * * @example Set base URL: * ```typescript * const client = new FetchClient(); * client.setBaseUrl('https://api.example.com'); * * // Now relative URLs work * await client.get('/users'); // → GET https://api.example.com/users * ``` * * @example Chain with middleware: * ```typescript * const client = useProductionStack(new FetchClient()) * .setBaseUrl(process.env.API_BASE_URL); * ``` */ setBaseUrl(baseUrl) { this.baseUrl = baseUrl; return this; } async request(url, init = {}) { const resolvedUrl = this.resolveUrl(url); let index = 0; const execute = async (request) => { const currentRequest = request || { ...init, url: resolvedUrl }; const currentUrl = currentRequest.url || resolvedUrl; if (index >= this.middlewares.length) { const { url: _, ...requestInit } = currentRequest; return this.coreFetch(requestInit, currentUrl); } const middleware = this.middlewares[index++]; if (!middleware) { const { url: _, ...requestInit } = currentRequest; return this.coreFetch(requestInit, currentUrl); } return middleware(currentRequest, execute); }; const result = await execute(); return result; } async coreFetch(request, url) { try { const finalInit = { credentials: this.credentials, ...request }; if (finalInit.headers instanceof Headers) { const headersObj = {}; finalInit.headers.forEach((value, key) => { headersObj[key] = value; }); finalInit.headers = headersObj; } const response = await fetch(url, finalInit); const data = await this.parseResponse(response); return { data: response.ok ? data : null, status: response.status, statusText: response.statusText, headers: response.headers, url: response.url, ok: response.ok, ...response.ok ? {} : { error: { message: response.statusText, body: data } } }; } catch (error) { if (error instanceof TypeError && error.message.includes("fetch")) { return { data: null, status: 0, statusText: "Network Error", headers: new Headers(), url, ok: false, error: { message: "Failed to fetch", body: error } }; } throw error; } } async parseResponse(res) { const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { return res.json(); } if (contentType.includes("text/")) { return res.text(); } if (contentType.includes("application/octet-stream") || contentType.includes("image/") || contentType.includes("video/") || contentType.includes("audio/")) { return res.blob(); } if (res.body) { const text = await res.text(); return text || null; } return null; } // Helper method to build URL with query parameters buildUrlWithParams(url, params) { if (!params) { return url; } const resolvedUrl = this.resolveUrl(url); if (!resolvedUrl.startsWith("http://") && !resolvedUrl.startsWith("https://") && !resolvedUrl.startsWith("//")) { const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== void 0 && value !== null) { searchParams.set(key, String(value)); } }); const queryString = searchParams.toString(); return queryString ? `${resolvedUrl}?${queryString}` : resolvedUrl; } const urlObj = new URL(resolvedUrl); Object.entries(params).forEach(([key, value]) => { if (value !== void 0 && value !== null) { urlObj.searchParams.set(key, String(value)); } }); return urlObj.toString(); } /** * Resolves a URL with the base URL if it's relative and base URL is configured * @param url - The URL to resolve * @returns The resolved URL * @private */ resolveUrl(url) { if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//")) { return url; } if (!this.baseUrl) { return url; } try { const baseUrl = new URL(this.baseUrl); const resolvedUrl = new URL(url, baseUrl); return resolvedUrl.toString(); } catch { throw new Error( `Invalid URL: Unable to resolve "${url}" with baseUrl "${this.baseUrl}"` ); } } // 🎯 PIT OF SUCCESS: Convenience methods with smart defaults /** * HEAD request with query parameter support. * * HEAD requests are used to retrieve metadata about a resource without downloading * the response body. Useful for checking if a resource exists, getting content length, * last modified date, etc. * * @template T - Expected response data type (will be null for HEAD requests) * @param url - Request URL * @param params - Query parameters to append to URL * @returns Promise resolving to typed response (data will always be null) * * @example Check if resource exists: * ```typescript * const headResponse = await client.head('/api/large-file.zip'); * if (headResponse.ok) { * const contentLength = headResponse.headers.get('content-length'); * const lastModified = headResponse.headers.get('last-modified'); * console.log(`File size: ${contentLength} bytes`); * } * ``` * * @example Check with query parameters: * ```typescript * const exists = await client.head('/api/users', { id: 123 }); * if (exists.status === 404) { * console.log('User not found'); * } * ``` */ head(url, params) { const finalUrl = this.buildUrlWithParams(url, params); return this.request(finalUrl, { method: "HEAD" }); } /** * HEAD request that returns useful metadata about a resource. * * This is a convenience method that extracts common metadata from HEAD responses * for easier consumption. * * @param url - Request URL * @param params - Query parameters to append to URL * @returns Promise resolving to response with extracted metadata * * @example Get resource metadata: * ```typescript * const metadata = await client.headMetadata('/api/large-file.zip'); * if (metadata.ok) { * console.log('File exists:', metadata.exists); * console.log('Content type:', metadata.contentType); * console.log('Size:', metadata.contentLength, 'bytes'); * console.log('Last modified:', metadata.lastModified); * } * ``` */ async headMetadata(url, params) { const response = await this.head(url, params); const contentLengthHeader = response.headers.get("content-length"); const lastModifiedHeader = response.headers.get("last-modified"); return { ...response, exists: response.ok, contentType: response.headers.get("content-type") || void 0, contentLength: contentLengthHeader ? parseInt(contentLengthHeader, 10) : void 0, lastModified: lastModifiedHeader ? new Date(lastModifiedHeader) : void 0, etag: response.headers.get("etag") || void 0, cacheControl: response.headers.get("cache-control") || void 0 }; } /** * GET request with query parameter support. * * @template T - Expected response data type * @param url - Request URL * @param params - Query parameters to append to URL * @returns Promise resolving to typed response * * @example * ```typescript * const users = await client.get<User[]>('/api/users'); * const filteredUsers = await client.get<User[]>('/api/users', { status: 'active', limit: 10 }); * if (users.ok) console.log(users.data); * ``` */ get(url, params) { const finalUrl = this.buildUrlWithParams(url, params); return this.request(finalUrl, { method: "GET" }); } /** * POST request with automatic JSON serialization. * * @template T - Expected response data type * @param url - Request URL * @param body - Request body (auto-serialized to JSON) * @param headers - Additional headers (Content-Type: application/json is default) * @returns Promise resolving to typed response * * @example * ```typescript * const result = await client.post<User>('/api/users', { name: 'John' }); * ``` */ post(url, body, headers) { const requestHeaders = { "Content-Type": "application/json", ...headers ?? {} }; return this.request(url, { method: "POST", headers: requestHeaders, ...body !== void 0 ? { body: JSON.stringify(body) } : {} }); } /** * PUT request with automatic JSON serialization. * * @template T - Expected response data type * @param url - Request URL * @param body - Request body (auto-serialized to JSON) * @param headers - Additional headers (Content-Type: application/json is default) * @returns Promise resolving to typed response */ put(url, body, headers) { const requestHeaders = { "Content-Type": "application/json", ...headers ?? {} }; return this.request(url, { method: "PUT", headers: requestHeaders, ...body !== void 0 ? { body: JSON.stringify(body) } : {} }); } /** * PATCH request with automatic JSON serialization. * * @template T - Expected response data type * @param url - Request URL * @param body - Request body (auto-serialized to JSON) * @param headers - Additional headers (Content-Type: application/json is default) * @returns Promise resolving to typed response */ patch(url, body, headers) { const requestHeaders = { "Content-Type": "application/json", ...headers ?? {} }; return this.request(url, { method: "PATCH", headers: requestHeaders, ...body !== void 0 ? { body: JSON.stringify(body) } : {} }); } /** * DELETE request with query parameter support. * * @template T - Expected response data type * @param url - Request URL * @param params - Query parameters to append to URL * @returns Promise resolving to typed response * * @example * ```typescript * const result = await client.del('/api/users/123'); * const bulkResult = await client.del('/api/users', { status: 'inactive', force: true }); * if (result.ok) console.log('Deleted successfully'); * ``` */ del(url, params) { const finalUrl = this.buildUrlWithParams(url, params); return this.request(finalUrl, { method: "DELETE" }); } }; // src/middleware/authentication/authentication.ts function shouldSkipAuth(url, skipPatterns = []) { return skipPatterns.some((pattern) => { if (typeof pattern === "string") { return url.includes(pattern); } return pattern.test(url); }); } function shouldIncludeAuth(url, includePatterns) { if (!includePatterns || includePatterns.length === 0) { return true; } return includePatterns.some((pattern) => { if (typeof pattern === "string") { return url.includes(pattern); } return pattern.test(url); }); } function createAuthenticationMiddleware(options) { const { tokenProvider, headerName = "Authorization", tokenType = "Bearer", skipPatterns = [], includePatterns } = options; return async (request, next) => { const url = request.url || ""; const parsedUrl = new URL(url); const pathname = parsedUrl.pathname; if (shouldSkipAuth(pathname, skipPatterns) || !shouldIncludeAuth(pathname, includePatterns)) { return next(request); } try { const token = await tokenProvider(); if (!token) { return next(request); } const headers = new Headers(request.headers); headers.set(headerName, `${tokenType} ${token}`); const modifiedRequest = { ...request, headers }; return next(modifiedRequest); } catch { return next(request); } }; } // src/middleware/authentication/index.ts function useAuthentication(client, options) { return client.use(createAuthenticationMiddleware(options)); } // src/middleware/authorization/authorization.ts function createRedirectHandler(config = {}) { const { redirectPath = "/login", returnUrlParam = "return_url", includeReturnUrl = true } = config; return () => { let redirectUrl = redirectPath; if (includeReturnUrl && typeof window !== "undefined") { const currentUrl = encodeURIComponent(window.location.href); const separator = redirectPath.includes("?") ? "&" : "?"; redirectUrl = `${redirectPath}${separator}${returnUrlParam}=${currentUrl}`; } if (typeof window !== "undefined") { window.location.href = redirectUrl; } }; } function selectUnauthorizedHandler(providedHandler, redirectConfig) { if (providedHandler) { return providedHandler; } return createRedirectHandler(redirectConfig); } function shouldSkipAuth2(url, skipPatterns = []) { let pathname; try { pathname = new URL(url).pathname; } catch { pathname = url; } return skipPatterns.some((pattern) => { if (typeof pattern === "string") { return pathname.includes(pattern); } return pattern.test(pathname); }); } function createAuthorizationMiddleware(options = {}) { const { onUnauthorized: providedOnUnauthorized, redirectConfig, onForbidden, skipPatterns = [], statusCodes = [401] } = options; const onUnauthorized = selectUnauthorizedHandler( providedOnUnauthorized, redirectConfig ); return async (request, next) => { const url = request.url || ""; if (shouldSkipAuth2(url, skipPatterns)) { return next(request); } const response = await next(request); if (statusCodes.includes(response.status)) { try { if (response.status === 401 && onUnauthorized) { await onUnauthorized(response, request); } else if (response.status === 403 && onForbidden) { await onForbidden(response, request); } else if (onUnauthorized) { await onUnauthorized(response, request); } } catch (error) { console.warn("Authorization handler failed:", error); } } return response; }; } // src/middleware/authorization/index.ts function useAuthorization(client, options = {}) { return client.use(createAuthorizationMiddleware(options)); } // src/middleware/cache/cache.ts var MemoryStorage = class { constructor() { this.cache = /* @__PURE__ */ new Map(); } async get(key) { const entry = this.cache.get(key); if (!entry) { return null; } if (Date.now() > entry.expiresAt) { this.cache.delete(key); return null; } return entry; } async getWithExpiry(key) { const entry = this.cache.get(key); if (!entry) { return { entry: null, isExpired: false }; } const isExpired = Date.now() > entry.expiresAt; return { entry, isExpired }; } async set(key, entry) { this.cache.set(key, entry); } async delete(key) { this.cache.delete(key); } async clear() { this.cache.clear(); } }; var defaultKeyGenerator = (request) => { const url = request.url || ""; const method = request.method || "GET"; const headers = request.headers ? JSON.stringify(request.headers) : ""; return `${method}:${url}:${headers}`; }; function shouldSkipCache(url, skipPatterns = []) { return skipPatterns.some((pattern) => { if (typeof pattern === "string") { return url.includes(pattern); } return pattern.test(url); }); } function createCacheMiddleware(options = {}) { const { ttl = 5 * 60 * 1e3, // 5 minutes methods = ["GET"], storage = new MemoryStorage(), keyGenerator = defaultKeyGenerator, skipPatterns = [], staleWhileRevalidate = false } = options; return async (request, next) => { const method = (request.method || "GET").toUpperCase(); const url = request.url || ""; if (!methods.includes(method) || shouldSkipCache(url, skipPatterns)) { return next(request); } const cacheKey = keyGenerator(request); try { const { entry: cached, isExpired } = storage.getWithExpiry ? await storage.getWithExpiry(cacheKey) : await (async () => { const entry = await storage.get(cacheKey); return { entry, isExpired: false }; })(); if (cached && !isExpired) { return { ...cached.response, headers: new Headers(cached.response.headers), data: cached.response.data }; } if (cached && staleWhileRevalidate) { const cachedResponse = { ...cached.response, headers: new Headers(cached.response.headers), data: cached.response.data }; if (isExpired) { next(request).then(async (freshResponse) => { const headersObj = {}; freshResponse.headers.forEach((value, key) => { headersObj[key] = value; }); const cacheEntry = { response: { status: freshResponse.status, statusText: freshResponse.statusText, headers: headersObj, data: freshResponse.data }, timestamp: Date.now(), expiresAt: Date.now() + ttl }; await storage.set(cacheKey, cacheEntry); }).catch(() => { }); } return cachedResponse; } const response = await next(request); if (response.ok) { try { const headersObj = {}; response.headers.forEach((value, key) => { headersObj[key] = value; }); const cacheEntry = { response: { status: response.status, statusText: response.statusText, headers: headersObj, data: response.data }, timestamp: Date.now(), expiresAt: Date.now() + ttl }; await storage.set(cacheKey, cacheEntry); } catch { } } return response; } catch (error) { if (error && typeof error === "object" && "message" in error) { const errorMessage = error.message; if (errorMessage.includes("Network") || errorMessage.includes("fetch")) { throw error; } } return next(request); } }; } // src/middleware/cache/index.ts function useCache(client, options = {}) { return client.use(createCacheMiddleware(options)); } // src/middleware/csrf/csrf.ts function getTokenFromCookie(cookieName = "XSRF-TOKEN") { if (typeof document === "undefined") { return ""; } const name = `${cookieName}=`; const decodedCookie = decodeURIComponent(document.cookie); const cookies = decodedCookie.split(";"); for (const cookie of cookies) { const c = cookie.trim(); if (c.indexOf(name) === 0) { return c.substring(name.length); } } return ""; } function shouldSkipCSRF(url, skipPatterns = []) { return skipPatterns.some((pattern) => { if (typeof pattern === "string") { return url.includes(pattern); } return pattern.test(url); }); } function createCSRFMiddleware(options = {}) { const { headerName = "X-XSRF-TOKEN", cookieName = "XSRF-TOKEN", protectedMethods = ["POST", "PUT", "PATCH", "DELETE"], skipPatterns = [], tokenProvider = () => getTokenFromCookie(cookieName) } = options; return async (request, next) => { const method = (request.method || "GET").toUpperCase(); const url = request.url || ""; if (!protectedMethods.includes(method) || shouldSkipCSRF(url, skipPatterns)) { return next(request); } const token = tokenProvider(); if (!token) { return next(request); } const headers = new Headers(request.headers); headers.set(headerName, token); const modifiedRequest = { ...request, headers }; return next(modifiedRequest); }; } // src/middleware/csrf/index.ts function useCSRF(client, options = {}) { return client.use(createCSRFMiddleware(options)); } // src/middleware/logging/logging.ts var defaultLogger = { // eslint-disable-next-line no-console -- allow console.debug in logger implementation debug: (message, data) => console.debug(message, data), // eslint-disable-next-line no-console -- allow console.info in logger implementation info: (message, data) => console.info(message, data), // eslint-disable-next-line no-console -- allow console.warn in logger implementation warn: (message, data) => console.warn(message, data), // eslint-disable-next-line no-console -- allow console.error in logger implementation error: (message, data) => console.error(message, data) }; var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 }; var defaultFormatter = (entry) => { const { method, url, status, duration } = entry; let message = `${method} ${url}`; if (status) { message += ` \u2192 ${status}`; } if (duration) { message += ` (${duration}ms)`; } return message; }; function shouldSkipLogging(url, skipPatterns = []) { return skipPatterns.some((pattern) => { if (typeof pattern === "string") { return url.includes(pattern); } return pattern.test(url); }); } function createLoggingMiddleware(options = {}) { const { level = "info", logger = defaultLogger, includeRequestHeaders = false, includeResponseHeaders = false, includeRequestBody = false, includeResponseBody = false, skipPatterns = [], formatter = defaultFormatter } = options; const minLevel = LOG_LEVELS[level]; return async (request, next) => { const url = request.url || ""; const method = (request.method || "GET").toUpperCase(); if (shouldSkipLogging(url, skipPatterns)) { return next(request); } const startTime = Date.now(); if (LOG_LEVELS.debug >= minLevel) { const requestHeaders = includeRequestHeaders ? getHeadersObject( request.headers ) : void 0; const requestBody = includeRequestBody ? request.body : void 0; const requestEntry = { level: "debug", timestamp: startTime, method, url, ...requestHeaders && { requestHeaders }, ...requestBody && { requestBody } }; logger.debug(`\u2192 ${formatter(requestEntry)}`, requestEntry); } try { const response = await next(request); const duration = Date.now() - startTime; const logLevel = response.status >= 400 ? "error" : "info"; if (LOG_LEVELS[logLevel] >= minLevel) { const responseHeaders = includeResponseHeaders ? getHeadersObject(response.headers) : void 0; const responseBody = includeResponseBody ? response.data : void 0; const responseEntry = { level: logLevel, timestamp: Date.now(), method, url, status: response.status, duration, ...responseHeaders ? { responseHeaders } : {}, ...responseBody !== void 0 ? { responseBody } : {} }; const logMessage = `\u2190 ${formatter(responseEntry)}`; if (logLevel === "error") { logger.error(logMessage, responseEntry); } else { logger.info(logMessage, responseEntry); } } return response; } catch (error) { const duration = Date.now() - startTime; if (LOG_LEVELS.error >= minLevel) { const errorEntry = { level: "error", timestamp: Date.now(), method, url, duration, error: error instanceof Error ? error : new Error(String(error)) }; logger.error(`\u2717 ${formatter(errorEntry)}`, errorEntry); } throw error; } }; } function getHeadersObject(headers) { if (!headers) { return void 0; } const obj = {}; if (headers instanceof Headers) { headers.forEach((value, key) => { obj[key] = value; }); return obj; } else { return headers; } } // src/middleware/logging/index.ts function useLogging(client, options = {}) { return client.use(createLoggingMiddleware(options)); } // src/middleware/rate-limit/rate-limit.ts var TokenBucket = class { constructor(maxTokens, refillRate, timeProvider = () => Date.now()) { this.maxTokens = maxTokens; this.refillRate = refillRate; this.timeProvider = timeProvider; this.tokens = maxTokens; this.lastRefill = this.timeProvider(); } tryConsume() { this.refill(); if (this.tokens >= 1) { this.tokens--; return { allowed: true }; } const retryAfter = (1 - this.tokens) / this.refillRate; return { allowed: false, retryAfter: Math.ceil(retryAfter) }; } refill() { const now = this.timeProvider(); const timePassed = now - this.lastRefill; const tokensToAdd = timePassed * this.refillRate; this.tokens = Math.min(this.maxTokens, this.tokens + tokensToAdd); this.lastRefill = now; } }; function createRateLimitMiddleware(options = {}) { const { maxRequests = 60, windowMs = 6e4, keyGenerator = () => "default", skipPatterns = [], onRateLimitExceeded } = options; const buckets = /* @__PURE__ */ new Map(); const refillRate = maxRequests / windowMs; return async (request, next) => { const url = request.url || ""; if (skipPatterns.some( (pattern) => typeof pattern === "string" ? url.includes(pattern) : pattern.test(url) )) { return next(request); } const key = keyGenerator(request); if (!buckets.has(key)) { buckets.set(key, new TokenBucket(maxRequests, refillRate)); } const bucket = buckets.get(key); const result = bucket.tryConsume(); if (!result.allowed) { if (onRateLimitExceeded) { const customResponse = await onRateLimitExceeded( result.retryAfter || 0, request ); if (customResponse) { return customResponse; } } return { data: null, status: 429, statusText: "Too Many Requests", headers: new Headers({ "Retry-After": Math.ceil((result.retryAfter || 0) / 1e3).toString() }), url: request.url || "", ok: false, error: { message: `Rate limit exceeded. Retry after ${result.retryAfter}ms`, body: { retryAfter: result.retryAfter } } }; } return next(request); }; } // src/middleware/rate-limit/index.ts function useRateLimit(client, options = {}) { return client.use(createRateLimitMiddleware(options)); } // src/middleware/retry/retry.ts var defaultShouldRetry = (response) => { return response.status === 0 || response.status >= 500 && response.status < 600; }; var calculateDelay = (attempt, baseDelay, backoff, maxDelay) => { let delay; switch (backoff) { case "exponential": delay = baseDelay * Math.pow(2, attempt - 1); break; case "linear": delay = baseDelay * attempt; break; case "fixed": default: delay = baseDelay; break; } return Math.min(delay, maxDelay); }; var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function createRetryMiddleware(options = {}) { const { maxRetries = 3, delay = 1e3, backoff = "exponential", maxDelay = 3e4, shouldRetry = defaultShouldRetry, onRetry } = options; return async (request, next) => { let lastResponse; let attempt = 0; while (attempt <= maxRetries) { try { const response = await next(request); if (response.ok) { return response; } if (!shouldRetry( { status: response.status, ok: response.ok }, attempt + 1 )) { return response; } if (attempt >= maxRetries) { return response; } lastResponse = response; attempt++; const retryDelay = calculateDelay(attempt, delay, backoff, maxDelay); if (onRetry) { onRetry(attempt, retryDelay, { status: response.status, statusText: response.statusText }); } await sleep(retryDelay); } catch (error) { const errorResponse = { data: null, status: 0, statusText: "Network Error", headers: new Headers(), url: request.url || "", ok: false, error: { message: error instanceof Error ? error.message : "Unknown error", body: error } }; if (!shouldRetry(errorResponse, attempt + 1)) { return errorResponse; } if (attempt >= maxRetries) { return errorResponse; } lastResponse = errorResponse; attempt++; const retryDelay = calculateDelay(attempt, delay, backoff, maxDelay); if (onRetry) { onRetry(attempt, retryDelay, { status: errorResponse.status, statusText: errorResponse.statusText }); } await sleep(retryDelay); } } return lastResponse; }; } // src/middleware/retry/index.ts function useRetry(client, options = {}) { return client.use(createRetryMiddleware(options)); } // src/middleware/index.ts function useProductionStack(client, config = {}) { let enhanced = client; if (config.auth) { enhanced = useAuthentication(enhanced, config.auth); } if (config.cache !== void 0) { enhanced = useCache(enhanced, config.cache); } if (config.retry !== void 0) { enhanced = useRetry(enhanced, config.retry); } if (config.rateLimit !== void 0) { enhanced = useRateLimit(enhanced, config.rateLimit); } if (config.logging !== void 0) { enhanced = useLogging(enhanced, config.logging); } return enhanced; } function useDevelopmentStack(client, config = {}) { let enhanced = client; if (config.auth) { enhanced = useAuthentication(enhanced, config.auth); } enhanced = useRetry(enhanced, { maxRetries: 1, delay: 100 }); enhanced = useLogging(enhanced, { level: "debug", includeRequestHeaders: true, includeResponseHeaders: true, includeRequestBody: true, includeResponseBody: true }); return enhanced; } function useBasicStack(client, config) { return useRetry(useAuthentication(client, config.auth), { maxRetries: 2 }); } // src/errors/index.ts var FetchError = class extends Error { /** * Creates a new FetchError. * @param message - Error message * @param cause - Optional underlying cause */ constructor(message, cause) { super(message); this.name = "FetchError"; if (cause !== void 0) { this.cause = cause; } } }; var HttpError = class extends FetchError { /** * Creates a new HttpError. * @param status - HTTP status code * @param statusText - HTTP status text * @param body - Response body * @param url - The request URL */ constructor(status, statusText, body, url) { super(`HTTP ${status} ${statusText} at ${url}`); this.name = "HttpError"; this.status = status; this.statusText = statusText; this.body = body; } }; var NetworkError = class extends FetchError { /** * Creates a new NetworkError. * @param message - Error message * @param url - The request URL * @param cause - The underlying network error */ constructor(message, url, cause) { super(`Network error for ${url}: ${message}`, cause); this.name = "NetworkError"; } }; // src/client/query.ts function buildQueryParams(query) { const params = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (value !== void 0) { if (Array.isArray(value)) { value.forEach((item) => { if (item !== void 0) { params.append(key, String(item)); } }); } else { params.set(key, String(value)); } } } return params.toString(); } function appendQueryParams(baseUrl, query) { const queryString = buildQueryParams(query); if (!queryString) { return baseUrl; } const fragmentIndex = baseUrl.indexOf("#"); if (fragmentIndex !== -1) { const urlPart = baseUrl.substring(0, fragmentIndex); const fragmentPart = baseUrl.substring(fragmentIndex); const separator2 = urlPart.includes("?") ? "&" : "?"; return `${urlPart}${separator2}${queryString}${fragmentPart}`; } const separator = baseUrl.includes("?") ? "&" : "?"; return `${baseUrl}${separator}${queryString}`; } // src/index.ts var api = useProductionStack( new FetchClient({ // Smart default: include cookies for session-based auth // Can be overridden by creating a custom FetchClient credentials: "same-origin" }), { // Smart defaults - users can override as needed retry: { maxRetries: 2, delay: 1e3 }, cache: { ttl: 5 * 60 * 1e3, // 5 minutes methods: ["GET"] }, logging: { level: "info" }, rateLimit: { maxRequests: 100, windowMs: 60 * 1e3 // 100 requests per minute } } ); var index_default = api; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FetchClient, FetchError, HttpError, NetworkError, appendQueryParams, buildQueryParams, createAuthenticationMiddleware, createAuthorizationMiddleware, createCacheMiddleware, createLoggingMiddleware, createRateLimitMiddleware, createRetryMiddleware, useAuthentication, useAuthorization, useBasicStack, useCSRF, useCache, useDevelopmentStack, useLogging, useProductionStack, useRateLimit, useRetry }); //# sourceMappingURL=index.js.map