@blimu/fetch
Advanced tools
+28
-14
@@ -302,3 +302,3 @@ "use strict"; | ||
| } | ||
| this.hooks.get(stage).push(hook); | ||
| this.hooks.get(stage)?.push(hook); | ||
| } | ||
@@ -349,3 +349,10 @@ /** | ||
| has(stage) { | ||
| return this.hooks.has(stage) && this.hooks.get(stage).length > 0; | ||
| if (!this.hooks.has(stage)) { | ||
| return false; | ||
| } | ||
| const hooks = this.hooks.get(stage); | ||
| if (!hooks) { | ||
| return false; | ||
| } | ||
| return hooks.length > 0; | ||
| } | ||
@@ -469,3 +476,3 @@ }; | ||
| yield parsed; | ||
| } catch (error) { | ||
| } catch { | ||
| console.warn("Skipping invalid JSON line:", trimmed); | ||
@@ -479,3 +486,3 @@ } | ||
| yield parsed; | ||
| } catch (error) { | ||
| } catch { | ||
| console.warn("Skipping invalid JSON in buffer:", buffer.trim()); | ||
@@ -727,4 +734,5 @@ } | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| async request(init) { | ||
| let url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const headers = new Headers(this.cfg.headers || {}); | ||
@@ -766,3 +774,3 @@ if (init.headers) { | ||
| try { | ||
| let attemptUrl = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const attemptUrl = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const attemptHeaders = new Headers(headers); | ||
@@ -776,2 +784,3 @@ await this.applyAuthentication(attemptHeaders, attemptUrl); | ||
| if (this.hookRegistry.has("beforeRetry")) { | ||
| const serializedBody = serializeBody(init.body, bodyContentType); | ||
| await this.hookRegistry.execute("beforeRetry", { | ||
@@ -781,3 +790,4 @@ url: url.toString(), | ||
| ...init, | ||
| headers | ||
| headers, | ||
| body: serializedBody | ||
| }, | ||
@@ -792,2 +802,3 @@ attempt, | ||
| if (this.hookRegistry.has("afterRetry")) { | ||
| const serializedBody = serializeBody(init.body, bodyContentType); | ||
| await this.hookRegistry.execute("afterRetry", { | ||
@@ -797,3 +808,4 @@ url: url.toString(), | ||
| ...init, | ||
| headers | ||
| headers, | ||
| body: serializedBody | ||
| }, | ||
@@ -817,3 +829,3 @@ attempt, | ||
| async *requestStream(init) { | ||
| let url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const headers = new Headers(this.cfg.headers || {}); | ||
@@ -892,3 +904,4 @@ if (init.headers) { | ||
| const parsed = await parseResponse(res); | ||
| const error = createFetchError(res.status, parsed?.message || `HTTP ${res.status}`, parsed, res.headers); | ||
| const message = parsed?.message; | ||
| const error = createFetchError(res.status, message || `HTTP ${res.status}`, parsed, res.headers); | ||
| if (this.hookRegistry.has("onError")) { | ||
@@ -915,3 +928,3 @@ await this.hookRegistry.execute("onError", { | ||
| for await (const chunk of parseSSEStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -929,3 +942,3 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| for await (const chunk of parseNDJSONStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -943,3 +956,3 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| for await (const chunk of parseChunkedStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -1049,3 +1062,4 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| if (!isSuccessResponse(res)) { | ||
| const error = createFetchError(res.status, parsed?.message || `HTTP ${res.status}`, parsed, res.headers); | ||
| const message = parsed?.message; | ||
| const error = createFetchError(res.status, message || `HTTP ${res.status}`, parsed, res.headers); | ||
| if (this.hookRegistry.has("onError")) { | ||
@@ -1052,0 +1066,0 @@ await this.hookRegistry.execute("onError", { |
+146
-120
| /** | ||
| * Lifecycle stages for request execution | ||
| */ | ||
| type HookStage = "beforeRequest" | "afterRequest" | "afterResponse" | "onError" | "beforeRetry" | "afterRetry" | "onTimeout" | "onStreamStart" | "onStreamChunk" | "onStreamEnd"; | ||
| /** | ||
| * Base context available to all hooks | ||
| */ | ||
| interface BaseHookContext { | ||
| url: string; | ||
| init: RequestInit & { | ||
| path?: string; | ||
| method: string; | ||
| query?: Record<string, any>; | ||
| headers?: Headers; | ||
| }; | ||
| attempt: number; | ||
| } | ||
| /** | ||
| * Context for beforeRequest hook | ||
| */ | ||
| interface BeforeRequestHookContext extends BaseHookContext { | ||
| } | ||
| /** | ||
| * Context for afterRequest hook (before parsing) | ||
| */ | ||
| interface AfterRequestHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for afterResponse hook (after parsing) | ||
| */ | ||
| interface AfterResponseHookContext extends BaseHookContext { | ||
| response: Response; | ||
| data: unknown; | ||
| } | ||
| /** | ||
| * Context for onError hook | ||
| */ | ||
| interface OnErrorHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| } | ||
| /** | ||
| * Context for beforeRetry hook | ||
| */ | ||
| interface BeforeRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| } | ||
| /** | ||
| * Context for afterRetry hook | ||
| */ | ||
| interface AfterRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| success: boolean; | ||
| } | ||
| /** | ||
| * Context for onTimeout hook | ||
| */ | ||
| interface OnTimeoutHookContext extends BaseHookContext { | ||
| timeoutMs: number; | ||
| } | ||
| /** | ||
| * Context for onStreamStart hook | ||
| */ | ||
| interface OnStreamStartHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for onStreamChunk hook | ||
| */ | ||
| interface OnStreamChunkHookContext extends BaseHookContext { | ||
| chunk: unknown; | ||
| } | ||
| /** | ||
| * Context for onStreamEnd hook | ||
| */ | ||
| interface OnStreamEndHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Union type of all hook contexts | ||
| */ | ||
| type HookContext = BeforeRequestHookContext | AfterRequestHookContext | AfterResponseHookContext | OnErrorHookContext | BeforeRetryHookContext | AfterRetryHookContext | OnTimeoutHookContext | OnStreamStartHookContext | OnStreamChunkHookContext | OnStreamEndHookContext; | ||
| /** | ||
| * Hook function type | ||
| */ | ||
| type Hook = (context: HookContext) => void | Promise<void>; | ||
| /** | ||
| * Hook configuration interface | ||
| */ | ||
| interface HooksConfig { | ||
| beforeRequest?: Hook[]; | ||
| afterRequest?: Hook[]; | ||
| afterResponse?: Hook[]; | ||
| onError?: Hook[]; | ||
| beforeRetry?: Hook[]; | ||
| afterRetry?: Hook[]; | ||
| onTimeout?: Hook[]; | ||
| onStreamStart?: Hook[]; | ||
| onStreamChunk?: Hook[]; | ||
| onStreamEnd?: Hook[]; | ||
| } | ||
| /** | ||
| * Hook registry that manages hooks for different lifecycle stages | ||
@@ -134,3 +30,3 @@ */ | ||
| */ | ||
| execute(stage: HookStage, context: any): Promise<void>; | ||
| execute(stage: HookStage, context: HookContext): Promise<void>; | ||
| /** | ||
@@ -160,3 +56,3 @@ * Check if any hooks are registered for a stage | ||
| */ | ||
| strategy: "exponential" | "linear" | RetryStrategyFunction; | ||
| strategy: 'exponential' | 'linear' | RetryStrategyFunction; | ||
| /** | ||
@@ -189,7 +85,7 @@ * Base backoff time in milliseconds (used for exponential and linear strategies) | ||
| */ | ||
| declare function getRetryStrategy(strategy: "exponential" | "linear" | RetryStrategyFunction): RetryStrategyFunction; | ||
| declare function getRetryStrategy(strategy: 'exponential' | 'linear' | RetryStrategyFunction): RetryStrategyFunction; | ||
| /** | ||
| * Calculate the delay for a retry attempt | ||
| */ | ||
| declare function calculateRetryDelay(attempt: number, strategy: "exponential" | "linear" | RetryStrategyFunction, baseBackoffMs: number): number; | ||
| declare function calculateRetryDelay(attempt: number, strategy: 'exponential' | 'linear' | RetryStrategyFunction, baseBackoffMs: number): number; | ||
@@ -201,2 +97,3 @@ /** | ||
| declare function parseSSEStream(response: Response): AsyncGenerator<string, void, unknown>; | ||
| type AnyParsedNDJSON = string | object | ArrayBuffer | unknown; | ||
| /** | ||
@@ -206,3 +103,3 @@ * Parse NDJSON (Newline-Delimited JSON) stream | ||
| */ | ||
| declare function parseNDJSONStream<T = any>(response: Response): AsyncGenerator<T, void, unknown>; | ||
| declare function parseNDJSONStream<T = AnyParsedNDJSON>(response: Response): AsyncGenerator<T, void, unknown>; | ||
| /** | ||
@@ -217,3 +114,3 @@ * Parse generic chunked stream | ||
| */ | ||
| type StreamingFormat = "sse" | "ndjson" | "chunked"; | ||
| type StreamingFormat = 'sse' | 'ndjson' | 'chunked'; | ||
@@ -294,2 +191,25 @@ /** | ||
| /** | ||
| * Query parameter value types | ||
| * Query parameters can be strings, numbers, booleans, arrays of strings (including string literal unions), or null/undefined | ||
| * | ||
| * Note: Arrays of string literal unions (e.g., ('active' | 'inactive')[]) are accepted | ||
| * because they are assignable to arrays of string-like values at runtime. The runtime | ||
| * serialization converts all values to strings using String(). | ||
| * | ||
| * We use a more permissive array type to accept arrays of string literals, which TypeScript | ||
| * doesn't automatically widen to string[] due to array covariance rules. The type accepts | ||
| * arrays of any primitive type since they all get stringified at runtime. | ||
| */ | ||
| type QueryParamValue = any; | ||
| /** | ||
| * Query parameters object | ||
| * Maps parameter names to their values | ||
| * | ||
| * The runtime implementation uses Object.entries() which works with any object type. | ||
| * TypeScript interfaces are not directly assignable to Record types, so when passing | ||
| * interface types (like generated SDK query interfaces), a type assertion may be needed. | ||
| * The runtime serialization code safely handles the conversion. | ||
| */ | ||
| type QueryParams = Record<string, QueryParamValue>; | ||
| /** | ||
| * Body type that can be serialized by the fetch client | ||
@@ -314,7 +234,7 @@ * Includes standard BodyInit types plus plain objects/arrays that will be JSON serialized | ||
| */ | ||
| query?: Record<string, any>; | ||
| query?: QueryParams | undefined; | ||
| /** | ||
| * Request body - can be BodyInit, plain object/array (will be JSON serialized), or null/undefined | ||
| */ | ||
| body?: SerializableBody; | ||
| body?: SerializableBody | undefined; | ||
| } | ||
@@ -336,2 +256,107 @@ /** | ||
| /** | ||
| * Lifecycle stages for request execution | ||
| */ | ||
| type HookStage = 'beforeRequest' | 'afterRequest' | 'afterResponse' | 'onError' | 'beforeRetry' | 'afterRetry' | 'onTimeout' | 'onStreamStart' | 'onStreamChunk' | 'onStreamEnd'; | ||
| /** | ||
| * Base context available to all hooks | ||
| */ | ||
| interface BaseHookContext { | ||
| url: string; | ||
| init: Omit<RequestInit, 'body'> & { | ||
| path?: string | undefined; | ||
| method: string; | ||
| query?: QueryParams | undefined; | ||
| headers?: Headers | undefined; | ||
| body?: RequestInit['body'] | null | undefined; | ||
| }; | ||
| attempt: number; | ||
| } | ||
| /** | ||
| * Context for beforeRequest hook | ||
| */ | ||
| interface BeforeRequestHookContext extends BaseHookContext { | ||
| } | ||
| /** | ||
| * Context for afterRequest hook (before parsing) | ||
| */ | ||
| interface AfterRequestHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for afterResponse hook (after parsing) | ||
| */ | ||
| interface AfterResponseHookContext extends BaseHookContext { | ||
| response: Response; | ||
| data: unknown; | ||
| } | ||
| /** | ||
| * Context for onError hook | ||
| */ | ||
| interface OnErrorHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| } | ||
| /** | ||
| * Context for beforeRetry hook | ||
| */ | ||
| interface BeforeRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| } | ||
| /** | ||
| * Context for afterRetry hook | ||
| */ | ||
| interface AfterRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| success: boolean; | ||
| } | ||
| /** | ||
| * Context for onTimeout hook | ||
| */ | ||
| interface OnTimeoutHookContext extends BaseHookContext { | ||
| timeoutMs: number; | ||
| } | ||
| /** | ||
| * Context for onStreamStart hook | ||
| */ | ||
| interface OnStreamStartHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for onStreamChunk hook | ||
| */ | ||
| interface OnStreamChunkHookContext extends BaseHookContext { | ||
| chunk: unknown; | ||
| } | ||
| /** | ||
| * Context for onStreamEnd hook | ||
| */ | ||
| interface OnStreamEndHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Union type of all hook contexts | ||
| */ | ||
| type HookContext = BeforeRequestHookContext | AfterRequestHookContext | AfterResponseHookContext | OnErrorHookContext | BeforeRetryHookContext | AfterRetryHookContext | OnTimeoutHookContext | OnStreamStartHookContext | OnStreamChunkHookContext | OnStreamEndHookContext; | ||
| /** | ||
| * Hook function type | ||
| */ | ||
| type Hook = (context: HookContext) => void | Promise<void>; | ||
| /** | ||
| * Hook configuration interface | ||
| */ | ||
| interface HooksConfig { | ||
| beforeRequest?: Hook[]; | ||
| afterRequest?: Hook[]; | ||
| afterResponse?: Hook[]; | ||
| onError?: Hook[]; | ||
| beforeRetry?: Hook[]; | ||
| afterRetry?: Hook[]; | ||
| onTimeout?: Hook[]; | ||
| onStreamStart?: Hook[]; | ||
| onStreamChunk?: Hook[]; | ||
| onStreamEnd?: Hook[]; | ||
| } | ||
| /** | ||
| * Universal HTTP fetch client with hooks, retries, and streaming support | ||
@@ -354,11 +379,11 @@ */ | ||
| */ | ||
| useHook(stage: string, hook: any): void; | ||
| useHook(stage: HookStage, hook: Hook): void; | ||
| /** | ||
| * Remove a hook | ||
| */ | ||
| removeHook(stage: string, hook: any): boolean; | ||
| removeHook(stage: HookStage, hook: Hook): boolean; | ||
| /** | ||
| * Clear hooks for a stage or all hooks | ||
| */ | ||
| clearHooks(stage?: string): void; | ||
| clearHooks(stage?: HookStage): void; | ||
| /** | ||
@@ -371,3 +396,3 @@ * Make an HTTP request | ||
| */ | ||
| requestStream<T = any>(init: StreamingRequestOptions): AsyncGenerator<T, void, unknown>; | ||
| requestStream<T = unknown>(init: StreamingRequestOptions): AsyncGenerator<T, void, unknown>; | ||
| /** | ||
@@ -487,3 +512,3 @@ * Internal method to execute a single request attempt | ||
| */ | ||
| declare function serializeQueryParams(query: Record<string, any>): URLSearchParams; | ||
| declare function serializeQueryParams(query: QueryParams): URLSearchParams; | ||
| /** | ||
@@ -496,3 +521,3 @@ * Builds a URL with query parameters | ||
| */ | ||
| declare function buildUrl(baseUrl: string, path: string, query?: Record<string, any>): URL; | ||
| declare function buildUrl(baseUrl: string, path: string, query?: QueryParams | undefined): URL; | ||
| /** | ||
@@ -517,3 +542,4 @@ * Determines the content type from a body value | ||
| */ | ||
| declare function parseResponse(response: Response): Promise<any>; | ||
| type AnyPromiseResponse = Promise<string | object | ArrayBuffer | unknown>; | ||
| declare function parseResponse(response: Response): AnyPromiseResponse; | ||
| /** | ||
@@ -526,2 +552,2 @@ * Checks if a response indicates success (status 200-299) | ||
| export { type AfterRequestHookContext, type AfterResponseHookContext, type AfterRetryHookContext, type ApiKeyAuthStrategy, type AuthStrategy, BadGatewayError, BadRequestError, type BaseHookContext, type BasicAuthStrategy, type BearerAuthStrategy, type BeforeRequestHookContext, type BeforeRetryHookContext, ClientError, ConflictError, type CustomAuthStrategy, FetchClient, type FetchClientConfig, FetchError, ForbiddenError, GatewayTimeoutError, type Hook, type HookContext, HookRegistry, type HookStage, type HooksConfig, InternalServerError, MethodNotAllowedError, NotFoundError, type OnErrorHookContext, type OnStreamChunkHookContext, type OnStreamEndHookContext, type OnStreamStartHookContext, type OnTimeoutHookContext, type RequestOptions, type RetryConfig, type RetryStrategyFunction, type SerializableBody, ServerError, ServiceUnavailableError, type StreamingFormat, type StreamingRequestOptions, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, buildUrl, calculateRetryDelay, createFetchError, encodeBase64, exponentialStrategy, getContentType, getFetchErrorMessage, getRetryStrategy, isAbortControllerAvailable, isBrowser, isFetchAvailable, isNode, isSuccessResponse, linearStrategy, parseChunkedStream, parseNDJSONStream, parseResponse, parseSSEStream, serializeBody, serializeQueryParams }; | ||
| export { type AfterRequestHookContext, type AfterResponseHookContext, type AfterRetryHookContext, type ApiKeyAuthStrategy, type AuthStrategy, BadGatewayError, BadRequestError, type BaseHookContext, type BasicAuthStrategy, type BearerAuthStrategy, type BeforeRequestHookContext, type BeforeRetryHookContext, ClientError, ConflictError, type CustomAuthStrategy, FetchClient, type FetchClientConfig, FetchError, ForbiddenError, GatewayTimeoutError, type Hook, type HookContext, HookRegistry, type HookStage, type HooksConfig, InternalServerError, MethodNotAllowedError, NotFoundError, type OnErrorHookContext, type OnStreamChunkHookContext, type OnStreamEndHookContext, type OnStreamStartHookContext, type OnTimeoutHookContext, type QueryParamValue, type QueryParams, type RequestOptions, type RetryConfig, type RetryStrategyFunction, type SerializableBody, ServerError, ServiceUnavailableError, type StreamingFormat, type StreamingRequestOptions, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, buildUrl, calculateRetryDelay, createFetchError, encodeBase64, exponentialStrategy, getContentType, getFetchErrorMessage, getRetryStrategy, isAbortControllerAvailable, isBrowser, isFetchAvailable, isNode, isSuccessResponse, linearStrategy, parseChunkedStream, parseNDJSONStream, parseResponse, parseSSEStream, serializeBody, serializeQueryParams }; |
+146
-120
| /** | ||
| * Lifecycle stages for request execution | ||
| */ | ||
| type HookStage = "beforeRequest" | "afterRequest" | "afterResponse" | "onError" | "beforeRetry" | "afterRetry" | "onTimeout" | "onStreamStart" | "onStreamChunk" | "onStreamEnd"; | ||
| /** | ||
| * Base context available to all hooks | ||
| */ | ||
| interface BaseHookContext { | ||
| url: string; | ||
| init: RequestInit & { | ||
| path?: string; | ||
| method: string; | ||
| query?: Record<string, any>; | ||
| headers?: Headers; | ||
| }; | ||
| attempt: number; | ||
| } | ||
| /** | ||
| * Context for beforeRequest hook | ||
| */ | ||
| interface BeforeRequestHookContext extends BaseHookContext { | ||
| } | ||
| /** | ||
| * Context for afterRequest hook (before parsing) | ||
| */ | ||
| interface AfterRequestHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for afterResponse hook (after parsing) | ||
| */ | ||
| interface AfterResponseHookContext extends BaseHookContext { | ||
| response: Response; | ||
| data: unknown; | ||
| } | ||
| /** | ||
| * Context for onError hook | ||
| */ | ||
| interface OnErrorHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| } | ||
| /** | ||
| * Context for beforeRetry hook | ||
| */ | ||
| interface BeforeRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| } | ||
| /** | ||
| * Context for afterRetry hook | ||
| */ | ||
| interface AfterRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| success: boolean; | ||
| } | ||
| /** | ||
| * Context for onTimeout hook | ||
| */ | ||
| interface OnTimeoutHookContext extends BaseHookContext { | ||
| timeoutMs: number; | ||
| } | ||
| /** | ||
| * Context for onStreamStart hook | ||
| */ | ||
| interface OnStreamStartHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for onStreamChunk hook | ||
| */ | ||
| interface OnStreamChunkHookContext extends BaseHookContext { | ||
| chunk: unknown; | ||
| } | ||
| /** | ||
| * Context for onStreamEnd hook | ||
| */ | ||
| interface OnStreamEndHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Union type of all hook contexts | ||
| */ | ||
| type HookContext = BeforeRequestHookContext | AfterRequestHookContext | AfterResponseHookContext | OnErrorHookContext | BeforeRetryHookContext | AfterRetryHookContext | OnTimeoutHookContext | OnStreamStartHookContext | OnStreamChunkHookContext | OnStreamEndHookContext; | ||
| /** | ||
| * Hook function type | ||
| */ | ||
| type Hook = (context: HookContext) => void | Promise<void>; | ||
| /** | ||
| * Hook configuration interface | ||
| */ | ||
| interface HooksConfig { | ||
| beforeRequest?: Hook[]; | ||
| afterRequest?: Hook[]; | ||
| afterResponse?: Hook[]; | ||
| onError?: Hook[]; | ||
| beforeRetry?: Hook[]; | ||
| afterRetry?: Hook[]; | ||
| onTimeout?: Hook[]; | ||
| onStreamStart?: Hook[]; | ||
| onStreamChunk?: Hook[]; | ||
| onStreamEnd?: Hook[]; | ||
| } | ||
| /** | ||
| * Hook registry that manages hooks for different lifecycle stages | ||
@@ -134,3 +30,3 @@ */ | ||
| */ | ||
| execute(stage: HookStage, context: any): Promise<void>; | ||
| execute(stage: HookStage, context: HookContext): Promise<void>; | ||
| /** | ||
@@ -160,3 +56,3 @@ * Check if any hooks are registered for a stage | ||
| */ | ||
| strategy: "exponential" | "linear" | RetryStrategyFunction; | ||
| strategy: 'exponential' | 'linear' | RetryStrategyFunction; | ||
| /** | ||
@@ -189,7 +85,7 @@ * Base backoff time in milliseconds (used for exponential and linear strategies) | ||
| */ | ||
| declare function getRetryStrategy(strategy: "exponential" | "linear" | RetryStrategyFunction): RetryStrategyFunction; | ||
| declare function getRetryStrategy(strategy: 'exponential' | 'linear' | RetryStrategyFunction): RetryStrategyFunction; | ||
| /** | ||
| * Calculate the delay for a retry attempt | ||
| */ | ||
| declare function calculateRetryDelay(attempt: number, strategy: "exponential" | "linear" | RetryStrategyFunction, baseBackoffMs: number): number; | ||
| declare function calculateRetryDelay(attempt: number, strategy: 'exponential' | 'linear' | RetryStrategyFunction, baseBackoffMs: number): number; | ||
@@ -201,2 +97,3 @@ /** | ||
| declare function parseSSEStream(response: Response): AsyncGenerator<string, void, unknown>; | ||
| type AnyParsedNDJSON = string | object | ArrayBuffer | unknown; | ||
| /** | ||
@@ -206,3 +103,3 @@ * Parse NDJSON (Newline-Delimited JSON) stream | ||
| */ | ||
| declare function parseNDJSONStream<T = any>(response: Response): AsyncGenerator<T, void, unknown>; | ||
| declare function parseNDJSONStream<T = AnyParsedNDJSON>(response: Response): AsyncGenerator<T, void, unknown>; | ||
| /** | ||
@@ -217,3 +114,3 @@ * Parse generic chunked stream | ||
| */ | ||
| type StreamingFormat = "sse" | "ndjson" | "chunked"; | ||
| type StreamingFormat = 'sse' | 'ndjson' | 'chunked'; | ||
@@ -294,2 +191,25 @@ /** | ||
| /** | ||
| * Query parameter value types | ||
| * Query parameters can be strings, numbers, booleans, arrays of strings (including string literal unions), or null/undefined | ||
| * | ||
| * Note: Arrays of string literal unions (e.g., ('active' | 'inactive')[]) are accepted | ||
| * because they are assignable to arrays of string-like values at runtime. The runtime | ||
| * serialization converts all values to strings using String(). | ||
| * | ||
| * We use a more permissive array type to accept arrays of string literals, which TypeScript | ||
| * doesn't automatically widen to string[] due to array covariance rules. The type accepts | ||
| * arrays of any primitive type since they all get stringified at runtime. | ||
| */ | ||
| type QueryParamValue = any; | ||
| /** | ||
| * Query parameters object | ||
| * Maps parameter names to their values | ||
| * | ||
| * The runtime implementation uses Object.entries() which works with any object type. | ||
| * TypeScript interfaces are not directly assignable to Record types, so when passing | ||
| * interface types (like generated SDK query interfaces), a type assertion may be needed. | ||
| * The runtime serialization code safely handles the conversion. | ||
| */ | ||
| type QueryParams = Record<string, QueryParamValue>; | ||
| /** | ||
| * Body type that can be serialized by the fetch client | ||
@@ -314,7 +234,7 @@ * Includes standard BodyInit types plus plain objects/arrays that will be JSON serialized | ||
| */ | ||
| query?: Record<string, any>; | ||
| query?: QueryParams | undefined; | ||
| /** | ||
| * Request body - can be BodyInit, plain object/array (will be JSON serialized), or null/undefined | ||
| */ | ||
| body?: SerializableBody; | ||
| body?: SerializableBody | undefined; | ||
| } | ||
@@ -336,2 +256,107 @@ /** | ||
| /** | ||
| * Lifecycle stages for request execution | ||
| */ | ||
| type HookStage = 'beforeRequest' | 'afterRequest' | 'afterResponse' | 'onError' | 'beforeRetry' | 'afterRetry' | 'onTimeout' | 'onStreamStart' | 'onStreamChunk' | 'onStreamEnd'; | ||
| /** | ||
| * Base context available to all hooks | ||
| */ | ||
| interface BaseHookContext { | ||
| url: string; | ||
| init: Omit<RequestInit, 'body'> & { | ||
| path?: string | undefined; | ||
| method: string; | ||
| query?: QueryParams | undefined; | ||
| headers?: Headers | undefined; | ||
| body?: RequestInit['body'] | null | undefined; | ||
| }; | ||
| attempt: number; | ||
| } | ||
| /** | ||
| * Context for beforeRequest hook | ||
| */ | ||
| interface BeforeRequestHookContext extends BaseHookContext { | ||
| } | ||
| /** | ||
| * Context for afterRequest hook (before parsing) | ||
| */ | ||
| interface AfterRequestHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for afterResponse hook (after parsing) | ||
| */ | ||
| interface AfterResponseHookContext extends BaseHookContext { | ||
| response: Response; | ||
| data: unknown; | ||
| } | ||
| /** | ||
| * Context for onError hook | ||
| */ | ||
| interface OnErrorHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| } | ||
| /** | ||
| * Context for beforeRetry hook | ||
| */ | ||
| interface BeforeRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| } | ||
| /** | ||
| * Context for afterRetry hook | ||
| */ | ||
| interface AfterRetryHookContext extends BaseHookContext { | ||
| error: unknown; | ||
| retryCount: number; | ||
| success: boolean; | ||
| } | ||
| /** | ||
| * Context for onTimeout hook | ||
| */ | ||
| interface OnTimeoutHookContext extends BaseHookContext { | ||
| timeoutMs: number; | ||
| } | ||
| /** | ||
| * Context for onStreamStart hook | ||
| */ | ||
| interface OnStreamStartHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Context for onStreamChunk hook | ||
| */ | ||
| interface OnStreamChunkHookContext extends BaseHookContext { | ||
| chunk: unknown; | ||
| } | ||
| /** | ||
| * Context for onStreamEnd hook | ||
| */ | ||
| interface OnStreamEndHookContext extends BaseHookContext { | ||
| response: Response; | ||
| } | ||
| /** | ||
| * Union type of all hook contexts | ||
| */ | ||
| type HookContext = BeforeRequestHookContext | AfterRequestHookContext | AfterResponseHookContext | OnErrorHookContext | BeforeRetryHookContext | AfterRetryHookContext | OnTimeoutHookContext | OnStreamStartHookContext | OnStreamChunkHookContext | OnStreamEndHookContext; | ||
| /** | ||
| * Hook function type | ||
| */ | ||
| type Hook = (context: HookContext) => void | Promise<void>; | ||
| /** | ||
| * Hook configuration interface | ||
| */ | ||
| interface HooksConfig { | ||
| beforeRequest?: Hook[]; | ||
| afterRequest?: Hook[]; | ||
| afterResponse?: Hook[]; | ||
| onError?: Hook[]; | ||
| beforeRetry?: Hook[]; | ||
| afterRetry?: Hook[]; | ||
| onTimeout?: Hook[]; | ||
| onStreamStart?: Hook[]; | ||
| onStreamChunk?: Hook[]; | ||
| onStreamEnd?: Hook[]; | ||
| } | ||
| /** | ||
| * Universal HTTP fetch client with hooks, retries, and streaming support | ||
@@ -354,11 +379,11 @@ */ | ||
| */ | ||
| useHook(stage: string, hook: any): void; | ||
| useHook(stage: HookStage, hook: Hook): void; | ||
| /** | ||
| * Remove a hook | ||
| */ | ||
| removeHook(stage: string, hook: any): boolean; | ||
| removeHook(stage: HookStage, hook: Hook): boolean; | ||
| /** | ||
| * Clear hooks for a stage or all hooks | ||
| */ | ||
| clearHooks(stage?: string): void; | ||
| clearHooks(stage?: HookStage): void; | ||
| /** | ||
@@ -371,3 +396,3 @@ * Make an HTTP request | ||
| */ | ||
| requestStream<T = any>(init: StreamingRequestOptions): AsyncGenerator<T, void, unknown>; | ||
| requestStream<T = unknown>(init: StreamingRequestOptions): AsyncGenerator<T, void, unknown>; | ||
| /** | ||
@@ -487,3 +512,3 @@ * Internal method to execute a single request attempt | ||
| */ | ||
| declare function serializeQueryParams(query: Record<string, any>): URLSearchParams; | ||
| declare function serializeQueryParams(query: QueryParams): URLSearchParams; | ||
| /** | ||
@@ -496,3 +521,3 @@ * Builds a URL with query parameters | ||
| */ | ||
| declare function buildUrl(baseUrl: string, path: string, query?: Record<string, any>): URL; | ||
| declare function buildUrl(baseUrl: string, path: string, query?: QueryParams | undefined): URL; | ||
| /** | ||
@@ -517,3 +542,4 @@ * Determines the content type from a body value | ||
| */ | ||
| declare function parseResponse(response: Response): Promise<any>; | ||
| type AnyPromiseResponse = Promise<string | object | ArrayBuffer | unknown>; | ||
| declare function parseResponse(response: Response): AnyPromiseResponse; | ||
| /** | ||
@@ -526,2 +552,2 @@ * Checks if a response indicates success (status 200-299) | ||
| export { type AfterRequestHookContext, type AfterResponseHookContext, type AfterRetryHookContext, type ApiKeyAuthStrategy, type AuthStrategy, BadGatewayError, BadRequestError, type BaseHookContext, type BasicAuthStrategy, type BearerAuthStrategy, type BeforeRequestHookContext, type BeforeRetryHookContext, ClientError, ConflictError, type CustomAuthStrategy, FetchClient, type FetchClientConfig, FetchError, ForbiddenError, GatewayTimeoutError, type Hook, type HookContext, HookRegistry, type HookStage, type HooksConfig, InternalServerError, MethodNotAllowedError, NotFoundError, type OnErrorHookContext, type OnStreamChunkHookContext, type OnStreamEndHookContext, type OnStreamStartHookContext, type OnTimeoutHookContext, type RequestOptions, type RetryConfig, type RetryStrategyFunction, type SerializableBody, ServerError, ServiceUnavailableError, type StreamingFormat, type StreamingRequestOptions, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, buildUrl, calculateRetryDelay, createFetchError, encodeBase64, exponentialStrategy, getContentType, getFetchErrorMessage, getRetryStrategy, isAbortControllerAvailable, isBrowser, isFetchAvailable, isNode, isSuccessResponse, linearStrategy, parseChunkedStream, parseNDJSONStream, parseResponse, parseSSEStream, serializeBody, serializeQueryParams }; | ||
| export { type AfterRequestHookContext, type AfterResponseHookContext, type AfterRetryHookContext, type ApiKeyAuthStrategy, type AuthStrategy, BadGatewayError, BadRequestError, type BaseHookContext, type BasicAuthStrategy, type BearerAuthStrategy, type BeforeRequestHookContext, type BeforeRetryHookContext, ClientError, ConflictError, type CustomAuthStrategy, FetchClient, type FetchClientConfig, FetchError, ForbiddenError, GatewayTimeoutError, type Hook, type HookContext, HookRegistry, type HookStage, type HooksConfig, InternalServerError, MethodNotAllowedError, NotFoundError, type OnErrorHookContext, type OnStreamChunkHookContext, type OnStreamEndHookContext, type OnStreamStartHookContext, type OnTimeoutHookContext, type QueryParamValue, type QueryParams, type RequestOptions, type RetryConfig, type RetryStrategyFunction, type SerializableBody, ServerError, ServiceUnavailableError, type StreamingFormat, type StreamingRequestOptions, TooManyRequestsError, UnauthorizedError, UnprocessableEntityError, buildUrl, calculateRetryDelay, createFetchError, encodeBase64, exponentialStrategy, getContentType, getFetchErrorMessage, getRetryStrategy, isAbortControllerAvailable, isBrowser, isFetchAvailable, isNode, isSuccessResponse, linearStrategy, parseChunkedStream, parseNDJSONStream, parseResponse, parseSSEStream, serializeBody, serializeQueryParams }; |
+28
-14
@@ -242,3 +242,3 @@ var __defProp = Object.defineProperty; | ||
| } | ||
| this.hooks.get(stage).push(hook); | ||
| this.hooks.get(stage)?.push(hook); | ||
| } | ||
@@ -289,3 +289,10 @@ /** | ||
| has(stage) { | ||
| return this.hooks.has(stage) && this.hooks.get(stage).length > 0; | ||
| if (!this.hooks.has(stage)) { | ||
| return false; | ||
| } | ||
| const hooks = this.hooks.get(stage); | ||
| if (!hooks) { | ||
| return false; | ||
| } | ||
| return hooks.length > 0; | ||
| } | ||
@@ -409,3 +416,3 @@ }; | ||
| yield parsed; | ||
| } catch (error) { | ||
| } catch { | ||
| console.warn("Skipping invalid JSON line:", trimmed); | ||
@@ -419,3 +426,3 @@ } | ||
| yield parsed; | ||
| } catch (error) { | ||
| } catch { | ||
| console.warn("Skipping invalid JSON in buffer:", buffer.trim()); | ||
@@ -667,4 +674,5 @@ } | ||
| */ | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| async request(init) { | ||
| let url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const headers = new Headers(this.cfg.headers || {}); | ||
@@ -706,3 +714,3 @@ if (init.headers) { | ||
| try { | ||
| let attemptUrl = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const attemptUrl = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const attemptHeaders = new Headers(headers); | ||
@@ -716,2 +724,3 @@ await this.applyAuthentication(attemptHeaders, attemptUrl); | ||
| if (this.hookRegistry.has("beforeRetry")) { | ||
| const serializedBody = serializeBody(init.body, bodyContentType); | ||
| await this.hookRegistry.execute("beforeRetry", { | ||
@@ -721,3 +730,4 @@ url: url.toString(), | ||
| ...init, | ||
| headers | ||
| headers, | ||
| body: serializedBody | ||
| }, | ||
@@ -732,2 +742,3 @@ attempt, | ||
| if (this.hookRegistry.has("afterRetry")) { | ||
| const serializedBody = serializeBody(init.body, bodyContentType); | ||
| await this.hookRegistry.execute("afterRetry", { | ||
@@ -737,3 +748,4 @@ url: url.toString(), | ||
| ...init, | ||
| headers | ||
| headers, | ||
| body: serializedBody | ||
| }, | ||
@@ -757,3 +769,3 @@ attempt, | ||
| async *requestStream(init) { | ||
| let url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const url = buildUrl(this.cfg.baseURL || "", init.path, init.query); | ||
| const headers = new Headers(this.cfg.headers || {}); | ||
@@ -832,3 +844,4 @@ if (init.headers) { | ||
| const parsed = await parseResponse(res); | ||
| const error = createFetchError(res.status, parsed?.message || `HTTP ${res.status}`, parsed, res.headers); | ||
| const message = parsed?.message; | ||
| const error = createFetchError(res.status, message || `HTTP ${res.status}`, parsed, res.headers); | ||
| if (this.hookRegistry.has("onError")) { | ||
@@ -855,3 +868,3 @@ await this.hookRegistry.execute("onError", { | ||
| for await (const chunk of parseSSEStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -869,3 +882,3 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| for await (const chunk of parseNDJSONStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -883,3 +896,3 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| for await (const chunk of parseChunkedStream(res)) { | ||
| let transformedChunk = chunk; | ||
| const transformedChunk = chunk; | ||
| if (this.hookRegistry.has("onStreamChunk")) { | ||
@@ -989,3 +1002,4 @@ await this.hookRegistry.execute("onStreamChunk", { | ||
| if (!isSuccessResponse(res)) { | ||
| const error = createFetchError(res.status, parsed?.message || `HTTP ${res.status}`, parsed, res.headers); | ||
| const message = parsed?.message; | ||
| const error = createFetchError(res.status, message || `HTTP ${res.status}`, parsed, res.headers); | ||
| if (this.hookRegistry.has("onError")) { | ||
@@ -992,0 +1006,0 @@ await this.hookRegistry.execute("onError", { |
+20
-18
| { | ||
| "name": "@blimu/fetch", | ||
| "version": "0.3.0", | ||
| "version": "0.4.0", | ||
| "description": "Universal HTTP fetch client with hooks, retries, and streaming support for browser and Node.js", | ||
| "repository": "https://github.com/blimu-dev/packages", | ||
| "author": "viniciusdacal", | ||
| "license": "MIT", | ||
| "keywords": [ | ||
| "fetch", | ||
| "http", | ||
| "client", | ||
| "retry", | ||
| "streaming", | ||
| "hooks", | ||
| "browser", | ||
| "nodejs" | ||
| ], | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "type": "module", | ||
@@ -18,4 +34,5 @@ "main": "dist/index.cjs", | ||
| "files": [ | ||
| "dist" | ||
| "dist/**/*" | ||
| ], | ||
| "sideEffects": false, | ||
| "scripts": { | ||
@@ -29,17 +46,2 @@ "build": "tsup", | ||
| }, | ||
| "keywords": [ | ||
| "fetch", | ||
| "http", | ||
| "client", | ||
| "retry", | ||
| "streaming", | ||
| "hooks", | ||
| "browser", | ||
| "nodejs" | ||
| ], | ||
| "author": "", | ||
| "license": "MIT", | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "engines": { | ||
@@ -49,3 +51,3 @@ "node": ">=22.0.0" | ||
| "devDependencies": { | ||
| "@types/node": "^25.0.8", | ||
| "@types/node": "^25.0.9", | ||
| "@vitest/browser": "^4.0.17", | ||
@@ -52,0 +54,0 @@ "@vitest/coverage-v8": "^4.0.17", |
+1
-1
@@ -452,3 +452,3 @@ # @blimu/fetch | ||
| method: string; | ||
| query?: Record<string, any>; | ||
| query?: Record<string, any> | undefined; | ||
| } | ||
@@ -455,0 +455,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
246741
2.76%2838
1.94%1
-50%3
200%