From 25303e7913ba886c131beae101c3215ec2f9cfd5 Mon Sep 17 00:00:00 2001 From: anonpenguin23 Date: Tue, 13 Jan 2026 15:32:19 +0200 Subject: [PATCH] Refactor HttpClient and WSClient to simplify configuration and improve clarity; remove unused gateway health logic --- package.json | 2 +- src/core/http.ts | 157 ++++--------------------------------------- src/core/ws.ts | 19 ++++-- src/index.ts | 10 +-- src/pubsub/client.ts | 2 +- 5 files changed, 33 insertions(+), 157 deletions(-) diff --git a/package.json b/package.json index 8127868..4363ecb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@debros/network-ts-sdk", - "version": "0.4.1", + "version": "0.4.2", "description": "TypeScript SDK for DeBros Network Gateway", "type": "module", "main": "./dist/index.js", diff --git a/src/core/http.ts b/src/core/http.ts index 3037a82..8104ffa 100644 --- a/src/core/http.ts +++ b/src/core/http.ts @@ -1,12 +1,11 @@ import { SDKError } from "../errors"; export interface HttpClientConfig { - baseURL: string | string[]; + baseURL: string; timeout?: number; maxRetries?: number; retryDelayMs?: number; fetch?: typeof fetch; - gatewayHealthCheckCooldownMs?: number; } /** @@ -32,38 +31,22 @@ function createFetchWithTLSConfig(): typeof fetch { return globalThis.fetch; } -interface GatewayHealth { - url: string; - unhealthyUntil: number | null; // Timestamp when gateway becomes healthy again -} - export class HttpClient { - private baseURLs: string[]; - private currentURLIndex: number = 0; + private baseURL: string; private timeout: number; private maxRetries: number; private retryDelayMs: number; private fetch: typeof fetch; private apiKey?: string; private jwt?: string; - private gatewayHealthCheckCooldownMs: number; - private gatewayHealth: Map; constructor(config: HttpClientConfig) { - this.baseURLs = (Array.isArray(config.baseURL) ? config.baseURL : [config.baseURL]) - .map(url => url.replace(/\/$/, "")); + this.baseURL = config.baseURL.replace(/\/$/, ""); this.timeout = config.timeout ?? 60000; this.maxRetries = config.maxRetries ?? 3; this.retryDelayMs = config.retryDelayMs ?? 1000; - this.gatewayHealthCheckCooldownMs = config.gatewayHealthCheckCooldownMs ?? 600000; // Default 10 minutes // Use provided fetch or create one with proper TLS configuration for staging certificates this.fetch = config.fetch ?? createFetchWithTLSConfig(); - - // Initialize gateway health tracking - this.gatewayHealth = new Map(); - this.baseURLs.forEach(url => { - this.gatewayHealth.set(url, { url, unhealthyUntil: null }); - }); } setApiKey(apiKey?: string) { @@ -139,89 +122,10 @@ export class HttpClient { } /** - * Get the current base URL (for single gateway or current gateway in multi-gateway setup) + * Get the base URL */ - private getCurrentBaseURL(): string { - return this.baseURLs[this.currentURLIndex]; - } - - /** - * Get all base URLs (for WebSocket or other purposes that need all gateways) - */ - getBaseURLs(): string[] { - return [...this.baseURLs]; - } - - /** - * Check if a gateway is healthy (not in cooldown period) - */ - private isGatewayHealthy(url: string): boolean { - const health = this.gatewayHealth.get(url); - if (!health || health.unhealthyUntil === null) { - return true; - } - const now = Date.now(); - if (now >= health.unhealthyUntil) { - // Cooldown period expired, mark as healthy again - health.unhealthyUntil = null; - return true; - } - return false; - } - - /** - * Mark a gateway as unhealthy for the cooldown period - */ - private markGatewayUnhealthy(url: string): void { - const health = this.gatewayHealth.get(url); - if (health) { - health.unhealthyUntil = Date.now() + this.gatewayHealthCheckCooldownMs; - if (typeof console !== "undefined") { - console.warn( - `[HttpClient] Gateway marked unhealthy for ${this.gatewayHealthCheckCooldownMs / 1000}s:`, - url - ); - } - } - } - - /** - * Try the next healthy gateway in the list - * Returns the index of the next healthy gateway, or -1 if none available - */ - private findNextHealthyGateway(): number { - if (this.baseURLs.length <= 1) { - return -1; // No other gateways to try - } - - const startIndex = this.currentURLIndex; - let attempts = 0; - - // Try each gateway once (excluding current) - while (attempts < this.baseURLs.length - 1) { - const nextIndex = (startIndex + attempts + 1) % this.baseURLs.length; - const nextUrl = this.baseURLs[nextIndex]; - - if (this.isGatewayHealthy(nextUrl)) { - return nextIndex; - } - - attempts++; - } - - return -1; // No healthy gateways found - } - - /** - * Move to the next healthy gateway - */ - private moveToNextGateway(): boolean { - const nextIndex = this.findNextHealthyGateway(); - if (nextIndex === -1) { - return false; - } - this.currentURLIndex = nextIndex; - return true; + getBaseURL(): string { + return this.baseURL; } async request( @@ -235,7 +139,7 @@ export class HttpClient { } = {} ): Promise { const startTime = performance.now(); // Track request start time - const url = new URL(this.getCurrentBaseURL() + path); + const url = new URL(this.baseURL + path); if (options.query) { Object.entries(options.query).forEach(([key, value]) => { url.searchParams.append(key, String(value)); @@ -364,11 +268,8 @@ export class HttpClient { url: string, options: RequestInit, attempt: number = 0, - startTime?: number, // Track start time for timing across retries - gatewayAttempt: number = 0 // Track gateway failover attempts + startTime?: number // Track start time for timing across retries ): Promise { - const currentGatewayUrl = this.getCurrentBaseURL(); - try { const response = await this.fetch(url, options); @@ -393,50 +294,20 @@ export class HttpClient { error instanceof SDKError && [408, 429, 500, 502, 503, 504].includes(error.httpStatus); - const isNetworkError = - error instanceof TypeError || - (error instanceof Error && error.message.includes('fetch')); - - // Retry on the same gateway first (for retryable HTTP errors) + // Retry on same gateway for retryable HTTP errors if (isRetryableError && attempt < this.maxRetries) { if (typeof console !== "undefined") { console.warn( - `[HttpClient] Retrying request on same gateway (attempt ${attempt + 1}/${this.maxRetries}):`, - currentGatewayUrl + `[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})` ); } await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs * (attempt + 1)) ); - return this.requestWithRetry(url, options, attempt + 1, startTime, gatewayAttempt); - } - - // If all retries on current gateway failed, mark it unhealthy and try next gateway - if ((isNetworkError || isRetryableError) && gatewayAttempt < this.baseURLs.length - 1) { - // Mark current gateway as unhealthy - this.markGatewayUnhealthy(currentGatewayUrl); - - // Try to move to next healthy gateway - if (this.moveToNextGateway()) { - if (typeof console !== "undefined") { - console.warn( - `[HttpClient] Gateway exhausted retries, trying next gateway (${gatewayAttempt + 1}/${this.baseURLs.length - 1}):`, - this.getCurrentBaseURL() - ); - } - - // Update URL to use the new gateway - const currentPath = url.substring(url.indexOf('/', 8)); // Get path after protocol://host - const newUrl = this.getCurrentBaseURL() + currentPath; - - // Small delay before trying next gateway - await new Promise((resolve) => setTimeout(resolve, this.retryDelayMs)); - - // Reset attempt counter for new gateway - return this.requestWithRetry(newUrl, options, 0, startTime, gatewayAttempt + 1); - } + return this.requestWithRetry(url, options, attempt + 1, startTime); } + // All retries exhausted - throw error for app to handle throw error; } } @@ -483,7 +354,7 @@ export class HttpClient { } ): Promise { const startTime = performance.now(); // Track upload start time - const url = new URL(this.getCurrentBaseURL() + path); + const url = new URL(this.baseURL + path); const headers: Record = { ...this.getAuthHeaders(path), // Don't set Content-Type - browser will set it with boundary @@ -536,7 +407,7 @@ export class HttpClient { * Get a binary response (returns Response object for streaming) */ async getBinary(path: string): Promise { - const url = new URL(this.getCurrentBaseURL() + path); + const url = new URL(this.baseURL + path); const headers: Record = { ...this.getAuthHeaders(path), }; diff --git a/src/core/ws.ts b/src/core/ws.ts index 8c714c4..b7bad03 100644 --- a/src/core/ws.ts +++ b/src/core/ws.ts @@ -15,10 +15,11 @@ export type WSOpenHandler = () => void; /** * Simple WebSocket client with minimal abstractions - * No complex reconnection, no heartbeats - keep it simple + * No complex reconnection, no failover - keep it simple + * Gateway failover is handled at the application layer */ export class WSClient { - private url: string; + private wsURL: string; private timeout: number; private authToken?: string; private WebSocketClass: typeof WebSocket; @@ -31,12 +32,19 @@ export class WSClient { private isClosed = false; constructor(config: WSClientConfig) { - this.url = config.wsURL; + this.wsURL = config.wsURL; this.timeout = config.timeout ?? 30000; this.authToken = config.authToken; this.WebSocketClass = config.WebSocket ?? WebSocket; } + /** + * Get the current WebSocket URL + */ + get url(): string { + return this.wsURL; + } + /** * Connect to WebSocket server */ @@ -56,7 +64,7 @@ export class WSClient { this.ws.addEventListener("open", () => { clearTimeout(timeout); - console.log("[WSClient] Connected to", this.url); + console.log("[WSClient] Connected to", this.wsURL); this.openHandlers.forEach((handler) => handler()); resolve(); }); @@ -71,6 +79,7 @@ export class WSClient { clearTimeout(timeout); const error = new SDKError("WebSocket error", 500, "WS_ERROR", event); this.errorHandlers.forEach((handler) => handler(error)); + reject(error); }); this.ws.addEventListener("close", () => { @@ -88,7 +97,7 @@ export class WSClient { * Build WebSocket URL with auth token */ private buildWSUrl(): string { - let url = this.url; + let url = this.wsURL; if (this.authToken) { const separator = url.includes("?") ? "&" : "?"; diff --git a/src/index.ts b/src/index.ts index c97c428..f116728 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ export interface ClientConfig extends Omit { apiKey?: string; jwt?: string; storage?: StorageAdapter; - wsConfig?: Partial; + wsConfig?: Partial>; functionsConfig?: FunctionsClientConfig; fetch?: typeof fetch; } @@ -48,12 +48,8 @@ export function createClient(config: ClientConfig): Client { jwt: config.jwt, }); - // Derive WebSocket URL from baseURL if not explicitly provided - // If multiple base URLs are provided, use the first one for WebSocket (primary gateway) - const primaryBaseURL = Array.isArray(config.baseURL) ? config.baseURL[0] : config.baseURL; - const wsURL = - config.wsConfig?.wsURL ?? - primaryBaseURL.replace(/^http/, "ws").replace(/\/$/, ""); + // Derive WebSocket URL from baseURL + const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, ""); const db = new DBClient(httpClient); const pubsub = new PubSubClient(httpClient, { diff --git a/src/pubsub/client.ts b/src/pubsub/client.ts index 1805a30..87cdd29 100644 --- a/src/pubsub/client.ts +++ b/src/pubsub/client.ts @@ -55,7 +55,7 @@ function base64Decode(b64: string): string { /** * Simple PubSub client - one WebSocket connection per topic - * No connection pooling, no reference counting - keep it simple + * Gateway failover is handled at the application layer */ export class PubSubClient { private httpClient: HttpClient;