ethers-rpc-pool 1.0.5 → 1.0.6

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 CHANGED
@@ -1,4 +1,4 @@
1
- ![npm](https://img.shields.io/npm/v/ethers-rpc-pool)
1
+ [![npm (tag)](https://img.shields.io/npm/v/ethers-rpc-pool)](https://www.npmjs.com/package/ethers-rpc-pool)
2
2
  ![license](https://img.shields.io/npm/l/ethers-rpc-pool)
3
3
 
4
4
  # ethers-rpc-pool
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ var Stats = class {
38
38
  _perProviderTotal = {};
39
39
  _perProviderTimeout = {};
40
40
  _perProviderRateLimited = {};
41
+ _perProviderError = {};
41
42
  _providerCooldownUntil = {};
42
43
  _bump(map, key) {
43
44
  map[key] = (map[key] || 0) + 1;
@@ -83,6 +84,9 @@ var Stats = class {
83
84
  this._bumpTotal();
84
85
  this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;
85
86
  }
87
+ bumpServerErrorPerProvider(id) {
88
+ this._bump(this._perProviderError, id);
89
+ }
86
90
  timeoutRatio(id) {
87
91
  const t = this._perProviderTimeout[id] || 0;
88
92
  const n = this._perProviderTotal[id] || 0;
@@ -104,6 +108,7 @@ var Stats = class {
104
108
  perProviderInFlight: { ...this._perProviderInFlight },
105
109
  perProviderRateLimited: { ...this._perProviderRateLimited },
106
110
  perProviderTimeout: { ...this._perProviderTimeout },
111
+ perProviderError: { ...this._perProviderError },
107
112
  perProviderTotal: { ...this._perProviderTotal },
108
113
  providerCooldownUntil: { ...this._providerCooldownUntil }
109
114
  };
@@ -119,7 +124,11 @@ function isRateLimitError(e) {
119
124
  if (status === 429 || status === 402) return true;
120
125
  const msg = String(e?.message || e);
121
126
  if (/error code:\s*1015/i.test(msg)) return true;
122
- return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);
127
+ return /rate limit|too many requests|429|quota|throttl/i.test(msg);
128
+ }
129
+ function isServerError(e) {
130
+ const status = getHttpStatus(e);
131
+ return status !== void 0 && status >= 500;
123
132
  }
124
133
  function getRetryAfterMs(e) {
125
134
  const ra = e?.response?.headers?.get?.("retry-after") ?? e?.response?.headers?.["retry-after"] ?? e?.headers?.["retry-after"];
@@ -150,9 +159,13 @@ function withTimeout(p, ms, meta) {
150
159
  function shouldFailover(e) {
151
160
  const to = isTimeoutError(e);
152
161
  const rl = isRateLimitError(e);
153
- return to || rl;
162
+ const se = isServerError(e);
163
+ return to || rl || se;
154
164
  }
155
165
 
166
+ // src/InstrumentedProvider.ts
167
+ var import_ethers = require("ethers");
168
+
156
169
  // src/Semaphore.ts
157
170
  var Semaphore = class {
158
171
  constructor(max) {
@@ -192,9 +205,6 @@ var Semaphore = class {
192
205
  }
193
206
  };
194
207
 
195
- // src/InstrumentedProvider.ts
196
- var import_ethers = require("ethers");
197
-
198
208
  // src/RpsLimiter.ts
199
209
  var RpsLimiter = class {
200
210
  constructor(rps, burst = Math.max(1, Math.ceil(rps))) {
@@ -228,7 +238,7 @@ var RpsLimiter = class {
228
238
  }
229
239
  const need = count - this.tokens;
230
240
  const waitMs = Math.ceil(need / this.rps * 1e3);
231
- await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));
241
+ await new Promise((r) => setTimeout(r, Math.min(waitMs, 50)));
232
242
  }
233
243
  }
234
244
  };
@@ -241,10 +251,15 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
241
251
  inFlightLimiter;
242
252
  rpsLimiter;
243
253
  stats;
254
+ fetchRequest;
255
+ lastCooldownMs = 0;
244
256
  constructor(params) {
245
257
  const { url, providerId, chainId } = params;
246
258
  const network = import_ethers.Network.from(chainId);
247
- super(url, chainId, { staticNetwork: network });
259
+ const fetchRequest = new import_ethers.FetchRequest(url);
260
+ fetchRequest.timeout = params.timeout || 1e4;
261
+ super(fetchRequest, chainId, { staticNetwork: network });
262
+ this.fetchRequest = fetchRequest;
248
263
  this.providerId = providerId;
249
264
  this.chainId = chainId;
250
265
  this.params = params;
@@ -253,46 +268,51 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
253
268
  this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);
254
269
  this.stats = params.stats;
255
270
  }
256
- async send(method, params) {
271
+ async _send(payload) {
257
272
  await this.rpsLimiter.take(1);
258
273
  const release = this.inFlightLimiter ? await this.inFlightLimiter.acquire() : void 0;
259
274
  try {
260
- return await this._sendInstrumented(method, params);
275
+ return await this._sendInstrumented(payload);
261
276
  } finally {
262
277
  release?.();
263
278
  }
264
279
  }
265
280
  // ethers v5 calls send(method, params)
266
281
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
267
- async _sendInstrumented(method, params) {
282
+ async _sendInstrumented(payload) {
268
283
  const startedAt = Date.now();
284
+ const payloads = Array.isArray(payload) ? payload : [payload];
269
285
  this.stats.bumpInFlightPerProvider(this.providerId);
270
286
  this.stats.bumpProviderTotal(this.providerId);
271
- this.stats.bumpPerMethod(method);
272
- this.params.onEvent?.({
273
- type: "request",
274
- chainId: this.chainId,
275
- providerId: this.providerId,
276
- method,
277
- startedAt
278
- });
279
- try {
280
- const base = super.send(method, params);
281
- const res = await withTimeout(base, this.params.timeout || 1e5, {
287
+ for (const p of payloads) {
288
+ this.stats.bumpPerMethod(p.method);
289
+ this.params.onEvent?.({
290
+ type: "request",
282
291
  chainId: this.chainId,
283
292
  providerId: this.providerId,
284
- method
293
+ method: p.method,
294
+ startedAt
285
295
  });
286
- const endedAt = Date.now();
287
- this.params.onEvent?.({
288
- type: "response",
296
+ }
297
+ try {
298
+ const base = super._send(payload);
299
+ const res = await withTimeout(base, this.params.timeout || 1e4, {
289
300
  chainId: this.chainId,
290
- providerId: this.providerId,
291
- method,
292
- startedAt,
293
- endedAt,
294
- ms: endedAt - startedAt
301
+ providerId: this.providerId
295
302
  });
303
+ const endedAt = Date.now();
304
+ for (const p of payloads) {
305
+ this.params.onEvent?.({
306
+ type: "response",
307
+ chainId: this.chainId,
308
+ providerId: this.providerId,
309
+ method: p.method,
310
+ startedAt,
311
+ endedAt,
312
+ ms: endedAt - startedAt
313
+ });
314
+ }
315
+ this.lastCooldownMs = 0;
296
316
  return res;
297
317
  } catch (e) {
298
318
  const endedAt = Date.now();
@@ -304,30 +324,40 @@ var InstrumentedStaticJsonRpcProvider = class extends import_ethers.JsonRpcProvi
304
324
  this.stats.setCooldown(this.providerId, raMs);
305
325
  }
306
326
  const isTimeout = isTimeoutError(e);
307
- if (isTimeout) {
327
+ if (isTimeout && !rl) {
308
328
  this.stats.bumpTimeoutPerProvider(this.providerId);
309
329
  const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;
310
330
  const ratio = this.stats.timeoutRatio(this.providerId);
311
331
  if (n >= 50 && ratio >= 0.2) {
312
- const cooldownMs = ratio >= 0.5 ? 600 * 1e3 : 6e4;
332
+ const cooldownMs = ratio >= 0.5 ? 6e5 : 6e4;
313
333
  const raMs = getRetryAfterMs(e) ?? cooldownMs;
334
+ this.lastCooldownMs = raMs;
314
335
  this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1e3));
315
336
  }
316
337
  }
317
- this.params.onEvent?.({
318
- type: "error",
319
- chainId: this.chainId,
320
- providerId: this.providerId,
321
- method,
322
- startedAt,
323
- endedAt,
324
- ms: endedAt - startedAt,
325
- isRateLimit: rl,
326
- isTimeout,
327
- status: getHttpStatus(e),
328
- code: e?.code,
329
- message: String(e?.message || e)
330
- });
338
+ const isError = isServerError(e);
339
+ if (isError && !rl && !isTimeout) {
340
+ this.stats.bumpServerErrorPerProvider(this.providerId);
341
+ const cooldownMs = (this.lastCooldownMs * 2 || 1e4) + Math.floor(Math.random() * 1e3);
342
+ this.lastCooldownMs = cooldownMs;
343
+ this.stats.setCooldown(this.providerId, cooldownMs);
344
+ }
345
+ for (const p of payloads) {
346
+ this.params.onEvent?.({
347
+ type: "error",
348
+ chainId: this.chainId,
349
+ providerId: this.providerId,
350
+ method: p.method,
351
+ startedAt,
352
+ endedAt,
353
+ ms: endedAt - startedAt,
354
+ isRateLimit: rl,
355
+ isTimeout,
356
+ status: getHttpStatus(e),
357
+ code: e?.code,
358
+ message: String(e?.message || e)
359
+ });
360
+ }
331
361
  throw e;
332
362
  } finally {
333
363
  this.stats.decreaseInFlightPerProvider(this.providerId);
@@ -369,7 +399,6 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
369
399
  const endpoints = this.params.urls.map((url, i) => {
370
400
  const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
371
401
  const { inFlight = 1, rps = 1, rpsBurst, timeout = 1e4 } = this.params.perUrl;
372
- const limiter = new Semaphore(inFlight);
373
402
  const provider = new InstrumentedStaticJsonRpcProvider({
374
403
  url,
375
404
  chainId: this.params.chainId,
@@ -381,7 +410,7 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
381
410
  timeout,
382
411
  onEvent: this.params.hooks?.onEvent
383
412
  });
384
- return { providerId, url, provider, limiter };
413
+ return { providerId, url, provider };
385
414
  });
386
415
  this.router = new Router(endpoints, this.stats);
387
416
  }
@@ -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/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"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/InstrumentedProvider.ts","../src/Semaphore.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 { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\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 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 };\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 perProviderError: 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 private _perProviderError: 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 bumpServerErrorPerProvider(id: string) {\n this._bump(this._perProviderError, id);\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 perProviderError: { ...this._perProviderError },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\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|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function isServerError(e: any): boolean {\n const status = getHttpStatus(e);\n return status !== undefined && status >= 500;\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 const se = isServerError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl || se;\n}\n","import { JsonRpcProvider, Network, JsonRpcPayload, FetchRequest } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isServerError,\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 readonly fetchRequest: FetchRequest;\n\n private lastCooldownMs: number = 0;\n\n constructor(params: ProviderParams) {\n const { url, providerId, chainId } = params;\n\n const network = Network.from(chainId);\n const fetchRequest = new FetchRequest(url);\n\n fetchRequest.timeout = params.timeout || 10_000;\n\n super(fetchRequest, chainId, { staticNetwork: network });\n this.fetchRequest = fetchRequest;\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 override async _send(payload: JsonRpcPayload | JsonRpcPayload[]): 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(payload);\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(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<any> {\n const startedAt = Date.now();\n const payloads = Array.isArray(payload) ? payload : [payload];\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n\n for (const p of payloads) {\n this.stats.bumpPerMethod(p.method);\n this.params.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.method,\n startedAt,\n });\n }\n\n try {\n const base = super._send(payload);\n const res = await withTimeout(base, this.params.timeout || 10_000, {\n chainId: this.chainId,\n providerId: this.providerId,\n });\n\n const endedAt = Date.now();\n\n for (const p of payloads) {\n this.params.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n }\n\n this.lastCooldownMs = 0;\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 && !rl) {\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_000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.lastCooldownMs = raMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n const isError = isServerError(e);\n if (isError && !rl && !isTimeout) {\n this.stats.bumpServerErrorPerProvider(this.providerId);\n const cooldownMs = (this.lastCooldownMs * 2 || 10_000) + Math.floor(Math.random() * 1000);\n this.lastCooldownMs = cooldownMs;\n this.stats.setCooldown(this.providerId, cooldownMs);\n }\n\n for (const p of payloads) {\n this.params.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.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\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\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","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, 50)));\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
@@ -1,4 +1,4 @@
1
- import { JsonRpcProvider } from 'ethers';
1
+ import { JsonRpcProvider, FetchRequest, JsonRpcPayload } from 'ethers';
2
2
 
3
3
  interface RpcStatsSnapshot {
4
4
  total: number;
@@ -11,6 +11,7 @@ interface RpcStatsSnapshot {
11
11
  perProviderTotal: Record<string, number>;
12
12
  providerCooldownUntil: Record<string, number>;
13
13
  perProviderInFlight: Record<string, number>;
14
+ perProviderError: Record<string, number>;
14
15
  }
15
16
  declare class Stats {
16
17
  private _total;
@@ -22,6 +23,7 @@ declare class Stats {
22
23
  private _perProviderTotal;
23
24
  private _perProviderTimeout;
24
25
  private _perProviderRateLimited;
26
+ private _perProviderError;
25
27
  private _providerCooldownUntil;
26
28
  private _bump;
27
29
  private _decrease;
@@ -36,6 +38,7 @@ declare class Stats {
36
38
  bumpRateLimitedPerProvider(id: string): void;
37
39
  bumpTimeoutPerProvider(id: string): void;
38
40
  bumpProviderTotal(id: string): void;
41
+ bumpServerErrorPerProvider(id: string): void;
39
42
  timeoutRatio(id: string): number;
40
43
  isInCooldown(id: string): boolean;
41
44
  setCooldown(id: string, ms: number): void;
@@ -83,8 +86,10 @@ declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
83
86
  readonly inFlightLimiter: Semaphore;
84
87
  readonly rpsLimiter: RpsLimiter;
85
88
  readonly stats: Stats;
89
+ readonly fetchRequest: FetchRequest;
90
+ private lastCooldownMs;
86
91
  constructor(params: ProviderParams);
87
- send(method: string, params: any): Promise<any>;
92
+ _send(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<any>;
88
93
  private _sendInstrumented;
89
94
  }
90
95
 
@@ -92,7 +97,6 @@ interface Endpoint {
92
97
  providerId: string;
93
98
  url: string;
94
99
  provider: InstrumentedStaticJsonRpcProvider;
95
- limiter: Semaphore;
96
100
  }
97
101
  type RpcEvent = {
98
102
  type: 'request';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { JsonRpcProvider } from 'ethers';
1
+ import { JsonRpcProvider, FetchRequest, JsonRpcPayload } from 'ethers';
2
2
 
3
3
  interface RpcStatsSnapshot {
4
4
  total: number;
@@ -11,6 +11,7 @@ interface RpcStatsSnapshot {
11
11
  perProviderTotal: Record<string, number>;
12
12
  providerCooldownUntil: Record<string, number>;
13
13
  perProviderInFlight: Record<string, number>;
14
+ perProviderError: Record<string, number>;
14
15
  }
15
16
  declare class Stats {
16
17
  private _total;
@@ -22,6 +23,7 @@ declare class Stats {
22
23
  private _perProviderTotal;
23
24
  private _perProviderTimeout;
24
25
  private _perProviderRateLimited;
26
+ private _perProviderError;
25
27
  private _providerCooldownUntil;
26
28
  private _bump;
27
29
  private _decrease;
@@ -36,6 +38,7 @@ declare class Stats {
36
38
  bumpRateLimitedPerProvider(id: string): void;
37
39
  bumpTimeoutPerProvider(id: string): void;
38
40
  bumpProviderTotal(id: string): void;
41
+ bumpServerErrorPerProvider(id: string): void;
39
42
  timeoutRatio(id: string): number;
40
43
  isInCooldown(id: string): boolean;
41
44
  setCooldown(id: string, ms: number): void;
@@ -83,8 +86,10 @@ declare class InstrumentedStaticJsonRpcProvider extends JsonRpcProvider {
83
86
  readonly inFlightLimiter: Semaphore;
84
87
  readonly rpsLimiter: RpsLimiter;
85
88
  readonly stats: Stats;
89
+ readonly fetchRequest: FetchRequest;
90
+ private lastCooldownMs;
86
91
  constructor(params: ProviderParams);
87
- send(method: string, params: any): Promise<any>;
92
+ _send(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<any>;
88
93
  private _sendInstrumented;
89
94
  }
90
95
 
@@ -92,7 +97,6 @@ interface Endpoint {
92
97
  providerId: string;
93
98
  url: string;
94
99
  provider: InstrumentedStaticJsonRpcProvider;
95
- limiter: Semaphore;
96
100
  }
97
101
  type RpcEvent = {
98
102
  type: 'request';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ var Stats = class {
12
12
  _perProviderTotal = {};
13
13
  _perProviderTimeout = {};
14
14
  _perProviderRateLimited = {};
15
+ _perProviderError = {};
15
16
  _providerCooldownUntil = {};
16
17
  _bump(map, key) {
17
18
  map[key] = (map[key] || 0) + 1;
@@ -57,6 +58,9 @@ var Stats = class {
57
58
  this._bumpTotal();
58
59
  this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;
59
60
  }
61
+ bumpServerErrorPerProvider(id) {
62
+ this._bump(this._perProviderError, id);
63
+ }
60
64
  timeoutRatio(id) {
61
65
  const t = this._perProviderTimeout[id] || 0;
62
66
  const n = this._perProviderTotal[id] || 0;
@@ -78,6 +82,7 @@ var Stats = class {
78
82
  perProviderInFlight: { ...this._perProviderInFlight },
79
83
  perProviderRateLimited: { ...this._perProviderRateLimited },
80
84
  perProviderTimeout: { ...this._perProviderTimeout },
85
+ perProviderError: { ...this._perProviderError },
81
86
  perProviderTotal: { ...this._perProviderTotal },
82
87
  providerCooldownUntil: { ...this._providerCooldownUntil }
83
88
  };
@@ -93,7 +98,11 @@ function isRateLimitError(e) {
93
98
  if (status === 429 || status === 402) return true;
94
99
  const msg = String(e?.message || e);
95
100
  if (/error code:\s*1015/i.test(msg)) return true;
96
- return /rate limit|payment required|too many requests|429|quota|throttl/i.test(msg);
101
+ return /rate limit|too many requests|429|quota|throttl/i.test(msg);
102
+ }
103
+ function isServerError(e) {
104
+ const status = getHttpStatus(e);
105
+ return status !== void 0 && status >= 500;
97
106
  }
98
107
  function getRetryAfterMs(e) {
99
108
  const ra = e?.response?.headers?.get?.("retry-after") ?? e?.response?.headers?.["retry-after"] ?? e?.headers?.["retry-after"];
@@ -124,9 +133,13 @@ function withTimeout(p, ms, meta) {
124
133
  function shouldFailover(e) {
125
134
  const to = isTimeoutError(e);
126
135
  const rl = isRateLimitError(e);
127
- return to || rl;
136
+ const se = isServerError(e);
137
+ return to || rl || se;
128
138
  }
129
139
 
140
+ // src/InstrumentedProvider.ts
141
+ import { JsonRpcProvider, Network, FetchRequest } from "ethers";
142
+
130
143
  // src/Semaphore.ts
131
144
  var Semaphore = class {
132
145
  constructor(max) {
@@ -166,9 +179,6 @@ var Semaphore = class {
166
179
  }
167
180
  };
168
181
 
169
- // src/InstrumentedProvider.ts
170
- import { JsonRpcProvider, Network } from "ethers";
171
-
172
182
  // src/RpsLimiter.ts
173
183
  var RpsLimiter = class {
174
184
  constructor(rps, burst = Math.max(1, Math.ceil(rps))) {
@@ -202,7 +212,7 @@ var RpsLimiter = class {
202
212
  }
203
213
  const need = count - this.tokens;
204
214
  const waitMs = Math.ceil(need / this.rps * 1e3);
205
- await new Promise((r) => setTimeout(r, Math.min(waitMs, 250)));
215
+ await new Promise((r) => setTimeout(r, Math.min(waitMs, 50)));
206
216
  }
207
217
  }
208
218
  };
@@ -215,10 +225,15 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
215
225
  inFlightLimiter;
216
226
  rpsLimiter;
217
227
  stats;
228
+ fetchRequest;
229
+ lastCooldownMs = 0;
218
230
  constructor(params) {
219
231
  const { url, providerId, chainId } = params;
220
232
  const network = Network.from(chainId);
221
- super(url, chainId, { staticNetwork: network });
233
+ const fetchRequest = new FetchRequest(url);
234
+ fetchRequest.timeout = params.timeout || 1e4;
235
+ super(fetchRequest, chainId, { staticNetwork: network });
236
+ this.fetchRequest = fetchRequest;
222
237
  this.providerId = providerId;
223
238
  this.chainId = chainId;
224
239
  this.params = params;
@@ -227,46 +242,51 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
227
242
  this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);
228
243
  this.stats = params.stats;
229
244
  }
230
- async send(method, params) {
245
+ async _send(payload) {
231
246
  await this.rpsLimiter.take(1);
232
247
  const release = this.inFlightLimiter ? await this.inFlightLimiter.acquire() : void 0;
233
248
  try {
234
- return await this._sendInstrumented(method, params);
249
+ return await this._sendInstrumented(payload);
235
250
  } finally {
236
251
  release?.();
237
252
  }
238
253
  }
239
254
  // ethers v5 calls send(method, params)
240
255
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
- async _sendInstrumented(method, params) {
256
+ async _sendInstrumented(payload) {
242
257
  const startedAt = Date.now();
258
+ const payloads = Array.isArray(payload) ? payload : [payload];
243
259
  this.stats.bumpInFlightPerProvider(this.providerId);
244
260
  this.stats.bumpProviderTotal(this.providerId);
245
- this.stats.bumpPerMethod(method);
246
- this.params.onEvent?.({
247
- type: "request",
248
- chainId: this.chainId,
249
- providerId: this.providerId,
250
- method,
251
- startedAt
252
- });
253
- try {
254
- const base = super.send(method, params);
255
- const res = await withTimeout(base, this.params.timeout || 1e5, {
261
+ for (const p of payloads) {
262
+ this.stats.bumpPerMethod(p.method);
263
+ this.params.onEvent?.({
264
+ type: "request",
256
265
  chainId: this.chainId,
257
266
  providerId: this.providerId,
258
- method
267
+ method: p.method,
268
+ startedAt
259
269
  });
260
- const endedAt = Date.now();
261
- this.params.onEvent?.({
262
- type: "response",
270
+ }
271
+ try {
272
+ const base = super._send(payload);
273
+ const res = await withTimeout(base, this.params.timeout || 1e4, {
263
274
  chainId: this.chainId,
264
- providerId: this.providerId,
265
- method,
266
- startedAt,
267
- endedAt,
268
- ms: endedAt - startedAt
275
+ providerId: this.providerId
269
276
  });
277
+ const endedAt = Date.now();
278
+ for (const p of payloads) {
279
+ this.params.onEvent?.({
280
+ type: "response",
281
+ chainId: this.chainId,
282
+ providerId: this.providerId,
283
+ method: p.method,
284
+ startedAt,
285
+ endedAt,
286
+ ms: endedAt - startedAt
287
+ });
288
+ }
289
+ this.lastCooldownMs = 0;
270
290
  return res;
271
291
  } catch (e) {
272
292
  const endedAt = Date.now();
@@ -278,30 +298,40 @@ var InstrumentedStaticJsonRpcProvider = class extends JsonRpcProvider {
278
298
  this.stats.setCooldown(this.providerId, raMs);
279
299
  }
280
300
  const isTimeout = isTimeoutError(e);
281
- if (isTimeout) {
301
+ if (isTimeout && !rl) {
282
302
  this.stats.bumpTimeoutPerProvider(this.providerId);
283
303
  const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;
284
304
  const ratio = this.stats.timeoutRatio(this.providerId);
285
305
  if (n >= 50 && ratio >= 0.2) {
286
- const cooldownMs = ratio >= 0.5 ? 600 * 1e3 : 6e4;
306
+ const cooldownMs = ratio >= 0.5 ? 6e5 : 6e4;
287
307
  const raMs = getRetryAfterMs(e) ?? cooldownMs;
308
+ this.lastCooldownMs = raMs;
288
309
  this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1e3));
289
310
  }
290
311
  }
291
- this.params.onEvent?.({
292
- type: "error",
293
- chainId: this.chainId,
294
- providerId: this.providerId,
295
- method,
296
- startedAt,
297
- endedAt,
298
- ms: endedAt - startedAt,
299
- isRateLimit: rl,
300
- isTimeout,
301
- status: getHttpStatus(e),
302
- code: e?.code,
303
- message: String(e?.message || e)
304
- });
312
+ const isError = isServerError(e);
313
+ if (isError && !rl && !isTimeout) {
314
+ this.stats.bumpServerErrorPerProvider(this.providerId);
315
+ const cooldownMs = (this.lastCooldownMs * 2 || 1e4) + Math.floor(Math.random() * 1e3);
316
+ this.lastCooldownMs = cooldownMs;
317
+ this.stats.setCooldown(this.providerId, cooldownMs);
318
+ }
319
+ for (const p of payloads) {
320
+ this.params.onEvent?.({
321
+ type: "error",
322
+ chainId: this.chainId,
323
+ providerId: this.providerId,
324
+ method: p.method,
325
+ startedAt,
326
+ endedAt,
327
+ ms: endedAt - startedAt,
328
+ isRateLimit: rl,
329
+ isTimeout,
330
+ status: getHttpStatus(e),
331
+ code: e?.code,
332
+ message: String(e?.message || e)
333
+ });
334
+ }
305
335
  throw e;
306
336
  } finally {
307
337
  this.stats.decreaseInFlightPerProvider(this.providerId);
@@ -343,7 +373,6 @@ var RPCPoolProvider = class extends JsonRpcProvider2 {
343
373
  const endpoints = this.params.urls.map((url, i) => {
344
374
  const providerId = `rpc#${i + 1}-chainId:${this.params.chainId}-${url}`;
345
375
  const { inFlight = 1, rps = 1, rpsBurst, timeout = 1e4 } = this.params.perUrl;
346
- const limiter = new Semaphore(inFlight);
347
376
  const provider = new InstrumentedStaticJsonRpcProvider({
348
377
  url,
349
378
  chainId: this.params.chainId,
@@ -355,7 +384,7 @@ var RPCPoolProvider = class extends JsonRpcProvider2 {
355
384
  timeout,
356
385
  onEvent: this.params.hooks?.onEvent
357
386
  });
358
- return { providerId, url, provider, limiter };
387
+ return { providerId, url, provider };
359
388
  });
360
389
  this.router = new Router(endpoints, this.stats);
361
390
  }
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/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"]}
1
+ {"version":3,"sources":["../src/RpcPoolProvider.ts","../src/Stats.ts","../src/utils.ts","../src/InstrumentedProvider.ts","../src/Semaphore.ts","../src/RpsLimiter.ts","../src/Router.ts"],"sourcesContent":["import { JsonRpcProvider, Network } from 'ethers';\nimport { Stats } from './Stats';\nimport { Endpoint, RpcEvent, shouldFailover } from './utils';\nimport { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\nimport { Router } from './Router';\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 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 };\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 perProviderError: 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 private _perProviderError: 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 bumpServerErrorPerProvider(id: string) {\n this._bump(this._perProviderError, id);\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 perProviderError: { ...this._perProviderError },\n perProviderTotal: { ...this._perProviderTotal },\n providerCooldownUntil: { ...this._providerCooldownUntil },\n };\n }\n}\n","import { InstrumentedStaticJsonRpcProvider } from './InstrumentedProvider';\n\nexport interface Endpoint {\n providerId: string;\n url: string;\n provider: InstrumentedStaticJsonRpcProvider;\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|too many requests|429|quota|throttl/i.test(msg);\n}\n\nexport function isServerError(e: any): boolean {\n const status = getHttpStatus(e);\n return status !== undefined && status >= 500;\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 const se = isServerError(e);\n\n // failover on timeouts and rate limits, but not on logical errors (e.g. invalid params)\n return to || rl || se;\n}\n","import { JsonRpcProvider, Network, JsonRpcPayload, FetchRequest } from 'ethers';\nimport { Semaphore } from './Semaphore';\nimport { Stats } from './Stats';\nimport {\n getHttpStatus,\n getRetryAfterMs,\n isRateLimitError,\n isServerError,\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 readonly fetchRequest: FetchRequest;\n\n private lastCooldownMs: number = 0;\n\n constructor(params: ProviderParams) {\n const { url, providerId, chainId } = params;\n\n const network = Network.from(chainId);\n const fetchRequest = new FetchRequest(url);\n\n fetchRequest.timeout = params.timeout || 10_000;\n\n super(fetchRequest, chainId, { staticNetwork: network });\n this.fetchRequest = fetchRequest;\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 override async _send(payload: JsonRpcPayload | JsonRpcPayload[]): 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(payload);\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(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<any> {\n const startedAt = Date.now();\n const payloads = Array.isArray(payload) ? payload : [payload];\n\n this.stats.bumpInFlightPerProvider(this.providerId);\n this.stats.bumpProviderTotal(this.providerId);\n\n for (const p of payloads) {\n this.stats.bumpPerMethod(p.method);\n this.params.onEvent?.({\n type: 'request',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.method,\n startedAt,\n });\n }\n\n try {\n const base = super._send(payload);\n const res = await withTimeout(base, this.params.timeout || 10_000, {\n chainId: this.chainId,\n providerId: this.providerId,\n });\n\n const endedAt = Date.now();\n\n for (const p of payloads) {\n this.params.onEvent?.({\n type: 'response',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.method,\n startedAt,\n endedAt,\n ms: endedAt - startedAt,\n });\n }\n\n this.lastCooldownMs = 0;\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 && !rl) {\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_000 : 60_000;\n const raMs = getRetryAfterMs(e) ?? cooldownMs;\n this.lastCooldownMs = raMs;\n this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1000));\n }\n }\n\n const isError = isServerError(e);\n if (isError && !rl && !isTimeout) {\n this.stats.bumpServerErrorPerProvider(this.providerId);\n const cooldownMs = (this.lastCooldownMs * 2 || 10_000) + Math.floor(Math.random() * 1000);\n this.lastCooldownMs = cooldownMs;\n this.stats.setCooldown(this.providerId, cooldownMs);\n }\n\n for (const p of payloads) {\n this.params.onEvent?.({\n type: 'error',\n chainId: this.chainId,\n providerId: this.providerId,\n method: p.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\n throw e;\n } finally {\n this.stats.decreaseInFlightPerProvider(this.providerId);\n }\n }\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","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, 50)));\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":";AAAA,SAAS,mBAAAA,kBAAiB,WAAAC,gBAAe;;;ACclC,IAAM,QAAN,MAAY;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EAEZ,aAAqC,CAAC;AAAA,EAEtC,oBAAoB;AAAA,EACpB,gBAAgB;AAAA,EAEhB,uBAA+C,CAAC;AAAA,EAChD,oBAA4C,CAAC;AAAA,EAC7C,sBAA8C,CAAC;AAAA,EAC/C,0BAAkD,CAAC;AAAA,EACnD,oBAA4C,CAAC;AAAA,EAE7C,yBAAiD,CAAC;AAAA,EAElD,MAAM,KAA6B,KAAa;AACtD,QAAI,GAAG,KAAK,IAAI,GAAG,KAAK,KAAK;AAAA,EAC/B;AAAA,EAEQ,UAAU,KAA6B,KAAa;AAC1D,QAAI,GAAG,IAAI,KAAK,KAAK,IAAI,GAAG,KAAK,KAAK,GAAG,CAAC;AAAA,EAC5C;AAAA,EAEQ,aAAa;AACnB,SAAK;AAAA,EACP;AAAA,EAEQ,gBAAgB;AACtB,SAAK;AAAA,EACP;AAAA,EAEQ,wBAAwB;AAC9B,SAAK;AAAA,EACP;AAAA,EAEQ,oBAAoB;AAC1B,SAAK;AAAA,EACP;AAAA,EAEA,wBAAwB,IAAY;AAClC,SAAK,cAAc;AACnB,SAAK,MAAM,KAAK,sBAAsB,EAAE;AAAA,EAC1C;AAAA,EAEA,4BAA4B,IAAY;AACtC,SAAK,iBAAiB;AACtB,SAAK,UAAU,KAAK,sBAAsB,EAAE;AAAA,EAC9C;AAAA,EAEA,mBAAmB;AACjB,SAAK,YAAY,KAAK,IAAI,KAAK,YAAY,GAAG,CAAC;AAAA,EACjD;AAAA,EAEA,cAAc,QAAgB;AAC5B,SAAK,MAAM,KAAK,YAAY,MAAM;AAAA,EACpC;AAAA,EAEA,2BAA2B,IAAY;AACrC,SAAK,sBAAsB;AAC3B,SAAK,MAAM,KAAK,yBAAyB,EAAE;AAAA,EAC7C;AAAA,EAEA,uBAAuB,IAAY;AACjC,SAAK,kBAAkB;AACvB,SAAK,MAAM,KAAK,qBAAqB,EAAE;AAAA,EACzC;AAAA,EAEA,kBAAkB,IAAY;AAC5B,SAAK,WAAW;AAChB,SAAK,kBAAkB,EAAE,KAAK,KAAK,kBAAkB,EAAE,KAAK,KAAK;AAAA,EACnE;AAAA,EACA,2BAA2B,IAAY;AACrC,SAAK,MAAM,KAAK,mBAAmB,EAAE;AAAA,EACvC;AAAA,EAEA,aAAa,IAAY;AACvB,UAAM,IAAI,KAAK,oBAAoB,EAAE,KAAK;AAC1C,UAAM,IAAI,KAAK,kBAAkB,EAAE,KAAK;AACxC,WAAO,IAAI,IAAI,IAAI;AAAA,EACrB;AAAA,EAEA,aAAa,IAAY;AACvB,YAAQ,KAAK,uBAAuB,EAAE,KAAK,KAAK,KAAK,IAAI;AAAA,EAC3D;AAAA,EAEA,YAAY,IAAY,IAAY;AAClC,SAAK,uBAAuB,EAAE,IAAI,KAAK,IAAI,IAAI;AAAA,EACjD;AAAA,EAEA,WAAuC;AACrC,WAAO;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,UAAU,KAAK;AAAA,MACf,gBAAgB,EAAE,GAAG,KAAK,WAAW;AAAA,MACrC,kBAAkB,KAAK;AAAA,MACvB,cAAc,KAAK;AAAA,MACnB,qBAAqB,EAAE,GAAG,KAAK,qBAAqB;AAAA,MACpD,wBAAwB,EAAE,GAAG,KAAK,wBAAwB;AAAA,MAC1D,oBAAoB,EAAE,GAAG,KAAK,oBAAoB;AAAA,MAClD,kBAAkB,EAAE,GAAG,KAAK,kBAAkB;AAAA,MAC9C,kBAAkB,EAAE,GAAG,KAAK,kBAAkB;AAAA,MAC9C,uBAAuB,EAAE,GAAG,KAAK,uBAAuB;AAAA,IAC1D;AAAA,EACF;AACF;;;AChFO,SAAS,cAAc,GAA4B;AACxD,SACE,GAAG,UACH,GAAG,UAAU,UACb,GAAG,UAAU,cACb,GAAG,OAAO,UACV,GAAG,OAAO,UAAU,UACpB,GAAG,MAAM;AAEb;AAEO,SAAS,iBAAiB,GAAiB;AAChD,QAAM,SAAS,cAAc,CAAC;AAC9B,MAAI,WAAW,OAAO,WAAW,IAAK,QAAO;AAE7C,QAAM,MAAM,OAAO,GAAG,WAAW,CAAC;AAClC,MAAI,sBAAsB,KAAK,GAAG,EAAG,QAAO;AAC5C,SAAO,kDAAkD,KAAK,GAAG;AACnE;AAEO,SAAS,cAAc,GAAiB;AAC7C,QAAM,SAAS,cAAc,CAAC;AAC9B,SAAO,WAAW,UAAa,UAAU;AAC3C;AAEO,SAAS,gBAAgB,GAAuB;AACrD,QAAM,KACJ,GAAG,UAAU,SAAS,MAAM,aAAa,KACzC,GAAG,UAAU,UAAU,aAAa,KACpC,GAAG,UAAU,aAAa;AAC5B,QAAM,IAAI,OAAO,EAAE;AACnB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI,MAAO;AACzC;AAEO,SAAS,eAAe,GAAiB;AAE9C,MAAI,GAAG,SAAS,UAAW,QAAO;AAElC,QAAM,SAAS,cAAc,CAAC;AAE9B,MAAI,WAAW,IAAK,QAAO;AAE3B,QAAM,MAAM,OAAO,GAAG,WAAW,CAAC;AAGlC,SAAO,wEAAwE,KAAK,GAAG;AACzF;AAOO,SAAS,YACd,GACA,IACA,MACY;AACZ,MAAI;AAEJ,QAAM,UAAU,IAAI,QAAW,CAAC,GAAG,WAAW;AAC5C,QAAI,WAAW,MAAM;AACnB,YAAM,MAAW,IAAI;AAAA,QACnB,qBAAqB,EAAE,QACpB,MAAM,SAAS,WAAW,KAAK,MAAM,KAAK,OAC1C,MAAM,aAAa,aAAa,KAAK,UAAU,KAAK,OACpD,MAAM,WAAW,OAAO,YAAY,KAAK,OAAO,KAAK;AAAA,MAC1D;AACA,UAAI,OAAO;AACX,UAAI,UAAU;AACd,aAAO,GAAG;AAAA,IACZ,GAAG,EAAE;AAAA,EACP,CAAC;AAED,SAAO,QAAQ,KAAK,CAAC,GAAG,OAAO,CAAC,EAAE,QAAQ,MAAM,KAAK,aAAa,CAAC,CAAC;AACtE;AAEO,SAAS,eAAe,GAAiB;AAC9C,QAAM,KAAK,eAAe,CAAC;AAC3B,QAAM,KAAK,iBAAiB,CAAC;AAC7B,QAAM,KAAK,cAAc,CAAC;AAG1B,SAAO,MAAM,MAAM;AACrB;;;AC5HA,SAAS,iBAAiB,SAAyB,oBAAoB;;;ACAhE,IAAM,YAAN,MAAgB;AAAA,EAIrB,YAA6B,KAAa;AAAb;AAC3B,QAAI,CAAC,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG;AACrC,YAAM,IAAI,MAAM,iDAAiD,GAAG,EAAE;AAAA,IACxE;AAAA,EACF;AAAA,EAPQ,QAAQ;AAAA,EACR,QAA2B,CAAC;AAAA,EAQpC,MAAM,UAA+B;AACnC,QAAI,KAAK,QAAQ,KAAK,KAAK;AACzB,WAAK;AACL,UAAI,WAAW;AACf,aAAO,MAAM;AACX,YAAI,SAAU;AACd,mBAAW;AACX,aAAK,QAAQ;AAAA,MACf;AAAA,IACF;AAEA,WAAO,IAAI,QAAoB,CAAC,YAAY;AAC1C,WAAK,MAAM,KAAK,MAAM;AACpB,aAAK;AACL,YAAI,WAAW;AACf,gBAAQ,MAAM;AACZ,cAAI,SAAU;AACd,qBAAW;AACX,eAAK,QAAQ;AAAA,QACf,CAAC;AAAA,MACH,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,UAAU;AAChB,SAAK,QAAQ,KAAK,IAAI,GAAG,KAAK,QAAQ,CAAC;AACvC,UAAM,OAAO,KAAK,MAAM,MAAM;AAC9B,QAAI,KAAM,MAAK;AAAA,EACjB;AACF;;;ACvCO,IAAM,aAAN,MAAiB;AAAA,EAOtB,YAEmB,KAGA,QAAgB,KAAK,IAAI,GAAG,KAAK,KAAK,GAAG,CAAC,GAC3D;AAJiB;AAGA;AAGjB,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAdQ;AAAA;AAAA,EAGA,aAAa,KAAK,IAAI;AAAA;AAAA,EActB,OAAO,KAAa;AAE1B,QAAI,KAAK,OAAO,EAAG;AAEnB,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,WAAW,EAAG;AAIlB,UAAM,MAAO,UAAU,MAAQ,KAAK;AAGpC,SAAK,SAAS,KAAK,IAAI,KAAK,OAAO,KAAK,SAAS,GAAG;AAGpD,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA,EAIA,MAAM,KAAK,QAAQ,GAAkB;AACnC,QAAI,CAAC,KAAK,OAAO,KAAK,OAAO,EAAG;AAEhC,WAAO,MAAM;AACX,YAAM,MAAM,KAAK,IAAI;AAGrB,WAAK,OAAO,GAAG;AAGf,UAAI,KAAK,UAAU,OAAO;AACxB,aAAK,UAAU;AACf;AAAA,MACF;AAGA,YAAM,OAAO,QAAQ,KAAK;AAI1B,YAAM,SAAS,KAAK,KAAM,OAAO,KAAK,MAAO,GAAI;AAKjD,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC,CAAC;AAAA,IAC9D;AAAA,EACF;AACF;;;AFtCO,IAAM,oCAAN,cAAgD,gBAAgB;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAED,iBAAyB;AAAA,EAEjC,YAAY,QAAwB;AAClC,UAAM,EAAE,KAAK,YAAY,QAAQ,IAAI;AAErC,UAAM,UAAU,QAAQ,KAAK,OAAO;AACpC,UAAM,eAAe,IAAI,aAAa,GAAG;AAEzC,iBAAa,UAAU,OAAO,WAAW;AAEzC,UAAM,cAAc,SAAS,EAAE,eAAe,QAAQ,CAAC;AACvD,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,SAAK,UAAU;AACf,SAAK,SAAS;AAEd,UAAM,EAAE,MAAM,IAAI,UAAU,WAAW,EAAE,IAAI;AAE7C,SAAK,kBAAkB,IAAI,UAAU,QAAQ;AAC7C,SAAK,aAAa,IAAI,WAAW,KAAK,YAAY,GAAG;AACrD,SAAK,QAAQ,OAAO;AAAA,EACtB;AAAA,EAEA,MAAe,MAAM,SAA0D;AAC7E,UAAM,KAAK,WAAW,KAAK,CAAC;AAE5B,UAAM,UAAU,KAAK,kBAAkB,MAAM,KAAK,gBAAgB,QAAQ,IAAI;AAE9E,QAAI;AACF,aAAO,MAAM,KAAK,kBAAkB,OAAO;AAAA,IAC7C,UAAE;AACA,gBAAU;AAAA,IACZ;AAAA,EACF;AAAA;AAAA;AAAA,EAIA,MAAc,kBAAkB,SAA0D;AACxF,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,WAAW,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAE5D,SAAK,MAAM,wBAAwB,KAAK,UAAU;AAClD,SAAK,MAAM,kBAAkB,KAAK,UAAU;AAE5C,eAAW,KAAK,UAAU;AACxB,WAAK,MAAM,cAAc,EAAE,MAAM;AACjC,WAAK,OAAO,UAAU;AAAA,QACpB,MAAM;AAAA,QACN,SAAS,KAAK;AAAA,QACd,YAAY,KAAK;AAAA,QACjB,QAAQ,EAAE;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI;AACF,YAAM,OAAO,MAAM,MAAM,OAAO;AAChC,YAAM,MAAM,MAAM,YAAY,MAAM,KAAK,OAAO,WAAW,KAAQ;AAAA,QACjE,SAAS,KAAK;AAAA,QACd,YAAY,KAAK;AAAA,MACnB,CAAC;AAED,YAAM,UAAU,KAAK,IAAI;AAEzB,iBAAW,KAAK,UAAU;AACxB,aAAK,OAAO,UAAU;AAAA,UACpB,MAAM;AAAA,UACN,SAAS,KAAK;AAAA,UACd,YAAY,KAAK;AAAA,UACjB,QAAQ,EAAE;AAAA,UACV;AAAA,UACA;AAAA,UACA,IAAI,UAAU;AAAA,QAChB,CAAC;AAAA,MACH;AAEA,WAAK,iBAAiB;AAEtB,aAAO;AAAA,IACT,SAAS,GAAQ;AACf,YAAM,UAAU,KAAK,IAAI;AACzB,YAAM,KAAK,iBAAiB,CAAC;AAC7B,UAAI,IAAI;AACN,aAAK,MAAM,2BAA2B,KAAK,UAAU;AACrD,cAAM,aAAa;AACnB,cAAM,OAAO,gBAAgB,CAAC,KAAK;AACnC,aAAK,MAAM,YAAY,KAAK,YAAY,IAAI;AAAA,MAC9C;AAEA,YAAM,YAAY,eAAe,CAAC;AAClC,UAAI,aAAa,CAAC,IAAI;AACpB,aAAK,MAAM,uBAAuB,KAAK,UAAU;AAEjD,cAAM,IAAI,KAAK,MAAM,SAAS,EAAE,iBAAiB,KAAK,UAAU,KAAK;AACrE,cAAM,QAAQ,KAAK,MAAM,aAAa,KAAK,UAAU;AAGrD,YAAI,KAAK,MAAM,SAAS,KAAK;AAC3B,gBAAM,aAAa,SAAS,MAAM,MAAU;AAC5C,gBAAM,OAAO,gBAAgB,CAAC,KAAK;AACnC,eAAK,iBAAiB;AACtB,eAAK,MAAM,YAAY,KAAK,YAAY,OAAO,KAAK,MAAM,KAAK,OAAO,IAAI,GAAI,CAAC;AAAA,QACjF;AAAA,MACF;AAEA,YAAM,UAAU,cAAc,CAAC;AAC/B,UAAI,WAAW,CAAC,MAAM,CAAC,WAAW;AAChC,aAAK,MAAM,2BAA2B,KAAK,UAAU;AACrD,cAAM,cAAc,KAAK,iBAAiB,KAAK,OAAU,KAAK,MAAM,KAAK,OAAO,IAAI,GAAI;AACxF,aAAK,iBAAiB;AACtB,aAAK,MAAM,YAAY,KAAK,YAAY,UAAU;AAAA,MACpD;AAEA,iBAAW,KAAK,UAAU;AACxB,aAAK,OAAO,UAAU;AAAA,UACpB,MAAM;AAAA,UACN,SAAS,KAAK;AAAA,UACd,YAAY,KAAK;AAAA,UACjB,QAAQ,EAAE;AAAA,UACV;AAAA,UACA;AAAA,UACA,IAAI,UAAU;AAAA,UACd,aAAa;AAAA,UACb;AAAA,UACA,QAAQ,cAAc,CAAC;AAAA,UACvB,MAAM,GAAG;AAAA,UACT,SAAS,OAAO,GAAG,WAAW,CAAC;AAAA,QACjC,CAAC;AAAA,MACH;AAEA,YAAM;AAAA,IACR,UAAE;AACA,WAAK,MAAM,4BAA4B,KAAK,UAAU;AAAA,IACxD;AAAA,EACF;AACF;;;AG3KO,IAAM,SAAN,MAAa;AAAA,EAGlB,YACmB,WACA,OACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EALK,KAAK;AAAA,EAOb,OAAe;AACb,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA,EAEA,OAAiB;AACf,UAAM,IAAI,KAAK,UAAU;AACzB,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,YAAM,KAAM,KAAK,OAAO,IAAK,KAAK;AAClC,YAAM,KAAK,KAAK,UAAU,CAAC;AAE3B,UAAI,CAAC,KAAK,MAAM,aAAa,GAAG,UAAU,EAAG,QAAO;AAAA,IACtD;AAEA,WAAO,KAAK,WAAY,KAAK,OAAO,IAAK,KAAK,CAAC;AAAA,EACjD;AACF;;;ANNO,IAAM,kBAAN,cAA8BC,iBAAgB;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,QAA+B;AACzC,UAAM,UAAUC,SAAQ,KAAK,OAAO,OAAO;AAC3C,UAAM,oBAAoB,SAAS,EAAE,eAAe,QAAQ,CAAC;AAE7D,SAAK,SAAS;AAEd,SAAK,QAAQ,IAAI,MAAM;AAEvB,UAAM,YAAwB,KAAK,OAAO,KAAK,IAAI,CAAC,KAAK,MAAM;AAC7D,YAAM,aAAa,OAAO,IAAI,CAAC,YAAY,KAAK,OAAO,OAAO,IAAI,GAAG;AAErE,YAAM,EAAE,WAAW,GAAG,MAAM,GAAG,UAAU,UAAU,IAAO,IAAI,KAAK,OAAO;AAE1E,YAAM,WAAW,IAAI,kCAAkC;AAAA,QACrD;AAAA,QACA,SAAS,KAAK,OAAO;AAAA,QACrB;AAAA,QACA,OAAO,KAAK;AAAA,QACZ;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,KAAK,OAAO,OAAO;AAAA,MAC9B,CAAC;AAED,aAAO,EAAE,YAAY,KAAK,SAAS;AAAA,IACrC,CAAC;AAED,SAAK,SAAS,IAAI,OAAO,WAAW,KAAK,KAAK;AAAA,EAChD;AAAA;AAAA,EAGA,MAAM,KAAK,QAAgB,QAA2B;AACpD,UAAM,QAAQ,oBAAI,IAAY;AAC9B,UAAM,iBAAiB,KAAK,IAAI,KAAK,OAAO,MAAM,UAAU,KAAK,OAAO,KAAK,CAAC;AAE9E,WAAO,MAAM,OAAO,gBAAgB;AAClC,YAAM,KAAK,KAAK,OAAO,KAAK;AAC5B,UAAI,MAAM,IAAI,GAAG,UAAU,EAAG;AAC9B,YAAM,IAAI,GAAG,UAAU;AAEvB,UAAI;AACF,eAAO,MAAM,GAAG,SAAS,KAAK,QAAQ,MAAM;AAAA,MAC9C,SAAS,GAAQ;AACf,YAAI,CAAC,eAAe,CAAC,EAAG,OAAM;AAC9B,YAAI,MAAM,QAAQ,eAAgB,OAAM;AAGxC,cAAM,YAAY,KAAK,IAAI,MAAO,KAAK,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,GAAI;AACnE,cAAM,SAAS,KAAK,OAAO,IAAI;AAC/B,cAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,MAAM,CAAC;AAAA,MAC5D;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,kBAAkB;AAAA,EACpC;AAAA,EAEA,WAAkB;AAChB,WAAO,KAAK;AAAA,EACd;AACF;","names":["JsonRpcProvider","Network","JsonRpcProvider","Network"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ethers-rpc-pool",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "EVM RPC multiplexer for ethers.js with load balancing, rate limiting, failover and consistency controls.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -11,6 +11,12 @@
11
11
  "evm",
12
12
  "failover"
13
13
  ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
14
20
  "repository": {
15
21
  "type": "git",
16
22
  "url": "https://github.com/ahiipsa/ethers-rpc-pool.git"
@@ -32,7 +38,9 @@
32
38
  },
33
39
  "type": "module",
34
40
  "files": [
35
- "dist"
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
36
44
  ],
37
45
  "main": "./dist/index.cjs",
38
46
  "module": "./dist/index.js",
@@ -49,8 +57,10 @@
49
57
  },
50
58
  "devDependencies": {
51
59
  "@types/node": "^25.2.3",
60
+ "express": "^5.2.1",
52
61
  "husky": "^9.1.7",
53
62
  "lint-staged": "^16.2.7",
63
+ "msw": "^2.12.10",
54
64
  "prettier": "^3.8.1",
55
65
  "tsup": "^8.5.1",
56
66
  "tsx": "^4.21.0",