mirror of
https://github.com/DeBrosOfficial/network-ts-sdk.git
synced 2026-01-30 05:03:02 +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",
|
"name": "@debros/network-ts-sdk",
|
||||||
"version": "0.4.1",
|
"version": "0.4.2",
|
||||||
"description": "TypeScript SDK for DeBros Network Gateway",
|
"description": "TypeScript SDK for DeBros Network Gateway",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
157
src/core/http.ts
157
src/core/http.ts
@ -1,12 +1,11 @@
|
|||||||
import { SDKError } from "../errors";
|
import { SDKError } from "../errors";
|
||||||
|
|
||||||
export interface HttpClientConfig {
|
export interface HttpClientConfig {
|
||||||
baseURL: string | string[];
|
baseURL: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
retryDelayMs?: number;
|
retryDelayMs?: number;
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
gatewayHealthCheckCooldownMs?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,38 +31,22 @@ function createFetchWithTLSConfig(): typeof fetch {
|
|||||||
return globalThis.fetch;
|
return globalThis.fetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GatewayHealth {
|
|
||||||
url: string;
|
|
||||||
unhealthyUntil: number | null; // Timestamp when gateway becomes healthy again
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HttpClient {
|
export class HttpClient {
|
||||||
private baseURLs: string[];
|
private baseURL: string;
|
||||||
private currentURLIndex: number = 0;
|
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
private maxRetries: number;
|
private maxRetries: number;
|
||||||
private retryDelayMs: number;
|
private retryDelayMs: number;
|
||||||
private fetch: typeof fetch;
|
private fetch: typeof fetch;
|
||||||
private apiKey?: string;
|
private apiKey?: string;
|
||||||
private jwt?: string;
|
private jwt?: string;
|
||||||
private gatewayHealthCheckCooldownMs: number;
|
|
||||||
private gatewayHealth: Map<string, GatewayHealth>;
|
|
||||||
|
|
||||||
constructor(config: HttpClientConfig) {
|
constructor(config: HttpClientConfig) {
|
||||||
this.baseURLs = (Array.isArray(config.baseURL) ? config.baseURL : [config.baseURL])
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
||||||
.map(url => url.replace(/\/$/, ""));
|
|
||||||
this.timeout = config.timeout ?? 60000;
|
this.timeout = config.timeout ?? 60000;
|
||||||
this.maxRetries = config.maxRetries ?? 3;
|
this.maxRetries = config.maxRetries ?? 3;
|
||||||
this.retryDelayMs = config.retryDelayMs ?? 1000;
|
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
|
// Use provided fetch or create one with proper TLS configuration for staging certificates
|
||||||
this.fetch = config.fetch ?? createFetchWithTLSConfig();
|
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) {
|
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 {
|
getBaseURL(): string {
|
||||||
return this.baseURLs[this.currentURLIndex];
|
return this.baseURL;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T = any>(
|
async request<T = any>(
|
||||||
@ -235,7 +139,7 @@ export class HttpClient {
|
|||||||
} = {}
|
} = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = performance.now(); // Track request start time
|
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) {
|
if (options.query) {
|
||||||
Object.entries(options.query).forEach(([key, value]) => {
|
Object.entries(options.query).forEach(([key, value]) => {
|
||||||
url.searchParams.append(key, String(value));
|
url.searchParams.append(key, String(value));
|
||||||
@ -364,11 +268,8 @@ export class HttpClient {
|
|||||||
url: string,
|
url: string,
|
||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
attempt: number = 0,
|
attempt: number = 0,
|
||||||
startTime?: number, // Track start time for timing across retries
|
startTime?: number // Track start time for timing across retries
|
||||||
gatewayAttempt: number = 0 // Track gateway failover attempts
|
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const currentGatewayUrl = this.getCurrentBaseURL();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetch(url, options);
|
const response = await this.fetch(url, options);
|
||||||
|
|
||||||
@ -393,50 +294,20 @@ export class HttpClient {
|
|||||||
error instanceof SDKError &&
|
error instanceof SDKError &&
|
||||||
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
||||||
|
|
||||||
const isNetworkError =
|
// Retry on same gateway for retryable HTTP errors
|
||||||
error instanceof TypeError ||
|
|
||||||
(error instanceof Error && error.message.includes('fetch'));
|
|
||||||
|
|
||||||
// Retry on the same gateway first (for retryable HTTP errors)
|
|
||||||
if (isRetryableError && attempt < this.maxRetries) {
|
if (isRetryableError && attempt < this.maxRetries) {
|
||||||
if (typeof console !== "undefined") {
|
if (typeof console !== "undefined") {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[HttpClient] Retrying request on same gateway (attempt ${attempt + 1}/${this.maxRetries}):`,
|
`[HttpClient] Retrying request (attempt ${attempt + 1}/${this.maxRetries})`
|
||||||
currentGatewayUrl
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await new Promise((resolve) =>
|
await new Promise((resolve) =>
|
||||||
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
setTimeout(resolve, this.retryDelayMs * (attempt + 1))
|
||||||
);
|
);
|
||||||
return this.requestWithRetry(url, options, attempt + 1, startTime, gatewayAttempt);
|
return this.requestWithRetry(url, options, attempt + 1, startTime);
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All retries exhausted - throw error for app to handle
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -483,7 +354,7 @@ export class HttpClient {
|
|||||||
}
|
}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const startTime = performance.now(); // Track upload start time
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
...this.getAuthHeaders(path),
|
...this.getAuthHeaders(path),
|
||||||
// Don't set Content-Type - browser will set it with boundary
|
// 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)
|
* Get a binary response (returns Response object for streaming)
|
||||||
*/
|
*/
|
||||||
async getBinary(path: string): Promise<Response> {
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
...this.getAuthHeaders(path),
|
...this.getAuthHeaders(path),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,10 +15,11 @@ export type WSOpenHandler = () => void;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple WebSocket client with minimal abstractions
|
* 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 {
|
export class WSClient {
|
||||||
private url: string;
|
private wsURL: string;
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
private authToken?: string;
|
private authToken?: string;
|
||||||
private WebSocketClass: typeof WebSocket;
|
private WebSocketClass: typeof WebSocket;
|
||||||
@ -31,12 +32,19 @@ export class WSClient {
|
|||||||
private isClosed = false;
|
private isClosed = false;
|
||||||
|
|
||||||
constructor(config: WSClientConfig) {
|
constructor(config: WSClientConfig) {
|
||||||
this.url = config.wsURL;
|
this.wsURL = config.wsURL;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current WebSocket URL
|
||||||
|
*/
|
||||||
|
get url(): string {
|
||||||
|
return this.wsURL;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to WebSocket server
|
* Connect to WebSocket server
|
||||||
*/
|
*/
|
||||||
@ -56,7 +64,7 @@ export class WSClient {
|
|||||||
|
|
||||||
this.ws.addEventListener("open", () => {
|
this.ws.addEventListener("open", () => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
console.log("[WSClient] Connected to", this.url);
|
console.log("[WSClient] Connected to", this.wsURL);
|
||||||
this.openHandlers.forEach((handler) => handler());
|
this.openHandlers.forEach((handler) => handler());
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
@ -71,6 +79,7 @@ export class WSClient {
|
|||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
const error = new SDKError("WebSocket error", 500, "WS_ERROR", event);
|
||||||
this.errorHandlers.forEach((handler) => handler(error));
|
this.errorHandlers.forEach((handler) => handler(error));
|
||||||
|
reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ws.addEventListener("close", () => {
|
this.ws.addEventListener("close", () => {
|
||||||
@ -88,7 +97,7 @@ export class WSClient {
|
|||||||
* Build WebSocket URL with auth token
|
* Build WebSocket URL with auth token
|
||||||
*/
|
*/
|
||||||
private buildWSUrl(): string {
|
private buildWSUrl(): string {
|
||||||
let url = this.url;
|
let url = this.wsURL;
|
||||||
|
|
||||||
if (this.authToken) {
|
if (this.authToken) {
|
||||||
const separator = url.includes("?") ? "&" : "?";
|
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;
|
apiKey?: string;
|
||||||
jwt?: string;
|
jwt?: string;
|
||||||
storage?: StorageAdapter;
|
storage?: StorageAdapter;
|
||||||
wsConfig?: Partial<WSClientConfig>;
|
wsConfig?: Partial<Omit<WSClientConfig, "wsURL">>;
|
||||||
functionsConfig?: FunctionsClientConfig;
|
functionsConfig?: FunctionsClientConfig;
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
}
|
}
|
||||||
@ -48,12 +48,8 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
jwt: config.jwt,
|
jwt: config.jwt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive WebSocket URL from baseURL if not explicitly provided
|
// Derive WebSocket URL from baseURL
|
||||||
// If multiple base URLs are provided, use the first one for WebSocket (primary gateway)
|
const wsURL = config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
||||||
const primaryBaseURL = Array.isArray(config.baseURL) ? config.baseURL[0] : config.baseURL;
|
|
||||||
const wsURL =
|
|
||||||
config.wsConfig?.wsURL ??
|
|
||||||
primaryBaseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
|
||||||
|
|
||||||
const db = new DBClient(httpClient);
|
const db = new DBClient(httpClient);
|
||||||
const pubsub = new PubSubClient(httpClient, {
|
const pubsub = new PubSubClient(httpClient, {
|
||||||
|
|||||||
@ -55,7 +55,7 @@ function base64Decode(b64: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple PubSub client - one WebSocket connection per topic
|
* 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 {
|
export class PubSubClient {
|
||||||
private httpClient: HttpClient;
|
private httpClient: HttpClient;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user