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