@hiveio/dhive 1.3.4 → 1.3.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/dist/dhive.d.ts +17 -0
- package/dist/dhive.js +3 -3
- package/dist/dhive.js.gz +0 -0
- package/dist/dhive.js.map +1 -1
- package/lib/health-tracker.d.ts +17 -0
- package/lib/health-tracker.js +29 -1
- package/lib/utils.js +43 -13
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/health-tracker.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export interface HealthTrackerOptions {
|
|
|
38
38
|
* Default: 2 minutes.
|
|
39
39
|
*/
|
|
40
40
|
headBlockTtlMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Default duration (ms) to skip a node after receiving a 429 response,
|
|
43
|
+
* used when the server doesn't provide a Retry-After header.
|
|
44
|
+
* Default: 10 seconds.
|
|
45
|
+
*/
|
|
46
|
+
defaultRateLimitMs?: number;
|
|
41
47
|
}
|
|
42
48
|
export declare class NodeHealthTracker {
|
|
43
49
|
private health;
|
|
@@ -49,6 +55,7 @@ export declare class NodeHealthTracker {
|
|
|
49
55
|
private readonly maxApiFailuresBeforeCooldown;
|
|
50
56
|
private readonly staleBlockThreshold;
|
|
51
57
|
private readonly headBlockTtlMs;
|
|
58
|
+
private readonly defaultRateLimitMs;
|
|
52
59
|
constructor(options?: HealthTrackerOptions);
|
|
53
60
|
private getOrCreate;
|
|
54
61
|
/**
|
|
@@ -61,6 +68,16 @@ export declare class NodeHealthTracker {
|
|
|
61
68
|
* Increments both the global consecutive failure counter and the API-specific counter.
|
|
62
69
|
*/
|
|
63
70
|
recordFailure(node: string, api: string): void;
|
|
71
|
+
/**
|
|
72
|
+
* Record that a node returned HTTP 429 (Too Many Requests).
|
|
73
|
+
* The node will be skipped until the rate limit expires.
|
|
74
|
+
* @param retryAfterSeconds Value from the Retry-After header, or undefined to use default.
|
|
75
|
+
*/
|
|
76
|
+
recordRateLimit(node: string, retryAfterSeconds?: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Check if a node is currently rate-limited (429 cooldown active).
|
|
79
|
+
*/
|
|
80
|
+
isRateLimited(node: string): boolean;
|
|
64
81
|
/**
|
|
65
82
|
* Record an API/plugin-specific failure (e.g. "method not found", "plugin not enabled").
|
|
66
83
|
* Only increments the per-API counter, NOT the global consecutive failure counter.
|
package/lib/health-tracker.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
class NodeHealthTracker {
|
|
13
13
|
constructor(options = {}) {
|
|
14
|
-
var _a, _b, _c, _d, _e, _f;
|
|
14
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
15
15
|
this.health = new Map();
|
|
16
16
|
this.bestKnownHeadBlock = 0;
|
|
17
17
|
this.bestKnownHeadBlockTime = 0;
|
|
@@ -21,6 +21,7 @@ class NodeHealthTracker {
|
|
|
21
21
|
this.maxApiFailuresBeforeCooldown = (_d = options.maxApiFailuresBeforeCooldown) !== null && _d !== void 0 ? _d : 2;
|
|
22
22
|
this.staleBlockThreshold = (_e = options.staleBlockThreshold) !== null && _e !== void 0 ? _e : 30;
|
|
23
23
|
this.headBlockTtlMs = (_f = options.headBlockTtlMs) !== null && _f !== void 0 ? _f : 120000;
|
|
24
|
+
this.defaultRateLimitMs = (_g = options.defaultRateLimitMs) !== null && _g !== void 0 ? _g : 10000;
|
|
24
25
|
}
|
|
25
26
|
getOrCreate(node) {
|
|
26
27
|
let state = this.health.get(node);
|
|
@@ -55,6 +56,29 @@ class NodeHealthTracker {
|
|
|
55
56
|
state.lastFailure = Date.now();
|
|
56
57
|
this.incrementApiFailure(state, api);
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Record that a node returned HTTP 429 (Too Many Requests).
|
|
61
|
+
* The node will be skipped until the rate limit expires.
|
|
62
|
+
* @param retryAfterSeconds Value from the Retry-After header, or undefined to use default.
|
|
63
|
+
*/
|
|
64
|
+
recordRateLimit(node, retryAfterSeconds) {
|
|
65
|
+
const state = this.getOrCreate(node);
|
|
66
|
+
const delayMs = retryAfterSeconds != null
|
|
67
|
+
? retryAfterSeconds * 1000
|
|
68
|
+
: this.defaultRateLimitMs;
|
|
69
|
+
state.rateLimit = { retryAfter: Date.now() + delayMs };
|
|
70
|
+
state.consecutiveFailures++;
|
|
71
|
+
state.lastFailure = Date.now();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check if a node is currently rate-limited (429 cooldown active).
|
|
75
|
+
*/
|
|
76
|
+
isRateLimited(node) {
|
|
77
|
+
const state = this.health.get(node);
|
|
78
|
+
if (!(state === null || state === void 0 ? void 0 : state.rateLimit))
|
|
79
|
+
return false;
|
|
80
|
+
return Date.now() < state.rateLimit.retryAfter;
|
|
81
|
+
}
|
|
58
82
|
/**
|
|
59
83
|
* Record an API/plugin-specific failure (e.g. "method not found", "plugin not enabled").
|
|
60
84
|
* Only increments the per-API counter, NOT the global consecutive failure counter.
|
|
@@ -93,6 +117,10 @@ class NodeHealthTracker {
|
|
|
93
117
|
if (!state)
|
|
94
118
|
return true; // Unknown nodes are assumed healthy
|
|
95
119
|
const now = Date.now();
|
|
120
|
+
// Check rate-limit cooldown (429 received)
|
|
121
|
+
if (state.rateLimit && now < state.rateLimit.retryAfter) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
96
124
|
// Check overall node health (consecutive failures)
|
|
97
125
|
if (state.consecutiveFailures >= this.maxFailuresBeforeCooldown) {
|
|
98
126
|
if (now - state.lastFailure < this.nodeCooldownMs) {
|
package/lib/utils.js
CHANGED
|
@@ -155,7 +155,7 @@ function nextNode(nodes, currentIndex) {
|
|
|
155
155
|
* - NEVER retry after timeout or response errors to prevent double-broadcasting
|
|
156
156
|
*/
|
|
157
157
|
function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThreshold, consoleOnFailover, backoff, fetchTimeout, retryContext) {
|
|
158
|
-
var _a;
|
|
158
|
+
var _a, _b, _c;
|
|
159
159
|
return __awaiter(this, void 0, void 0, function* () {
|
|
160
160
|
const { healthTracker, api, isBroadcast } = retryContext || {};
|
|
161
161
|
const logFailover = (_a = retryContext === null || retryContext === void 0 ? void 0 : retryContext.consoleOnFailover) !== null && _a !== void 0 ? _a : consoleOnFailover;
|
|
@@ -182,26 +182,51 @@ function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThre
|
|
|
182
182
|
while (true) {
|
|
183
183
|
const node = orderedNodes[nodeIndex];
|
|
184
184
|
try {
|
|
185
|
+
// Skip nodes that are currently rate-limited
|
|
186
|
+
if (healthTracker && healthTracker.isRateLimited(node)) {
|
|
187
|
+
lastError = new Error(`Node ${node} is rate-limited, skipping`);
|
|
188
|
+
if (healthTracker && api)
|
|
189
|
+
healthTracker.recordFailure(node, api);
|
|
190
|
+
throw lastError;
|
|
191
|
+
}
|
|
185
192
|
if (fetchTimeout) {
|
|
186
193
|
opts.timeout = fetchTimeout(nodesTriedInRound);
|
|
187
194
|
}
|
|
188
195
|
const response = yield cross_fetch_1.default(node, opts);
|
|
189
196
|
if (!response.ok) {
|
|
190
|
-
//
|
|
191
|
-
if (response.status ===
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
healthTracker.recordSuccess(node, api);
|
|
197
|
-
return { response: resJson, currentAddress: node };
|
|
198
|
-
}
|
|
197
|
+
// Handle 429 — record rate limit and fail over immediately
|
|
198
|
+
if (response.status === 429) {
|
|
199
|
+
const retryAfterHeader = (_c = (_b = response.headers) === null || _b === void 0 ? void 0 : _b.get) === null || _c === void 0 ? void 0 : _c.call(_b, 'retry-after');
|
|
200
|
+
const retryAfterSec = retryAfterHeader ? parseInt(retryAfterHeader, 10) : undefined;
|
|
201
|
+
if (healthTracker) {
|
|
202
|
+
healthTracker.recordRateLimit(node, !isNaN(retryAfterSec) ? retryAfterSec : undefined);
|
|
199
203
|
}
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
throw new Error(`HTTP 429: Too Many Requests`);
|
|
205
|
+
}
|
|
206
|
+
// Handle 503 — don't parse JSON body, just fail over
|
|
207
|
+
if (response.status === 503) {
|
|
208
|
+
if (healthTracker && api)
|
|
209
|
+
healthTracker.recordFailure(node, api);
|
|
210
|
+
throw new Error(`HTTP 503: Service Temporarily Unavailable`);
|
|
211
|
+
}
|
|
212
|
+
// Some Hive nodes return non-200 HTTP status (500, 502, etc.)
|
|
213
|
+
// but still include a valid JSON-RPC response in the body.
|
|
214
|
+
// This happens when a node is overloaded — it processes the transaction
|
|
215
|
+
// but returns an error HTTP status. For broadcasts, ignoring the body
|
|
216
|
+
// would cause the caller to think it failed, leading to double-posts.
|
|
217
|
+
try {
|
|
218
|
+
const resJson = yield response.json();
|
|
219
|
+
if (resJson.jsonrpc === '2.0') {
|
|
220
|
+
if (healthTracker && api)
|
|
221
|
+
healthTracker.recordSuccess(node, api);
|
|
222
|
+
return { response: resJson, currentAddress: node };
|
|
202
223
|
}
|
|
203
224
|
}
|
|
204
|
-
|
|
225
|
+
catch (_d) {
|
|
226
|
+
// JSON parse failed, fall through to error handling
|
|
227
|
+
}
|
|
228
|
+
const statusText = response.statusText || `status code ${response.status}`;
|
|
229
|
+
throw new Error(`HTTP ${response.status}: ${statusText}`);
|
|
205
230
|
}
|
|
206
231
|
const responseJson = yield response.json();
|
|
207
232
|
// Record success in health tracker
|
|
@@ -244,6 +269,11 @@ function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThre
|
|
|
244
269
|
// Unrecognized error type — don't failover, throw immediately
|
|
245
270
|
throw error;
|
|
246
271
|
}
|
|
272
|
+
// Small delay between node attempts within a round to prevent
|
|
273
|
+
// flooding all nodes when multiple concurrent requests fail over
|
|
274
|
+
if (totalNodes > 1 && nodesTriedInRound > 0) {
|
|
275
|
+
yield sleep(50 + Math.random() * 50); // 50-100ms jitter
|
|
276
|
+
}
|
|
247
277
|
// Try next node immediately (no backoff within a round)
|
|
248
278
|
if (totalNodes > 1) {
|
|
249
279
|
nodeIndex = nextNode(orderedNodes, nodeIndex);
|
package/lib/version.js
CHANGED