@dashevo/dapi-client 0.23.0 → 0.23.1
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/lib/BlockHeadersProvider/BlockHeadersProvider.js +4 -0
- package/lib/BlockHeadersProvider/BlockHeadersReader.js +80 -48
- package/lib/index.js +2 -0
- package/lib/test/bootstrap.js +2 -0
- package/lib/test/mocks/BlockHeadersWithChainLocksStreamMock.js +9 -8
- package/lib/transport/GrpcTransport/createGrpcTransportError.js +2 -10
- package/lib/transport/JsonRpcTransport/requestJsonRpc.js +21 -23
- package/lib/transport/ReconnectableStream.js +148 -44
- package/package.json +6 -7
|
@@ -205,6 +205,10 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
205
205
|
headersHandler(headersData, reject) {
|
|
206
206
|
const { headers, headHeight } = headersData;
|
|
207
207
|
|
|
208
|
+
// TODO: simulate headers rejection and debug
|
|
209
|
+
// BlockHeadersReader might not correctly set startBlockHeight
|
|
210
|
+
// which results in orphan chunks in SPV
|
|
211
|
+
|
|
208
212
|
try {
|
|
209
213
|
const headersAdded = this.spvChain.addHeaders(headers, headHeight);
|
|
210
214
|
|
|
@@ -86,7 +86,7 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
86
86
|
stopReadingHistorical() {
|
|
87
87
|
while (this.historicalStreams.length) {
|
|
88
88
|
const stream = this.historicalStreams.shift();
|
|
89
|
-
|
|
89
|
+
this.cancelStream(stream);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -105,11 +105,20 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
105
105
|
throw new Error(`Invalid fromBlockHeight: ${fromBlockHeight}`);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// We don't know yet whether we already have header at fromBlockHeight
|
|
108
109
|
let lastKnownChainHeight = fromBlockHeight - 1;
|
|
109
110
|
|
|
110
111
|
const stream = await this.createContinuousSyncStream(fromBlockHeight);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
const errorHandler = (e) => {
|
|
114
|
+
this.continuousSyncStream = null;
|
|
115
|
+
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.emit(EVENTS.ERROR, e);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const dataHandler = (data) => {
|
|
113
122
|
const blockHeadersResponse = data.getBlockHeaders();
|
|
114
123
|
|
|
115
124
|
if (blockHeadersResponse) {
|
|
@@ -125,35 +134,42 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
125
134
|
*
|
|
126
135
|
* @param e
|
|
127
136
|
*/
|
|
128
|
-
const rejectHeaders = (e) => {
|
|
129
|
-
|
|
137
|
+
const rejectHeaders = async (e) => {
|
|
138
|
+
// Don't use cancelStream there because it's going to unsubscribe from events
|
|
139
|
+
stream.cancel();
|
|
140
|
+
stream.retryOnError(e);
|
|
130
141
|
};
|
|
142
|
+
|
|
131
143
|
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
132
144
|
headers,
|
|
133
145
|
headHeight: batchHeadHeight,
|
|
134
146
|
}, rejectHeaders);
|
|
135
147
|
}
|
|
136
|
-
}
|
|
148
|
+
};
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
const beforeReconnectHandler = (updateArguments) => {
|
|
139
151
|
updateArguments({
|
|
140
152
|
fromBlockHeight: lastKnownChainHeight,
|
|
153
|
+
count: 0,
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
lastKnownChainHeight -= 1;
|
|
144
|
-
}
|
|
157
|
+
};
|
|
145
158
|
|
|
146
|
-
|
|
159
|
+
const endHandler = () => {
|
|
147
160
|
this.continuousSyncStream = null;
|
|
148
|
-
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
this.emit(EVENTS.ERROR, e);
|
|
152
|
-
});
|
|
161
|
+
};
|
|
153
162
|
|
|
154
|
-
stream.on('
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
stream.on('data', dataHandler);
|
|
164
|
+
stream.on('beforeReconnect', beforeReconnectHandler);
|
|
165
|
+
stream.on('error', errorHandler);
|
|
166
|
+
stream.on('end', endHandler);
|
|
167
|
+
|
|
168
|
+
stream.removeAllListeners = () => {
|
|
169
|
+
stream.removeListener('data', dataHandler);
|
|
170
|
+
stream.removeListener('end', endHandler);
|
|
171
|
+
stream.removeListener('beforeReconnect', beforeReconnectHandler);
|
|
172
|
+
};
|
|
157
173
|
|
|
158
174
|
this.continuousSyncStream = stream;
|
|
159
175
|
return stream;
|
|
@@ -161,11 +177,13 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
161
177
|
|
|
162
178
|
unsubscribeFromNew() {
|
|
163
179
|
if (this.continuousSyncStream) {
|
|
164
|
-
this.continuousSyncStream
|
|
180
|
+
this.cancelStream(this.continuousSyncStream);
|
|
165
181
|
this.continuousSyncStream = null;
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
// TODO: refactor whole thing with ReconnectableStream that supports
|
|
186
|
+
// retry on error logic
|
|
169
187
|
/**
|
|
170
188
|
* A HOF that returns a function to subscribe to historical block headers and chain locks
|
|
171
189
|
* and handles retry logic
|
|
@@ -189,37 +207,6 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
189
207
|
|
|
190
208
|
const stream = await this.createHistoricalSyncStream(fromBlockHeight, count);
|
|
191
209
|
|
|
192
|
-
const dataHandler = (data) => {
|
|
193
|
-
const blockHeaders = data.getBlockHeaders();
|
|
194
|
-
|
|
195
|
-
if (blockHeaders) {
|
|
196
|
-
const headersList = blockHeaders.getHeadersList()
|
|
197
|
-
.map((header) => new BlockHeader(Buffer.from(header)));
|
|
198
|
-
|
|
199
|
-
let rejected = false;
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Kills stream in case of deliberate rejection from the outside
|
|
203
|
-
*
|
|
204
|
-
* @param e
|
|
205
|
-
*/
|
|
206
|
-
const rejectHeaders = (e) => {
|
|
207
|
-
rejected = true;
|
|
208
|
-
stream.destroy(e);
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
212
|
-
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
213
|
-
headers: headersList,
|
|
214
|
-
headHeight: batchHeadHeight,
|
|
215
|
-
}, rejectHeaders);
|
|
216
|
-
|
|
217
|
-
if (!rejected) {
|
|
218
|
-
headersObtained += headersList.length;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
210
|
const errorHandler = (streamError) => {
|
|
224
211
|
if (streamError.code === GrpcErrorCodes.CANCELLED) {
|
|
225
212
|
// TODO: consider reworking with COMMANDS instead
|
|
@@ -251,6 +238,40 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
251
238
|
}
|
|
252
239
|
};
|
|
253
240
|
|
|
241
|
+
const dataHandler = (data) => {
|
|
242
|
+
const blockHeaders = data.getBlockHeaders();
|
|
243
|
+
|
|
244
|
+
if (blockHeaders) {
|
|
245
|
+
const headersList = blockHeaders.getHeadersList()
|
|
246
|
+
.map((header) => new BlockHeader(Buffer.from(header)));
|
|
247
|
+
|
|
248
|
+
let rejected = false;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Kills stream in case of deliberate rejection from the outside
|
|
252
|
+
*
|
|
253
|
+
* @param e
|
|
254
|
+
*/
|
|
255
|
+
const rejectHeaders = (e) => {
|
|
256
|
+
rejected = true;
|
|
257
|
+
// Cancel stream and unsubscribe from all data events
|
|
258
|
+
// because they are going th be re-created in errorHandler
|
|
259
|
+
this.cancelStream(stream);
|
|
260
|
+
errorHandler(e);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
264
|
+
this.emit(EVENTS.BLOCK_HEADERS, {
|
|
265
|
+
headers: headersList,
|
|
266
|
+
headHeight: batchHeadHeight,
|
|
267
|
+
}, rejectHeaders);
|
|
268
|
+
|
|
269
|
+
if (!rejected) {
|
|
270
|
+
headersObtained += headersList.length;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
254
275
|
const endHandler = () => {
|
|
255
276
|
const index = this.historicalStreams.indexOf(stream);
|
|
256
277
|
this.historicalStreams.splice(index, 1);
|
|
@@ -267,11 +288,22 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
267
288
|
stream.on('error', errorHandler);
|
|
268
289
|
stream.on('end', endHandler);
|
|
269
290
|
|
|
291
|
+
stream.removeAllListeners = () => {
|
|
292
|
+
stream.removeListener('data', dataHandler);
|
|
293
|
+
stream.removeListener('end', endHandler);
|
|
294
|
+
};
|
|
295
|
+
|
|
270
296
|
return stream;
|
|
271
297
|
};
|
|
272
298
|
|
|
273
299
|
return subscribeWithRetries;
|
|
274
300
|
}
|
|
301
|
+
|
|
302
|
+
// eslint-disable-next-line class-methods-use-this
|
|
303
|
+
cancelStream(stream) {
|
|
304
|
+
stream.removeAllListeners();
|
|
305
|
+
stream.cancel();
|
|
306
|
+
}
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
BlockHeadersReader.EVENTS = EVENTS;
|
package/lib/index.js
CHANGED
package/lib/test/bootstrap.js
CHANGED
|
@@ -7,20 +7,12 @@ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
|
|
|
7
7
|
sinon.spy(this, 'on');
|
|
8
8
|
sinon.spy(this, 'removeListener');
|
|
9
9
|
sinon.spy(this, 'emit');
|
|
10
|
-
sinon.spy(this, 'destroy');
|
|
11
10
|
sinon.spy(this, 'removeAllListeners');
|
|
12
11
|
sinon.spy(this, 'cancel');
|
|
13
12
|
|
|
14
13
|
this.errored = false;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
destroy(e) {
|
|
18
|
-
this.emit('end');
|
|
19
|
-
if (e) {
|
|
20
|
-
this.emit('error', e);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
16
|
cancel() {
|
|
25
17
|
if (!this.errored) {
|
|
26
18
|
const err = new Error('CANCELED_ON_CLIENT');
|
|
@@ -48,6 +40,15 @@ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
|
|
|
48
40
|
}),
|
|
49
41
|
});
|
|
50
42
|
}
|
|
43
|
+
|
|
44
|
+
errorHandler(e) {
|
|
45
|
+
this.emit('error', e);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
end() {
|
|
49
|
+
this.emit('end');
|
|
50
|
+
this.removeAllListeners();
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
module.exports = BlockHeadersWithChainLocksStreamMock;
|
|
@@ -2,7 +2,7 @@ const cbor = require('cbor');
|
|
|
2
2
|
|
|
3
3
|
const createConsensusError = require('@dashevo/dpp/lib/errors/consensus/createConsensusError');
|
|
4
4
|
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
5
|
-
const {
|
|
5
|
+
const { parseMetadata } = require('@dashevo/dapi-grpc');
|
|
6
6
|
|
|
7
7
|
const NotFoundError = require('./errors/NotFoundError');
|
|
8
8
|
const TimeoutError = require('./errors/TimeoutError');
|
|
@@ -50,15 +50,7 @@ function createGrpcTransportError(grpcError, dapiAddress) {
|
|
|
50
50
|
|
|
51
51
|
const message = grpcError.details || grpcError.message;
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (grpcError.metadata) {
|
|
56
|
-
// In cases of gRPC-Web client we get plain map instead of Metadata instance
|
|
57
|
-
metadata = grpcError.metadata;
|
|
58
|
-
if (grpcError.metadata instanceof Metadata) {
|
|
59
|
-
metadata = grpcError.metadata.getMap();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
53
|
+
const metadata = parseMetadata(grpcError.metadata) || {};
|
|
62
54
|
|
|
63
55
|
// Error data
|
|
64
56
|
const driveErrorData = metadata['drive-error-data-bin'];
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const axios = require('axios');
|
|
2
|
-
|
|
3
1
|
const JsonRpcError = require('./errors/JsonRpcError');
|
|
4
2
|
const WrongHttpCodeError = require('./errors/WrongHttpCodeError');
|
|
5
3
|
|
|
@@ -24,11 +22,6 @@ async function requestJsonRpc(host, port, method, params, options = {}) {
|
|
|
24
22
|
id: 1,
|
|
25
23
|
};
|
|
26
24
|
|
|
27
|
-
const postOptions = {};
|
|
28
|
-
if (options.timeout !== undefined) {
|
|
29
|
-
postOptions.timeout = options.timeout;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
const requestInfo = {
|
|
33
26
|
host,
|
|
34
27
|
port,
|
|
@@ -37,27 +30,32 @@ async function requestJsonRpc(host, port, method, params, options = {}) {
|
|
|
37
30
|
options,
|
|
38
31
|
};
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
if (error.response && error.response.status >= 500) {
|
|
50
|
-
throw new WrongHttpCodeError(requestInfo, error.response.status, error.response.statusText);
|
|
51
|
-
}
|
|
33
|
+
const requestOptions = {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify(payload),
|
|
36
|
+
headers: {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
52
40
|
|
|
53
|
-
|
|
41
|
+
let requestTimeoutId;
|
|
42
|
+
if (options.timeout) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
requestTimeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
45
|
+
Object.assign(requestOptions, { signal: controller.signal });
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
// eslint-disable-next-line
|
|
49
|
+
const response = await fetch(url, requestOptions);
|
|
50
|
+
|
|
51
|
+
if (typeof requestTimeoutId !== 'undefined') {
|
|
52
|
+
clearTimeout(requestTimeoutId);
|
|
58
53
|
}
|
|
54
|
+
const data = await response.json();
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new WrongHttpCodeError(requestInfo, response.status, response.statusText);
|
|
58
|
+
}
|
|
61
59
|
|
|
62
60
|
if (data.error) {
|
|
63
61
|
throw new JsonRpcError(requestInfo, data.error);
|
|
@@ -2,9 +2,18 @@ const EventEmitter = require('events');
|
|
|
2
2
|
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
3
3
|
const wait = require('../utils/wait');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef ReconnectableStreamOptions
|
|
7
|
+
* @property {number} [autoReconnectInterval]
|
|
8
|
+
* interval in MS to perform auto reconnect
|
|
9
|
+
* @property {number} [maxRetriesOnError]
|
|
10
|
+
* maximum amount of retry attempts on error happens. If set to -1, retries are infinite
|
|
11
|
+
* @property {number} [retryOnErrorDelay]
|
|
12
|
+
* delay in MS to perform retry after an error
|
|
13
|
+
*/
|
|
5
14
|
const defaultOptions = {
|
|
6
15
|
// TODO: manage timeout according to the Nginx setting of the node
|
|
7
|
-
|
|
16
|
+
autoReconnectInterval: 50000,
|
|
8
17
|
maxRetriesOnError: 10,
|
|
9
18
|
retryOnErrorDelay: 1000,
|
|
10
19
|
};
|
|
@@ -17,69 +26,121 @@ const EVENTS = {
|
|
|
17
26
|
};
|
|
18
27
|
|
|
19
28
|
/**
|
|
20
|
-
*
|
|
29
|
+
* A wrapper around grpc-js/grpc-web streams that provides auto-reconnect
|
|
30
|
+
* and retry on error functionality
|
|
31
|
+
* - Auto reconnect is needed to not hang on one node for too long.
|
|
32
|
+
* It happens within provided interval
|
|
33
|
+
* - Retry on error logic performs retry attempts until it reaches it's limit.
|
|
34
|
+
* After reaching limit, ReconnectableStream emits an error.
|
|
21
35
|
*/
|
|
22
36
|
class ReconnectableStream extends EventEmitter {
|
|
23
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Helper that wraps stream creation function and performs auto connection
|
|
39
|
+
*
|
|
40
|
+
* @param {Function} createStreamFunction - function returning grpc-js/grpc-web stream
|
|
41
|
+
* @param {ReconnectableStreamOptions} options
|
|
42
|
+
* @returns {function(...[*]): Promise<ReconnectableStream>}
|
|
43
|
+
*/
|
|
44
|
+
static create(createStreamFunction, options) {
|
|
24
45
|
return async (...args) => {
|
|
25
|
-
const reconnectableStream = new ReconnectableStream(
|
|
46
|
+
const reconnectableStream = new ReconnectableStream(createStreamFunction, options);
|
|
26
47
|
await reconnectableStream.connect(...args);
|
|
27
48
|
return reconnectableStream;
|
|
28
49
|
};
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @param {Function} createStream - function returning grpc-js/grpc-web stream
|
|
54
|
+
* @param {ReconnectableStreamOptions} options
|
|
55
|
+
*/
|
|
56
|
+
constructor(createStream, options = {}) {
|
|
32
57
|
super();
|
|
33
|
-
this.stream = null;
|
|
34
|
-
this.streamFunction = streamFunction;
|
|
35
|
-
this.reconnectTimeout = null;
|
|
36
58
|
|
|
37
59
|
const opts = { ...defaultOptions, ...options };
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Auto-reconnect interval in millisecond
|
|
63
|
+
* It is needed to automatically reconnect to another DAPI node
|
|
64
|
+
*
|
|
65
|
+
* @type {number}
|
|
66
|
+
*/
|
|
67
|
+
this.autoReconnectInterval = opts.autoReconnectInterval;
|
|
40
68
|
this.retryOnErrorDelay = opts.retryOnErrorDelay;
|
|
41
|
-
// Assign setTimeout and clearTimeout as class method for tests
|
|
42
|
-
this.setTimeout = (callback, delay) => setTimeout(callback, delay);
|
|
43
69
|
|
|
44
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Max amount of retries on error
|
|
72
|
+
*/
|
|
73
|
+
this.maxRetriesOnError = opts.maxRetriesOnError;
|
|
45
74
|
|
|
46
75
|
/**
|
|
47
|
-
*
|
|
76
|
+
* Current amount of retries on error
|
|
77
|
+
* (Does not have effect if maxRetriesOnError === -1)
|
|
78
|
+
*/
|
|
79
|
+
this.retriesOnError = 0;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* createStream arguments
|
|
48
83
|
*/
|
|
49
84
|
this.args = null;
|
|
85
|
+
/**
|
|
86
|
+
* grpc stream
|
|
87
|
+
*/
|
|
88
|
+
this.stream = null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Function to wrap around to handle interval reconnects and error retries
|
|
92
|
+
*/
|
|
93
|
+
this.createStream = createStream;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reference to setTimeout managed by autoReconnectInterval
|
|
97
|
+
*/
|
|
98
|
+
this.reconnectTimeout = null;
|
|
99
|
+
|
|
100
|
+
// For mocks
|
|
101
|
+
this.setTimeout = (callback, delay) => setTimeout(callback, delay);
|
|
102
|
+
this.clearTimeout = (reference) => clearTimeout(reference);
|
|
50
103
|
|
|
51
104
|
this.connect = this.connect.bind(this);
|
|
52
105
|
this.reconnect = this.reconnect.bind(this);
|
|
53
106
|
this.cancel = this.cancel.bind(this);
|
|
54
|
-
this.destroy = this.destroy.bind(this);
|
|
55
107
|
this.errorHandler = this.errorHandler.bind(this);
|
|
56
108
|
this.dataHandler = this.dataHandler.bind(this);
|
|
57
109
|
this.endHandler = this.endHandler.bind(this);
|
|
58
110
|
this.addListeners = this.addListeners.bind(this);
|
|
59
|
-
|
|
60
|
-
// Manages retry attempts when error happens
|
|
61
|
-
this.retriesOnError = 0;
|
|
62
111
|
}
|
|
63
112
|
|
|
64
113
|
async connect(...args) {
|
|
114
|
+
// Memorize current stream args (which can be altered by beforeReconnect logic)
|
|
65
115
|
this.args = args;
|
|
66
|
-
|
|
116
|
+
|
|
117
|
+
// Create grpc stream
|
|
118
|
+
this.stream = await this.createStream(...this.args);
|
|
119
|
+
|
|
120
|
+
// Add event listeners
|
|
67
121
|
this.addListeners();
|
|
68
122
|
|
|
69
|
-
if (this.
|
|
70
|
-
this.
|
|
71
|
-
this.reconnect,
|
|
72
|
-
this.reconnectTimeoutDelay,
|
|
73
|
-
);
|
|
123
|
+
if (this.autoReconnectInterval > 0) {
|
|
124
|
+
this.startAutoReconnect();
|
|
74
125
|
}
|
|
75
126
|
}
|
|
76
127
|
|
|
128
|
+
startAutoReconnect() {
|
|
129
|
+
if (this.reconnectTimeout) {
|
|
130
|
+
throw new Error('Auto reconnect timeout is already running.');
|
|
131
|
+
}
|
|
132
|
+
this.reconnectTimeout = this.setTimeout(
|
|
133
|
+
this.reconnect,
|
|
134
|
+
this.autoReconnectInterval,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
77
138
|
reconnect() {
|
|
78
139
|
if (this.reconnectTimeout) {
|
|
79
140
|
this.reconnectTimeout = null;
|
|
80
141
|
this.stream.cancel();
|
|
81
142
|
|
|
82
|
-
let newArgs = this.args;
|
|
143
|
+
let newArgs = this.args || [];
|
|
83
144
|
const updateArgs = (...args) => {
|
|
84
145
|
newArgs = args;
|
|
85
146
|
};
|
|
@@ -91,6 +152,10 @@ class ReconnectableStream extends EventEmitter {
|
|
|
91
152
|
}
|
|
92
153
|
|
|
93
154
|
/**
|
|
155
|
+
* Function that adds EventEmitter style listeners
|
|
156
|
+
* to the stream and also rewires stream cancel function
|
|
157
|
+
* in order to automatically unsubscribe from events
|
|
158
|
+
*
|
|
94
159
|
* @private
|
|
95
160
|
*/
|
|
96
161
|
addListeners() {
|
|
@@ -100,6 +165,10 @@ class ReconnectableStream extends EventEmitter {
|
|
|
100
165
|
|
|
101
166
|
const { cancel } = this.stream;
|
|
102
167
|
|
|
168
|
+
// Rewire cancel function in order to
|
|
169
|
+
// unsubscribe from DATA and END events.
|
|
170
|
+
// We don't unsubscribe from ERROR event because it
|
|
171
|
+
// handles `CANCELLED_ON_CLIENT` error after calling .cancel() and must stay
|
|
103
172
|
this.stream.cancel = () => {
|
|
104
173
|
this.stream.removeListener(EVENTS.DATA, this.dataHandler);
|
|
105
174
|
this.stream.removeListener(EVENTS.END, this.endHandler);
|
|
@@ -107,72 +176,107 @@ class ReconnectableStream extends EventEmitter {
|
|
|
107
176
|
};
|
|
108
177
|
}
|
|
109
178
|
|
|
179
|
+
/**
|
|
180
|
+
* stream.on('data') handler
|
|
181
|
+
*
|
|
182
|
+
* @param data
|
|
183
|
+
*/
|
|
110
184
|
dataHandler(data) {
|
|
111
185
|
this.emit(EVENTS.DATA, data);
|
|
112
186
|
}
|
|
113
187
|
|
|
114
188
|
/**
|
|
189
|
+
* stream.on('end') handler
|
|
190
|
+
*
|
|
115
191
|
* @private
|
|
116
192
|
*/
|
|
117
193
|
endHandler() {
|
|
118
|
-
this.
|
|
194
|
+
this.stopAutoReconnect();
|
|
119
195
|
this.stream = null;
|
|
120
196
|
this.emit(EVENTS.END);
|
|
121
197
|
}
|
|
122
198
|
|
|
123
199
|
/**
|
|
200
|
+
* stream.on('error') handler
|
|
201
|
+
*
|
|
124
202
|
* @private
|
|
125
203
|
* @param e
|
|
126
204
|
*/
|
|
127
205
|
errorHandler(e) {
|
|
128
|
-
|
|
206
|
+
// In case of cancellation nothing has to happen.
|
|
207
|
+
// TODO: do we need to propagate GrpcErrorCodes.CANCELLED further?
|
|
129
208
|
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
130
209
|
return;
|
|
131
210
|
}
|
|
132
|
-
this.stopReconnectTimeout();
|
|
133
211
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
212
|
+
// Retry stream
|
|
213
|
+
this.retryOnError(e);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Manages retry logic
|
|
218
|
+
*
|
|
219
|
+
* @param e
|
|
220
|
+
*/
|
|
221
|
+
retryOnError(e) {
|
|
222
|
+
// Stop reconnect timeout if there is one
|
|
223
|
+
this.stopAutoReconnect();
|
|
224
|
+
|
|
225
|
+
// Unsubscribe from events in case of the external call
|
|
226
|
+
this.stream.removeListener(EVENTS.END, this.endHandler);
|
|
227
|
+
this.stream.removeListener(EVENTS.DATA, this.dataHandler);
|
|
228
|
+
|
|
229
|
+
const canRetry = this.maxRetriesOnError === -1 // Infinite retries
|
|
230
|
+
|| this.retriesOnError < this.maxRetriesOnError; // Or less than max limit
|
|
231
|
+
|
|
232
|
+
if (canRetry) {
|
|
233
|
+
// Handle 'beforeReconnect` logic
|
|
234
|
+
let newArgs = this.args || [];
|
|
235
|
+
const updateArgsBeforeReconnect = (...args) => {
|
|
140
236
|
newArgs = args;
|
|
141
237
|
};
|
|
142
238
|
|
|
143
|
-
|
|
239
|
+
// Emit beforeReconnect event in case parent would want to alter arguments
|
|
240
|
+
this.emit(EVENTS.BEFORE_RECONNECT, updateArgsBeforeReconnect);
|
|
144
241
|
|
|
242
|
+
// Wait before reconnecting
|
|
145
243
|
wait(this.retryOnErrorDelay)
|
|
146
|
-
.then(() => this.connect(...newArgs))
|
|
244
|
+
.then(() => this.connect(...newArgs))// Reconnect with new args
|
|
147
245
|
.then(() => {
|
|
148
|
-
this.retriesOnError += 1;
|
|
246
|
+
this.retriesOnError += 1; // Increase amount of current retries
|
|
149
247
|
})
|
|
150
248
|
.catch((connectError) => {
|
|
151
|
-
this.emit(EVENTS.ERROR, connectError);
|
|
249
|
+
this.emit(EVENTS.ERROR, connectError); // Or emit error in case retry has failed
|
|
152
250
|
});
|
|
153
251
|
} else {
|
|
252
|
+
// Or simply emit error in case retry attempts were exhausted
|
|
154
253
|
this.emit(EVENTS.ERROR, e);
|
|
155
254
|
}
|
|
156
255
|
}
|
|
157
256
|
|
|
158
257
|
/**
|
|
258
|
+
* Stops auto reconnect timeout
|
|
259
|
+
*
|
|
159
260
|
* @private
|
|
160
261
|
*/
|
|
161
|
-
|
|
262
|
+
stopAutoReconnect() {
|
|
162
263
|
if (this.reconnectTimeout) {
|
|
163
264
|
this.clearTimeout(this.reconnectTimeout);
|
|
164
265
|
this.reconnectTimeout = null;
|
|
165
266
|
}
|
|
166
267
|
}
|
|
167
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Cancels stream.
|
|
271
|
+
* Returns `stream.cancel()` to handle it from parent if needed.
|
|
272
|
+
* (grpc-js cancel() is asynchronous and grpc-web is synchronous)
|
|
273
|
+
*
|
|
274
|
+
* @returns {*}
|
|
275
|
+
*/
|
|
168
276
|
cancel() {
|
|
169
|
-
this.
|
|
277
|
+
this.stopAutoReconnect();
|
|
170
278
|
return this.stream.cancel();
|
|
171
279
|
}
|
|
172
|
-
|
|
173
|
-
destroy(e) {
|
|
174
|
-
this.stream.destroy(e);
|
|
175
|
-
}
|
|
176
280
|
}
|
|
177
281
|
|
|
178
282
|
ReconnectableStream.EVENTS = EVENTS;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashevo/dapi-client",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"description": "Client library used to access Dash DAPI endpoints",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"contributors": [
|
|
@@ -26,16 +26,15 @@
|
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dashevo/dapi-grpc": "0.23.
|
|
30
|
-
"@dashevo/dash-spv": "0.23.
|
|
29
|
+
"@dashevo/dapi-grpc": "0.23.1",
|
|
30
|
+
"@dashevo/dash-spv": "0.23.1",
|
|
31
31
|
"@dashevo/dashcore-lib": "~0.19.44",
|
|
32
|
-
"@dashevo/dpp": "0.23.
|
|
33
|
-
"@dashevo/grpc-common": "0.23.
|
|
34
|
-
"@grpc/grpc-js": "^1.3.7",
|
|
35
|
-
"axios": "^0.27.2",
|
|
32
|
+
"@dashevo/dpp": "0.23.1",
|
|
33
|
+
"@dashevo/grpc-common": "0.23.1",
|
|
36
34
|
"bs58": "^4.0.1",
|
|
37
35
|
"cbor": "^8.0.0",
|
|
38
36
|
"lodash.sample": "^4.2.1",
|
|
37
|
+
"node-fetch": "^2.6.7",
|
|
39
38
|
"node-inspect-extracted": "^1.0.8",
|
|
40
39
|
"wasm-x11-hash": "~0.0.2"
|
|
41
40
|
},
|