@dashevo/dapi-client 0.23.0-dev.9 → 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/README.md +5 -5
- package/lib/BlockHeadersProvider/BlockHeadersProvider.js +187 -50
- package/lib/BlockHeadersProvider/BlockHeadersReader.js +170 -88
- package/lib/BlockHeadersProvider/createBlockHeadersProviderFromOptions.js +31 -16
- package/lib/DAPIClient.js +0 -6
- package/lib/index.js +5 -0
- package/lib/networkConfigs.js +114 -150
- package/lib/test/bootstrap.js +8 -0
- package/lib/test/mocks/BlockHeadersWithChainLocksStreamMock.js +54 -0
- package/lib/test/mocks/mockHeadersChain.js +55 -0
- package/lib/transport/GrpcTransport/createGrpcTransportError.js +21 -34
- package/lib/transport/JsonRpcTransport/requestJsonRpc.js +21 -23
- package/lib/transport/ReconnectableStream.js +284 -0
- package/lib/{test/utils → utils}/wait.js +0 -0
- package/package.json +10 -10
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
3
|
+
const wait = require('../utils/wait');
|
|
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
|
+
*/
|
|
14
|
+
const defaultOptions = {
|
|
15
|
+
// TODO: manage timeout according to the Nginx setting of the node
|
|
16
|
+
autoReconnectInterval: 50000,
|
|
17
|
+
maxRetriesOnError: 10,
|
|
18
|
+
retryOnErrorDelay: 1000,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const EVENTS = {
|
|
22
|
+
DATA: 'data',
|
|
23
|
+
ERROR: 'error',
|
|
24
|
+
END: 'end',
|
|
25
|
+
BEFORE_RECONNECT: 'beforeReconnect',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
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.
|
|
35
|
+
*/
|
|
36
|
+
class ReconnectableStream extends EventEmitter {
|
|
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) {
|
|
45
|
+
return async (...args) => {
|
|
46
|
+
const reconnectableStream = new ReconnectableStream(createStreamFunction, options);
|
|
47
|
+
await reconnectableStream.connect(...args);
|
|
48
|
+
return reconnectableStream;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Function} createStream - function returning grpc-js/grpc-web stream
|
|
54
|
+
* @param {ReconnectableStreamOptions} options
|
|
55
|
+
*/
|
|
56
|
+
constructor(createStream, options = {}) {
|
|
57
|
+
super();
|
|
58
|
+
|
|
59
|
+
const opts = { ...defaultOptions, ...options };
|
|
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;
|
|
68
|
+
this.retryOnErrorDelay = opts.retryOnErrorDelay;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Max amount of retries on error
|
|
72
|
+
*/
|
|
73
|
+
this.maxRetriesOnError = opts.maxRetriesOnError;
|
|
74
|
+
|
|
75
|
+
/**
|
|
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
|
|
83
|
+
*/
|
|
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);
|
|
103
|
+
|
|
104
|
+
this.connect = this.connect.bind(this);
|
|
105
|
+
this.reconnect = this.reconnect.bind(this);
|
|
106
|
+
this.cancel = this.cancel.bind(this);
|
|
107
|
+
this.errorHandler = this.errorHandler.bind(this);
|
|
108
|
+
this.dataHandler = this.dataHandler.bind(this);
|
|
109
|
+
this.endHandler = this.endHandler.bind(this);
|
|
110
|
+
this.addListeners = this.addListeners.bind(this);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async connect(...args) {
|
|
114
|
+
// Memorize current stream args (which can be altered by beforeReconnect logic)
|
|
115
|
+
this.args = args;
|
|
116
|
+
|
|
117
|
+
// Create grpc stream
|
|
118
|
+
this.stream = await this.createStream(...this.args);
|
|
119
|
+
|
|
120
|
+
// Add event listeners
|
|
121
|
+
this.addListeners();
|
|
122
|
+
|
|
123
|
+
if (this.autoReconnectInterval > 0) {
|
|
124
|
+
this.startAutoReconnect();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
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
|
+
|
|
138
|
+
reconnect() {
|
|
139
|
+
if (this.reconnectTimeout) {
|
|
140
|
+
this.reconnectTimeout = null;
|
|
141
|
+
this.stream.cancel();
|
|
142
|
+
|
|
143
|
+
let newArgs = this.args || [];
|
|
144
|
+
const updateArgs = (...args) => {
|
|
145
|
+
newArgs = args;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
this.emit(EVENTS.BEFORE_RECONNECT, updateArgs);
|
|
149
|
+
this.connect(...newArgs)
|
|
150
|
+
.catch((connectError) => this.emit(EVENTS.ERROR, connectError));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
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
|
+
*
|
|
159
|
+
* @private
|
|
160
|
+
*/
|
|
161
|
+
addListeners() {
|
|
162
|
+
this.stream.on(EVENTS.DATA, this.dataHandler);
|
|
163
|
+
this.stream.on(EVENTS.ERROR, this.errorHandler);
|
|
164
|
+
this.stream.on(EVENTS.END, this.endHandler);
|
|
165
|
+
|
|
166
|
+
const { cancel } = this.stream;
|
|
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
|
|
172
|
+
this.stream.cancel = () => {
|
|
173
|
+
this.stream.removeListener(EVENTS.DATA, this.dataHandler);
|
|
174
|
+
this.stream.removeListener(EVENTS.END, this.endHandler);
|
|
175
|
+
cancel.call(this.stream);
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* stream.on('data') handler
|
|
181
|
+
*
|
|
182
|
+
* @param data
|
|
183
|
+
*/
|
|
184
|
+
dataHandler(data) {
|
|
185
|
+
this.emit(EVENTS.DATA, data);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* stream.on('end') handler
|
|
190
|
+
*
|
|
191
|
+
* @private
|
|
192
|
+
*/
|
|
193
|
+
endHandler() {
|
|
194
|
+
this.stopAutoReconnect();
|
|
195
|
+
this.stream = null;
|
|
196
|
+
this.emit(EVENTS.END);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* stream.on('error') handler
|
|
201
|
+
*
|
|
202
|
+
* @private
|
|
203
|
+
* @param e
|
|
204
|
+
*/
|
|
205
|
+
errorHandler(e) {
|
|
206
|
+
// In case of cancellation nothing has to happen.
|
|
207
|
+
// TODO: do we need to propagate GrpcErrorCodes.CANCELLED further?
|
|
208
|
+
if (e.code === GrpcErrorCodes.CANCELLED) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
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) => {
|
|
236
|
+
newArgs = args;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// Emit beforeReconnect event in case parent would want to alter arguments
|
|
240
|
+
this.emit(EVENTS.BEFORE_RECONNECT, updateArgsBeforeReconnect);
|
|
241
|
+
|
|
242
|
+
// Wait before reconnecting
|
|
243
|
+
wait(this.retryOnErrorDelay)
|
|
244
|
+
.then(() => this.connect(...newArgs))// Reconnect with new args
|
|
245
|
+
.then(() => {
|
|
246
|
+
this.retriesOnError += 1; // Increase amount of current retries
|
|
247
|
+
})
|
|
248
|
+
.catch((connectError) => {
|
|
249
|
+
this.emit(EVENTS.ERROR, connectError); // Or emit error in case retry has failed
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
// Or simply emit error in case retry attempts were exhausted
|
|
253
|
+
this.emit(EVENTS.ERROR, e);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Stops auto reconnect timeout
|
|
259
|
+
*
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
stopAutoReconnect() {
|
|
263
|
+
if (this.reconnectTimeout) {
|
|
264
|
+
this.clearTimeout(this.reconnectTimeout);
|
|
265
|
+
this.reconnectTimeout = null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
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
|
+
*/
|
|
276
|
+
cancel() {
|
|
277
|
+
this.stopAutoReconnect();
|
|
278
|
+
return this.stream.cancel();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
ReconnectableStream.EVENTS = EVENTS;
|
|
283
|
+
|
|
284
|
+
module.exports = ReconnectableStream;
|
|
File without changes
|
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,17 +26,17 @@
|
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dashevo/dapi-grpc": "
|
|
30
|
-
"@dashevo/dash-spv": "
|
|
31
|
-
"@dashevo/dashcore-lib": "~0.19.
|
|
32
|
-
"@dashevo/dpp": "
|
|
33
|
-
"@dashevo/grpc-common": "
|
|
34
|
-
"@grpc/grpc-js": "^1.3.7",
|
|
35
|
-
"axios": "^0.21.1",
|
|
29
|
+
"@dashevo/dapi-grpc": "0.23.1",
|
|
30
|
+
"@dashevo/dash-spv": "0.23.1",
|
|
31
|
+
"@dashevo/dashcore-lib": "~0.19.44",
|
|
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",
|
|
39
|
-
"node-
|
|
37
|
+
"node-fetch": "^2.6.7",
|
|
38
|
+
"node-inspect-extracted": "^1.0.8",
|
|
39
|
+
"wasm-x11-hash": "~0.0.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@babel/core": "^7.15.5",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"eslint-plugin-import": "^2.24.2",
|
|
55
55
|
"eslint-plugin-jsdoc": "^27.0.0",
|
|
56
56
|
"events": "^3.3.0",
|
|
57
|
-
"karma": "^6.
|
|
57
|
+
"karma": "^6.4.1",
|
|
58
58
|
"karma-chai": "^0.1.0",
|
|
59
59
|
"karma-chrome-launcher": "^3.1.0",
|
|
60
60
|
"karma-firefox-launcher": "^2.1.1",
|