ethers-rpc-pool 1.1.4 → 3.2.0

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/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ var Stats = class {
35
35
  _perProviderMethod = {};
36
36
  _rateLimitedTotal = 0;
37
37
  _timeoutTotal = 0;
38
+ _serverErrorTotal = 0;
38
39
  _rpcErrorTotal = 0;
39
40
  _perProviderInFlight = {};
40
41
  _perProviderTotal = {};
@@ -43,79 +44,55 @@ var Stats = class {
43
44
  _perProviderError = {};
44
45
  _perProviderRpcError = {};
45
46
  _perMethodRpcError = {};
46
- _providerCooldownUntil = {};
47
47
  _bump(map, key) {
48
48
  map[key] = (map[key] || 0) + 1;
49
49
  }
50
50
  _decrease(map, key) {
51
51
  map[key] = Math.max((map[key] || 0) - 1, 0);
52
52
  }
53
- _bumpTotal() {
54
- this._total++;
55
- }
56
- _bumpInFlight() {
57
- this._inFlight++;
58
- }
59
- _bumpRateLimitedTotal() {
60
- this._rateLimitedTotal++;
61
- }
62
- _bumpTimeoutTotal() {
63
- this._timeoutTotal++;
64
- }
65
- _bumpRpcErrorTotal() {
66
- this._rpcErrorTotal++;
67
- }
68
- bumpInFlightPerProvider(id) {
69
- this._bumpInFlight();
70
- this._bump(this._perProviderInFlight, id);
71
- }
72
- decreaseInFlightPerProvider(id) {
73
- this.decreaseInFlight();
74
- this._decrease(this._perProviderInFlight, id);
75
- }
76
- decreaseInFlight() {
53
+ onEvent(e) {
54
+ const id = e.providerId;
55
+ if (e.type === "request") {
56
+ this._inFlight++;
57
+ this._bump(this._perProviderInFlight, id);
58
+ this._total++;
59
+ this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;
60
+ this._bump(this._perMethod, e.method);
61
+ if (!this._perProviderMethod[id]) this._perProviderMethod[id] = {};
62
+ this._bump(this._perProviderMethod[id], e.method);
63
+ return;
64
+ }
77
65
  this._inFlight = Math.max(this._inFlight - 1, 0);
78
- }
79
- bumpPerMethod(id, method) {
80
- this._bump(this._perMethod, method);
81
- if (!this._perProviderMethod[id]) {
82
- this._perProviderMethod[id] = {};
66
+ this._decrease(this._perProviderInFlight, id);
67
+ if (e.type === "error") {
68
+ if (e.isRateLimit) {
69
+ this._rateLimitedTotal++;
70
+ this._bump(this._perProviderRateLimited, id);
71
+ } else if (e.isTimeout) {
72
+ this._timeoutTotal++;
73
+ this._bump(this._perProviderTimeout, id);
74
+ } else if (e.status !== void 0 && e.status >= 500) {
75
+ this._serverErrorTotal++;
76
+ this._bump(this._perProviderError, id);
77
+ }
83
78
  }
84
- this._bump(this._perProviderMethod[id], method);
85
- }
86
- bumpRateLimitedPerProvider(id) {
87
- this._bumpRateLimitedTotal();
88
- this._bump(this._perProviderRateLimited, id);
89
- }
90
- bumpTimeoutPerProvider(id) {
91
- this._bumpTimeoutTotal();
92
- this._bump(this._perProviderTimeout, id);
93
- }
94
- bumpProviderTotal(id) {
95
- this._bumpTotal();
96
- this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;
97
- }
98
- bumpServerErrorPerProvider(id) {
99
- this._bump(this._perProviderError, id);
100
79
  }
101
80
  bumpRpcError(providerId, method) {
102
- this._bumpRpcErrorTotal();
81
+ this._rpcErrorTotal++;
103
82
  if (!this._perProviderRpcError[providerId]) {
104
83
  this._perProviderRpcError[providerId] = {};
105
84
  }
106
85
  this._bump(this._perProviderRpcError[providerId], method);
107
86
  this._bump(this._perMethodRpcError, method);
108
87
  }
109
- timeoutRatio(id) {
110
- const t = this._perProviderTimeout[id] || 0;
111
- const n = this._perProviderTotal[id] || 0;
112
- return n ? t / n : 0;
113
- }
114
- isInCooldown(id) {
115
- return (this._providerCooldownUntil[id] || 0) > Date.now();
116
- }
117
- setCooldown(id, ms) {
118
- this._providerCooldownUntil[id] = Date.now() + ms;
88
+ removeProvider(id) {
89
+ delete this._perProviderInFlight[id];
90
+ delete this._perProviderTotal[id];
91
+ delete this._perProviderTimeout[id];
92
+ delete this._perProviderRateLimited[id];
93
+ delete this._perProviderError[id];
94
+ delete this._perProviderRpcError[id];
95
+ delete this._perProviderMethod[id];
119
96
  }
120
97
  snapshot() {
121
98
  return {
@@ -124,16 +101,20 @@ var Stats = class {
124
101
  perMethodTotal: { ...this._perMethod },
125
102
  rateLimitedTotal: this._rateLimitedTotal,
126
103
  timeoutTotal: this._timeoutTotal,
104
+ serverErrorTotal: this._serverErrorTotal,
127
105
  rpcErrorTotal: this._rpcErrorTotal,
128
- perProviderMethod: { ...this._perProviderMethod },
106
+ perProviderMethod: Object.fromEntries(
107
+ Object.entries(this._perProviderMethod).map(([k, v]) => [k, { ...v }])
108
+ ),
129
109
  perProviderInFlight: { ...this._perProviderInFlight },
130
110
  perProviderRateLimited: { ...this._perProviderRateLimited },
131
111
  perProviderTimeout: { ...this._perProviderTimeout },
132
112
  perProviderError: { ...this._perProviderError },
133
- perProviderRpcError: { ...this._perProviderRpcError },
113
+ perProviderRpcError: Object.fromEntries(
114
+ Object.entries(this._perProviderRpcError).map(([k, v]) => [k, { ...v }])
115
+ ),
134
116
  perMethodRpcError: { ...this._perMethodRpcError },
135
- perProviderTotal: { ...this._perProviderTotal },
136
- providerCooldownUntil: { ...this._providerCooldownUntil }
117
+ perProviderTotal: { ...this._perProviderTotal }
137
118
  };
138
119
  }
139
120
  };
@@ -165,8 +146,12 @@ function isTimeoutError(e) {
165
146
  const msg = String(e?.message || e);
166
147
  return /timeout|timed out|ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED|504 Gateway/i.test(msg);
167
148
  }
149
+ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ECONNRESET", "ENOTFOUND", "EAI_AGAIN"]);
150
+ function isNetworkError(e) {
151
+ return NETWORK_ERROR_CODES.has(e?.code);
152
+ }
168
153
  function isTransportError(e) {
169
- return isTimeoutError(e) || isRateLimitError(e) || isServerError(e);
154
+ return isTimeoutError(e) || isRateLimitError(e) || isServerError(e) || isNetworkError(e);
170
155
  }
171
156
  function isRpcLogicalError(e) {
172
157
  if (isTransportError(e)) return false;
@@ -179,10 +164,7 @@ function isRpcLogicalError(e) {
179
164
  );
180
165
  }
181
166
  function shouldFailover(e) {
182
- const to = isTimeoutError(e);
183
- const rl = isRateLimitError(e);
184
- const se = isServerError(e);
185
- return to || rl || se;
167
+ return isTimeoutError(e) || isRateLimitError(e) || isServerError(e) || isNetworkError(e);
186
168
  }
187
169
 
188
170
  // src/InstrumentedProvider.ts
@@ -257,20 +239,16 @@ var RpsLimiter = class {
257
239
  return this.tokens >= count;
258
240
  }
259
241
  // Take count tokens (usually 1 request = 1 token).
260
- // If not enough tokens — wait and try again.
242
+ // If not enough tokens — enqueue exactly one setTimeout for the precise wake-up time.
243
+ // tokens may go negative (debt); each caller's wait time is derived from that debt.
261
244
  async take(count = 1) {
262
245
  if (!this.rps || this.rps <= 0) return;
263
- while (true) {
264
- const now = Date.now();
265
- this.refill(now);
266
- if (this.tokens >= count) {
267
- this.tokens -= count;
268
- return;
269
- }
270
- const need = count - this.tokens;
271
- const waitMs = Math.ceil(need / this.rps * 1e3);
272
- await new Promise((r) => setTimeout(r, Math.min(waitMs, 50)));
273
- }
246
+ const now = Date.now();
247
+ this.refill(now);
248
+ this.tokens -= count;
249
+ if (this.tokens >= 0) return;
250
+ const waitMs = Math.ceil(-this.tokens / this.rps * 1e3);
251
+ return new Promise((resolve) => setTimeout(resolve, waitMs));
274
252
  }
275
253
  };
276
254
 
@@ -281,9 +259,7 @@ var InstrumentedJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
281
259
  options;
282
260
  inFlightLimiter;
283
261
  rpsLimiter;
284
- stats;
285
262
  fetchRequest;
286
- lastCooldownMs = 0;
287
263
  constructor(url, network, options) {
288
264
  let fetchRequest;
289
265
  if (typeof url == "string") {
@@ -301,29 +277,23 @@ var InstrumentedJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
301
277
  const { rps = 10, rpsBurst, inFlight = 1 } = options;
302
278
  this.inFlightLimiter = new Semaphore(inFlight);
303
279
  this.rpsLimiter = new RpsLimiter(rps, rpsBurst || rps);
304
- this.stats = options.stats;
305
280
  }
306
281
  async _send(payload) {
307
282
  await this.rpsLimiter.take(1);
308
- const release = this.inFlightLimiter ? await this.inFlightLimiter.acquire() : void 0;
283
+ const release = await this.inFlightLimiter.acquire();
309
284
  try {
310
285
  return await this._sendInstrumented(payload);
311
286
  } finally {
312
- release?.();
287
+ release();
313
288
  }
314
289
  }
315
290
  isAvailable(count = 1) {
316
291
  return this.rpsLimiter.isAvailable(count) && this.inFlightLimiter.isAvailable();
317
292
  }
318
- // ethers v5 calls send(method, params)
319
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
320
293
  async _sendInstrumented(payload) {
321
294
  const startedAt = Date.now();
322
295
  const payloads = Array.isArray(payload) ? payload : [payload];
323
- this.stats.bumpInFlightPerProvider(this.providerId);
324
- this.stats.bumpProviderTotal(this.providerId);
325
296
  for (const p of payloads) {
326
- this.stats.bumpPerMethod(this.providerId, p.method);
327
297
  this.options.onEvent?.({
328
298
  type: "request",
329
299
  chainId: this.chainId,
@@ -346,36 +316,13 @@ var InstrumentedJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
346
316
  ms: endedAt - startedAt
347
317
  });
348
318
  }
349
- this.lastCooldownMs = 0;
350
319
  return res;
351
320
  } catch (e) {
352
321
  const endedAt = Date.now();
353
322
  const rl = isRateLimitError(e);
354
- if (rl) {
355
- this.stats.bumpRateLimitedPerProvider(this.providerId);
356
- const cooldownMs = 1e4;
357
- const raMs = getRetryAfterMs(e) ?? cooldownMs;
358
- this.stats.setCooldown(this.providerId, raMs);
359
- }
360
323
  const isTimeout = isTimeoutError(e);
361
- if (isTimeout && !rl) {
362
- this.stats.bumpTimeoutPerProvider(this.providerId);
363
- const n = this.stats.snapshot().perProviderTotal[this.providerId] || 0;
364
- const ratio = this.stats.timeoutRatio(this.providerId);
365
- if (n >= 50 && ratio >= 0.2) {
366
- const cooldownMs = ratio >= 0.5 ? 6e5 : 6e4;
367
- const raMs = getRetryAfterMs(e) ?? cooldownMs;
368
- this.lastCooldownMs = raMs;
369
- this.stats.setCooldown(this.providerId, raMs + Math.floor(Math.random() * 1e3));
370
- }
371
- }
372
- const isError = isServerError(e);
373
- if (isError && !rl && !isTimeout) {
374
- this.stats.bumpServerErrorPerProvider(this.providerId);
375
- const cooldownMs = (this.lastCooldownMs * 2 || 1e4) + Math.floor(Math.random() * 1e3);
376
- this.lastCooldownMs = cooldownMs;
377
- this.stats.setCooldown(this.providerId, cooldownMs);
378
- }
324
+ const ne = isNetworkError(e);
325
+ const retryAfterMs = getRetryAfterMs(e) ?? void 0;
379
326
  for (const p of payloads) {
380
327
  this.options.onEvent?.({
381
328
  type: "error",
@@ -387,37 +334,174 @@ var InstrumentedJsonRpcProvider = class extends import_ethers.JsonRpcProvider {
387
334
  ms: endedAt - startedAt,
388
335
  isRateLimit: rl,
389
336
  isTimeout,
337
+ isNetworkError: ne,
390
338
  status: getHttpStatus(e),
339
+ retryAfterMs,
391
340
  code: e?.code,
392
341
  message: String(e?.message || e),
393
342
  errorKind: "transport"
394
343
  });
395
344
  }
396
345
  throw e;
397
- } finally {
398
- this.stats.decreaseInFlightPerProvider(this.providerId);
399
346
  }
400
347
  }
401
348
  };
402
349
 
403
350
  // src/Router.ts
351
+ var EWMA_ALPHA = 0.2;
404
352
  var Router = class {
405
- constructor(endpoints, stats) {
406
- this.endpoints = endpoints;
407
- this.stats = stats;
353
+ constructor(inputs, availability) {
354
+ this.availability = availability;
355
+ const byPriority = /* @__PURE__ */ new Map();
356
+ for (const { endpoint, priority } of inputs) {
357
+ if (!byPriority.has(priority)) byPriority.set(priority, []);
358
+ byPriority.get(priority).push({ endpoint });
359
+ }
360
+ this._groups = [...byPriority.entries()].sort(([a], [b]) => b - a).map(([, entries]) => ({ entries, rr: 0 }));
361
+ this._size = inputs.length;
408
362
  }
409
- rr = 0;
363
+ _groups;
364
+ _size;
365
+ _ewma = /* @__PURE__ */ new Map();
410
366
  size() {
411
- return this.endpoints.length;
367
+ return this._size;
368
+ }
369
+ totalSlots() {
370
+ return this._size;
371
+ }
372
+ recordLatency(providerId, ms) {
373
+ const prev = this._ewma.get(providerId);
374
+ this._ewma.set(providerId, prev === void 0 ? ms : EWMA_ALPHA * ms + (1 - EWMA_ALPHA) * prev);
375
+ }
376
+ ewmaSnapshot() {
377
+ return Object.fromEntries(this._ewma);
412
378
  }
413
379
  pick() {
414
- const n = this.endpoints.length;
415
- for (let k = 0; k < n; k++) {
416
- const i = (this.rr++ % n + n) % n;
417
- const ep = this.endpoints[i];
418
- if (!this.stats.isInCooldown(ep.providerId) && ep.provider.isAvailable(1)) return ep;
380
+ for (const group of this._groups) {
381
+ const ep = this._pickFromGroup(group);
382
+ if (ep !== null) return ep;
383
+ }
384
+ const g = this._groups[0];
385
+ const entry = g.entries[g.rr % g.entries.length];
386
+ g.rr++;
387
+ return entry.endpoint;
388
+ }
389
+ _pickFromGroup(group) {
390
+ const available = group.entries.filter(
391
+ (e) => !this.availability.isInCooldown(e.endpoint.providerId) && e.endpoint.provider.isAvailable(1)
392
+ );
393
+ if (available.length === 0) return null;
394
+ if (available.length === 1) return available[0].endpoint;
395
+ const i = Math.floor(Math.random() * available.length);
396
+ let j = Math.floor(Math.random() * (available.length - 1));
397
+ if (j >= i) j++;
398
+ const a = available[i].endpoint;
399
+ const b = available[j].endpoint;
400
+ const ewmaA = this._ewma.get(a.providerId) ?? 0;
401
+ const ewmaB = this._ewma.get(b.providerId) ?? 0;
402
+ return ewmaA <= ewmaB ? a : b;
403
+ }
404
+ };
405
+
406
+ // src/CooldownManager.ts
407
+ var CooldownManager = class {
408
+ _cooldownUntil = {};
409
+ _lastCooldownMs = {};
410
+ _perProviderTotal = {};
411
+ _perProviderTimeout = {};
412
+ _circuitState = {};
413
+ _probeInFlight = /* @__PURE__ */ new Set();
414
+ // atomically claims the probe slot when entering half-open
415
+ isInCooldown(id) {
416
+ const state = this._circuitState[id];
417
+ if (!state) return false;
418
+ if (state === "half-open") {
419
+ if (!this._probeInFlight.has(id)) {
420
+ this._probeInFlight.add(id);
421
+ return false;
422
+ }
423
+ return true;
424
+ }
425
+ const until = this._cooldownUntil[id];
426
+ if (until === void 0 || until <= Date.now()) {
427
+ delete this._cooldownUntil[id];
428
+ this._circuitState[id] = "half-open";
429
+ this._probeInFlight.add(id);
430
+ return false;
431
+ }
432
+ return true;
433
+ }
434
+ _openCircuit(id, ms) {
435
+ this._cooldownUntil[id] = Date.now() + ms;
436
+ this._circuitState[id] = "open";
437
+ this._probeInFlight.delete(id);
438
+ }
439
+ _openWithBackoff(id) {
440
+ const prev = this._lastCooldownMs[id] || 0;
441
+ const ms = (prev ? prev * 2 : 1e4) + Math.floor(Math.random() * 1e3);
442
+ this._lastCooldownMs[id] = ms;
443
+ this._openCircuit(id, ms);
444
+ }
445
+ cooldownSnapshot() {
446
+ return { ...this._cooldownUntil };
447
+ }
448
+ circuitStateSnapshot() {
449
+ const result = {};
450
+ for (const [id, state] of Object.entries(this._circuitState)) {
451
+ result[id] = state;
452
+ }
453
+ return result;
454
+ }
455
+ removeProvider(id) {
456
+ delete this._cooldownUntil[id];
457
+ delete this._lastCooldownMs[id];
458
+ delete this._perProviderTotal[id];
459
+ delete this._perProviderTimeout[id];
460
+ delete this._circuitState[id];
461
+ this._probeInFlight.delete(id);
462
+ }
463
+ onEvent(e) {
464
+ const id = e.providerId;
465
+ if (e.type === "request") {
466
+ this._perProviderTotal[id] = (this._perProviderTotal[id] || 0) + 1;
467
+ return;
468
+ }
469
+ if (e.type === "response") {
470
+ if (this._circuitState[id] === "half-open") {
471
+ delete this._circuitState[id];
472
+ this._probeInFlight.delete(id);
473
+ }
474
+ this._lastCooldownMs[id] = 0;
475
+ return;
476
+ }
477
+ if (this._circuitState[id] === "half-open") {
478
+ if (e.isRateLimit) {
479
+ this._openCircuit(id, e.retryAfterMs ?? 1e4);
480
+ } else {
481
+ this._openWithBackoff(id);
482
+ }
483
+ return;
484
+ }
485
+ if (e.isRateLimit) {
486
+ this._openCircuit(id, e.retryAfterMs ?? 1e4);
487
+ return;
488
+ }
489
+ if (e.isTimeout) {
490
+ this._perProviderTimeout[id] = (this._perProviderTimeout[id] || 0) + 1;
491
+ const n = this._perProviderTotal[id] || 0;
492
+ const t = this._perProviderTimeout[id];
493
+ const ratio = n ? t / n : 0;
494
+ if (n >= 50 && ratio >= 0.2) {
495
+ const baseCooldown = ratio >= 0.5 ? 6e5 : 6e4;
496
+ const ms = (e.retryAfterMs ?? baseCooldown) + Math.floor(Math.random() * 1e3);
497
+ this._lastCooldownMs[id] = ms;
498
+ this._openCircuit(id, ms);
499
+ }
500
+ return;
501
+ }
502
+ if (e.status !== void 0 && e.status >= 500 || e.isNetworkError) {
503
+ this._openWithBackoff(id);
419
504
  }
420
- return this.endpoints[(this.rr++ % n + n) % n];
421
505
  }
422
506
  };
423
507
 
@@ -425,35 +509,55 @@ var Router = class {
425
509
  var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
426
510
  router;
427
511
  params;
428
- stats;
512
+ _stats;
513
+ _cooldown;
514
+ _endpoints;
515
+ _probeInterval;
429
516
  constructor(params) {
430
517
  const network = import_ethers2.Network.from(params.network);
431
518
  super("http://localhost", network, { staticNetwork: network });
432
519
  this.params = params;
433
- this.stats = new Stats();
434
- const endpoints = this.params.rpc.map((options, i) => {
520
+ this._stats = new Stats();
521
+ this._cooldown = new CooldownManager();
522
+ const endpoints = [];
523
+ const routerInputs = this.params.rpc.map((options, i) => {
524
+ const { priority, network: _n, ...providerOptions } = options;
435
525
  const url = typeof options.url === "string" ? options.url : options.url.url;
436
526
  const providerId = `rpc#${i + 1}-chainId:${this.params.network}-${url}`;
437
527
  const provider = new InstrumentedJsonRpcProvider(options.url, this.params.network, {
438
528
  providerId,
439
- stats: this.stats,
440
529
  ...this.params.defaultRpcOptions,
441
- ...options,
442
- onEvent: this.params.hooks?.onEvent
530
+ ...providerOptions,
531
+ onEvent: (e) => this._handleTransportEvent(e)
443
532
  });
444
- return { providerId, url, provider };
533
+ endpoints.push({ id: providerId, provider });
534
+ const endpoint = { providerId, url, provider };
535
+ return { endpoint, priority: priority ?? 0 };
445
536
  });
446
- this.router = new Router(endpoints, this.stats);
537
+ this._endpoints = endpoints;
538
+ this.router = new Router(routerInputs, this._cooldown);
539
+ if (params.healthProbe !== void 0) {
540
+ const intervalMs = params.healthProbe.intervalMs ?? 15e3;
541
+ this._probeInterval = setInterval(() => this._runHealthProbe(), intervalMs);
542
+ this._probeInterval.unref?.();
543
+ }
544
+ }
545
+ destroy() {
546
+ if (this._probeInterval !== void 0) {
547
+ clearInterval(this._probeInterval);
548
+ this._probeInterval = void 0;
549
+ }
447
550
  }
448
551
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
449
552
  async send(method, params) {
553
+ if (this.router.size() === 0) throw new Error("No RPC available");
450
554
  const tried = /* @__PURE__ */ new Set();
451
555
  const maxAttempts = this.params.retry.attempts;
452
556
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
453
557
  let endpoint = this.router.pick();
454
558
  if (tried.size < this.router.size()) {
455
559
  let retries = 0;
456
- while (tried.has(endpoint.providerId) && retries < this.router.size()) {
560
+ while (tried.has(endpoint.providerId) && retries < this.router.totalSlots()) {
457
561
  endpoint = this.router.pick();
458
562
  retries++;
459
563
  }
@@ -464,23 +568,60 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
464
568
  return await endpoint.provider.send(method, params);
465
569
  } catch (e) {
466
570
  if (isRpcLogicalError(e)) {
467
- this.emitRpcLogicalError(endpoint, method, startedAt, e);
571
+ this._emitRpcLogicalError(endpoint, method, startedAt, e);
468
572
  }
469
573
  if (!shouldFailover(e)) throw e;
470
574
  if (attempt === maxAttempts - 1) throw e;
471
- await this.sleepWithBackoff(attempt);
575
+ await this._sleepWithBackoff(attempt);
472
576
  }
473
577
  }
474
578
  throw new Error("No RPC available");
475
579
  }
476
- async sleepWithBackoff(attempt) {
580
+ getSnapshot() {
581
+ return {
582
+ ...this._stats.snapshot(),
583
+ providerCooldownUntil: this._cooldown.cooldownSnapshot(),
584
+ providerCircuitState: this._cooldown.circuitStateSnapshot(),
585
+ perProviderLatencyEwma: this.router.ewmaSnapshot()
586
+ };
587
+ }
588
+ // Returns a provider pinned to a single RPC node selected right now.
589
+ // Use when consecutive calls must see consistent chain state — e.g. read
590
+ // eth_getBalance on block N, then eth_call on the same block N. Without
591
+ // pinning, each call may route to a different node that lags behind.
592
+ pinnedProvider() {
593
+ return this.router.pick().provider;
594
+ }
595
+ _runHealthProbe() {
596
+ const states = this._cooldown.circuitStateSnapshot();
597
+ const cooldowns = this._cooldown.cooldownSnapshot();
598
+ const now = Date.now();
599
+ for (const { id, provider } of this._endpoints) {
600
+ if (states[id] !== "open") continue;
601
+ const cooldownUntil = cooldowns[id];
602
+ if (cooldownUntil !== void 0 && cooldownUntil > now) continue;
603
+ if (!this._cooldown.isInCooldown(id)) {
604
+ provider.send("eth_blockNumber", []).catch(() => {
605
+ });
606
+ }
607
+ }
608
+ }
609
+ _handleTransportEvent(e) {
610
+ this._stats.onEvent(e);
611
+ this._cooldown.onEvent(e);
612
+ if (e.type === "response" || e.type === "error") {
613
+ this.router.recordLatency(e.providerId, e.ms);
614
+ }
615
+ this.params.hooks?.onEvent?.(e);
616
+ }
617
+ async _sleepWithBackoff(attempt) {
477
618
  const baseDelay = Math.min(1e3 * 2 ** attempt, 5e3);
478
619
  const jitter = Math.random() * baseDelay;
479
620
  await new Promise((resolve) => setTimeout(resolve, jitter));
480
621
  }
481
- emitRpcLogicalError(ep, method, startedAt, error) {
622
+ _emitRpcLogicalError(ep, method, startedAt, error) {
482
623
  const endedAt = Date.now();
483
- this.stats.bumpRpcError(ep.providerId, method);
624
+ this._stats.bumpRpcError(ep.providerId, method);
484
625
  this.params.hooks?.onEvent?.({
485
626
  type: "error",
486
627
  chainId: BigInt(import_ethers2.Network.from(this.params.network).chainId),
@@ -491,15 +632,13 @@ var RPCPoolProvider = class extends import_ethers2.JsonRpcProvider {
491
632
  ms: endedAt - startedAt,
492
633
  isRateLimit: false,
493
634
  isTimeout: false,
635
+ isNetworkError: false,
494
636
  status: void 0,
495
637
  code: error?.code,
496
638
  message: String(error?.message || error),
497
639
  errorKind: "rpc"
498
640
  });
499
641
  }
500
- getStats() {
501
- return this.stats;
502
- }
503
642
  };
504
643
  // Annotate the CommonJS export names for ESM import in node:
505
644
  0 && (module.exports = {