mirror of
https://github.com/DeBrosOfficial/network-ts-sdk.git
synced 2026-01-30 05:03:02 +00:00
Bump version to 0.4.1 and enhance HttpClient to support multiple base URLs and gateway health checks
This commit is contained in:
parent
0ed9e9a00e
commit
dbe40c6f16
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@debros/network-ts-sdk",
|
"name": "@debros/network-ts-sdk",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"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",
|
||||||
|
|||||||
171
src/core/http.ts
171
src/core/http.ts
@ -1,11 +1,12 @@
|
|||||||
import { SDKError } from "../errors";
|
import { SDKError } from "../errors";
|
||||||
|
|
||||||
export interface HttpClientConfig {
|
export interface HttpClientConfig {
|
||||||
baseURL: string;
|
baseURL: string | string[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
maxRetries?: number;
|
maxRetries?: number;
|
||||||
retryDelayMs?: number;
|
retryDelayMs?: number;
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
|
gatewayHealthCheckCooldownMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,22 +32,38 @@ 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 baseURL: string;
|
private baseURLs: 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.baseURL = config.baseURL.replace(/\/$/, "");
|
this.baseURLs = (Array.isArray(config.baseURL) ? config.baseURL : [config.baseURL])
|
||||||
this.timeout = config.timeout ?? 60000; // Increased from 30s to 60s for pub/sub operations
|
.map(url => url.replace(/\/$/, ""));
|
||||||
|
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) {
|
||||||
@ -121,6 +138,92 @@ export class HttpClient {
|
|||||||
return this.apiKey;
|
return this.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current base URL (for single gateway or current gateway in multi-gateway setup)
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
async request<T = any>(
|
async request<T = any>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
path: string,
|
path: string,
|
||||||
@ -132,7 +235,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.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + 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));
|
||||||
@ -261,8 +364,11 @@ 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);
|
||||||
|
|
||||||
@ -276,22 +382,61 @@ export class HttpClient {
|
|||||||
throw SDKError.fromResponse(response.status, body);
|
throw SDKError.fromResponse(response.status, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request succeeded - return response
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
if (contentType?.includes("application/json")) {
|
if (contentType?.includes("application/json")) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
return response.text();
|
return response.text();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
const isRetryableError =
|
||||||
error instanceof SDKError &&
|
error instanceof SDKError &&
|
||||||
attempt < this.maxRetries &&
|
[408, 429, 500, 502, 503, 504].includes(error.httpStatus);
|
||||||
[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)
|
||||||
|
if (isRetryableError && attempt < this.maxRetries) {
|
||||||
|
if (typeof console !== "undefined") {
|
||||||
|
console.warn(
|
||||||
|
`[HttpClient] Retrying request on same gateway (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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -338,7 +483,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.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + 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
|
||||||
@ -391,7 +536,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.baseURL + path);
|
const url = new URL(this.getCurrentBaseURL() + path);
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
...this.getAuthHeaders(path),
|
...this.getAuthHeaders(path),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,9 +49,11 @@ export function createClient(config: ClientConfig): Client {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Derive WebSocket URL from baseURL if not explicitly provided
|
// 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 =
|
const wsURL =
|
||||||
config.wsConfig?.wsURL ??
|
config.wsConfig?.wsURL ??
|
||||||
config.baseURL.replace(/^http/, "ws").replace(/\/$/, "");
|
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, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user