From 58097e3ff838a30ec7ef779a54cbc53dbca29258 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Wed, 14 Jan 2026 16:43:42 +0200 Subject: [PATCH] fixes --- src/core/http.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++++-- src/core/ws.ts | 42 +++++++++++++++++++-- src/index.ts | 10 ++++- 3 files changed, 141 insertions(+), 8 deletions(-) diff --git a/src/core/http.ts b/src/core/http.ts index 8104ffa..9a18315 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -1,11 +1,35 @@ import { SDKError } from "../errors"; +/** + * Context provided to the onNetworkError callback + */ +export interface NetworkErrorContext { + method: "GET" | "POST" | "PUT" | "DELETE" | "WS"; + path: string; + isRetry: boolean; + attempt: number; +} + +/** + * Callback invoked when a network error occurs. + * Use this to trigger gateway failover or other error handling. + */ +export type NetworkErrorCallback = ( + error: SDKError, + context: NetworkErrorContext +) => void; + export interface HttpClientConfig { baseURL: string; timeout?: number; maxRetries?: number; retryDelayMs?: number; fetch?: typeof fetch; + /** + * Callback invoked on network errors (after all retries exhausted). + * Use this to trigger gateway failover at the application layer. + */ + onNetworkError?: NetworkErrorCallback; } /** @@ -39,6 +63,7 @@ export class HttpClient { private fetch: typeof fetch; private apiKey?: string; private jwt?: string; + private onNetworkError?: NetworkErrorCallback; constructor(config: HttpClientConfig) { this.baseURL = config.baseURL.replace(/\/$/, ""); @@ -47,6 +72,14 @@ export class HttpClient { this.retryDelayMs = config.retryDelayMs ?? 1000; // Use provided fetch or create one with proper TLS configuration for staging certificates this.fetch = config.fetch ?? createFetchWithTLSConfig(); + this.onNetworkError = config.onNetworkError; + } + + /** + * Set the network error callback + */ + setOnNetworkError(callback: NetworkErrorCallback | undefined): void { + this.onNetworkError = callback; } setApiKey(apiKey?: string) { @@ -258,6 +291,27 @@ export class HttpClient { } } } + + // Call the network error callback if configured + // This allows the app to trigger gateway failover + if (this.onNetworkError) { + // Convert native errors (TypeError, AbortError) to SDKError for the callback + const sdkError = + error instanceof SDKError + ? error + : new SDKError( + error instanceof Error ? error.message : String(error), + 0, // httpStatus 0 indicates network-level failure + "NETWORK_ERROR" + ); + this.onNetworkError(sdkError, { + method, + path, + isRetry: false, + attempt: this.maxRetries, // All retries exhausted + }); + } + throw error; } finally { clearTimeout(timeoutId); @@ -397,6 +451,25 @@ export class HttpClient { error ); } + + // Call the network error callback if configured + if (this.onNetworkError) { + const sdkError = + error instanceof SDKError + ? error + : new SDKError( + error instanceof Error ? error.message : String(error), + 0, + "NETWORK_ERROR" + ); + this.onNetworkError(sdkError, { + method: "POST", + path, + isRetry: false, + attempt: this.maxRetries, + }); + } + throw error; } finally { clearTimeout(timeoutId); @@ -425,17 +498,33 @@ export class HttpClient { const response = await this.fetch(url.toString(), fetchOptions); if (!response.ok) { clearTimeout(timeoutId); - const error = await response.json().catch(() => ({ + const errorBody = await response.json().catch(() => ({ error: response.statusText, })); - throw SDKError.fromResponse(response.status, error); + throw SDKError.fromResponse(response.status, errorBody); } return response; } catch (error) { clearTimeout(timeoutId); - if (error instanceof SDKError) { - throw error; + + // Call the network error callback if configured + if (this.onNetworkError) { + const sdkError = + error instanceof SDKError + ? error + : new SDKError( + error instanceof Error ? error.message : String(error), + 0, + "NETWORK_ERROR" + ); + this.onNetworkError(sdkError, { + method: "GET", + path, + isRetry: false, + attempt: 0, + }); } + throw error; } } diff --git a/src/core/ws.ts b/src/core/ws.ts index b7bad03..8e1f997 100644 --- a/src/core/ws.ts +++ b/src/core/ws.ts @@ -1,11 +1,17 @@ import WebSocket from "isomorphic-ws"; import { SDKError } from "../errors"; +import { NetworkErrorCallback } from "./http"; export interface WSClientConfig { wsURL: string; timeout?: number; authToken?: string; WebSocket?: typeof WebSocket; + /** + * Callback invoked on WebSocket errors. + * Use this to trigger gateway failover at the application layer. + */ + onNetworkError?: NetworkErrorCallback; } export type WSMessageHandler = (data: string) => void; @@ -23,6 +29,7 @@ export class WSClient { private timeout: number; private authToken?: string; private WebSocketClass: typeof WebSocket; + private onNetworkError?: NetworkErrorCallback; private ws?: WebSocket; private messageHandlers: Set = new Set(); @@ -36,6 +43,14 @@ export class WSClient { this.timeout = config.timeout ?? 30000; this.authToken = config.authToken; this.WebSocketClass = config.WebSocket ?? WebSocket; + this.onNetworkError = config.onNetworkError; + } + + /** + * Set the network error callback + */ + setOnNetworkError(callback: NetworkErrorCallback | undefined): void { + this.onNetworkError = callback; } /** @@ -57,9 +72,19 @@ export class WSClient { const timeout = setTimeout(() => { this.ws?.close(); - reject( - new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT") - ); + const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT"); + + // Call the network error callback if configured + if (this.onNetworkError) { + this.onNetworkError(error, { + method: "WS", + path: this.wsURL, + isRetry: false, + attempt: 0, + }); + } + + reject(error); }, this.timeout); this.ws.addEventListener("open", () => { @@ -78,6 +103,17 @@ export class WSClient { console.error("[WSClient] WebSocket error:", event); clearTimeout(timeout); const error = new SDKError("WebSocket error", 500, "WS_ERROR", event); + + // Call the network error callback if configured + if (this.onNetworkError) { + this.onNetworkError(error, { + method: "WS", + path: this.wsURL, + isRetry: false, + attempt: 0, + }); + } + this.errorHandlers.forEach((handler) => handler(error)); reject(error); }); diff --git a/src/index.ts b/src/index.ts index f116728..cdb63ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { HttpClient, HttpClientConfig } from "./core/http"; +import { HttpClient, HttpClientConfig, NetworkErrorCallback } from "./core/http"; import { AuthClient } from "./auth/client"; import { DBClient } from "./db/client"; import { PubSubClient } from "./pubsub/client"; @@ -20,6 +20,11 @@ export interface ClientConfig extends Omit { wsConfig?: Partial>; functionsConfig?: FunctionsClientConfig; fetch?: typeof fetch; + /** + * Callback invoked on network errors (HTTP and WebSocket). + * Use this to trigger gateway failover at the application layer. + */ + onNetworkError?: NetworkErrorCallback; } export interface Client { @@ -39,6 +44,7 @@ export function createClient(config: ClientConfig): Client { maxRetries: config.maxRetries, retryDelayMs: config.retryDelayMs, fetch: config.fetch, + onNetworkError: config.onNetworkError, }); const auth = new AuthClient({ @@ -55,6 +61,7 @@ export function createClient(config: ClientConfig): Client { const pubsub = new PubSubClient(httpClient, { ...config.wsConfig, wsURL, + onNetworkError: config.onNetworkError, }); const network = new NetworkClient(httpClient); const cache = new CacheClient(httpClient); @@ -73,6 +80,7 @@ export function createClient(config: ClientConfig): Client { } export { HttpClient } from "./core/http"; +export type { NetworkErrorCallback, NetworkErrorContext } from "./core/http"; export { WSClient } from "./core/ws"; export { AuthClient } from "./auth/client"; export { DBClient } from "./db/client";