ethers-rpc-pool 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -15
- package/dist/index.cjs +5 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/package.json +18 -1
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
|
|
1
4
|
# ethers-rpc-pool
|
|
2
5
|
|
|
3
6
|
Multi-endpoint RPC pool provider for **ethers.js** with built-in load balancing, per-endpoint concurrency limits, retry with exponential backoff, and instrumentation.
|
|
@@ -17,7 +20,7 @@ Designed for production backends and dApps that need:
|
|
|
17
20
|
Most production apps rely on a single RPC provider. This creates:
|
|
18
21
|
|
|
19
22
|
- Single point of failure
|
|
20
|
-
- Hard
|
|
23
|
+
- Hard concurrency limits (RPS / in-flight)
|
|
21
24
|
- Increased timeout risk during traffic spikes
|
|
22
25
|
- Cascading retry storms
|
|
23
26
|
|
|
@@ -42,6 +45,13 @@ Most production apps rely on a single RPC provider. This creates:
|
|
|
42
45
|
|
|
43
46
|
---
|
|
44
47
|
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- Node >= 18
|
|
51
|
+
- ethers v6
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
45
55
|
## Installation
|
|
46
56
|
|
|
47
57
|
```bash
|
|
@@ -55,17 +65,17 @@ npm install ethers-rpc-pool
|
|
|
55
65
|
```ts
|
|
56
66
|
import { RPCPoolProvider } from 'ethers-rpc-pool';
|
|
57
67
|
|
|
58
|
-
const
|
|
68
|
+
const poolProvider = new RPCPoolProvider({
|
|
59
69
|
chainId: 1,
|
|
60
70
|
urls: ['http://rpc1.invalid', 'http://rpc2.invalid'],
|
|
61
|
-
perUrl: { inFlight: 1 },
|
|
71
|
+
perUrl: { inFlight: 1, timeout: 3000 },
|
|
62
72
|
retry: { attempts: 2 },
|
|
63
73
|
});
|
|
64
74
|
|
|
65
75
|
// Use it like a regular `JsonRpcProvider`:
|
|
66
76
|
|
|
67
|
-
const blockNumber = await
|
|
68
|
-
const balance = await
|
|
77
|
+
const blockNumber = await poolProvider.getBlockNumber();
|
|
78
|
+
const balance = await poolProvider.getBalance('0x...');
|
|
69
79
|
```
|
|
70
80
|
|
|
71
81
|
---
|
|
@@ -92,13 +102,14 @@ interface RPCPoolProviderParams {
|
|
|
92
102
|
|
|
93
103
|
### Options Explained
|
|
94
104
|
|
|
95
|
-
| Option | Description
|
|
96
|
-
| ----------------- |
|
|
97
|
-
| `chainId` | Target chain ID
|
|
98
|
-
| `urls` | List of RPC endpoints
|
|
99
|
-
| `perUrl.inFlight` | Max concurrent requests per endpoint
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
105
|
+
| Option | Description |
|
|
106
|
+
| ----------------- | ------------------------------------------------------- |
|
|
107
|
+
| `chainId` | Target chain ID |
|
|
108
|
+
| `urls` | List of RPC endpoints |
|
|
109
|
+
| `perUrl.inFlight` | Max concurrent requests per endpoint |
|
|
110
|
+
| `perUrl.timeout` | Timeout in ms for each request to this URL, default 10s |
|
|
111
|
+
| `retry.attempts` | Maximum number of unique endpoints to try |
|
|
112
|
+
| `hooks.onEvent` | Optional instrumentation hook |
|
|
102
113
|
|
|
103
114
|
---
|
|
104
115
|
|
|
@@ -150,7 +161,7 @@ Retries only happen on errors considered failover-safe.
|
|
|
150
161
|
You can subscribe to RPC lifecycle events:
|
|
151
162
|
|
|
152
163
|
```typescript
|
|
153
|
-
const
|
|
164
|
+
const poolProvider = new RPCPoolProvider({
|
|
154
165
|
// ...
|
|
155
166
|
hooks: {
|
|
156
167
|
onEvent(event) {
|
|
@@ -170,7 +181,7 @@ This allows integration with:
|
|
|
170
181
|
|
|
171
182
|
```ts
|
|
172
183
|
const stats = pool.getStats();
|
|
173
|
-
console.log(
|
|
184
|
+
console.log(stats.snapshot());
|
|
174
185
|
```
|
|
175
186
|
|
|
176
187
|
### Example output:
|
|
@@ -228,7 +239,7 @@ Useful for:
|
|
|
228
239
|
|
|
229
240
|
### Known Limitations
|
|
230
241
|
|
|
231
|
-
-
|
|
242
|
+
- Basic circuit breaker/cooldown
|
|
232
243
|
- No sticky session/blockTag consistency yet
|
|
233
244
|
- No built-in JSON-RPC batching
|
|
234
245
|
- Archive/debug/trace methods depend on underlying RPC support
|
|
@@ -271,6 +282,7 @@ Not intended for:
|
|
|
271
282
|
|
|
272
283
|
## Roadmap
|
|
273
284
|
|
|
285
|
+
- RPS rate limiting
|
|
274
286
|
- Circuit breaker + health scoring
|
|
275
287
|
- Sticky session / blockTag consistency
|
|
276
288
|
- Adaptive latency-based routing
|
package/dist/index.cjs
CHANGED
|
@@ -195,11 +195,12 @@ var Semaphore = class {
|
|
|
195
195
|
// src/InstrumentedProvider.ts
|
|
196
196
|
var import_ethers = require("ethers");
|
|
197
197
|
var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
|
|
198
|
-
constructor(url, chainId, providerId, stats, limiter, onEvent) {
|
|
198
|
+
constructor(url, chainId, providerId, stats, limiter, timeout, onEvent) {
|
|
199
199
|
const network = import_ethers.Network.from(chainId);
|
|
200
200
|
super(url, chainId, { staticNetwork: network });
|
|
201
201
|
this.stats = stats;
|
|
202
202
|
this.limiter = limiter;
|
|
203
|
+
this.timeout = timeout;
|
|
203
204
|
this.onEvent = onEvent;
|
|
204
205
|
this.providerId = providerId;
|
|
205
206
|
this.chainId = chainId;
|
|
@@ -230,7 +231,7 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
|
|
|
230
231
|
});
|
|
231
232
|
try {
|
|
232
233
|
const base = super.send(method, params);
|
|
233
|
-
const res = await withTimeout(base,
|
|
234
|
+
const res = await withTimeout(base, this.timeout, {
|
|
234
235
|
chainId: this.chainId,
|
|
235
236
|
providerId: this.providerId,
|
|
236
237
|
method
|
|
@@ -321,12 +322,14 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
|
|
|
321
322
|
const endpoints = this.params.urls.map((url, i) => {
|
|
322
323
|
const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
|
|
323
324
|
const limiter = new Semaphore(this.params.perUrl.inFlight);
|
|
325
|
+
const timeout = this.params.perUrl.timeout ?? 1e4;
|
|
324
326
|
const provider = new InstrumentedStaticJsonRpcProvider(
|
|
325
327
|
url,
|
|
326
328
|
this.params.chainId,
|
|
327
329
|
providerId,
|
|
328
330
|
this.stats,
|
|
329
331
|
limiter,
|
|
332
|
+
timeout,
|
|
330
333
|
this.params.hooks?.onEvent
|
|
331
334
|
);
|
|
332
335
|
return { providerId, url, provider, limiter };
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/Semaphore.ts","../src/InstrumentedProvider.ts","../src/Router.ts"],"sourcesContent":["export { RPCPoolProvider } from './RpcPoolProvider';\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Stats } from './Stats';\nimport { Endpoint, RpcEvent, shouldFailover } from './utils';\nimport { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\n\ninterface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number };\n retry: { attempts: number };\n hooks?: {\n onEvent(e: RpcEvent): void;\n };\n}\n\n// TODO\n// -- circuit breaker + health checks\n// -- sticky “session”\n\nexport class RPCPoolProvider extends JsonRpcProvider {\n readonly router: Router;\n readonly params: RPCPoolProviderParams;\n readonly stats: Stats;\n\n constructor(params: RPCPoolProviderParams) {\n const network = Network.from(params.chainId);\n super('http://localhost', network, { staticNetwork: network });\n\n this.params = params;\n\n this.stats = new Stats();\n\n const endpoints: Endpoint[] = this.params.urls.map((url, i) => {\n const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;\n const limiter = new Semaphore(this.params.perUrl.inFlight);\n\n const provider = new InstrumentedStaticJsonRpcProvider(\n url,\n this.params.chainId,\n providerId,\n this.stats,\n limiter,\n this.params.hooks?.onEvent,\n );\n\n return { providerId, url, provider, limiter };\n });\n\n this.router = new Router(endpoints, this.stats);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async send(method: string, params: any): Promise<any> {\n const tried = new Set<string>();\n const maxUniqueTries = Math.min(this.params.retry.attempts, this.router.size());\n\n while (tried.size < maxUniqueTries) {\n const ep = this.router.pick();\n if (tried.has(ep.providerId)) continue;\n tried.add(ep.providerId);\n\n try {\n return await ep.provider.send(method, params);\n } catch (e: any) {\n if (!shouldFailover(e)) throw e;\n if (tried.size >= maxUniqueTries) throw e;\n\n // Add exponential backoff with jitter before retry\n const baseDelay = Math.min(1000 * Math.pow(2, tried.size - 1), 5000);\n const jitter = Math.random() * baseDelay;\n await new Promise((resolve) => setTimeout(resolve, jitter));\n }\n }\n\n throw new Error('No RPC available');\n }\n\n getStats(): Stats {\n return this.stats;\n }\n}\n","export interface RpcStatsSnapshot {\n total: number;\n inFlight: number;\n perMethodTotal: Record<string, number>;\n rateLimitedTotal: number;\n perProviderRateLimited: Record<string, number>;\n timeoutTotal: number;\n perProviderTimeout: Record<string, number>;\n perProviderTotal: Record<string, number>;\n providerCooldownUntil: Record<string, number>;\n perProviderInFlight: Record<string, number>;\n}\n\nexport class Stats {\n private _total = 0;\n private _inFlight = 0;\n\n private _perMethod: Record<string, number> = {};\n\n private _rateLimitedTotal = 0;\n private _timeoutTotal = 0;\n\n private _perProviderInFlight: Record<string, number> = {};\n private _perProviderTotal: Record<string, number> = {};\n private _perProviderTimeout: Record<string, number> = {};\n private _perProviderRateLimited: Record<string, number> = {};\n\n private _providerCooldownUntil: Record<string, number> = {};\n\n private _bump(map: Record<string, number>, key: string) {\n map[key] = (map[key] || 0) + 1;\n }\n\n private _decrease(map: Record<string, number>, key: string) {\n map[key] = Math.max((map[key] || 0) - 1, 0);\n }\n\n private _bumpTotal() {\n this._total++;\n }\n\n private _bumpInFlight() {\n this._inFlight++;\n }\n\n private _bumpRateLimitedTotal() {\n this._rateLimitedTotal++;\n }\n\n private _bumpTimeoutTotal() {\n this._timeoutTotal++;\n }\n\n bumpInFlightPerProvider(id: string) {\n this._bumpInFlight();\n this._bump(this._perProviderInFlight, id);\n }\n\n decreaseInFlightPerProvider(id: string) {\n this.decreaseInFlight();\n this._decrease(this._perProviderInFlight, id);\n }\n\n decreaseInFlight() {\n this._inFlight = Math.max(this._inFlight - 1, 0);\n }\n\n bumpPerMethod(method: string) {\n this._bump(this._perMethod, method);\n }\n\n bumpRateLimitedPerProvider(id: string) {\n this._bumpRateLimitedTotal();\n this._bump(this._perProviderRateLimited, id);\n }\n\n bumpTimeoutPerProvider(id: string) {\n this._bumpTimeoutTotal();\n this._bump(this._perProviderTimeout, id);\n }\n\n bumpProviderTotal(id: string) {\n this._bumpTotal();\n this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;\n }\n\n timeoutRatio(id: string) {\n const t = this._perProviderTimeout[id] || 0;\n const n = this._perProviderTotal[id] || 0;\n return n ? t / n : 0;\n }\n\n isInCooldown(id: string) {\n return (this._providerCooldownUntil[id] || 0) > Date.now();\n }\n\n setCooldown(id: string, ms: number) {\n this._providerCooldownUntil[id] = Date.now() + ms;\n }\n\n snapshot(): Readonly<RpcStatsSnapshot> {\n return {\n total: this._total,\n inFlight: this._inFlight,\n perMethodTotal: { ...this._perMethod },\n rateLimitedTotal: this._rateLimitedTotal,\n timeoutTotal: this._timeoutTotal,\n perProviderInFlight: { ...this._perProviderInFlight },\n perProviderRateLimited: { ...this._perProviderRateLimited },\n perProviderTimeout: { ...this._perProviderTimeout },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { FetchResponse } from 'ethers';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\n limiter: Semaphore;\n}\n\nexport type RpcEvent =\n | {\n type: 'request';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n }\n | {\n type: 'response';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n }\n | {\n type: 'error';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n isRateLimit: boolean;\n isTimeout: boolean;\n status?: number;\n code?: string;\n message: string;\n };\n\nexport function getHttpStatus(e: any): number | undefined {\n return (\n e?.status ??\n e?.response?.status ??\n e?.response?.statusCode ??\n e?.error?.status ??\n e?.error?.response?.status ??\n e?.body?.statusCode // sometimes present\n );\n}\n\nexport function isRateLimitError(e: any): boolean {\n const status = getHttpStatus(e);\n if (status === 429 || status === 402) return true;\n\n const msg = String(e?.message || e);\n if (/error code:\\s*1015/i.test(msg)) return true; // Cloudflare\n return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function getRetryAfterMs(e: any): number | null {\n const ra =\n e?.response?.headers?.get?.('retry-after') ??\n e?.response?.headers?.['retry-after'] ??\n e?.headers?.['retry-after'];\n const n = Number(ra);\n return Number.isFinite(n) ? n * 1000 : null;\n}\n\nexport function isTimeoutError(e: any): boolean {\n // ethers v5\n if (e?.code === 'TIMEOUT') return true;\n\n const status = getHttpStatus(e);\n // some RPCs / proxies return 504 on timeout\n if (status === 504) return true;\n\n const msg = String(e?.message || e);\n\n // node-fetch / undici / axios / nginx / generic\n return /timeout|timed out|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|504 Gateway/i.test(msg);\n}\n\n/**\n * Wraps a promise with a timeout (useful when an RPC hangs without emitting TIMEOUT).\n * Important: this does not cancel the network request, but you get a controlled error\n * and FallbackProvider can switch to another RPC.\n */\nexport function withTimeout<T>(\n p: Promise<T>,\n ms: number,\n meta?: { chainId?: number; providerId?: string; method?: string },\n): Promise<T> {\n let t: NodeJS.Timeout | undefined;\n\n const timeout = new Promise<T>((_, reject) => {\n t = setTimeout(() => {\n const err: any = new Error(\n `RPC timeout after ${ms}ms` +\n (meta?.method ? ` method=${meta.method}` : '') +\n (meta?.providerId ? ` provider=${meta.providerId}` : '') +\n (meta?.chainId != null ? ` chainId=${meta.chainId}` : ''),\n );\n err.code = 'TIMEOUT';\n err.timeout = ms;\n reject(err);\n }, ms);\n });\n\n return Promise.race([p, timeout]).finally(() => t && clearTimeout(t));\n}\n\nexport function shouldFailover(e: any): boolean {\n const to = isTimeoutError(e);\n const rl = isRateLimitError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl;\n}\n","export class Semaphore {\n private inUse = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly max: number) {\n if (!Number.isFinite(max) || max <= 0) {\n throw new Error(`Semaphore max must be a positive number, got: ${max}`);\n }\n }\n\n async acquire(): Promise<() => void> {\n if (this.inUse < this.max) {\n this.inUse++;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n this.release();\n };\n }\n\n return new Promise<() => void>((resolve) => {\n this.queue.push(() => {\n this.inUse++;\n let released = false;\n resolve(() => {\n if (released) return;\n released = true;\n this.release();\n });\n });\n });\n }\n\n private release() {\n this.inUse = Math.max(0, this.inUse - 1);\n const next = this.queue.shift();\n if (next) next();\n }\n}\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isTimeoutError,\n RpcEvent,\n withTimeout,\n} from './utils';\n\n/**\n * Instrumented StaticJsonRpcProvider.\n * Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.\n */\nexport class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {\n readonly providerId: string;\n readonly chainId: number;\n\n constructor(\n url: string,\n chainId: number,\n providerId: string,\n private readonly stats: Stats,\n private readonly limiter: Semaphore,\n private readonly onEvent?: (e: RpcEvent) => void,\n ) {\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n }\n\n async send(method: string, params: any): Promise<any> {\n const release = this.limiter ? await this.limiter.acquire() : undefined;\n\n try {\n return await this._sendInstrumented(method, params);\n } finally {\n release?.();\n }\n }\n\n // ethers v5 calls send(method, params)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async _sendInstrumented(method: string, params: any): Promise<any> {\n const startedAt = Date.now();\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n this.stats.bumpPerMethod(method);\n\n this.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n });\n\n try {\n const base = super.send(method, params);\n const res = await withTimeout(base, 10_000, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n\n return res;\n } catch (e: any) {\n const endedAt = Date.now();\n const rl = isRateLimitError(e);\n if (rl) {\n this.stats.bumpRateLimitedPerProvider(this.providerId);\n const cooldownMs = 10_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs);\n }\n\n const isTimeout = isTimeoutError(e);\n if (isTimeout) {\n this.stats.bumpTimeoutPerProvider(this.providerId);\n\n const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;\n const ratio = this.stats.timeoutRatio(this.providerId);\n\n // thresholds: do not ban on a single timeout, only after enough data\n if (n >= 50 && ratio >= 0.2) {\n const cooldownMs = ratio >= 0.5 ? 600 * 1000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n this.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n isRateLimit: rl,\n isTimeout: isTimeout,\n status: getHttpStatus(e),\n code: e?.code,\n message: String(e?.message || e),\n });\n\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\n}\n","import { Endpoint } from './utils';\nimport { Stats } from './Stats';\n\nexport class Router {\n private rr = 0;\n\n constructor(\n private readonly endpoints: Endpoint[],\n private readonly stats: Stats,\n ) {}\n\n size(): number {\n return this.endpoints.length;\n }\n\n pick(): Endpoint {\n const n = this.endpoints.length;\n for (let k = 0; k < n; k++) {\n const i = ((this.rr++ % n) + n) % n;\n const ep = this.endpoints[i];\n\n if (!this.stats.isInCooldown(ep.providerId)) return ep;\n }\n // if all are in cooldown, return the next one in round-robin order\n return this.endpoints[((this.rr++ % n) + n) % n];\n }\n}\n"],"mappings":"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","names":["import_ethers"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/Semaphore.ts","../src/InstrumentedProvider.ts","../src/Router.ts"],"sourcesContent":["export { RPCPoolProvider, RPCPoolProviderParams } from './RpcPoolProvider';\nexport type { RpcEvent } from './utils';\nexport type { RpcStatsSnapshot } from './Stats';\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Stats } from './Stats';\nimport { Endpoint, RpcEvent, shouldFailover } from './utils';\nimport { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\n\nexport interface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number; timeout?: number };\n retry: { attempts: number };\n hooks?: {\n onEvent(e: RpcEvent): void;\n };\n}\n\n// TODO\n// -- circuit breaker + health checks\n// -- sticky “session”\n\nexport class RPCPoolProvider extends JsonRpcProvider {\n readonly router: Router;\n readonly params: RPCPoolProviderParams;\n readonly stats: Stats;\n\n constructor(params: RPCPoolProviderParams) {\n const network = Network.from(params.chainId);\n super('http://localhost', network, { staticNetwork: network });\n\n this.params = params;\n\n this.stats = new Stats();\n\n const endpoints: Endpoint[] = this.params.urls.map((url, i) => {\n const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;\n const limiter = new Semaphore(this.params.perUrl.inFlight);\n\n const timeout = this.params.perUrl.timeout ?? 10_000;\n\n const provider = new InstrumentedStaticJsonRpcProvider(\n url,\n this.params.chainId,\n providerId,\n this.stats,\n limiter,\n timeout,\n this.params.hooks?.onEvent,\n );\n\n return { providerId, url, provider, limiter };\n });\n\n this.router = new Router(endpoints, this.stats);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async send(method: string, params: any): Promise<any> {\n const tried = new Set<string>();\n const maxUniqueTries = Math.min(this.params.retry.attempts, this.router.size());\n\n while (tried.size < maxUniqueTries) {\n const ep = this.router.pick();\n if (tried.has(ep.providerId)) continue;\n tried.add(ep.providerId);\n\n try {\n return await ep.provider.send(method, params);\n } catch (e: any) {\n if (!shouldFailover(e)) throw e;\n if (tried.size >= maxUniqueTries) throw e;\n\n // Add exponential backoff with jitter before retry\n const baseDelay = Math.min(1000 * Math.pow(2, tried.size - 1), 5000);\n const jitter = Math.random() * baseDelay;\n await new Promise((resolve) => setTimeout(resolve, jitter));\n }\n }\n\n throw new Error('No RPC available');\n }\n\n getStats(): Stats {\n return this.stats;\n }\n}\n","export interface RpcStatsSnapshot {\n total: number;\n inFlight: number;\n perMethodTotal: Record<string, number>;\n rateLimitedTotal: number;\n perProviderRateLimited: Record<string, number>;\n timeoutTotal: number;\n perProviderTimeout: Record<string, number>;\n perProviderTotal: Record<string, number>;\n providerCooldownUntil: Record<string, number>;\n perProviderInFlight: Record<string, number>;\n}\n\nexport class Stats {\n private _total = 0;\n private _inFlight = 0;\n\n private _perMethod: Record<string, number> = {};\n\n private _rateLimitedTotal = 0;\n private _timeoutTotal = 0;\n\n private _perProviderInFlight: Record<string, number> = {};\n private _perProviderTotal: Record<string, number> = {};\n private _perProviderTimeout: Record<string, number> = {};\n private _perProviderRateLimited: Record<string, number> = {};\n\n private _providerCooldownUntil: Record<string, number> = {};\n\n private _bump(map: Record<string, number>, key: string) {\n map[key] = (map[key] || 0) + 1;\n }\n\n private _decrease(map: Record<string, number>, key: string) {\n map[key] = Math.max((map[key] || 0) - 1, 0);\n }\n\n private _bumpTotal() {\n this._total++;\n }\n\n private _bumpInFlight() {\n this._inFlight++;\n }\n\n private _bumpRateLimitedTotal() {\n this._rateLimitedTotal++;\n }\n\n private _bumpTimeoutTotal() {\n this._timeoutTotal++;\n }\n\n bumpInFlightPerProvider(id: string) {\n this._bumpInFlight();\n this._bump(this._perProviderInFlight, id);\n }\n\n decreaseInFlightPerProvider(id: string) {\n this.decreaseInFlight();\n this._decrease(this._perProviderInFlight, id);\n }\n\n decreaseInFlight() {\n this._inFlight = Math.max(this._inFlight - 1, 0);\n }\n\n bumpPerMethod(method: string) {\n this._bump(this._perMethod, method);\n }\n\n bumpRateLimitedPerProvider(id: string) {\n this._bumpRateLimitedTotal();\n this._bump(this._perProviderRateLimited, id);\n }\n\n bumpTimeoutPerProvider(id: string) {\n this._bumpTimeoutTotal();\n this._bump(this._perProviderTimeout, id);\n }\n\n bumpProviderTotal(id: string) {\n this._bumpTotal();\n this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;\n }\n\n timeoutRatio(id: string) {\n const t = this._perProviderTimeout[id] || 0;\n const n = this._perProviderTotal[id] || 0;\n return n ? t / n : 0;\n }\n\n isInCooldown(id: string) {\n return (this._providerCooldownUntil[id] || 0) > Date.now();\n }\n\n setCooldown(id: string, ms: number) {\n this._providerCooldownUntil[id] = Date.now() + ms;\n }\n\n snapshot(): Readonly<RpcStatsSnapshot> {\n return {\n total: this._total,\n inFlight: this._inFlight,\n perMethodTotal: { ...this._perMethod },\n rateLimitedTotal: this._rateLimitedTotal,\n timeoutTotal: this._timeoutTotal,\n perProviderInFlight: { ...this._perProviderInFlight },\n perProviderRateLimited: { ...this._perProviderRateLimited },\n perProviderTimeout: { ...this._perProviderTimeout },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { FetchResponse } from 'ethers';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\n limiter: Semaphore;\n}\n\nexport type RpcEvent =\n | {\n type: 'request';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n }\n | {\n type: 'response';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n }\n | {\n type: 'error';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n isRateLimit: boolean;\n isTimeout: boolean;\n status?: number;\n code?: string;\n message: string;\n };\n\nexport function getHttpStatus(e: any): number | undefined {\n return (\n e?.status ??\n e?.response?.status ??\n e?.response?.statusCode ??\n e?.error?.status ??\n e?.error?.response?.status ??\n e?.body?.statusCode // sometimes present\n );\n}\n\nexport function isRateLimitError(e: any): boolean {\n const status = getHttpStatus(e);\n if (status === 429 || status === 402) return true;\n\n const msg = String(e?.message || e);\n if (/error code:\\s*1015/i.test(msg)) return true; // Cloudflare\n return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function getRetryAfterMs(e: any): number | null {\n const ra =\n e?.response?.headers?.get?.('retry-after') ??\n e?.response?.headers?.['retry-after'] ??\n e?.headers?.['retry-after'];\n const n = Number(ra);\n return Number.isFinite(n) ? n * 1000 : null;\n}\n\nexport function isTimeoutError(e: any): boolean {\n // ethers v5\n if (e?.code === 'TIMEOUT') return true;\n\n const status = getHttpStatus(e);\n // some RPCs / proxies return 504 on timeout\n if (status === 504) return true;\n\n const msg = String(e?.message || e);\n\n // node-fetch / undici / axios / nginx / generic\n return /timeout|timed out|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|504 Gateway/i.test(msg);\n}\n\n/**\n * Wraps a promise with a timeout (useful when an RPC hangs without emitting TIMEOUT).\n * Important: this does not cancel the network request, but you get a controlled error\n * and FallbackProvider can switch to another RPC.\n */\nexport function withTimeout<T>(\n p: Promise<T>,\n ms: number,\n meta?: { chainId?: number; providerId?: string; method?: string },\n): Promise<T> {\n let t: NodeJS.Timeout | undefined;\n\n const timeout = new Promise<T>((_, reject) => {\n t = setTimeout(() => {\n const err: any = new Error(\n `RPC timeout after ${ms}ms` +\n (meta?.method ? ` method=${meta.method}` : '') +\n (meta?.providerId ? ` provider=${meta.providerId}` : '') +\n (meta?.chainId != null ? ` chainId=${meta.chainId}` : ''),\n );\n err.code = 'TIMEOUT';\n err.timeout = ms;\n reject(err);\n }, ms);\n });\n\n return Promise.race([p, timeout]).finally(() => t && clearTimeout(t));\n}\n\nexport function shouldFailover(e: any): boolean {\n const to = isTimeoutError(e);\n const rl = isRateLimitError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl;\n}\n","export class Semaphore {\n private inUse = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly max: number) {\n if (!Number.isFinite(max) || max <= 0) {\n throw new Error(`Semaphore max must be a positive number, got: ${max}`);\n }\n }\n\n async acquire(): Promise<() => void> {\n if (this.inUse < this.max) {\n this.inUse++;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n this.release();\n };\n }\n\n return new Promise<() => void>((resolve) => {\n this.queue.push(() => {\n this.inUse++;\n let released = false;\n resolve(() => {\n if (released) return;\n released = true;\n this.release();\n });\n });\n });\n }\n\n private release() {\n this.inUse = Math.max(0, this.inUse - 1);\n const next = this.queue.shift();\n if (next) next();\n }\n}\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isTimeoutError,\n RpcEvent,\n withTimeout,\n} from './utils';\n\n/**\n * Instrumented StaticJsonRpcProvider.\n * Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.\n */\nexport class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {\n readonly providerId: string;\n readonly chainId: number;\n\n constructor(\n url: string,\n chainId: number,\n providerId: string,\n private readonly stats: Stats,\n private readonly limiter: Semaphore,\n private readonly timeout: number,\n private readonly onEvent?: (e: RpcEvent) => void,\n ) {\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n }\n\n async send(method: string, params: any): Promise<any> {\n const release = this.limiter ? await this.limiter.acquire() : undefined;\n\n try {\n return await this._sendInstrumented(method, params);\n } finally {\n release?.();\n }\n }\n\n // ethers v5 calls send(method, params)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async _sendInstrumented(method: string, params: any): Promise<any> {\n const startedAt = Date.now();\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n this.stats.bumpPerMethod(method);\n\n this.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n });\n\n try {\n const base = super.send(method, params);\n const res = await withTimeout(base, this.timeout, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n\n return res;\n } catch (e: any) {\n const endedAt = Date.now();\n const rl = isRateLimitError(e);\n if (rl) {\n this.stats.bumpRateLimitedPerProvider(this.providerId);\n const cooldownMs = 10_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs);\n }\n\n const isTimeout = isTimeoutError(e);\n if (isTimeout) {\n this.stats.bumpTimeoutPerProvider(this.providerId);\n\n const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;\n const ratio = this.stats.timeoutRatio(this.providerId);\n\n // thresholds: do not ban on a single timeout, only after enough data\n if (n >= 50 && ratio >= 0.2) {\n const cooldownMs = ratio >= 0.5 ? 600 * 1000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n this.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n isRateLimit: rl,\n isTimeout: isTimeout,\n status: getHttpStatus(e),\n code: e?.code,\n message: String(e?.message || e),\n });\n\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\n}\n","import { Endpoint } from './utils';\nimport { Stats } from './Stats';\n\nexport class Router {\n private rr = 0;\n\n constructor(\n private readonly endpoints: Endpoint[],\n private readonly stats: Stats,\n ) {}\n\n size(): number {\n return this.endpoints.length;\n }\n\n pick(): Endpoint {\n const n = this.endpoints.length;\n for (let k = 0; k < n; k++) {\n const i = ((this.rr++ % n) + n) % n;\n const ep = this.endpoints[i];\n\n if (!this.stats.isInCooldown(ep.providerId)) return ep;\n }\n // if all are in cooldown, return the next one in round-robin order\n return this.endpoints[((this.rr++ % n) + n) % n];\n }\n}\n"],"mappings":"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","names":["import_ethers"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -58,10 +58,11 @@ declare class Semaphore {
|
|
|
58
58
|
declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
|
|
59
59
|
private readonly stats;
|
|
60
60
|
private readonly limiter;
|
|
61
|
+
private readonly timeout;
|
|
61
62
|
private readonly onEvent?;
|
|
62
63
|
readonly providerId: string;
|
|
63
64
|
readonly chainId: number;
|
|
64
|
-
constructor(url: string, chainId: number, providerId: string, stats: Stats, limiter: Semaphore, onEvent?: ((e: RpcEvent) => void) | undefined);
|
|
65
|
+
constructor(url: string, chainId: number, providerId: string, stats: Stats, limiter: Semaphore, timeout: number, onEvent?: ((e: RpcEvent) => void) | undefined);
|
|
65
66
|
send(method: string, params: any): Promise<any>;
|
|
66
67
|
private _sendInstrumented;
|
|
67
68
|
}
|
|
@@ -115,6 +116,7 @@ interface RPCPoolProviderParams {
|
|
|
115
116
|
urls: string[];
|
|
116
117
|
perUrl: {
|
|
117
118
|
inFlight: number;
|
|
119
|
+
timeout?: number;
|
|
118
120
|
};
|
|
119
121
|
retry: {
|
|
120
122
|
attempts: number;
|
|
@@ -132,4 +134,4 @@ declare class RPCPoolProvider extends JsonRpcProvider {
|
|
|
132
134
|
getStats(): Stats;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export { RPCPoolProvider };
|
|
137
|
+
export { RPCPoolProvider, type RPCPoolProviderParams, type RpcEvent, type RpcStatsSnapshot };
|
package/dist/index.d.ts
CHANGED
|
@@ -58,10 +58,11 @@ declare class Semaphore {
|
|
|
58
58
|
declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
|
|
59
59
|
private readonly stats;
|
|
60
60
|
private readonly limiter;
|
|
61
|
+
private readonly timeout;
|
|
61
62
|
private readonly onEvent?;
|
|
62
63
|
readonly providerId: string;
|
|
63
64
|
readonly chainId: number;
|
|
64
|
-
constructor(url: string, chainId: number, providerId: string, stats: Stats, limiter: Semaphore, onEvent?: ((e: RpcEvent) => void) | undefined);
|
|
65
|
+
constructor(url: string, chainId: number, providerId: string, stats: Stats, limiter: Semaphore, timeout: number, onEvent?: ((e: RpcEvent) => void) | undefined);
|
|
65
66
|
send(method: string, params: any): Promise<any>;
|
|
66
67
|
private _sendInstrumented;
|
|
67
68
|
}
|
|
@@ -115,6 +116,7 @@ interface RPCPoolProviderParams {
|
|
|
115
116
|
urls: string[];
|
|
116
117
|
perUrl: {
|
|
117
118
|
inFlight: number;
|
|
119
|
+
timeout?: number;
|
|
118
120
|
};
|
|
119
121
|
retry: {
|
|
120
122
|
attempts: number;
|
|
@@ -132,4 +134,4 @@ declare class RPCPoolProvider extends JsonRpcProvider {
|
|
|
132
134
|
getStats(): Stats;
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
export { RPCPoolProvider };
|
|
137
|
+
export { RPCPoolProvider, type RPCPoolProviderParams, type RpcEvent, type RpcStatsSnapshot };
|
package/dist/index.js
CHANGED
|
@@ -169,11 +169,12 @@ var Semaphore = class {
|
|
|
169
169
|
// src/InstrumentedProvider.ts
|
|
170
170
|
import { JsonRpcProvider, Network } from "ethers";
|
|
171
171
|
var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
172
|
-
constructor(url, chainId, providerId, stats, limiter, onEvent) {
|
|
172
|
+
constructor(url, chainId, providerId, stats, limiter, timeout, onEvent) {
|
|
173
173
|
const network = Network.from(chainId);
|
|
174
174
|
super(url, chainId, { staticNetwork: network });
|
|
175
175
|
this.stats = stats;
|
|
176
176
|
this.limiter = limiter;
|
|
177
|
+
this.timeout = timeout;
|
|
177
178
|
this.onEvent = onEvent;
|
|
178
179
|
this.providerId = providerId;
|
|
179
180
|
this.chainId = chainId;
|
|
@@ -204,7 +205,7 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
|
204
205
|
});
|
|
205
206
|
try {
|
|
206
207
|
const base = super.send(method, params);
|
|
207
|
-
const res = await withTimeout(base,
|
|
208
|
+
const res = await withTimeout(base, this.timeout, {
|
|
208
209
|
chainId: this.chainId,
|
|
209
210
|
providerId: this.providerId,
|
|
210
211
|
method
|
|
@@ -295,12 +296,14 @@ var RPCPoolProvider = class extends JsonRpcProvider2 {
|
|
|
295
296
|
const endpoints = this.params.urls.map((url, i) => {
|
|
296
297
|
const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
|
|
297
298
|
const limiter = new Semaphore(this.params.perUrl.inFlight);
|
|
299
|
+
const timeout = this.params.perUrl.timeout ?? 1e4;
|
|
298
300
|
const provider = new InstrumentedStaticJsonRpcProvider(
|
|
299
301
|
url,
|
|
300
302
|
this.params.chainId,
|
|
301
303
|
providerId,
|
|
302
304
|
this.stats,
|
|
303
305
|
limiter,
|
|
306
|
+
timeout,
|
|
304
307
|
this.params.hooks?.onEvent
|
|
305
308
|
);
|
|
306
309
|
return { providerId, url, provider, limiter };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/Semaphore.ts","../src/InstrumentedProvider.ts","../src/Router.ts"],"sourcesContent":["import { JsonRpcProvider, Network } from 'ethers';\nimport { Stats } from './Stats';\nimport { Endpoint, RpcEvent, shouldFailover } from './utils';\nimport { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\n\ninterface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number };\n retry: { attempts: number };\n hooks?: {\n onEvent(e: RpcEvent): void;\n };\n}\n\n// TODO\n// -- circuit breaker + health checks\n// -- sticky “session”\n\nexport class RPCPoolProvider extends JsonRpcProvider {\n readonly router: Router;\n readonly params: RPCPoolProviderParams;\n readonly stats: Stats;\n\n constructor(params: RPCPoolProviderParams) {\n const network = Network.from(params.chainId);\n super('http://localhost', network, { staticNetwork: network });\n\n this.params = params;\n\n this.stats = new Stats();\n\n const endpoints: Endpoint[] = this.params.urls.map((url, i) => {\n const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;\n const limiter = new Semaphore(this.params.perUrl.inFlight);\n\n const provider = new InstrumentedStaticJsonRpcProvider(\n url,\n this.params.chainId,\n providerId,\n this.stats,\n limiter,\n this.params.hooks?.onEvent,\n );\n\n return { providerId, url, provider, limiter };\n });\n\n this.router = new Router(endpoints, this.stats);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async send(method: string, params: any): Promise<any> {\n const tried = new Set<string>();\n const maxUniqueTries = Math.min(this.params.retry.attempts, this.router.size());\n\n while (tried.size < maxUniqueTries) {\n const ep = this.router.pick();\n if (tried.has(ep.providerId)) continue;\n tried.add(ep.providerId);\n\n try {\n return await ep.provider.send(method, params);\n } catch (e: any) {\n if (!shouldFailover(e)) throw e;\n if (tried.size >= maxUniqueTries) throw e;\n\n // Add exponential backoff with jitter before retry\n const baseDelay = Math.min(1000 * Math.pow(2, tried.size - 1), 5000);\n const jitter = Math.random() * baseDelay;\n await new Promise((resolve) => setTimeout(resolve, jitter));\n }\n }\n\n throw new Error('No RPC available');\n }\n\n getStats(): Stats {\n return this.stats;\n }\n}\n","export interface RpcStatsSnapshot {\n total: number;\n inFlight: number;\n perMethodTotal: Record<string, number>;\n rateLimitedTotal: number;\n perProviderRateLimited: Record<string, number>;\n timeoutTotal: number;\n perProviderTimeout: Record<string, number>;\n perProviderTotal: Record<string, number>;\n providerCooldownUntil: Record<string, number>;\n perProviderInFlight: Record<string, number>;\n}\n\nexport class Stats {\n private _total = 0;\n private _inFlight = 0;\n\n private _perMethod: Record<string, number> = {};\n\n private _rateLimitedTotal = 0;\n private _timeoutTotal = 0;\n\n private _perProviderInFlight: Record<string, number> = {};\n private _perProviderTotal: Record<string, number> = {};\n private _perProviderTimeout: Record<string, number> = {};\n private _perProviderRateLimited: Record<string, number> = {};\n\n private _providerCooldownUntil: Record<string, number> = {};\n\n private _bump(map: Record<string, number>, key: string) {\n map[key] = (map[key] || 0) + 1;\n }\n\n private _decrease(map: Record<string, number>, key: string) {\n map[key] = Math.max((map[key] || 0) - 1, 0);\n }\n\n private _bumpTotal() {\n this._total++;\n }\n\n private _bumpInFlight() {\n this._inFlight++;\n }\n\n private _bumpRateLimitedTotal() {\n this._rateLimitedTotal++;\n }\n\n private _bumpTimeoutTotal() {\n this._timeoutTotal++;\n }\n\n bumpInFlightPerProvider(id: string) {\n this._bumpInFlight();\n this._bump(this._perProviderInFlight, id);\n }\n\n decreaseInFlightPerProvider(id: string) {\n this.decreaseInFlight();\n this._decrease(this._perProviderInFlight, id);\n }\n\n decreaseInFlight() {\n this._inFlight = Math.max(this._inFlight - 1, 0);\n }\n\n bumpPerMethod(method: string) {\n this._bump(this._perMethod, method);\n }\n\n bumpRateLimitedPerProvider(id: string) {\n this._bumpRateLimitedTotal();\n this._bump(this._perProviderRateLimited, id);\n }\n\n bumpTimeoutPerProvider(id: string) {\n this._bumpTimeoutTotal();\n this._bump(this._perProviderTimeout, id);\n }\n\n bumpProviderTotal(id: string) {\n this._bumpTotal();\n this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;\n }\n\n timeoutRatio(id: string) {\n const t = this._perProviderTimeout[id] || 0;\n const n = this._perProviderTotal[id] || 0;\n return n ? t / n : 0;\n }\n\n isInCooldown(id: string) {\n return (this._providerCooldownUntil[id] || 0) > Date.now();\n }\n\n setCooldown(id: string, ms: number) {\n this._providerCooldownUntil[id] = Date.now() + ms;\n }\n\n snapshot(): Readonly<RpcStatsSnapshot> {\n return {\n total: this._total,\n inFlight: this._inFlight,\n perMethodTotal: { ...this._perMethod },\n rateLimitedTotal: this._rateLimitedTotal,\n timeoutTotal: this._timeoutTotal,\n perProviderInFlight: { ...this._perProviderInFlight },\n perProviderRateLimited: { ...this._perProviderRateLimited },\n perProviderTimeout: { ...this._perProviderTimeout },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { FetchResponse } from 'ethers';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\n limiter: Semaphore;\n}\n\nexport type RpcEvent =\n | {\n type: 'request';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n }\n | {\n type: 'response';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n }\n | {\n type: 'error';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n isRateLimit: boolean;\n isTimeout: boolean;\n status?: number;\n code?: string;\n message: string;\n };\n\nexport function getHttpStatus(e: any): number | undefined {\n return (\n e?.status ??\n e?.response?.status ??\n e?.response?.statusCode ??\n e?.error?.status ??\n e?.error?.response?.status ??\n e?.body?.statusCode // sometimes present\n );\n}\n\nexport function isRateLimitError(e: any): boolean {\n const status = getHttpStatus(e);\n if (status === 429 || status === 402) return true;\n\n const msg = String(e?.message || e);\n if (/error code:\\s*1015/i.test(msg)) return true; // Cloudflare\n return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function getRetryAfterMs(e: any): number | null {\n const ra =\n e?.response?.headers?.get?.('retry-after') ??\n e?.response?.headers?.['retry-after'] ??\n e?.headers?.['retry-after'];\n const n = Number(ra);\n return Number.isFinite(n) ? n * 1000 : null;\n}\n\nexport function isTimeoutError(e: any): boolean {\n // ethers v5\n if (e?.code === 'TIMEOUT') return true;\n\n const status = getHttpStatus(e);\n // some RPCs / proxies return 504 on timeout\n if (status === 504) return true;\n\n const msg = String(e?.message || e);\n\n // node-fetch / undici / axios / nginx / generic\n return /timeout|timed out|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|504 Gateway/i.test(msg);\n}\n\n/**\n * Wraps a promise with a timeout (useful when an RPC hangs without emitting TIMEOUT).\n * Important: this does not cancel the network request, but you get a controlled error\n * and FallbackProvider can switch to another RPC.\n */\nexport function withTimeout<T>(\n p: Promise<T>,\n ms: number,\n meta?: { chainId?: number; providerId?: string; method?: string },\n): Promise<T> {\n let t: NodeJS.Timeout | undefined;\n\n const timeout = new Promise<T>((_, reject) => {\n t = setTimeout(() => {\n const err: any = new Error(\n `RPC timeout after ${ms}ms` +\n (meta?.method ? ` method=${meta.method}` : '') +\n (meta?.providerId ? ` provider=${meta.providerId}` : '') +\n (meta?.chainId != null ? ` chainId=${meta.chainId}` : ''),\n );\n err.code = 'TIMEOUT';\n err.timeout = ms;\n reject(err);\n }, ms);\n });\n\n return Promise.race([p, timeout]).finally(() => t && clearTimeout(t));\n}\n\nexport function shouldFailover(e: any): boolean {\n const to = isTimeoutError(e);\n const rl = isRateLimitError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl;\n}\n","export class Semaphore {\n private inUse = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly max: number) {\n if (!Number.isFinite(max) || max <= 0) {\n throw new Error(`Semaphore max must be a positive number, got: ${max}`);\n }\n }\n\n async acquire(): Promise<() => void> {\n if (this.inUse < this.max) {\n this.inUse++;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n this.release();\n };\n }\n\n return new Promise<() => void>((resolve) => {\n this.queue.push(() => {\n this.inUse++;\n let released = false;\n resolve(() => {\n if (released) return;\n released = true;\n this.release();\n });\n });\n });\n }\n\n private release() {\n this.inUse = Math.max(0, this.inUse - 1);\n const next = this.queue.shift();\n if (next) next();\n }\n}\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isTimeoutError,\n RpcEvent,\n withTimeout,\n} from './utils';\n\n/**\n * Instrumented StaticJsonRpcProvider.\n * Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.\n */\nexport class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {\n readonly providerId: string;\n readonly chainId: number;\n\n constructor(\n url: string,\n chainId: number,\n providerId: string,\n private readonly stats: Stats,\n private readonly limiter: Semaphore,\n private readonly onEvent?: (e: RpcEvent) => void,\n ) {\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n }\n\n async send(method: string, params: any): Promise<any> {\n const release = this.limiter ? await this.limiter.acquire() : undefined;\n\n try {\n return await this._sendInstrumented(method, params);\n } finally {\n release?.();\n }\n }\n\n // ethers v5 calls send(method, params)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async _sendInstrumented(method: string, params: any): Promise<any> {\n const startedAt = Date.now();\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n this.stats.bumpPerMethod(method);\n\n this.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n });\n\n try {\n const base = super.send(method, params);\n const res = await withTimeout(base, 10_000, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n\n return res;\n } catch (e: any) {\n const endedAt = Date.now();\n const rl = isRateLimitError(e);\n if (rl) {\n this.stats.bumpRateLimitedPerProvider(this.providerId);\n const cooldownMs = 10_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs);\n }\n\n const isTimeout = isTimeoutError(e);\n if (isTimeout) {\n this.stats.bumpTimeoutPerProvider(this.providerId);\n\n const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;\n const ratio = this.stats.timeoutRatio(this.providerId);\n\n // thresholds: do not ban on a single timeout, only after enough data\n if (n >= 50 && ratio >= 0.2) {\n const cooldownMs = ratio >= 0.5 ? 600 * 1000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n this.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n isRateLimit: rl,\n isTimeout: isTimeout,\n status: getHttpStatus(e),\n code: e?.code,\n message: String(e?.message || e),\n });\n\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\n}\n","import { Endpoint } from './utils';\nimport { Stats } from './Stats';\n\nexport class Router {\n private rr = 0;\n\n constructor(\n private readonly endpoints: Endpoint[],\n private readonly stats: Stats,\n ) {}\n\n size(): number {\n return this.endpoints.length;\n }\n\n pick(): Endpoint {\n const n = this.endpoints.length;\n for (let k = 0; k < n; k++) {\n const i = ((this.rr++ % n) + n) % n;\n const ep = this.endpoints[i];\n\n if (!this.stats.isInCooldown(ep.providerId)) return ep;\n }\n // if all are in cooldown, return the next one in round-robin order\n return this.endpoints[((this.rr++ % n) + n) % n];\n }\n}\n"],"mappings":"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","names":["JsonRpcProvider","Network","JsonRpcProvider","Network"]}
|
|
1
|
+
{"version":3,"sources":["../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/Semaphore.ts","../src/InstrumentedProvider.ts","../src/Router.ts"],"sourcesContent":["import { JsonRpcProvider, Network } from 'ethers';\nimport { Stats } from './Stats';\nimport { Endpoint, RpcEvent, shouldFailover } from './utils';\nimport { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\n\nexport interface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number; timeout?: number };\n retry: { attempts: number };\n hooks?: {\n onEvent(e: RpcEvent): void;\n };\n}\n\n// TODO\n// -- circuit breaker + health checks\n// -- sticky “session”\n\nexport class RPCPoolProvider extends JsonRpcProvider {\n readonly router: Router;\n readonly params: RPCPoolProviderParams;\n readonly stats: Stats;\n\n constructor(params: RPCPoolProviderParams) {\n const network = Network.from(params.chainId);\n super('http://localhost', network, { staticNetwork: network });\n\n this.params = params;\n\n this.stats = new Stats();\n\n const endpoints: Endpoint[] = this.params.urls.map((url, i) => {\n const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;\n const limiter = new Semaphore(this.params.perUrl.inFlight);\n\n const timeout = this.params.perUrl.timeout ?? 10_000;\n\n const provider = new InstrumentedStaticJsonRpcProvider(\n url,\n this.params.chainId,\n providerId,\n this.stats,\n limiter,\n timeout,\n this.params.hooks?.onEvent,\n );\n\n return { providerId, url, provider, limiter };\n });\n\n this.router = new Router(endpoints, this.stats);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n async send(method: string, params: any): Promise<any> {\n const tried = new Set<string>();\n const maxUniqueTries = Math.min(this.params.retry.attempts, this.router.size());\n\n while (tried.size < maxUniqueTries) {\n const ep = this.router.pick();\n if (tried.has(ep.providerId)) continue;\n tried.add(ep.providerId);\n\n try {\n return await ep.provider.send(method, params);\n } catch (e: any) {\n if (!shouldFailover(e)) throw e;\n if (tried.size >= maxUniqueTries) throw e;\n\n // Add exponential backoff with jitter before retry\n const baseDelay = Math.min(1000 * Math.pow(2, tried.size - 1), 5000);\n const jitter = Math.random() * baseDelay;\n await new Promise((resolve) => setTimeout(resolve, jitter));\n }\n }\n\n throw new Error('No RPC available');\n }\n\n getStats(): Stats {\n return this.stats;\n }\n}\n","export interface RpcStatsSnapshot {\n total: number;\n inFlight: number;\n perMethodTotal: Record<string, number>;\n rateLimitedTotal: number;\n perProviderRateLimited: Record<string, number>;\n timeoutTotal: number;\n perProviderTimeout: Record<string, number>;\n perProviderTotal: Record<string, number>;\n providerCooldownUntil: Record<string, number>;\n perProviderInFlight: Record<string, number>;\n}\n\nexport class Stats {\n private _total = 0;\n private _inFlight = 0;\n\n private _perMethod: Record<string, number> = {};\n\n private _rateLimitedTotal = 0;\n private _timeoutTotal = 0;\n\n private _perProviderInFlight: Record<string, number> = {};\n private _perProviderTotal: Record<string, number> = {};\n private _perProviderTimeout: Record<string, number> = {};\n private _perProviderRateLimited: Record<string, number> = {};\n\n private _providerCooldownUntil: Record<string, number> = {};\n\n private _bump(map: Record<string, number>, key: string) {\n map[key] = (map[key] || 0) + 1;\n }\n\n private _decrease(map: Record<string, number>, key: string) {\n map[key] = Math.max((map[key] || 0) - 1, 0);\n }\n\n private _bumpTotal() {\n this._total++;\n }\n\n private _bumpInFlight() {\n this._inFlight++;\n }\n\n private _bumpRateLimitedTotal() {\n this._rateLimitedTotal++;\n }\n\n private _bumpTimeoutTotal() {\n this._timeoutTotal++;\n }\n\n bumpInFlightPerProvider(id: string) {\n this._bumpInFlight();\n this._bump(this._perProviderInFlight, id);\n }\n\n decreaseInFlightPerProvider(id: string) {\n this.decreaseInFlight();\n this._decrease(this._perProviderInFlight, id);\n }\n\n decreaseInFlight() {\n this._inFlight = Math.max(this._inFlight - 1, 0);\n }\n\n bumpPerMethod(method: string) {\n this._bump(this._perMethod, method);\n }\n\n bumpRateLimitedPerProvider(id: string) {\n this._bumpRateLimitedTotal();\n this._bump(this._perProviderRateLimited, id);\n }\n\n bumpTimeoutPerProvider(id: string) {\n this._bumpTimeoutTotal();\n this._bump(this._perProviderTimeout, id);\n }\n\n bumpProviderTotal(id: string) {\n this._bumpTotal();\n this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;\n }\n\n timeoutRatio(id: string) {\n const t = this._perProviderTimeout[id] || 0;\n const n = this._perProviderTotal[id] || 0;\n return n ? t / n : 0;\n }\n\n isInCooldown(id: string) {\n return (this._providerCooldownUntil[id] || 0) > Date.now();\n }\n\n setCooldown(id: string, ms: number) {\n this._providerCooldownUntil[id] = Date.now() + ms;\n }\n\n snapshot(): Readonly<RpcStatsSnapshot> {\n return {\n total: this._total,\n inFlight: this._inFlight,\n perMethodTotal: { ...this._perMethod },\n rateLimitedTotal: this._rateLimitedTotal,\n timeoutTotal: this._timeoutTotal,\n perProviderInFlight: { ...this._perProviderInFlight },\n perProviderRateLimited: { ...this._perProviderRateLimited },\n perProviderTimeout: { ...this._perProviderTimeout },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { Semaphore } from './Semaphore';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { FetchResponse } from 'ethers';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\n limiter: Semaphore;\n}\n\nexport type RpcEvent =\n | {\n type: 'request';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n }\n | {\n type: 'response';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n }\n | {\n type: 'error';\n chainId: number;\n providerId: string;\n method: string;\n startedAt: number;\n endedAt: number;\n ms: number;\n isRateLimit: boolean;\n isTimeout: boolean;\n status?: number;\n code?: string;\n message: string;\n };\n\nexport function getHttpStatus(e: any): number | undefined {\n return (\n e?.status ??\n e?.response?.status ??\n e?.response?.statusCode ??\n e?.error?.status ??\n e?.error?.response?.status ??\n e?.body?.statusCode // sometimes present\n );\n}\n\nexport function isRateLimitError(e: any): boolean {\n const status = getHttpStatus(e);\n if (status === 429 || status === 402) return true;\n\n const msg = String(e?.message || e);\n if (/error code:\\s*1015/i.test(msg)) return true; // Cloudflare\n return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function getRetryAfterMs(e: any): number | null {\n const ra =\n e?.response?.headers?.get?.('retry-after') ??\n e?.response?.headers?.['retry-after'] ??\n e?.headers?.['retry-after'];\n const n = Number(ra);\n return Number.isFinite(n) ? n * 1000 : null;\n}\n\nexport function isTimeoutError(e: any): boolean {\n // ethers v5\n if (e?.code === 'TIMEOUT') return true;\n\n const status = getHttpStatus(e);\n // some RPCs / proxies return 504 on timeout\n if (status === 504) return true;\n\n const msg = String(e?.message || e);\n\n // node-fetch / undici / axios / nginx / generic\n return /timeout|timed out|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|504 Gateway/i.test(msg);\n}\n\n/**\n * Wraps a promise with a timeout (useful when an RPC hangs without emitting TIMEOUT).\n * Important: this does not cancel the network request, but you get a controlled error\n * and FallbackProvider can switch to another RPC.\n */\nexport function withTimeout<T>(\n p: Promise<T>,\n ms: number,\n meta?: { chainId?: number; providerId?: string; method?: string },\n): Promise<T> {\n let t: NodeJS.Timeout | undefined;\n\n const timeout = new Promise<T>((_, reject) => {\n t = setTimeout(() => {\n const err: any = new Error(\n `RPC timeout after ${ms}ms` +\n (meta?.method ? ` method=${meta.method}` : '') +\n (meta?.providerId ? ` provider=${meta.providerId}` : '') +\n (meta?.chainId != null ? ` chainId=${meta.chainId}` : ''),\n );\n err.code = 'TIMEOUT';\n err.timeout = ms;\n reject(err);\n }, ms);\n });\n\n return Promise.race([p, timeout]).finally(() => t && clearTimeout(t));\n}\n\nexport function shouldFailover(e: any): boolean {\n const to = isTimeoutError(e);\n const rl = isRateLimitError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl;\n}\n","export class Semaphore {\n private inUse = 0;\n private queue: Array<() => void> = [];\n\n constructor(private readonly max: number) {\n if (!Number.isFinite(max) || max <= 0) {\n throw new Error(`Semaphore max must be a positive number, got: ${max}`);\n }\n }\n\n async acquire(): Promise<() => void> {\n if (this.inUse < this.max) {\n this.inUse++;\n let released = false;\n return () => {\n if (released) return;\n released = true;\n this.release();\n };\n }\n\n return new Promise<() => void>((resolve) => {\n this.queue.push(() => {\n this.inUse++;\n let released = false;\n resolve(() => {\n if (released) return;\n released = true;\n this.release();\n });\n });\n });\n }\n\n private release() {\n this.inUse = Math.max(0, this.inUse - 1);\n const next = this.queue.shift();\n if (next) next();\n }\n}\n","import { JsonRpcProvider, Network } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isTimeoutError,\n RpcEvent,\n withTimeout,\n} from './utils';\n\n/**\n * Instrumented StaticJsonRpcProvider.\n * Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.\n */\nexport class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {\n readonly providerId: string;\n readonly chainId: number;\n\n constructor(\n url: string,\n chainId: number,\n providerId: string,\n private readonly stats: Stats,\n private readonly limiter: Semaphore,\n private readonly timeout: number,\n private readonly onEvent?: (e: RpcEvent) => void,\n ) {\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n }\n\n async send(method: string, params: any): Promise<any> {\n const release = this.limiter ? await this.limiter.acquire() : undefined;\n\n try {\n return await this._sendInstrumented(method, params);\n } finally {\n release?.();\n }\n }\n\n // ethers v5 calls send(method, params)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async _sendInstrumented(method: string, params: any): Promise<any> {\n const startedAt = Date.now();\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n this.stats.bumpPerMethod(method);\n\n this.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n });\n\n try {\n const base = super.send(method, params);\n const res = await withTimeout(base, this.timeout, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n\n return res;\n } catch (e: any) {\n const endedAt = Date.now();\n const rl = isRateLimitError(e);\n if (rl) {\n this.stats.bumpRateLimitedPerProvider(this.providerId);\n const cooldownMs = 10_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs);\n }\n\n const isTimeout = isTimeoutError(e);\n if (isTimeout) {\n this.stats.bumpTimeoutPerProvider(this.providerId);\n\n const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;\n const ratio = this.stats.timeoutRatio(this.providerId);\n\n // thresholds: do not ban on a single timeout, only after enough data\n if (n >= 50 && ratio >= 0.2) {\n const cooldownMs = ratio >= 0.5 ? 600 * 1000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n this.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n isRateLimit: rl,\n isTimeout: isTimeout,\n status: getHttpStatus(e),\n code: e?.code,\n message: String(e?.message || e),\n });\n\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\n}\n","import { Endpoint } from './utils';\nimport { Stats } from './Stats';\n\nexport class Router {\n private rr = 0;\n\n constructor(\n private readonly endpoints: Endpoint[],\n private readonly stats: Stats,\n ) {}\n\n size(): number {\n return this.endpoints.length;\n }\n\n pick(): Endpoint {\n const n = this.endpoints.length;\n for (let k = 0; k < n; k++) {\n const i = ((this.rr++ % n) + n) % n;\n const ep = this.endpoints[i];\n\n if (!this.stats.isInCooldown(ep.providerId)) return ep;\n }\n // if all are in cooldown, return the next one in round-robin order\n return this.endpoints[((this.rr++ % n) + n) % n];\n }\n}\n"],"mappings":"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","names":["JsonRpcProvider","Network","JsonRpcProvider","Network"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ethers-rpc-pool",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "EVM RPC multiplexer for ethers.js with load balancing, rate limiting, failover and consistency controls.",
|
|
4
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ethereum",
|
|
8
|
+
"rpc",
|
|
9
|
+
"ethers",
|
|
10
|
+
"load-balancer",
|
|
11
|
+
"evm",
|
|
12
|
+
"failover"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/ahiipsa/ethers-rpc-pool.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/ahiipsa/ethers-rpc-pool#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ahiipsa/ethers-rpc-pool/issues"
|
|
21
|
+
},
|
|
5
22
|
"scripts": {
|
|
6
23
|
"build": "tsup",
|
|
7
24
|
"dev": "tsup --watch",
|