@hiveio/dhive 1.3.1-beta → 1.3.3
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 +142 -2
- package/dist/dhive.js +27 -18
- package/dist/dhive.js.gz +0 -0
- package/dist/dhive.js.map +1 -1
- package/lib/client.d.ts +11 -0
- package/lib/client.js +33 -4
- package/lib/health-tracker.d.ts +100 -0
- package/lib/health-tracker.js +167 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/utils.d.ts +26 -2
- package/lib/utils.js +150 -43
- package/lib/version.js +1 -1
- package/package.json +1 -1
package/lib/utils.js
CHANGED
|
@@ -52,8 +52,10 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
|
52
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
53
|
const cross_fetch_1 = require("cross-fetch");
|
|
54
54
|
const stream_1 = require("stream");
|
|
55
|
-
//
|
|
56
|
-
const
|
|
55
|
+
// Errors that indicate the request never reached the server — safe to retry even for broadcasts
|
|
56
|
+
const PRE_CONNECTION_ERRORS = ['ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH', 'EAI_AGAIN'];
|
|
57
|
+
// All errors that should trigger failover for read operations
|
|
58
|
+
const FAILOVER_ERRORS = [...PRE_CONNECTION_ERRORS, 'timeout', 'database lock', 'CERT_HAS_EXPIRED', 'ECONNRESET', 'ERR_TLS_CERT_ALTNAME_INVALID', 'ETIMEDOUT', 'EPIPE', 'EPROTO'];
|
|
57
59
|
/**
|
|
58
60
|
* Return a promise that will resove when a specific event is emitted.
|
|
59
61
|
*/
|
|
@@ -114,70 +116,175 @@ function copy(object) {
|
|
|
114
116
|
}
|
|
115
117
|
exports.copy = copy;
|
|
116
118
|
/**
|
|
117
|
-
*
|
|
119
|
+
* Check if an error code indicates the request never reached the server.
|
|
118
120
|
*/
|
|
119
|
-
function
|
|
121
|
+
function isPreConnectionError(error) {
|
|
122
|
+
if (!error || !error.code)
|
|
123
|
+
return false;
|
|
124
|
+
return PRE_CONNECTION_ERRORS.some((code) => error.code.includes(code));
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if an error should trigger failover for read operations.
|
|
128
|
+
* Matches any known network/timeout error, or errors with no code (HTTP errors).
|
|
129
|
+
*/
|
|
130
|
+
function shouldFailover(error) {
|
|
131
|
+
if (!error)
|
|
132
|
+
return true;
|
|
133
|
+
// HTTP errors (from !response.ok) have no .code — they should trigger failover
|
|
134
|
+
if (!error.code)
|
|
135
|
+
return true;
|
|
136
|
+
return FAILOVER_ERRORS.some((code) => error.code.includes(code));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the next node in the ordered list (wraps around).
|
|
140
|
+
*/
|
|
141
|
+
function nextNode(nodes, currentIndex) {
|
|
142
|
+
return (currentIndex + 1) % nodes.length;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Smart fetch with immediate failover and per-node health tracking.
|
|
146
|
+
*
|
|
147
|
+
* For read operations:
|
|
148
|
+
* - On failure, immediately try the next healthy node (no backoff within a round)
|
|
149
|
+
* - After trying all nodes once (one round), apply backoff before the next round
|
|
150
|
+
* - Stop after failoverThreshold rounds
|
|
151
|
+
*
|
|
152
|
+
* For broadcast operations:
|
|
153
|
+
* - Only retry on pre-connection errors (ECONNREFUSED, ENOTFOUND, etc.)
|
|
154
|
+
* where we know the request never reached the server
|
|
155
|
+
* - NEVER retry after timeout or response errors to prevent double-broadcasting
|
|
156
|
+
*/
|
|
157
|
+
function retryingFetch(currentAddress, allAddresses, opts, timeout, failoverThreshold, consoleOnFailover, backoff, fetchTimeout, retryContext) {
|
|
158
|
+
var _a;
|
|
120
159
|
return __awaiter(this, void 0, void 0, function* () {
|
|
121
|
-
|
|
122
|
-
|
|
160
|
+
const { healthTracker, api, isBroadcast } = retryContext || {};
|
|
161
|
+
const logFailover = (_a = retryContext === null || retryContext === void 0 ? void 0 : retryContext.consoleOnFailover) !== null && _a !== void 0 ? _a : consoleOnFailover;
|
|
162
|
+
// Build ordered node list: healthy nodes first, then unhealthy as fallback
|
|
163
|
+
let orderedNodes;
|
|
164
|
+
if (Array.isArray(allAddresses) && allAddresses.length > 1) {
|
|
165
|
+
orderedNodes = healthTracker
|
|
166
|
+
? healthTracker.getOrderedNodes(allAddresses, api)
|
|
167
|
+
: [...allAddresses];
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
orderedNodes = Array.isArray(allAddresses) ? allAddresses : [allAddresses];
|
|
171
|
+
}
|
|
172
|
+
// Always start from the healthiest node (index 0 of the ordered list).
|
|
173
|
+
// The health tracker already sorted nodes with healthy ones first,
|
|
174
|
+
// so starting from 0 ensures we use the best available node.
|
|
175
|
+
let nodeIndex = 0;
|
|
176
|
+
const totalNodes = orderedNodes.length;
|
|
177
|
+
const startTime = Date.now();
|
|
178
|
+
let nodesTriedInRound = 0;
|
|
123
179
|
let round = 0;
|
|
124
|
-
|
|
180
|
+
let lastError;
|
|
181
|
+
// tslint:disable-next-line: no-constant-condition
|
|
182
|
+
while (true) {
|
|
183
|
+
const node = orderedNodes[nodeIndex];
|
|
125
184
|
try {
|
|
126
185
|
if (fetchTimeout) {
|
|
127
|
-
opts.timeout = fetchTimeout(
|
|
186
|
+
opts.timeout = fetchTimeout(nodesTriedInRound);
|
|
128
187
|
}
|
|
129
|
-
const response = yield cross_fetch_1.default(
|
|
188
|
+
const response = yield cross_fetch_1.default(node, opts);
|
|
130
189
|
if (!response.ok) {
|
|
190
|
+
// Support for Drone: HTTP 500 with valid JSON-RPC response
|
|
191
|
+
if (response.status === 500) {
|
|
192
|
+
try {
|
|
193
|
+
const resJson = yield response.json();
|
|
194
|
+
if (resJson.jsonrpc === '2.0') {
|
|
195
|
+
if (healthTracker && api)
|
|
196
|
+
healthTracker.recordSuccess(node, api);
|
|
197
|
+
return { response: resJson, currentAddress: node };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (_b) {
|
|
201
|
+
// JSON parse failed, fall through to error handling
|
|
202
|
+
}
|
|
203
|
+
}
|
|
131
204
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
132
205
|
}
|
|
133
|
-
|
|
206
|
+
const responseJson = yield response.json();
|
|
207
|
+
// Record success in health tracker
|
|
208
|
+
if (healthTracker && api) {
|
|
209
|
+
healthTracker.recordSuccess(node, api);
|
|
210
|
+
}
|
|
211
|
+
return { response: responseJson, currentAddress: node };
|
|
134
212
|
}
|
|
135
213
|
catch (error) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
214
|
+
lastError = error;
|
|
215
|
+
// Record failure in health tracker
|
|
216
|
+
if (healthTracker && api) {
|
|
217
|
+
healthTracker.recordFailure(node, api);
|
|
218
|
+
}
|
|
219
|
+
// === BROADCAST SAFETY ===
|
|
220
|
+
// For broadcasts, only retry if the request definitely never reached the server.
|
|
221
|
+
// If there's any chance the server received it, throw immediately to prevent
|
|
222
|
+
// double-broadcasting (e.g. double transfers, double votes).
|
|
223
|
+
if (isBroadcast) {
|
|
224
|
+
if (isPreConnectionError(error) && totalNodes > 1) {
|
|
225
|
+
// Safe to try another node — request never left the client
|
|
226
|
+
nodeIndex = nextNode(orderedNodes, nodeIndex);
|
|
227
|
+
nodesTriedInRound++;
|
|
228
|
+
if (nodesTriedInRound >= totalNodes) {
|
|
229
|
+
// Tried all nodes, give up for broadcasts
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
if (logFailover) {
|
|
233
|
+
// tslint:disable-next-line: no-console
|
|
234
|
+
console.log(`Broadcast failover to: ${orderedNodes[nodeIndex]} (${error.code}, request never sent)`);
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
140
237
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
238
|
+
// Timeout, HTTP error, or unknown error — request may have been received.
|
|
239
|
+
// Do NOT retry. Throw immediately.
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
// === READ OPERATION FAILOVER ===
|
|
243
|
+
if (!shouldFailover(error)) {
|
|
244
|
+
// Unrecognized error type — don't failover, throw immediately
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
// Try next node immediately (no backoff within a round)
|
|
248
|
+
if (totalNodes > 1) {
|
|
249
|
+
nodeIndex = nextNode(orderedNodes, nodeIndex);
|
|
250
|
+
nodesTriedInRound++;
|
|
251
|
+
if (nodesTriedInRound >= totalNodes) {
|
|
252
|
+
// Completed a full round through all nodes
|
|
253
|
+
nodesTriedInRound = 0;
|
|
254
|
+
// failoverThreshold=0 means retry forever (only timeout can stop it)
|
|
255
|
+
if (failoverThreshold > 0) {
|
|
256
|
+
round++;
|
|
257
|
+
if (round >= failoverThreshold) {
|
|
258
|
+
error.message = `All ${totalNodes} nodes failed after ${failoverThreshold} rounds. ` +
|
|
259
|
+
`Last error: [${error.code || 'HTTP'}] ${error.message}. ` +
|
|
260
|
+
`Nodes: ${orderedNodes.join(', ')}`;
|
|
156
261
|
throw error;
|
|
157
262
|
}
|
|
158
263
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.error(`Didn't failover for error ${error.code ? 'code' : 'message'}: [${error.code || error.message}]`);
|
|
264
|
+
// Check total timeout before starting next round
|
|
265
|
+
if (timeout !== 0 && Date.now() - startTime > timeout) {
|
|
162
266
|
throw error;
|
|
163
267
|
}
|
|
268
|
+
// Backoff between rounds (not between individual node attempts)
|
|
269
|
+
yield sleep(backoff(round));
|
|
270
|
+
}
|
|
271
|
+
if (logFailover) {
|
|
272
|
+
// tslint:disable-next-line: no-console
|
|
273
|
+
console.log(`Switched Hive RPC: ${orderedNodes[nodeIndex]} (previous: ${node}, error: ${error.code || error.message})`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Single node: use backoff and retry same node (legacy behavior)
|
|
278
|
+
if (timeout !== 0 && Date.now() - startTime > timeout) {
|
|
279
|
+
throw error;
|
|
164
280
|
}
|
|
281
|
+
yield sleep(backoff(nodesTriedInRound++));
|
|
165
282
|
}
|
|
166
|
-
yield sleep(backoff(tries++));
|
|
167
283
|
}
|
|
168
|
-
}
|
|
284
|
+
}
|
|
169
285
|
});
|
|
170
286
|
}
|
|
171
287
|
exports.retryingFetch = retryingFetch;
|
|
172
|
-
const failover = (url, urls, currentAddress, consoleOnFailover) => {
|
|
173
|
-
const index = urls.indexOf(url);
|
|
174
|
-
const targetUrl = urls.length === index + 1 ? urls[0] : urls[index + 1];
|
|
175
|
-
if (consoleOnFailover) {
|
|
176
|
-
// tslint:disable-next-line: no-console
|
|
177
|
-
console.log(`Switched Hive RPC: ${targetUrl} (previous: ${currentAddress})`);
|
|
178
|
-
}
|
|
179
|
-
return targetUrl;
|
|
180
|
-
};
|
|
181
288
|
// Hack to be able to generate a valid witness_set_properties op
|
|
182
289
|
// Can hopefully be removed when hived's JSON representation is fixed
|
|
183
290
|
const ByteBuffer = require("@ecency/bytebuffer");
|
package/lib/version.js
CHANGED