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"; 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 { export interface HttpClientConfig {
baseURL: string; baseURL: string;
timeout?: number; timeout?: number;
maxRetries?: number; maxRetries?: number;
retryDelayMs?: number; retryDelayMs?: number;
fetch?: typeof fetch; 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 fetch: typeof fetch;
private apiKey?: string; private apiKey?: string;
private jwt?: string; private jwt?: string;
private onNetworkError?: NetworkErrorCallback;
constructor(config: HttpClientConfig) { constructor(config: HttpClientConfig) {
this.baseURL = config.baseURL.replace(/\/$/, ""); this.baseURL = config.baseURL.replace(/\/$/, "");
@ -47,6 +72,14 @@ export class HttpClient {
this.retryDelayMs = config.retryDelayMs ?? 1000; this.retryDelayMs = config.retryDelayMs ?? 1000;
// Use provided fetch or create one with proper TLS configuration for staging certificates // Use provided fetch or create one with proper TLS configuration for staging certificates
this.fetch = config.fetch ?? createFetchWithTLSConfig(); 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) { 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; throw error;
} finally { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@ -397,6 +451,25 @@ export class HttpClient {
error 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; throw error;
} finally { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
@ -425,17 +498,33 @@ export class HttpClient {
const response = await this.fetch(url.toString(), fetchOptions); const response = await this.fetch(url.toString(), fetchOptions);
if (!response.ok) { if (!response.ok) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
const error = await response.json().catch(() => ({ const errorBody = await response.json().catch(() => ({
error: response.statusText, error: response.statusText,
})); }));
throw SDKError.fromResponse(response.status, error); throw SDKError.fromResponse(response.status, errorBody);
} }
return response; return response;
} catch (error) { } catch (error) {
clearTimeout(timeoutId); 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; throw error;
} }
} }

View File

@ -1,11 +1,17 @@
import WebSocket from "isomorphic-ws"; import WebSocket from "isomorphic-ws";
import { SDKError } from "../errors"; import { SDKError } from "../errors";
import { NetworkErrorCallback } from "./http";
export interface WSClientConfig { export interface WSClientConfig {
wsURL: string; wsURL: string;
timeout?: number; timeout?: number;
authToken?: string; authToken?: string;
WebSocket?: typeof WebSocket; 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; export type WSMessageHandler = (data: string) => void;
@ -23,6 +29,7 @@ export class WSClient {
private timeout: number; private timeout: number;
private authToken?: string; private authToken?: string;
private WebSocketClass: typeof WebSocket; private WebSocketClass: typeof WebSocket;
private onNetworkError?: NetworkErrorCallback;
private ws?: WebSocket; private ws?: WebSocket;
private messageHandlers: Set<WSMessageHandler> = new Set(); private messageHandlers: Set<WSMessageHandler> = new Set();
@ -36,6 +43,14 @@ export class WSClient {
this.timeout = config.timeout ?? 30000; this.timeout = config.timeout ?? 30000;
this.authToken = config.authToken; this.authToken = config.authToken;
this.WebSocketClass = config.WebSocket ?? WebSocket; 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(() => { const timeout = setTimeout(() => {
this.ws?.close(); this.ws?.close();
reject( const error = new SDKError("WebSocket connection timeout", 408, "WS_TIMEOUT");
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.timeout);
this.ws.addEventListener("open", () => { this.ws.addEventListener("open", () => {
@ -78,6 +103,17 @@ export class WSClient {
console.error("[WSClient] WebSocket error:", event); console.error("[WSClient] WebSocket error:", event);
clearTimeout(timeout); clearTimeout(timeout);
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event); 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)); this.errorHandlers.forEach((handler) => handler(error));
reject(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 { AuthClient } from "./auth/client";
import { DBClient } from "./db/client"; import { DBClient } from "./db/client";
import { PubSubClient } from "./pubsub/client"; import { PubSubClient } from "./pubsub/client";
@ -20,6 +20,11 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>; wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
functionsConfig?: FunctionsClientConfig; functionsConfig?: FunctionsClientConfig;
fetch?: typeof fetch; 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 { export interface Client {
@ -39,6 +44,7 @@ export function createClient(config: ClientConfig): Client {
maxRetries: config.maxRetries, maxRetries: config.maxRetries,
retryDelayMs: config.retryDelayMs, retryDelayMs: config.retryDelayMs,
fetch: config.fetch, fetch: config.fetch,
onNetworkError: config.onNetworkError,
}); });
const auth = new AuthClient({ const auth = new AuthClient({
@ -55,6 +61,7 @@ export function createClient(config: ClientConfig): Client {
const pubsub = new PubSubClient(httpClient, { const pubsub = new PubSubClient(httpClient, {
...config.wsConfig, ...config.wsConfig,
wsURL, wsURL,
onNetworkError: config.onNetworkError,
}); });
const network = new NetworkClient(httpClient); const network = new NetworkClient(httpClient);
const cache = new CacheClient(httpClient); const cache = new CacheClient(httpClient);
@ -73,6 +80,7 @@ export function createClient(config: ClientConfig): Client {
} }
export { HttpClient } from "./core/http"; export { HttpClient } from "./core/http";
export type { NetworkErrorCallback, NetworkErrorContext } from "./core/http";
export { WSClient } from "./core/ws"; export { WSClient } from "./core/ws";
export { AuthClient } from "./auth/client"; export { AuthClient } from "./auth/client";
export { DBClient } from "./db/client"; export { DBClient } from "./db/client";