mirror of
https://github.com/DeBrosOfficial/network-ts-sdk.git
synced 2026-01-29 20:53:04 +00:00
Refactor HttpClient and WSClient to simplify configuration and improve clarity; remove unused gateway health logic
This commit is contained in:
parent
dbe40c6f16
commit
25303e7913
@ -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",
|
||||
|
||||
157
src/core/http.ts
157
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<string, GatewayHealth>;
|
||||
|
||||
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<T = any>(
|
||||
@ -235,7 +139,7 @@ export class HttpClient {
|
||||
} = {}
|
||||
): Promise<T> {
|
||||
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<any> {
|
||||
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<T> {
|
||||
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<string, string> = {
|
||||
...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<Response> {
|
||||
const url = new URL(this.getCurrentBaseURL() + path);
|
||||
const url = new URL(this.baseURL + path);
|
||||
const headers: Record<string, string> = {
|
||||
...this.getAuthHeaders(path),
|
||||
};
|
||||
|
||||
@ -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("?") ? "&" : "?";
|
||||
|
||||
10
src/index.ts
10
src/index.ts
@ -17,7 +17,7 @@ export interface ClientConfig extends Omit<HttpClientConfig, "fetch"> {
|
||||
apiKey?: string;
|
||||
jwt?: string;
|
||||
storage?: StorageAdapter;
|
||||
wsConfig?: Partial<WSClientConfig>;
|
||||
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
||||
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, {
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user