@hiveio/dhive 1.3.5 → 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 +30 -3
- 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,12 +182,34 @@ 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
|
-
//
|
|
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);
|
|
203
|
+
}
|
|
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.)
|
|
191
213
|
// but still include a valid JSON-RPC response in the body.
|
|
192
214
|
// This happens when a node is overloaded — it processes the transaction
|
|
193
215
|
// but returns an error HTTP status. For broadcasts, ignoring the body
|
|
@@ -200,7 +222,7 @@ function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThre
|
|
|
200
222
|
return { response: resJson, currentAddress: node };
|
|
201
223
|
}
|
|
202
224
|
}
|
|
203
|
-
catch (
|
|
225
|
+
catch (_d) {
|
|
204
226
|
// JSON parse failed, fall through to error handling
|
|
205
227
|
}
|
|
206
228
|
const statusText = response.statusText || `status code ${response.status}`;
|
|
@@ -247,6 +269,11 @@ function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThre
|
|
|
247
269
|
// Unrecognized error type — don't failover, throw immediately
|
|
248
270
|
throw error;
|
|
249
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
|
+
}
|
|
250
277
|
// Try next node immediately (no backoff within a round)
|
|
251
278
|
if (totalNodes > 1) {
|
|
252
279
|
nodeIndex = nextNode(orderedNodes, nodeIndex);
|
package/lib/version.js
CHANGED