ethers-rpc-pool 1.0.3 → 1.0.5
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 +26 -13
- package/dist/index.cjs +70 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.js +70 -18
- 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
|
|
@@ -58,14 +68,14 @@ import { RPCPoolProvider } from 'ethers-rpc-pool';
|
|
|
58
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, rps: 2, rpsBurst: 5 },
|
|
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,16 @@ 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
|
+
| `perUrl.rps` | Maximum number of requests per second allowed for a single RPC endpoint. Enforced using a token bucket rate limiter. |
|
|
112
|
+
| `perUrl.rpsBurst` | aximum burst capacity for the rate limiter. Allows short spikes above the sustained rate by accumulating tokens during idle periods. |
|
|
113
|
+
| `retry.attempts` | Maximum number of unique endpoints to try |
|
|
114
|
+
| `hooks.onEvent` | Optional instrumentation hook |
|
|
102
115
|
|
|
103
116
|
---
|
|
104
117
|
|
|
@@ -170,7 +183,7 @@ This allows integration with:
|
|
|
170
183
|
|
|
171
184
|
```ts
|
|
172
185
|
const stats = pool.getStats();
|
|
173
|
-
console.log(
|
|
186
|
+
console.log(stats.snapshot());
|
|
174
187
|
```
|
|
175
188
|
|
|
176
189
|
### Example output:
|
|
@@ -228,7 +241,7 @@ Useful for:
|
|
|
228
241
|
|
|
229
242
|
### Known Limitations
|
|
230
243
|
|
|
231
|
-
-
|
|
244
|
+
- Basic circuit breaker/cooldown
|
|
232
245
|
- No sticky session/blockTag consistency yet
|
|
233
246
|
- No built-in JSON-RPC batching
|
|
234
247
|
- Archive/debug/trace methods depend on underlying RPC support
|
package/dist/index.cjs
CHANGED
|
@@ -194,20 +194,68 @@ var Semaphore = class {
|
|
|
194
194
|
|
|
195
195
|
// src/InstrumentedProvider.ts
|
|
196
196
|
var import_ethers = require("ethers");
|
|
197
|
+
|
|
198
|
+
// src/RpsLimiter.ts
|
|
199
|
+
var RpsLimiter = class {
|
|
200
|
+
constructor(rps, burst = Math.max(1, Math.ceil(rps))) {
|
|
201
|
+
this.rps = rps;
|
|
202
|
+
this.burst = burst;
|
|
203
|
+
this.tokens = burst;
|
|
204
|
+
}
|
|
205
|
+
// Current number of tokens in the bucket (can be fractional)
|
|
206
|
+
tokens;
|
|
207
|
+
// Time of last token refill, in ms
|
|
208
|
+
lastRefill = Date.now();
|
|
209
|
+
// Refill tokens according to elapsed time
|
|
210
|
+
refill(now) {
|
|
211
|
+
if (this.rps <= 0) return;
|
|
212
|
+
const elapsed = now - this.lastRefill;
|
|
213
|
+
if (elapsed <= 0) return;
|
|
214
|
+
const add = elapsed / 1e3 * this.rps;
|
|
215
|
+
this.tokens = Math.min(this.burst, this.tokens + add);
|
|
216
|
+
this.lastRefill = now;
|
|
217
|
+
}
|
|
218
|
+
// Take count tokens (usually 1 request = 1 token).
|
|
219
|
+
// If not enough tokens — wait and try again.
|
|
220
|
+
async take(count = 1) {
|
|
221
|
+
if (!this.rps || this.rps <= 0) return;
|
|
222
|
+
while (true) {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
this.refill(now);
|
|
225
|
+
if (this.tokens >= count) {
|
|
226
|
+
this.tokens -= count;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const need = count - this.tokens;
|
|
230
|
+
const waitMs = Math.ceil(need / this.rps * 1e3);
|
|
231
|
+
await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/InstrumentedProvider.ts
|
|
197
237
|
var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
|
|
198
|
-
|
|
238
|
+
providerId;
|
|
239
|
+
chainId;
|
|
240
|
+
params;
|
|
241
|
+
inFlightLimiter;
|
|
242
|
+
rpsLimiter;
|
|
243
|
+
stats;
|
|
244
|
+
constructor(params) {
|
|
245
|
+
const { url, providerId, chainId } = params;
|
|
199
246
|
const network = import_ethers.Network.from(chainId);
|
|
200
247
|
super(url, chainId, { staticNetwork: network });
|
|
201
|
-
this.stats = stats;
|
|
202
|
-
this.limiter = limiter;
|
|
203
|
-
this.onEvent = onEvent;
|
|
204
248
|
this.providerId = providerId;
|
|
205
249
|
this.chainId = chainId;
|
|
250
|
+
this.params = params;
|
|
251
|
+
const { rps = 10, rpsBurst, inFlight = 1 } = params;
|
|
252
|
+
this.inFlightLimiter = new Semaphore(inFlight);
|
|
253
|
+
this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);
|
|
254
|
+
this.stats = params.stats;
|
|
206
255
|
}
|
|
207
|
-
providerId;
|
|
208
|
-
chainId;
|
|
209
256
|
async send(method, params) {
|
|
210
|
-
|
|
257
|
+
await this.rpsLimiter.take(1);
|
|
258
|
+
const release = this.inFlightLimiter ? await this.inFlightLimiter.acquire() : void 0;
|
|
211
259
|
try {
|
|
212
260
|
return await this._sendInstrumented(method, params);
|
|
213
261
|
} finally {
|
|
@@ -221,7 +269,7 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
|
|
|
221
269
|
this.stats.bumpInFlightPerProvider(this.providerId);
|
|
222
270
|
this.stats.bumpProviderTotal(this.providerId);
|
|
223
271
|
this.stats.bumpPerMethod(method);
|
|
224
|
-
this.onEvent?.({
|
|
272
|
+
this.params.onEvent?.({
|
|
225
273
|
type: "request",
|
|
226
274
|
chainId: this.chainId,
|
|
227
275
|
providerId: this.providerId,
|
|
@@ -230,13 +278,13 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
|
|
|
230
278
|
});
|
|
231
279
|
try {
|
|
232
280
|
const base = super.send(method, params);
|
|
233
|
-
const res = await withTimeout(base,
|
|
281
|
+
const res = await withTimeout(base, this.params.timeout || 1e5, {
|
|
234
282
|
chainId: this.chainId,
|
|
235
283
|
providerId: this.providerId,
|
|
236
284
|
method
|
|
237
285
|
});
|
|
238
286
|
const endedAt = Date.now();
|
|
239
|
-
this.onEvent?.({
|
|
287
|
+
this.params.onEvent?.({
|
|
240
288
|
type: "response",
|
|
241
289
|
chainId: this.chainId,
|
|
242
290
|
providerId: this.providerId,
|
|
@@ -266,7 +314,7 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
|
|
|
266
314
|
this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1e3));
|
|
267
315
|
}
|
|
268
316
|
}
|
|
269
|
-
this.onEvent?.({
|
|
317
|
+
this.params.onEvent?.({
|
|
270
318
|
type: "error",
|
|
271
319
|
chainId: this.chainId,
|
|
272
320
|
providerId: this.providerId,
|
|
@@ -320,15 +368,19 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
|
|
|
320
368
|
this.stats = new Stats();
|
|
321
369
|
const endpoints = this.params.urls.map((url, i) => {
|
|
322
370
|
const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
|
|
323
|
-
const
|
|
324
|
-
const
|
|
371
|
+
const { inFlight = 1, rps = 1, rpsBurst, timeout = 1e4 } = this.params.perUrl;
|
|
372
|
+
const limiter = new Semaphore(inFlight);
|
|
373
|
+
const provider = new InstrumentedStaticJsonRpcProvider({
|
|
325
374
|
url,
|
|
326
|
-
this.params.chainId,
|
|
375
|
+
chainId: this.params.chainId,
|
|
327
376
|
providerId,
|
|
328
|
-
this.stats,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
377
|
+
stats: this.stats,
|
|
378
|
+
inFlight,
|
|
379
|
+
rps,
|
|
380
|
+
rpsBurst,
|
|
381
|
+
timeout,
|
|
382
|
+
onEvent: this.params.hooks?.onEvent
|
|
383
|
+
});
|
|
332
384
|
return { providerId, url, provider, limiter };
|
|
333
385
|
});
|
|
334
386
|
this.router = new Router(endpoints, this.stats);
|
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, 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 };\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/RpsLimiter.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';\nimport { RpsLimiter } from './RpsLimiter';\n\nexport interface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number; timeout?: number; rps?: number; rpsBurst?: 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\n const { inFlight = 1, rps = 1, rpsBurst, timeout = 10_000 } = this.params.perUrl;\n\n const limiter = new Semaphore(inFlight);\n\n const provider = new InstrumentedStaticJsonRpcProvider({\n url,\n chainId: this.params.chainId,\n providerId,\n stats: this.stats,\n inFlight,\n rps,\n rpsBurst,\n timeout,\n onEvent: 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';\nimport { RpsLimiter } from './RpsLimiter';\n\ninterface ProviderParams {\n url: string;\n chainId: number;\n providerId: string;\n stats: Stats;\n inFlight?: number;\n timeout?: number;\n rps?: number;\n rpsBurst?: number;\n onEvent?: (e: RpcEvent) => void;\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 readonly params: ProviderParams;\n\n readonly inFlightLimiter: Semaphore;\n readonly rpsLimiter: RpsLimiter;\n readonly stats: Stats;\n\n constructor(params: ProviderParams) {\n const { url, providerId, chainId } = params;\n\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n this.params = params;\n\n const { rps = 10, rpsBurst, inFlight = 1 } = params;\n\n this.inFlightLimiter = new Semaphore(inFlight);\n this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);\n this.stats = params.stats;\n }\n\n async send(method: string, params: any): Promise<any> {\n await this.rpsLimiter.take(1);\n\n const release = this.inFlightLimiter ? await this.inFlightLimiter.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.params.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.params.timeout || 10_0000, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.params.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.params.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","export class RpsLimiter {\n // Current number of tokens in the bucket (can be fractional)\n private tokens: number;\n\n // Time of last token refill, in ms\n private lastRefill = Date.now();\n\n constructor(\n // rps: how many tokens we add per second\n private readonly rps: number,\n // burst: maximum bucket capacity.\n // Default: >=1 and approximately equal to rps (to allow a small burst)\n private readonly burst: number = Math.max(1, Math.ceil(rps)),\n ) {\n // At start, the bucket is full: can make burst requests immediately\n this.tokens = burst;\n }\n\n // Refill tokens according to elapsed time\n private refill(now: number) {\n // rps<=0 means \"limit is disabled\"\n if (this.rps <= 0) return;\n\n const elapsed = now - this.lastRefill; // ms since last refill\n if (elapsed <= 0) return;\n\n // How many tokens to add:\n // elapsed/1000 = seconds, multiply by rps\n const add = (elapsed / 1000) * this.rps;\n\n // Add tokens, but don't exceed burst (bucket capacity)\n this.tokens = Math.min(this.burst, this.tokens + add);\n\n // Remember that we refilled tokens at time now\n this.lastRefill = now;\n }\n\n // Take count tokens (usually 1 request = 1 token).\n // If not enough tokens — wait and try again.\n async take(count = 1): Promise<void> {\n if (!this.rps || this.rps <= 0) return;\n\n while (true) {\n const now = Date.now();\n\n // Before attempting — refill tokens\n this.refill(now);\n\n // If enough tokens — \"pay\" for the request and exit\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n\n // Not enough tokens: calculate how long to wait\n const need = count - this.tokens;\n\n // How many ms needed to accumulate need tokens:\n // need / rps = seconds, *1000 = ms\n const waitMs = Math.ceil((need / this.rps) * 1000);\n\n // Wait in chunks (not all waitMs at once), to:\n // - not sleep too long if time/state changed\n // - be more resilient to timer drift\n await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));\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
|
@@ -51,17 +51,39 @@ declare class Semaphore {
|
|
|
51
51
|
private release;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
declare class RpsLimiter {
|
|
55
|
+
private readonly rps;
|
|
56
|
+
private readonly burst;
|
|
57
|
+
private tokens;
|
|
58
|
+
private lastRefill;
|
|
59
|
+
constructor(rps: number, burst?: number);
|
|
60
|
+
private refill;
|
|
61
|
+
take(count?: number): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ProviderParams {
|
|
65
|
+
url: string;
|
|
66
|
+
chainId: number;
|
|
67
|
+
providerId: string;
|
|
68
|
+
stats: Stats;
|
|
69
|
+
inFlight?: number;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
rps?: number;
|
|
72
|
+
rpsBurst?: number;
|
|
73
|
+
onEvent?: (e: RpcEvent) => void;
|
|
74
|
+
}
|
|
54
75
|
/**
|
|
55
76
|
* Instrumented StaticJsonRpcProvider.
|
|
56
77
|
* Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.
|
|
57
78
|
*/
|
|
58
79
|
declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
|
|
59
|
-
private readonly stats;
|
|
60
|
-
private readonly limiter;
|
|
61
|
-
private readonly onEvent?;
|
|
62
80
|
readonly providerId: string;
|
|
63
81
|
readonly chainId: number;
|
|
64
|
-
|
|
82
|
+
readonly params: ProviderParams;
|
|
83
|
+
readonly inFlightLimiter: Semaphore;
|
|
84
|
+
readonly rpsLimiter: RpsLimiter;
|
|
85
|
+
readonly stats: Stats;
|
|
86
|
+
constructor(params: ProviderParams);
|
|
65
87
|
send(method: string, params: any): Promise<any>;
|
|
66
88
|
private _sendInstrumented;
|
|
67
89
|
}
|
|
@@ -115,6 +137,9 @@ interface RPCPoolProviderParams {
|
|
|
115
137
|
urls: string[];
|
|
116
138
|
perUrl: {
|
|
117
139
|
inFlight: number;
|
|
140
|
+
timeout?: number;
|
|
141
|
+
rps?: number;
|
|
142
|
+
rpsBurst?: number;
|
|
118
143
|
};
|
|
119
144
|
retry: {
|
|
120
145
|
attempts: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -51,17 +51,39 @@ declare class Semaphore {
|
|
|
51
51
|
private release;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
declare class RpsLimiter {
|
|
55
|
+
private readonly rps;
|
|
56
|
+
private readonly burst;
|
|
57
|
+
private tokens;
|
|
58
|
+
private lastRefill;
|
|
59
|
+
constructor(rps: number, burst?: number);
|
|
60
|
+
private refill;
|
|
61
|
+
take(count?: number): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ProviderParams {
|
|
65
|
+
url: string;
|
|
66
|
+
chainId: number;
|
|
67
|
+
providerId: string;
|
|
68
|
+
stats: Stats;
|
|
69
|
+
inFlight?: number;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
rps?: number;
|
|
72
|
+
rpsBurst?: number;
|
|
73
|
+
onEvent?: (e: RpcEvent) => void;
|
|
74
|
+
}
|
|
54
75
|
/**
|
|
55
76
|
* Instrumented StaticJsonRpcProvider.
|
|
56
77
|
* Tracks requests, inFlight count, rate limits, and per-method / per-provider metrics.
|
|
57
78
|
*/
|
|
58
79
|
declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
|
|
59
|
-
private readonly stats;
|
|
60
|
-
private readonly limiter;
|
|
61
|
-
private readonly onEvent?;
|
|
62
80
|
readonly providerId: string;
|
|
63
81
|
readonly chainId: number;
|
|
64
|
-
|
|
82
|
+
readonly params: ProviderParams;
|
|
83
|
+
readonly inFlightLimiter: Semaphore;
|
|
84
|
+
readonly rpsLimiter: RpsLimiter;
|
|
85
|
+
readonly stats: Stats;
|
|
86
|
+
constructor(params: ProviderParams);
|
|
65
87
|
send(method: string, params: any): Promise<any>;
|
|
66
88
|
private _sendInstrumented;
|
|
67
89
|
}
|
|
@@ -115,6 +137,9 @@ interface RPCPoolProviderParams {
|
|
|
115
137
|
urls: string[];
|
|
116
138
|
perUrl: {
|
|
117
139
|
inFlight: number;
|
|
140
|
+
timeout?: number;
|
|
141
|
+
rps?: number;
|
|
142
|
+
rpsBurst?: number;
|
|
118
143
|
};
|
|
119
144
|
retry: {
|
|
120
145
|
attempts: number;
|
package/dist/index.js
CHANGED
|
@@ -168,20 +168,68 @@ var Semaphore = class {
|
|
|
168
168
|
|
|
169
169
|
// src/InstrumentedProvider.ts
|
|
170
170
|
import { JsonRpcProvider, Network } from "ethers";
|
|
171
|
+
|
|
172
|
+
// src/RpsLimiter.ts
|
|
173
|
+
var RpsLimiter = class {
|
|
174
|
+
constructor(rps, burst = Math.max(1, Math.ceil(rps))) {
|
|
175
|
+
this.rps = rps;
|
|
176
|
+
this.burst = burst;
|
|
177
|
+
this.tokens = burst;
|
|
178
|
+
}
|
|
179
|
+
// Current number of tokens in the bucket (can be fractional)
|
|
180
|
+
tokens;
|
|
181
|
+
// Time of last token refill, in ms
|
|
182
|
+
lastRefill = Date.now();
|
|
183
|
+
// Refill tokens according to elapsed time
|
|
184
|
+
refill(now) {
|
|
185
|
+
if (this.rps <= 0) return;
|
|
186
|
+
const elapsed = now - this.lastRefill;
|
|
187
|
+
if (elapsed <= 0) return;
|
|
188
|
+
const add = elapsed / 1e3 * this.rps;
|
|
189
|
+
this.tokens = Math.min(this.burst, this.tokens + add);
|
|
190
|
+
this.lastRefill = now;
|
|
191
|
+
}
|
|
192
|
+
// Take count tokens (usually 1 request = 1 token).
|
|
193
|
+
// If not enough tokens — wait and try again.
|
|
194
|
+
async take(count = 1) {
|
|
195
|
+
if (!this.rps || this.rps <= 0) return;
|
|
196
|
+
while (true) {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
this.refill(now);
|
|
199
|
+
if (this.tokens >= count) {
|
|
200
|
+
this.tokens -= count;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const need = count - this.tokens;
|
|
204
|
+
const waitMs = Math.ceil(need / this.rps * 1e3);
|
|
205
|
+
await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/InstrumentedProvider.ts
|
|
171
211
|
var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
172
|
-
|
|
212
|
+
providerId;
|
|
213
|
+
chainId;
|
|
214
|
+
params;
|
|
215
|
+
inFlightLimiter;
|
|
216
|
+
rpsLimiter;
|
|
217
|
+
stats;
|
|
218
|
+
constructor(params) {
|
|
219
|
+
const { url, providerId, chainId } = params;
|
|
173
220
|
const network = Network.from(chainId);
|
|
174
221
|
super(url, chainId, { staticNetwork: network });
|
|
175
|
-
this.stats = stats;
|
|
176
|
-
this.limiter = limiter;
|
|
177
|
-
this.onEvent = onEvent;
|
|
178
222
|
this.providerId = providerId;
|
|
179
223
|
this.chainId = chainId;
|
|
224
|
+
this.params = params;
|
|
225
|
+
const { rps = 10, rpsBurst, inFlight = 1 } = params;
|
|
226
|
+
this.inFlightLimiter = new Semaphore(inFlight);
|
|
227
|
+
this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);
|
|
228
|
+
this.stats = params.stats;
|
|
180
229
|
}
|
|
181
|
-
providerId;
|
|
182
|
-
chainId;
|
|
183
230
|
async send(method, params) {
|
|
184
|
-
|
|
231
|
+
await this.rpsLimiter.take(1);
|
|
232
|
+
const release = this.inFlightLimiter ? await this.inFlightLimiter.acquire() : void 0;
|
|
185
233
|
try {
|
|
186
234
|
return await this._sendInstrumented(method, params);
|
|
187
235
|
} finally {
|
|
@@ -195,7 +243,7 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
|
195
243
|
this.stats.bumpInFlightPerProvider(this.providerId);
|
|
196
244
|
this.stats.bumpProviderTotal(this.providerId);
|
|
197
245
|
this.stats.bumpPerMethod(method);
|
|
198
|
-
this.onEvent?.({
|
|
246
|
+
this.params.onEvent?.({
|
|
199
247
|
type: "request",
|
|
200
248
|
chainId: this.chainId,
|
|
201
249
|
providerId: this.providerId,
|
|
@@ -204,13 +252,13 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
|
204
252
|
});
|
|
205
253
|
try {
|
|
206
254
|
const base = super.send(method, params);
|
|
207
|
-
const res = await withTimeout(base,
|
|
255
|
+
const res = await withTimeout(base, this.params.timeout || 1e5, {
|
|
208
256
|
chainId: this.chainId,
|
|
209
257
|
providerId: this.providerId,
|
|
210
258
|
method
|
|
211
259
|
});
|
|
212
260
|
const endedAt = Date.now();
|
|
213
|
-
this.onEvent?.({
|
|
261
|
+
this.params.onEvent?.({
|
|
214
262
|
type: "response",
|
|
215
263
|
chainId: this.chainId,
|
|
216
264
|
providerId: this.providerId,
|
|
@@ -240,7 +288,7 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
|
|
|
240
288
|
this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1e3));
|
|
241
289
|
}
|
|
242
290
|
}
|
|
243
|
-
this.onEvent?.({
|
|
291
|
+
this.params.onEvent?.({
|
|
244
292
|
type: "error",
|
|
245
293
|
chainId: this.chainId,
|
|
246
294
|
providerId: this.providerId,
|
|
@@ -294,15 +342,19 @@ var RPCPoolProvider = class extends JsonRpcProvider2 {
|
|
|
294
342
|
this.stats = new Stats();
|
|
295
343
|
const endpoints = this.params.urls.map((url, i) => {
|
|
296
344
|
const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
|
|
297
|
-
const
|
|
298
|
-
const
|
|
345
|
+
const { inFlight = 1, rps = 1, rpsBurst, timeout = 1e4 } = this.params.perUrl;
|
|
346
|
+
const limiter = new Semaphore(inFlight);
|
|
347
|
+
const provider = new InstrumentedStaticJsonRpcProvider({
|
|
299
348
|
url,
|
|
300
|
-
this.params.chainId,
|
|
349
|
+
chainId: this.params.chainId,
|
|
301
350
|
providerId,
|
|
302
|
-
this.stats,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
351
|
+
stats: this.stats,
|
|
352
|
+
inFlight,
|
|
353
|
+
rps,
|
|
354
|
+
rpsBurst,
|
|
355
|
+
timeout,
|
|
356
|
+
onEvent: this.params.hooks?.onEvent
|
|
357
|
+
});
|
|
306
358
|
return { providerId, url, provider, limiter };
|
|
307
359
|
});
|
|
308
360
|
this.router = new Router(endpoints, this.stats);
|
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\nexport interface 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/RpsLimiter.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';\nimport { RpsLimiter } from './RpsLimiter';\n\nexport interface RPCPoolProviderParams {\n chainId: number;\n urls: string[];\n perUrl: { inFlight: number; timeout?: number; rps?: number; rpsBurst?: 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\n const { inFlight = 1, rps = 1, rpsBurst, timeout = 10_000 } = this.params.perUrl;\n\n const limiter = new Semaphore(inFlight);\n\n const provider = new InstrumentedStaticJsonRpcProvider({\n url,\n chainId: this.params.chainId,\n providerId,\n stats: this.stats,\n inFlight,\n rps,\n rpsBurst,\n timeout,\n onEvent: 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';\nimport { RpsLimiter } from './RpsLimiter';\n\ninterface ProviderParams {\n url: string;\n chainId: number;\n providerId: string;\n stats: Stats;\n inFlight?: number;\n timeout?: number;\n rps?: number;\n rpsBurst?: number;\n onEvent?: (e: RpcEvent) => void;\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 readonly params: ProviderParams;\n\n readonly inFlightLimiter: Semaphore;\n readonly rpsLimiter: RpsLimiter;\n readonly stats: Stats;\n\n constructor(params: ProviderParams) {\n const { url, providerId, chainId } = params;\n\n const network = Network.from(chainId);\n super(url, chainId, { staticNetwork: network });\n this.providerId = providerId;\n this.chainId = chainId;\n this.params = params;\n\n const { rps = 10, rpsBurst, inFlight = 1 } = params;\n\n this.inFlightLimiter = new Semaphore(inFlight);\n this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);\n this.stats = params.stats;\n }\n\n async send(method: string, params: any): Promise<any> {\n await this.rpsLimiter.take(1);\n\n const release = this.inFlightLimiter ? await this.inFlightLimiter.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.params.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.params.timeout || 10_0000, {\n chainId: this.chainId,\n providerId: this.providerId,\n method,\n });\n\n const endedAt = Date.now();\n this.params.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.params.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","export class RpsLimiter {\n // Current number of tokens in the bucket (can be fractional)\n private tokens: number;\n\n // Time of last token refill, in ms\n private lastRefill = Date.now();\n\n constructor(\n // rps: how many tokens we add per second\n private readonly rps: number,\n // burst: maximum bucket capacity.\n // Default: >=1 and approximately equal to rps (to allow a small burst)\n private readonly burst: number = Math.max(1, Math.ceil(rps)),\n ) {\n // At start, the bucket is full: can make burst requests immediately\n this.tokens = burst;\n }\n\n // Refill tokens according to elapsed time\n private refill(now: number) {\n // rps<=0 means \"limit is disabled\"\n if (this.rps <= 0) return;\n\n const elapsed = now - this.lastRefill; // ms since last refill\n if (elapsed <= 0) return;\n\n // How many tokens to add:\n // elapsed/1000 = seconds, multiply by rps\n const add = (elapsed / 1000) * this.rps;\n\n // Add tokens, but don't exceed burst (bucket capacity)\n this.tokens = Math.min(this.burst, this.tokens + add);\n\n // Remember that we refilled tokens at time now\n this.lastRefill = now;\n }\n\n // Take count tokens (usually 1 request = 1 token).\n // If not enough tokens — wait and try again.\n async take(count = 1): Promise<void> {\n if (!this.rps || this.rps <= 0) return;\n\n while (true) {\n const now = Date.now();\n\n // Before attempting — refill tokens\n this.refill(now);\n\n // If enough tokens — \"pay\" for the request and exit\n if (this.tokens >= count) {\n this.tokens -= count;\n return;\n }\n\n // Not enough tokens: calculate how long to wait\n const need = count - this.tokens;\n\n // How many ms needed to accumulate need tokens:\n // need / rps = seconds, *1000 = ms\n const waitMs = Math.ceil((need / this.rps) * 1000);\n\n // Wait in chunks (not all waitMs at once), to:\n // - not sleep too long if time/state changed\n // - be more resilient to timer drift\n await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));\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.5",
|
|
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",
|