@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.
@@ -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.
@@ -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
- // Some Hive nodes return non-200 HTTP status (500, 502, 503, 429, etc.)
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 (_b) {
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
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.3.5';
3
+ exports.default = '1.3.6';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiveio/dhive",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "Hive blockchain RPC client library",
5
5
  "author": "hive-network",
6
6
  "license": "BSD-3-Clause-No-Military-License",