1000fetches 0.2.2 → 0.3.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.
Files changed (55) hide show
  1. package/README.md +122 -89
  2. package/dist/client/index.d.ts +23 -0
  3. package/dist/contract/index.d.ts +6 -0
  4. package/dist/core.d.ts +36 -10
  5. package/dist/{errors.d.ts → errors/errors.d.ts} +42 -7
  6. package/dist/errors/handling.d.ts +2 -0
  7. package/dist/errors/index.d.ts +1 -0
  8. package/dist/index.cjs +1 -1
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.ts +4 -12
  11. package/dist/index.mjs +1 -1
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/request/abort/abort.d.ts +12 -0
  14. package/dist/request/abort/index.d.ts +1 -0
  15. package/dist/request/fetchErrors/fetchErrors.d.ts +4 -0
  16. package/dist/request/fetchErrors/index.d.ts +1 -0
  17. package/dist/request/middleware/index.d.ts +1 -0
  18. package/dist/request/middleware/middleware.d.ts +7 -0
  19. package/dist/request/request.d.ts +16 -0
  20. package/dist/request/retry/index.d.ts +1 -0
  21. package/dist/request/retry/retry.d.ts +20 -0
  22. package/dist/request/serialization/index.d.ts +1 -0
  23. package/dist/request/serialization/serialization.d.ts +9 -0
  24. package/dist/request/url.d.ts +3 -0
  25. package/dist/response.d.ts +12 -0
  26. package/dist/retry-BWvlSmME.cjs +2 -0
  27. package/dist/retry-BWvlSmME.cjs.map +1 -0
  28. package/dist/retry-D07aIs-1.js +2 -0
  29. package/dist/retry-D07aIs-1.js.map +1 -0
  30. package/dist/{schema.d.ts → schema/index.d.ts} +1 -1
  31. package/dist/status.d.ts +2 -0
  32. package/dist/types.d.ts +26 -21
  33. package/dist/utils/index.d.ts +1 -2
  34. package/dist/utils/path.d.ts +6 -8
  35. package/docs/BEST_PRACTICES.md +124 -45
  36. package/package.json +30 -21
  37. package/skills/1000fetches/SKILL.md +35 -0
  38. package/skills/1000fetches/references/client-setup-and-defaults.md +49 -0
  39. package/skills/1000fetches/references/contracts-and-errors.md +50 -0
  40. package/skills/1000fetches/references/path-params-and-urls.md +39 -0
  41. package/skills/1000fetches/references/retries-timeouts-and-middleware.md +32 -0
  42. package/skills/1000fetches/references/serialization-and-body-types.md +22 -0
  43. package/dist/client-C7dvgNnD.js +0 -2
  44. package/dist/client-C7dvgNnD.js.map +0 -1
  45. package/dist/client-DOv6m2TJ.mjs +0 -2
  46. package/dist/client-DOv6m2TJ.mjs.map +0 -1
  47. package/dist/client.d.ts +0 -22
  48. package/dist/http.cjs +0 -2
  49. package/dist/http.cjs.map +0 -1
  50. package/dist/http.d.ts +0 -9
  51. package/dist/http.mjs +0 -2
  52. package/dist/http.mjs.map +0 -1
  53. package/dist/utils/streaming.d.ts +0 -9
  54. package/docs/API.md +0 -754
  55. package/docs/MIGRATION.md +0 -719
@@ -0,0 +1 @@
1
+ export * from './abort';
@@ -0,0 +1,4 @@
1
+ import { HttpError } from '../../errors';
2
+ import { CustomFetch, ResponseType } from '../../types';
3
+ export declare function fetchWithNetworkError(fetchImplementation: CustomFetch, url: string, requestInit: RequestInit): Promise<Response>;
4
+ export declare function createHttpError(response: ResponseType<unknown>, cause?: Error): HttpError;
@@ -0,0 +1 @@
1
+ export * from './fetchErrors';
@@ -0,0 +1 @@
1
+ export * from './middleware';
@@ -0,0 +1,7 @@
1
+ import { HttpMethod, ResponseType } from '../../types';
2
+ export declare function applyResponseMiddleware<TResponse>({ onResponseMiddleware, response, url, method, }: {
3
+ onResponseMiddleware?: (response: ResponseType<unknown>) => ResponseType<unknown> | Promise<ResponseType<unknown>>;
4
+ response: ResponseType<TResponse>;
5
+ url: string;
6
+ method: HttpMethod;
7
+ }): Promise<ResponseType<TResponse>>;
@@ -0,0 +1,16 @@
1
+ import { CustomFetch, ExtendedRequestInit, HttpMethod, RequestOptions, ResponseType, RetryOptions } from '../types';
2
+ interface ExecuteHttpRequestWithRetryOptions {
3
+ fetchImplementation: CustomFetch;
4
+ requestInit: ExtendedRequestInit;
5
+ requestRetry?: RetryOptions | boolean;
6
+ clientRetry?: RetryOptions | boolean;
7
+ url: string;
8
+ method: HttpMethod;
9
+ timeout: number;
10
+ signal?: AbortSignal;
11
+ responseType?: RequestOptions['responseType'];
12
+ onResponseMiddleware?: (response: ResponseType<unknown>) => ResponseType<unknown> | Promise<ResponseType<unknown>>;
13
+ isExpectedStatus: (status: number) => boolean;
14
+ }
15
+ export declare function executeHttpRequestWithRetry<TResponse>({ fetchImplementation, requestInit: baseRequestInit, requestRetry, clientRetry, url, method, timeout, signal, responseType, onResponseMiddleware, isExpectedStatus, }: ExecuteHttpRequestWithRetryOptions): Promise<ResponseType<TResponse>>;
16
+ export {};
@@ -0,0 +1 @@
1
+ export * from './retry';
@@ -0,0 +1,20 @@
1
+ import { HttpMethod, RetryContext, RetryEvent, RetryOptions } from '../../types';
2
+ type RetryPolicy = {
3
+ maxRetries: number;
4
+ retryDelay: number;
5
+ backoffFactor: number;
6
+ retryStatusCodes: number[];
7
+ retryNetworkErrors: boolean;
8
+ maxRetryDelay: number;
9
+ retryMethods: readonly HttpMethod[];
10
+ retryUnsafeMethods: boolean;
11
+ jitter: NonNullable<RetryOptions['jitter']>;
12
+ respectRetryAfter: boolean;
13
+ shouldRetry?: RetryOptions['shouldRetry'];
14
+ onRetry?: RetryOptions['onRetry'];
15
+ };
16
+ export declare function createRetryPolicy(clientRetry?: RetryOptions | boolean, requestRetry?: RetryOptions | boolean): RetryPolicy | undefined;
17
+ export declare function shouldRetry(error: Error, retryCount: number, retryConfig: RetryPolicy | undefined, context: Pick<RetryContext, 'bodyReplayable' | 'method' | 'url'>): Promise<boolean>;
18
+ export declare function calculateRetryDelay(attempt: number, retryConfig: RetryPolicy | undefined, error: Error): number;
19
+ export declare function notifyRetry(retryConfig: RetryPolicy | undefined, event: RetryEvent): Promise<void>;
20
+ export {};
@@ -0,0 +1 @@
1
+ export * from './serialization';
@@ -0,0 +1,9 @@
1
+ import { FetchOptions, RequestParamsType } from '../../types';
2
+ export declare function mergeFetchOptions(fetchOptions: FetchOptions | undefined, aliases: Pick<RequestInit, 'cache' | 'credentials' | 'mode' | 'redirect'>): FetchOptions;
3
+ export declare function sanitizeFetchOptions(fetchOptions: FetchOptions | undefined): FetchOptions;
4
+ export declare function serializeQueryParams(params: RequestParamsType): string;
5
+ export declare function serializeRequestBody(body: unknown): {
6
+ body: BodyInit;
7
+ contentType?: string;
8
+ };
9
+ export declare function isReplayableBody(body: BodyInit | null | undefined): boolean;
@@ -0,0 +1,3 @@
1
+ export declare function constructUrl(baseUrl: string, requestUrl: string): string;
2
+ export declare function appendQueryString(url: string, queryString: string): string;
3
+ export declare function assertRequestUrlSupported(url: string, usesNativeFetch: boolean): void;
@@ -0,0 +1,12 @@
1
+ import { SerializationError } from './errors';
2
+ import { HttpMethod, ResponseType } from './types';
3
+ type ParsedResponse<T> = ResponseType<T> & {
4
+ parseError?: SerializationError;
5
+ };
6
+ export declare function processResponse<T = unknown>(response: Response, options: {
7
+ responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData';
8
+ method: HttpMethod;
9
+ url: string;
10
+ tolerateParseError?: boolean;
11
+ }): Promise<ParsedResponse<T>>;
12
+ export {};
@@ -0,0 +1,2 @@
1
+ var t={maxRetries:3,retryDelay:300,backoffFactor:2,retryStatusCodes:[408,429,500,502,503,504],retryNetworkErrors:!0,maxRetryDelay:3e4,retryMethods:["GET","HEAD","OPTIONS","PUT","DELETE"],retryUnsafeMethods:!1,jitter:"none",respectRetryAfter:!0};function e(t){const e=t;return"HttpError"===t.name&&"number"==typeof e.status&&function(t){const e=t;return null!==e&&"object"==typeof e&&"function"==typeof e.get}(e.response?.headers)}exports.calculateRetryDelay=function(t,r,o){if(!r)return 0;const n=r.respectRetryAfter?function(t){if(!e(t))return;const r=t.response.headers.get("retry-after");if(!r)return;const o=Number(r);if(Number.isFinite(o))return Math.max(0,1e3*o);const n=Date.parse(r);if(Number.isNaN(n))return;return Math.max(0,n-Date.now())}(o):void 0;if(void 0!==n)return Math.min(n,r.maxRetryDelay);const a=r.retryDelay*Math.pow(r.backoffFactor,t);return function(t,e,r){return"function"==typeof e?Math.max(0,e(t,r)):"full"===e?Math.floor(Math.random()*t):"equal"===e?Math.floor(t/2+Math.random()*(t/2)):t}(Math.min(a,r.maxRetryDelay),r.jitter,t)},exports.createRetryPolicy=function(e,r){if(!function(t,e){return!1!==e&&(!0===t||"object"==typeof t||!0===e||"object"==typeof e)}(e,r))return;const o={...t,..."object"==typeof e?e:void 0};return!0===r?o:"object"==typeof r?{...o,...r}:o},exports.notifyRetry=async function(t,e){await(t?.onRetry?.(e))},exports.shouldRetry=async function(t,r,o,n){if(!o)return!1;const a={...n,error:t,retryCount:r};return o.shouldRetry?await o.shouldRetry(t,r,a):!(!n.bodyReplayable||!function(t,e){if(e.retryUnsafeMethods)return!0;const r=t.toUpperCase();return e.retryMethods.some(t=>t.toUpperCase()===r)}(n.method,o))&&(e(t)?o.retryStatusCodes.includes(t.status):!(!function(t){return"NetworkError"===t.name}(t)&&("TypeError"!==t.name||function(t){return t.message.includes("Invalid URL")||t.message.includes("Failed to parse URL")}(t)))&&o.retryNetworkErrors)};
2
+ //# sourceMappingURL=retry-BWvlSmME.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-BWvlSmME.cjs","names":[],"sources":["../src/request/retry/retry.ts"],"sourcesContent":["import type {\n HttpMethod,\n RetryContext,\n RetryEvent,\n RetryOptions,\n} from '../../types'\n\ntype RetryPolicy = {\n maxRetries: number\n retryDelay: number\n backoffFactor: number\n retryStatusCodes: number[]\n retryNetworkErrors: boolean\n maxRetryDelay: number\n retryMethods: readonly HttpMethod[]\n retryUnsafeMethods: boolean\n jitter: NonNullable<RetryOptions['jitter']>\n respectRetryAfter: boolean\n shouldRetry?: RetryOptions['shouldRetry']\n onRetry?: RetryOptions['onRetry']\n}\n\ntype HeaderReader = {\n get(name: string): string | null\n}\n\ntype HttpErrorLike = Error & {\n status: number\n response: {\n headers: HeaderReader\n }\n}\n\ntype HttpErrorCandidate = Error & {\n status?: unknown\n response?: {\n headers?: unknown\n }\n}\n\nconst DEFAULT_RETRY_POLICY: Omit<RetryPolicy, 'shouldRetry' | 'onRetry'> = {\n maxRetries: 3,\n retryDelay: 300,\n backoffFactor: 2,\n retryStatusCodes: [408, 429, 500, 502, 503, 504],\n retryNetworkErrors: true,\n maxRetryDelay: 30_000,\n retryMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],\n retryUnsafeMethods: false,\n jitter: 'none',\n respectRetryAfter: true,\n}\n\nfunction shouldUseRetryPolicy(\n clientRetry?: RetryOptions | boolean,\n requestRetry?: RetryOptions | boolean\n): boolean {\n return (\n requestRetry !== false &&\n (clientRetry === true ||\n typeof clientRetry === 'object' ||\n requestRetry === true ||\n typeof requestRetry === 'object')\n )\n}\n\nexport function createRetryPolicy(\n clientRetry?: RetryOptions | boolean,\n requestRetry?: RetryOptions | boolean\n): RetryPolicy | undefined {\n if (!shouldUseRetryPolicy(clientRetry, requestRetry)) {\n return undefined\n }\n\n const clientRetryOptions =\n typeof clientRetry === 'object' ? clientRetry : undefined\n const baseRetryOptions = {\n ...DEFAULT_RETRY_POLICY,\n ...clientRetryOptions,\n }\n\n if (requestRetry === true) {\n return baseRetryOptions\n }\n\n if (typeof requestRetry === 'object') {\n return {\n ...baseRetryOptions,\n ...requestRetry,\n }\n }\n\n return baseRetryOptions\n}\n\nexport async function shouldRetry(\n error: Error,\n retryCount: number,\n retryConfig: RetryPolicy | undefined,\n context: Pick<RetryContext, 'bodyReplayable' | 'method' | 'url'>\n): Promise<boolean> {\n if (!retryConfig) return false\n\n const retryContext: RetryContext = {\n ...context,\n error,\n retryCount,\n }\n\n if (retryConfig.shouldRetry) {\n return await retryConfig.shouldRetry(error, retryCount, retryContext)\n }\n\n if (\n !context.bodyReplayable ||\n !isRetryMethodAllowed(context.method, retryConfig)\n ) {\n return false\n }\n\n if (isHttpError(error)) {\n return retryConfig.retryStatusCodes.includes(error.status)\n }\n\n if (\n isNetworkError(error) ||\n (error.name === 'TypeError' && !isInvalidUrlError(error))\n ) {\n return retryConfig.retryNetworkErrors\n }\n\n return false\n}\n\nexport function calculateRetryDelay(\n attempt: number,\n retryConfig: RetryPolicy | undefined,\n error: Error\n): number {\n if (!retryConfig) return 0\n\n const retryAfterDelay = retryConfig.respectRetryAfter\n ? getRetryAfterDelay(error)\n : undefined\n\n if (retryAfterDelay !== undefined) {\n return Math.min(retryAfterDelay, retryConfig.maxRetryDelay)\n }\n\n const delay =\n retryConfig.retryDelay * Math.pow(retryConfig.backoffFactor, attempt)\n\n return applyJitter(\n Math.min(delay, retryConfig.maxRetryDelay),\n retryConfig.jitter,\n attempt\n )\n}\n\nexport async function notifyRetry(\n retryConfig: RetryPolicy | undefined,\n event: RetryEvent\n): Promise<void> {\n await retryConfig?.onRetry?.(event)\n}\n\nfunction isRetryMethodAllowed(\n method: HttpMethod,\n retryConfig: RetryPolicy\n): boolean {\n if (retryConfig.retryUnsafeMethods) {\n return true\n }\n\n const normalizedMethod = method.toUpperCase()\n return retryConfig.retryMethods.some(\n retryMethod => retryMethod.toUpperCase() === normalizedMethod\n )\n}\n\nfunction getRetryAfterDelay(error: Error): number | undefined {\n if (!isHttpError(error)) {\n return undefined\n }\n\n const retryAfter = error.response.headers.get('retry-after')\n if (!retryAfter) {\n return undefined\n }\n\n const retryAfterSeconds = Number(retryAfter)\n if (Number.isFinite(retryAfterSeconds)) {\n return Math.max(0, retryAfterSeconds * 1000)\n }\n\n const retryAfterDate = Date.parse(retryAfter)\n if (Number.isNaN(retryAfterDate)) {\n return undefined\n }\n\n return Math.max(0, retryAfterDate - Date.now())\n}\n\nfunction applyJitter(\n delay: number,\n jitter: RetryPolicy['jitter'],\n attempt: number\n): number {\n if (typeof jitter === 'function') {\n return Math.max(0, jitter(delay, attempt))\n }\n\n if (jitter === 'full') {\n return Math.floor(Math.random() * delay)\n }\n\n if (jitter === 'equal') {\n return Math.floor(delay / 2 + Math.random() * (delay / 2))\n }\n\n return delay\n}\n\nfunction isInvalidUrlError(error: Error): boolean {\n return (\n error.message.includes('Invalid URL') ||\n error.message.includes('Failed to parse URL')\n )\n}\n\nfunction isHttpError(error: Error): error is HttpErrorLike {\n const candidate = error as HttpErrorCandidate\n\n return (\n error.name === 'HttpError' &&\n typeof candidate.status === 'number' &&\n isHeaderReader(candidate.response?.headers)\n )\n}\n\nfunction isNetworkError(error: Error): boolean {\n return error.name === 'NetworkError'\n}\n\nfunction isHeaderReader(value: unknown): value is HeaderReader {\n const candidate = value as { get?: unknown }\n\n return (\n candidate !== null &&\n typeof candidate === 'object' &&\n typeof candidate.get === 'function'\n )\n}\n"],"mappings":"AAwCA,IAAM,EAAqE,CACzE,WAAY,EACZ,WAAY,IACZ,cAAe,EACf,iBAAkB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,KAC5C,oBAAoB,EACpB,cAAe,IACf,aAAc,CAAC,MAAO,OAAQ,UAAW,MAAO,UAChD,oBAAoB,EACpB,OAAQ,OACR,mBAAmB,GAoLrB,SAAS,EAAY,GACnB,MAAM,EAAY,EAElB,MACiB,cAAf,EAAM,MACsB,iBAArB,EAAU,QASrB,SAAwB,GACtB,MAAM,EAAY,EAElB,OACgB,OAAd,GACqB,iBAAd,GACkB,mBAAlB,EAAU,GAErB,CAhBI,CAAe,EAAU,UAAU,QAEvC,6BAxGA,SACE,EACA,EACA,GAEA,IAAK,EAAa,OAAO,EAEzB,MAAM,EAAkB,EAAY,kBAuCtC,SAA4B,GAC1B,IAAK,EAAY,GACf,OAGF,MAAM,EAAa,EAAM,SAAS,QAAQ,IAAI,eAC9C,IAAK,EACH,OAGF,MAAM,EAAoB,OAAO,GACjC,GAAI,OAAO,SAAS,GAClB,OAAO,KAAK,IAAI,EAAuB,IAApB,GAGrB,MAAM,EAAiB,KAAK,MAAM,GAClC,GAAI,OAAO,MAAM,GACf,OAGF,OAAO,KAAK,IAAI,EAAG,EAAiB,KAAK,MAC3C,CA3DM,CAAmB,QACnB,EAEJ,QAAwB,IAApB,EACF,OAAO,KAAK,IAAI,EAAiB,EAAY,eAG/C,MAAM,EACJ,EAAY,WAAa,KAAK,IAAI,EAAY,cAAe,GAE/D,OAmDF,SACE,EACA,EACA,GAEA,MAAsB,mBAAX,EACF,KAAK,IAAI,EAAG,EAAO,EAAO,IAGpB,SAAX,EACK,KAAK,MAAM,KAAK,SAAW,GAGrB,UAAX,EACK,KAAK,MAAM,EAAQ,EAAI,KAAK,UAAY,EAAQ,IAGlD,CACT,CArES,CACL,KAAK,IAAI,EAAO,EAAY,eAC5B,EAAY,OACZ,EAEJ,4BA3FA,SACE,EACA,GAEA,IAjBF,SACE,EACA,GAEA,OACmB,IAAjB,KACiB,IAAhB,GACwB,iBAAhB,IACU,IAAjB,GACwB,iBAAjB,EAEb,CAMO,CAAqB,EAAa,GACrC,OAGF,MAEM,EAAmB,IACpB,KAFoB,iBAAhB,EAA2B,OAAc,GAMlD,OAAqB,IAAjB,EACK,EAGmB,iBAAjB,EACF,IACF,KACA,GAIA,CACT,sBAkEA,eACE,EACA,SAEM,GAAa,UAAU,GAC/B,sBArEA,eACE,EACA,EACA,EACA,GAEA,IAAK,EAAa,OAAO,EAEzB,MAAM,EAA6B,IAC9B,EACH,QACA,cAGF,OAAI,EAAY,kBACD,EAAY,YAAY,EAAO,EAAY,MAIvD,EAAQ,iBAoDb,SACE,EACA,GAEA,GAAI,EAAY,mBACd,OAAO,EAGT,MAAM,EAAmB,EAAO,cAChC,OAAO,EAAY,aAAa,KAC9B,GAAe,EAAY,gBAAkB,EAEjD,CA/DK,CAAqB,EAAQ,OAAQ,MAKpC,EAAY,GACP,EAAY,iBAAiB,SAAS,EAAM,WAuHvD,SAAwB,GACtB,MAAsB,iBAAf,EAAM,IACf,CArHI,CAAe,KACC,cAAf,EAAM,MAiGX,SAA2B,GACzB,OACE,EAAM,QAAQ,SAAS,gBACvB,EAAM,QAAQ,SAAS,sBAE3B,CAtGoC,CAAkB,MAE3C,EAAY,mBAIvB"}
@@ -0,0 +1,2 @@
1
+ var t={maxRetries:3,retryDelay:300,backoffFactor:2,retryStatusCodes:[408,429,500,502,503,504],retryNetworkErrors:!0,maxRetryDelay:3e4,retryMethods:["GET","HEAD","OPTIONS","PUT","DELETE"],retryUnsafeMethods:!1,jitter:"none",respectRetryAfter:!0};function e(e,r){if(!function(t,e){return!1!==e&&(!0===t||"object"==typeof t||!0===e||"object"==typeof e)}(e,r))return;const n={...t,..."object"==typeof e?e:void 0};return!0===r?n:"object"==typeof r?{...n,...r}:n}async function r(t,e,r,n){if(!r)return!1;const o={...n,error:t,retryCount:e};return r.shouldRetry?await r.shouldRetry(t,e,o):!(!n.bodyReplayable||!function(t,e){if(e.retryUnsafeMethods)return!0;const r=t.toUpperCase();return e.retryMethods.some(t=>t.toUpperCase()===r)}(n.method,r))&&(a(t)?r.retryStatusCodes.includes(t.status):!(!function(t){return"NetworkError"===t.name}(t)&&("TypeError"!==t.name||function(t){return t.message.includes("Invalid URL")||t.message.includes("Failed to parse URL")}(t)))&&r.retryNetworkErrors)}function n(t,e,r){if(!e)return 0;const n=e.respectRetryAfter?function(t){if(!a(t))return;const e=t.response.headers.get("retry-after");if(!e)return;const r=Number(e);if(Number.isFinite(r))return Math.max(0,1e3*r);const n=Date.parse(e);if(Number.isNaN(n))return;return Math.max(0,n-Date.now())}(r):void 0;if(void 0!==n)return Math.min(n,e.maxRetryDelay);const o=e.retryDelay*Math.pow(e.backoffFactor,t);return function(t,e,r){return"function"==typeof e?Math.max(0,e(t,r)):"full"===e?Math.floor(Math.random()*t):"equal"===e?Math.floor(t/2+Math.random()*(t/2)):t}(Math.min(o,e.maxRetryDelay),e.jitter,t)}async function o(t,e){await(t?.onRetry?.(e))}function a(t){const e=t;return"HttpError"===t.name&&"number"==typeof e.status&&function(t){const e=t;return null!==e&&"object"==typeof e&&"function"==typeof e.get}(e.response?.headers)}export{n as calculateRetryDelay,e as createRetryPolicy,o as notifyRetry,r as shouldRetry};
2
+ //# sourceMappingURL=retry-D07aIs-1.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-D07aIs-1.js","names":[],"sources":["../src/request/retry/retry.ts"],"sourcesContent":["import type {\n HttpMethod,\n RetryContext,\n RetryEvent,\n RetryOptions,\n} from '../../types'\n\ntype RetryPolicy = {\n maxRetries: number\n retryDelay: number\n backoffFactor: number\n retryStatusCodes: number[]\n retryNetworkErrors: boolean\n maxRetryDelay: number\n retryMethods: readonly HttpMethod[]\n retryUnsafeMethods: boolean\n jitter: NonNullable<RetryOptions['jitter']>\n respectRetryAfter: boolean\n shouldRetry?: RetryOptions['shouldRetry']\n onRetry?: RetryOptions['onRetry']\n}\n\ntype HeaderReader = {\n get(name: string): string | null\n}\n\ntype HttpErrorLike = Error & {\n status: number\n response: {\n headers: HeaderReader\n }\n}\n\ntype HttpErrorCandidate = Error & {\n status?: unknown\n response?: {\n headers?: unknown\n }\n}\n\nconst DEFAULT_RETRY_POLICY: Omit<RetryPolicy, 'shouldRetry' | 'onRetry'> = {\n maxRetries: 3,\n retryDelay: 300,\n backoffFactor: 2,\n retryStatusCodes: [408, 429, 500, 502, 503, 504],\n retryNetworkErrors: true,\n maxRetryDelay: 30_000,\n retryMethods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'],\n retryUnsafeMethods: false,\n jitter: 'none',\n respectRetryAfter: true,\n}\n\nfunction shouldUseRetryPolicy(\n clientRetry?: RetryOptions | boolean,\n requestRetry?: RetryOptions | boolean\n): boolean {\n return (\n requestRetry !== false &&\n (clientRetry === true ||\n typeof clientRetry === 'object' ||\n requestRetry === true ||\n typeof requestRetry === 'object')\n )\n}\n\nexport function createRetryPolicy(\n clientRetry?: RetryOptions | boolean,\n requestRetry?: RetryOptions | boolean\n): RetryPolicy | undefined {\n if (!shouldUseRetryPolicy(clientRetry, requestRetry)) {\n return undefined\n }\n\n const clientRetryOptions =\n typeof clientRetry === 'object' ? clientRetry : undefined\n const baseRetryOptions = {\n ...DEFAULT_RETRY_POLICY,\n ...clientRetryOptions,\n }\n\n if (requestRetry === true) {\n return baseRetryOptions\n }\n\n if (typeof requestRetry === 'object') {\n return {\n ...baseRetryOptions,\n ...requestRetry,\n }\n }\n\n return baseRetryOptions\n}\n\nexport async function shouldRetry(\n error: Error,\n retryCount: number,\n retryConfig: RetryPolicy | undefined,\n context: Pick<RetryContext, 'bodyReplayable' | 'method' | 'url'>\n): Promise<boolean> {\n if (!retryConfig) return false\n\n const retryContext: RetryContext = {\n ...context,\n error,\n retryCount,\n }\n\n if (retryConfig.shouldRetry) {\n return await retryConfig.shouldRetry(error, retryCount, retryContext)\n }\n\n if (\n !context.bodyReplayable ||\n !isRetryMethodAllowed(context.method, retryConfig)\n ) {\n return false\n }\n\n if (isHttpError(error)) {\n return retryConfig.retryStatusCodes.includes(error.status)\n }\n\n if (\n isNetworkError(error) ||\n (error.name === 'TypeError' && !isInvalidUrlError(error))\n ) {\n return retryConfig.retryNetworkErrors\n }\n\n return false\n}\n\nexport function calculateRetryDelay(\n attempt: number,\n retryConfig: RetryPolicy | undefined,\n error: Error\n): number {\n if (!retryConfig) return 0\n\n const retryAfterDelay = retryConfig.respectRetryAfter\n ? getRetryAfterDelay(error)\n : undefined\n\n if (retryAfterDelay !== undefined) {\n return Math.min(retryAfterDelay, retryConfig.maxRetryDelay)\n }\n\n const delay =\n retryConfig.retryDelay * Math.pow(retryConfig.backoffFactor, attempt)\n\n return applyJitter(\n Math.min(delay, retryConfig.maxRetryDelay),\n retryConfig.jitter,\n attempt\n )\n}\n\nexport async function notifyRetry(\n retryConfig: RetryPolicy | undefined,\n event: RetryEvent\n): Promise<void> {\n await retryConfig?.onRetry?.(event)\n}\n\nfunction isRetryMethodAllowed(\n method: HttpMethod,\n retryConfig: RetryPolicy\n): boolean {\n if (retryConfig.retryUnsafeMethods) {\n return true\n }\n\n const normalizedMethod = method.toUpperCase()\n return retryConfig.retryMethods.some(\n retryMethod => retryMethod.toUpperCase() === normalizedMethod\n )\n}\n\nfunction getRetryAfterDelay(error: Error): number | undefined {\n if (!isHttpError(error)) {\n return undefined\n }\n\n const retryAfter = error.response.headers.get('retry-after')\n if (!retryAfter) {\n return undefined\n }\n\n const retryAfterSeconds = Number(retryAfter)\n if (Number.isFinite(retryAfterSeconds)) {\n return Math.max(0, retryAfterSeconds * 1000)\n }\n\n const retryAfterDate = Date.parse(retryAfter)\n if (Number.isNaN(retryAfterDate)) {\n return undefined\n }\n\n return Math.max(0, retryAfterDate - Date.now())\n}\n\nfunction applyJitter(\n delay: number,\n jitter: RetryPolicy['jitter'],\n attempt: number\n): number {\n if (typeof jitter === 'function') {\n return Math.max(0, jitter(delay, attempt))\n }\n\n if (jitter === 'full') {\n return Math.floor(Math.random() * delay)\n }\n\n if (jitter === 'equal') {\n return Math.floor(delay / 2 + Math.random() * (delay / 2))\n }\n\n return delay\n}\n\nfunction isInvalidUrlError(error: Error): boolean {\n return (\n error.message.includes('Invalid URL') ||\n error.message.includes('Failed to parse URL')\n )\n}\n\nfunction isHttpError(error: Error): error is HttpErrorLike {\n const candidate = error as HttpErrorCandidate\n\n return (\n error.name === 'HttpError' &&\n typeof candidate.status === 'number' &&\n isHeaderReader(candidate.response?.headers)\n )\n}\n\nfunction isNetworkError(error: Error): boolean {\n return error.name === 'NetworkError'\n}\n\nfunction isHeaderReader(value: unknown): value is HeaderReader {\n const candidate = value as { get?: unknown }\n\n return (\n candidate !== null &&\n typeof candidate === 'object' &&\n typeof candidate.get === 'function'\n )\n}\n"],"mappings":"AAwCA,IAAM,EAAqE,CACzE,WAAY,EACZ,WAAY,IACZ,cAAe,EACf,iBAAkB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,KAC5C,oBAAoB,EACpB,cAAe,IACf,aAAc,CAAC,MAAO,OAAQ,UAAW,MAAO,UAChD,oBAAoB,EACpB,OAAQ,OACR,mBAAmB,GAgBrB,SAAgB,EACd,EACA,GAEA,IAjBF,SACE,EACA,GAEA,OACmB,IAAjB,KACiB,IAAhB,GACwB,iBAAhB,IACU,IAAjB,GACwB,iBAAjB,EAEb,CAMO,CAAqB,EAAa,GACrC,OAGF,MAEM,EAAmB,IACpB,KAFoB,iBAAhB,EAA2B,OAAc,GAMlD,OAAqB,IAAjB,EACK,EAGmB,iBAAjB,EACF,IACF,KACA,GAIA,CACT,CAEA,eAAsB,EACpB,EACA,EACA,EACA,GAEA,IAAK,EAAa,OAAO,EAEzB,MAAM,EAA6B,IAC9B,EACH,QACA,cAGF,OAAI,EAAY,kBACD,EAAY,YAAY,EAAO,EAAY,MAIvD,EAAQ,iBAoDb,SACE,EACA,GAEA,GAAI,EAAY,mBACd,OAAO,EAGT,MAAM,EAAmB,EAAO,cAChC,OAAO,EAAY,aAAa,KAC9B,GAAe,EAAY,gBAAkB,EAEjD,CA/DK,CAAqB,EAAQ,OAAQ,MAKpC,EAAY,GACP,EAAY,iBAAiB,SAAS,EAAM,WAuHvD,SAAwB,GACtB,MAAsB,iBAAf,EAAM,IACf,CArHI,CAAe,KACC,cAAf,EAAM,MAiGX,SAA2B,GACzB,OACE,EAAM,QAAQ,SAAS,gBACvB,EAAM,QAAQ,SAAS,sBAE3B,CAtGoC,CAAkB,MAE3C,EAAY,mBAIvB,CAEA,SAAgB,EACd,EACA,EACA,GAEA,IAAK,EAAa,OAAO,EAEzB,MAAM,EAAkB,EAAY,kBAuCtC,SAA4B,GAC1B,IAAK,EAAY,GACf,OAGF,MAAM,EAAa,EAAM,SAAS,QAAQ,IAAI,eAC9C,IAAK,EACH,OAGF,MAAM,EAAoB,OAAO,GACjC,GAAI,OAAO,SAAS,GAClB,OAAO,KAAK,IAAI,EAAuB,IAApB,GAGrB,MAAM,EAAiB,KAAK,MAAM,GAClC,GAAI,OAAO,MAAM,GACf,OAGF,OAAO,KAAK,IAAI,EAAG,EAAiB,KAAK,MAC3C,CA3DM,CAAmB,QACnB,EAEJ,QAAwB,IAApB,EACF,OAAO,KAAK,IAAI,EAAiB,EAAY,eAG/C,MAAM,EACJ,EAAY,WAAa,KAAK,IAAI,EAAY,cAAe,GAE/D,OAmDF,SACE,EACA,EACA,GAEA,MAAsB,mBAAX,EACF,KAAK,IAAI,EAAG,EAAO,EAAO,IAGpB,SAAX,EACK,KAAK,MAAM,KAAK,SAAW,GAGrB,UAAX,EACK,KAAK,MAAM,EAAQ,EAAI,KAAK,UAAY,EAAQ,IAGlD,CACT,CArES,CACL,KAAK,IAAI,EAAO,EAAY,eAC5B,EAAY,OACZ,EAEJ,CAEA,eAAsB,EACpB,EACA,SAEM,GAAa,UAAU,GAC/B,CAkEA,SAAS,EAAY,GACnB,MAAM,EAAY,EAElB,MACiB,cAAf,EAAM,MACsB,iBAArB,EAAU,QASrB,SAAwB,GACtB,MAAM,EAAY,EAElB,OACgB,OAAd,GACqB,iBAAd,GACkB,mBAAlB,EAAU,GAErB,CAhBI,CAAe,EAAU,UAAU,QAEvC"}
@@ -1,4 +1,4 @@
1
- import { Schema } from './types';
1
+ import { Schema } from '../types';
2
2
  /**
3
3
  * Interface for schema validators that can validate data against schemas.
4
4
  *
@@ -0,0 +1,2 @@
1
+ export declare const successStatusValidator: unique symbol;
2
+ export declare function isSuccessfulStatus(status: number): boolean;
package/dist/types.d.ts CHANGED
@@ -68,8 +68,6 @@ export interface RequestOptions<TBody = unknown> {
68
68
  redirect?: RequestRedirect;
69
69
  fetchOptions?: FetchOptions;
70
70
  retry?: RetryOptions | boolean;
71
- onUploadStreaming?: (event: UploadStreamingEvent) => void;
72
- onDownloadStreaming?: (event: DownloadStreamingEvent) => void;
73
71
  }
74
72
  export interface ResponseType<T = unknown> {
75
73
  data: T;
@@ -84,8 +82,30 @@ export type ExtractableResponse<T> = Promise<ResponseType<T>> & {
84
82
  data(): Promise<T>;
85
83
  void(): Promise<void>;
86
84
  };
87
- export type SchemaableResponse<T> = ExtractableResponse<T> & {
88
- schema<S extends Schema>(schema: S): ExtractableResponse<InferSchemaOutput<S>>;
85
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
86
+ export type SuccessContractStatus = `${2 | 3}${Digit}${Digit}`;
87
+ export type ErrorContractStatus = `${4 | 5}${Digit}${Digit}`;
88
+ export type ContractStatus = SuccessContractStatus | ErrorContractStatus;
89
+ export type ContractStatusMap<Status extends string = ContractStatus> = {
90
+ default?: Schema;
91
+ } & Partial<Record<Status, Schema>>;
92
+ export type SuccessContractStatusMap = ContractStatusMap<SuccessContractStatus>;
93
+ export type ErrorContractStatusMap = ContractStatusMap<ErrorContractStatus>;
94
+ export type ResponseContractBranch<Status extends string = ContractStatus> = Schema | ContractStatusMap<Status>;
95
+ export type ResponseContract = Schema | {
96
+ success: ResponseContractBranch<SuccessContractStatus>;
97
+ error?: ResponseContractBranch<ErrorContractStatus>;
98
+ };
99
+ type InferContractBranchSchema<Branch> = Branch extends Schema ? Branch : Branch extends object ? Extract<Branch[keyof Branch], Schema> : never;
100
+ export type InferContractBranchOutput<Branch> = InferSchemaOutput<InferContractBranchSchema<Branch>>;
101
+ export type InferContractSuccess<Contract> = Contract extends Schema ? InferSchemaOutput<Contract> : Contract extends {
102
+ success: infer Success;
103
+ } ? InferContractBranchOutput<Success> : unknown;
104
+ export type InferContractError<Contract> = Contract extends {
105
+ error: infer ErrorBranch;
106
+ } ? InferContractBranchOutput<ErrorBranch> : unknown;
107
+ export type ContractableResponse<T> = ExtractableResponse<T> & {
108
+ contract<C extends ResponseContract>(contract: C): ExtractableResponse<InferContractSuccess<C>>;
89
109
  };
90
110
  /**
91
111
  * Request context that can be modified by onRequest hook
@@ -111,7 +131,7 @@ export interface ExtendedRequestInit extends RequestInit {
111
131
  }
112
132
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | (string & {});
113
133
  export type HttpHeaders = Record<string, string>;
114
- export type RequestParamsType = Record<string, string | number | boolean | (string | number | boolean | undefined | null)[] | undefined | null>;
134
+ export type RequestParamsType = Record<string, string | number | boolean | Date | (string | number | boolean | undefined | null)[] | readonly Date[] | undefined | null>;
115
135
  export type Schema<T = unknown> = StandardSchemaV1<unknown, T> | (StandardSchemaV1<unknown, T> & {
116
136
  _output: T;
117
137
  }) | (StandardSchemaV1<unknown, T> & {
@@ -124,22 +144,7 @@ export type InferSchemaOutput<ResponseSchema> = ResponseSchema extends unknown ?
124
144
  } ? Output : ResponseSchema extends {
125
145
  _output?: infer Output;
126
146
  } ? Output : ResponseSchema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<ResponseSchema> : unknown : never;
127
- export interface UploadStreamingEvent {
128
- /** Current data chunk being uploaded */
129
- chunk: Uint8Array;
130
- /** Total bytes to upload (from Content-Length header or body size) */
131
- totalBytes: number | undefined;
132
- /** Bytes already transferred */
133
- transferredBytes: number;
134
- }
135
- export interface DownloadStreamingEvent {
136
- /** Current data chunk being downloaded */
137
- chunk: Uint8Array;
138
- /** Total bytes to download (from Content-Length header, undefined if unknown) */
139
- totalBytes: number | undefined;
140
- /** Bytes already transferred */
141
- transferredBytes: number;
142
- }
143
147
  export type SerializeBody<TBody = unknown> = (body: TBody) => BodyInit | null | undefined;
144
148
  export type SerializeParams = (params: RequestParamsType) => string;
145
149
  export type CustomFetch = typeof fetch;
150
+ export {};
@@ -1,2 +1 @@
1
- export { type AssertSupportedPath, generatePath, type HasRequiredParams, type PathParams, type RequirePathParams, } from './path';
2
- export { toStreamableRequest, toStreamableResponse } from './streaming';
1
+ export { type AssertSupportedPath, type HasRequiredParams, type RequirePathParams, } from './path';
@@ -1,12 +1,11 @@
1
- /**
2
- * Strips protocol from URL (e.g., https://, http://)
3
- */
4
- type StripProtocol<T extends string> = T extends `${string}://${infer After}` ? After : T;
5
1
  /**
6
2
  * Strips host and optional port from URL, keeping only the path
7
- * Handles cases like "localhost:3000/users" -> "/users"
8
3
  */
9
4
  type StripHost<T extends string> = T extends `${infer _Host}/${infer Path}` ? `/${Path}` : T;
5
+ /**
6
+ * Strips protocol and host from full URLs, keeping bare relative paths intact.
7
+ */
8
+ type StripUrlOrigin<T extends string> = T extends `${string}://${infer After}` ? StripHost<After> : T;
10
9
  /**
11
10
  * Strips query string from path (e.g., "/users?x=1" -> "/users")
12
11
  * Only strips ? that comes after the path, not ? that's part of path parameters
@@ -15,7 +14,7 @@ type StripQuery<T extends string> = T extends `${infer Path}?${infer Query}` ? Q
15
14
  /**
16
15
  * Normalizes URL to path only, stripping protocol, host, port, and query
17
16
  */
18
- type NormalizePath<T extends string> = StripQuery<StripHost<StripProtocol<T>>>;
17
+ type NormalizePath<T extends string> = StripQuery<StripUrlOrigin<T>>;
19
18
  /**
20
19
  * Removes optional suffix from param name (e.g., ":id?" -> "id", ":id" -> "id")
21
20
  */
@@ -68,6 +67,5 @@ export type RequirePathParams<Path extends string, T> = [
68
67
  * // => '/users/123/posts/456'
69
68
  * ```
70
69
  */
71
- export declare function generatePath<Path extends string>(path: AssertSupportedPath<Path>): string;
72
- export declare function generatePath<Path extends string>(path: AssertSupportedPath<Path>, params: PathParams<Path>): string;
70
+ export declare function generatePath<Path extends string>(path: AssertSupportedPath<Path>, ...args: HasRequiredParams<Path> extends true ? [params: PathParams<Path>] : HasPathParams<Path> extends true ? [params?: PathParams<Path>] : [params?: PathParams<Path>]): string;
73
71
  export {};
@@ -5,6 +5,7 @@ This guide provides recommendations for using 1000fetches effectively in product
5
5
  ## Table of Contents
6
6
 
7
7
  - [Client Configuration](#client-configuration)
8
+ - [Dynamic Defaults](#dynamic-defaults)
8
9
  - [Error Handling](#error-handling)
9
10
  - [Schema Validation](#schema-validation)
10
11
  - [Middleware](#middleware)
@@ -68,12 +69,61 @@ const postClient = createHttpClient({ baseUrl: 'https://api.example.com' })
68
69
  const apiClient = createHttpClient({ baseUrl: 'https://api.example.com' })
69
70
  ```
70
71
 
72
+ ## Dynamic Defaults
73
+
74
+ ### ✅ DO: Use Defaults for Dynamic Auth and Request-Shaped Defaults
75
+
76
+ Use `defaults` when a value should be resolved for each request but does not need to mutate the final request context.
77
+
78
+ ```typescript
79
+ const apiClient = createHttpClient({
80
+ baseUrl: 'https://api.example.com',
81
+ defaults: async ({ resolvedPath, method }) => {
82
+ const token = await getAuthToken()
83
+
84
+ return {
85
+ headers: {
86
+ Authorization: `Bearer ${token}`,
87
+ 'X-Client': 'web',
88
+ },
89
+ timeout: resolvedPath.startsWith('/reports') ? 60_000 : 10_000,
90
+ retry: method === 'GET',
91
+ params: {
92
+ locale: 'en-US',
93
+ },
94
+ }
95
+ },
96
+ })
97
+ ```
98
+
99
+ ### ✅ DO: Let Request Options Override Defaults
100
+
101
+ Request options are applied after dynamic defaults, so callers can opt out or specialize one request without creating another client.
102
+
103
+ ```typescript
104
+ const apiClient = createHttpClient({
105
+ baseUrl: 'https://api.example.com',
106
+ defaults: () => ({
107
+ headers: { 'X-Client': 'web' },
108
+ timeout: 10_000,
109
+ retry: true,
110
+ }),
111
+ })
112
+
113
+ await apiClient.get('/health', {
114
+ timeout: 2_000,
115
+ retry: false,
116
+ headers: { 'X-Client': 'health-check' },
117
+ })
118
+ ```
119
+
71
120
  ## Error Handling
72
121
 
73
122
  ### ✅ DO: Handle Specific Error Types
74
123
 
75
124
  ```typescript
76
125
  import {
126
+ AbortError,
77
127
  HttpError,
78
128
  NetworkError,
79
129
  TimeoutError,
@@ -97,6 +147,8 @@ async function fetchUser(id: string) {
97
147
  throw new ConnectivityError('Network connection failed')
98
148
  } else if (error instanceof TimeoutError) {
99
149
  throw new TimeoutError('Request timed out')
150
+ } else if (error instanceof AbortError) {
151
+ throw error
100
152
  } else {
101
153
  throw error
102
154
  }
@@ -121,6 +173,27 @@ const newUser = await apiClient.post('/users', userData, {
121
173
  })
122
174
  ```
123
175
 
176
+ ### ✅ DO: Declare Valid Non-2xx Statuses Explicitly
177
+
178
+ The default success rule is any 2xx status. Use a response contract status map when the protocol intentionally uses another status, such as conditional requests that return `304 Not Modified`.
179
+
180
+ ```typescript
181
+ const response = await apiClient
182
+ .get('/reports/:id', {
183
+ pathParams: { id: reportId },
184
+ })
185
+ .contract({
186
+ success: {
187
+ default: ReportSchema,
188
+ 304: z.undefined(),
189
+ },
190
+ })
191
+
192
+ if (response.status === 304) {
193
+ return cachedReport
194
+ }
195
+ ```
196
+
124
197
  ## Schema Validation
125
198
 
126
199
  ### ✅ DO: Validate Response Data
@@ -137,7 +210,7 @@ const userSchema = z.object({
137
210
  updatedAt: z.iso.datetime(),
138
211
  })
139
212
 
140
- await apiClient.get(`/users/${id}`).schema(userSchema)
213
+ await apiClient.get(`/users/${id}`).contract(userSchema)
141
214
  ```
142
215
 
143
216
  ### ✅ DO: Create Reusable Schema Compositions
@@ -163,24 +236,24 @@ const PostSchema = BaseEntitySchema.extend({
163
236
 
164
237
  ## Middleware
165
238
 
166
- ### ✅ DO: Use Middleware for Cross-Cutting Concerns
239
+ ### ✅ DO: Use Middleware for Observation and Deliberate Mutation
167
240
 
168
241
  ```typescript
169
242
  const apiClient = createHttpClient({
170
243
  baseUrl: 'https://api.example.com',
171
- onRequestMiddleware: async context => {
172
- const token = await getAuthToken()
244
+ onRequestMiddleware: context => {
173
245
  console.log(`→ ${context.method} ${context.url}`)
174
- context.headers.set('Authorization', `Bearer ${token}`)
175
246
  return context
176
247
  },
177
- onResponseMiddleware: async response => {
248
+ onResponseMiddleware: response => {
178
249
  console.log(`← ${response.status} ${response.method} ${response.url}`)
179
250
  return response
180
251
  },
181
252
  })
182
253
  ```
183
254
 
255
+ Middleware can observe requests and responses, and it can mutate request/response state. Keep it side-effect-light; use mutation only when dynamic defaults or request options are not expressive enough.
256
+
184
257
  ### ✅ DO: Handle Middleware Errors Gracefully
185
258
 
186
259
  ```typescript
@@ -188,12 +261,10 @@ const apiClient = createHttpClient({
188
261
  baseUrl: 'https://api.example.com',
189
262
  onRequestMiddleware: async context => {
190
263
  try {
191
- const token = await getAuthToken()
192
- context.headers.set('Authorization', `Bearer ${token}`)
264
+ context.headers.set('X-Request-ID', await createRequestId())
193
265
  return context
194
266
  } catch (error) {
195
- // Log the error but don't fail the request
196
- console.warn('Failed to get auth token:', error)
267
+ console.warn('Failed to create request id:', error)
197
268
  return context
198
269
  }
199
270
  },
@@ -454,36 +525,43 @@ const apiClient = createHttpClient({
454
525
  ### ✅ DO: Add Request Tracking
455
526
 
456
527
  ```typescript
457
- const requestTimings = new Map<string, number>()
528
+ import type { ResponseType } from '1000fetches'
458
529
 
459
- const apiClient = createHttpClient({
460
- onRequestMiddleware: async context => {
461
- const requestId = generateRequestId()
462
- requestTimings.set(requestId, Date.now())
463
- context.headers.set('X-Request-ID', requestId)
464
- return context
465
- },
466
- onResponseMiddleware: async response => {
467
- const requestId = response.headers['x-request-id']
468
- const startTime = requestId ? requestTimings.get(requestId) : undefined
530
+ async function trackRequest<T>(
531
+ operation: string,
532
+ request: () => Promise<ResponseType<T>>
533
+ ) {
534
+ const startTime = Date.now()
469
535
 
470
- if (requestId && startTime !== undefined) {
471
- const duration = Date.now() - startTime
472
- console.log(`Request ${requestId} completed in ${duration}ms`)
536
+ try {
537
+ const response = await request()
538
+ const duration = Date.now() - startTime
539
+
540
+ analytics.track('api_request', {
541
+ operation,
542
+ url: response.url,
543
+ method: response.method,
544
+ status: response.status,
545
+ duration,
546
+ })
473
547
 
474
- analytics.track('api_request', {
475
- method: response.method,
476
- url: response.url,
477
- status: response.status,
478
- duration,
479
- })
548
+ return response
549
+ } catch (error) {
550
+ analytics.track('api_request_failed', {
551
+ operation,
552
+ duration: Date.now() - startTime,
553
+ })
480
554
 
481
- requestTimings.delete(requestId)
482
- }
555
+ throw error
556
+ }
557
+ }
483
558
 
484
- return response
485
- },
486
- })
559
+ const response = await trackRequest('fetch_user', () =>
560
+ apiClient.get('/users/:id', {
561
+ pathParams: { id: userId },
562
+ headers: { 'X-Request-ID': generateRequestId() },
563
+ })
564
+ )
487
565
  ```
488
566
 
489
567
  ### ✅ DO: Monitor Error Rates
@@ -577,8 +655,8 @@ try {
577
655
  // Bad: Synchronous operations in middleware
578
656
  const blockingClient = createHttpClient({
579
657
  onRequestMiddleware: context => {
580
- const token = fs.readFileSync('/path/to/token', 'utf8') // Blocks event loop
581
- context.headers.set('Authorization', `Bearer ${token}`)
658
+ const metadata = fs.readFileSync('/path/to/metadata', 'utf8') // Blocks event loop
659
+ context.headers.set('X-Metadata', metadata)
582
660
  return context
583
661
  },
584
662
  })
@@ -586,8 +664,8 @@ const blockingClient = createHttpClient({
586
664
  // Good: Asynchronous operations
587
665
  const asyncClient = createHttpClient({
588
666
  onRequestMiddleware: async context => {
589
- const token = await fs.promises.readFile('/path/to/token', 'utf8')
590
- context.headers.set('Authorization', `Bearer ${token}`)
667
+ const metadata = await fs.promises.readFile('/path/to/metadata', 'utf8')
668
+ context.headers.set('X-Metadata', metadata)
591
669
  return context
592
670
  },
593
671
  })
@@ -598,9 +676,10 @@ const asyncClient = createHttpClient({
598
676
  Following these best practices will help you build robust, maintainable, and performant applications with 1000fetches:
599
677
 
600
678
  1. **Configure once, use everywhere** - Create a single, well-configured client instance
601
- 2. **Handle errors explicitly** - Don't let errors fail silently
602
- 3. **Validate data** - Use schemas to ensure data integrity
603
- 4. **Keep middleware lightweight** - Avoid heavy operations that slow down requests
604
- 5. **Test thoroughly** - Mock HTTP calls and test error scenarios
605
- 6. **Monitor in production** - Track request metrics and error rates
606
- 7. **Secure by default** - Validate input and handle sensitive data carefully
679
+ 2. **Use dynamic defaults** - Resolve auth and request-shaped defaults per request
680
+ 3. **Handle errors explicitly** - Don't let errors fail silently
681
+ 4. **Validate data** - Use schemas to ensure data integrity
682
+ 5. **Keep middleware lightweight** - Avoid heavy operations that slow down requests
683
+ 6. **Test thoroughly** - Mock HTTP calls and test error scenarios
684
+ 7. **Monitor in production** - Track request metrics and error rates
685
+ 8. **Secure by default** - Validate input and handle sensitive data carefully