This commit is contained in:
anonpenguin23 2026-01-14 16:43:42 +02:00
parent 25303e7913
commit 58097e3ff8
3 changed files with 141 additions and 8 deletions

View File

@ -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;
}
}

View File

@ -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<WSMessageHandler> = 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);
});

View File

@ -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<HttpClientConfig, "fetch"> {
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
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";