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/README.md +336 -89
- package/dist/index.cjs +282 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -71
- package/dist/index.d.ts +57 -71
- package/dist/index.js +282 -143
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 —
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 =
|
|
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
|
-
|
|
340
|
-
|
|
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(
|
|
384
|
-
this.
|
|
385
|
-
|
|
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
|
-
|
|
341
|
+
_groups;
|
|
342
|
+
_size;
|
|
343
|
+
_ewma = /* @__PURE__ */ new Map();
|
|
388
344
|
size() {
|
|
389
|
-
return this.
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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.
|
|
412
|
-
|
|
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
|
-
...
|
|
420
|
-
onEvent: this.
|
|
508
|
+
...providerOptions,
|
|
509
|
+
onEvent: (e) => this._handleTransportEvent(e)
|
|
421
510
|
});
|
|
422
|
-
|
|
511
|
+
endpoints.push({ id: providerId, provider });
|
|
512
|
+
const endpoint = { providerId, url, provider };
|
|
513
|
+
return { endpoint, priority: priority ?? 0 };
|
|
423
514
|
});
|
|
424
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
553
|
+
await this._sleepWithBackoff(attempt);
|
|
450
554
|
}
|
|
451
555
|
}
|
|
452
556
|
throw new Error("No RPC available");
|
|
453
557
|
}
|
|
454
|
-
|
|
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
|
-
|
|
600
|
+
_emitRpcLogicalError(ep, method, startedAt, error) {
|
|
460
601
|
const endedAt = Date.now();
|
|
461
|
-
this.
|
|
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
|