forked from Vero-protocol/vero-core-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrpc-client.ts
More file actions
67 lines (56 loc) · 2.02 KB
/
Copy pathrpc-client.ts
File metadata and controls
67 lines (56 loc) · 2.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* rpc-client.ts — RPC node failover with round-robin and health probing.
*
* Maintains an ordered list of endpoints. On any network/RPC error the call
* is retried on the next healthy endpoint. Dead nodes are quarantined for
* QUARANTINE_MS before re-admission.
*/
import { SorobanRpc } from "@stellar/stellar-sdk";
import { logger } from "./logger";
const QUARANTINE_MS = 30_000;
const MAX_RETRIES = 3;
interface Endpoint {
url: string;
deadUntil: number; // epoch ms; 0 = healthy
}
export class RpcClient {
private readonly endpoints: Endpoint[];
private cursor = 0;
constructor(urls: string[]) {
if (urls.length === 0) throw new Error("RpcClient: at least one URL required");
this.endpoints = urls.map(url => ({ url, deadUntil: 0 }));
}
/** Execute `fn` with an active SorobanRpc.Server, failing over on error. */
async call<T>(fn: (server: SorobanRpc.Server) => Promise<T>): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const ep = this.pickEndpoint();
if (!ep) throw new Error("RpcClient: all endpoints unavailable");
try {
const server = new SorobanRpc.Server(ep.url, { allowHttp: ep.url.startsWith("http://") });
return await fn(server);
} catch (err) {
lastError = err;
ep.deadUntil = Date.now() + QUARANTINE_MS;
logger.warn(`[RpcClient] ${ep.url} quarantined — ${(err as Error).message}`);
}
}
throw lastError;
}
private pickEndpoint(): Endpoint | null {
const now = Date.now();
for (let i = 0; i < this.endpoints.length; i++) {
const ep = this.endpoints[(this.cursor + i) % this.endpoints.length];
if (ep.deadUntil <= now) {
this.cursor = (this.cursor + i + 1) % this.endpoints.length;
return ep;
}
}
return null;
}
/** Expose live endpoint count (useful for health checks). */
liveCount(): number {
const now = Date.now();
return this.endpoints.filter(ep => ep.deadUntil <= now).length;
}
}