@dashevo/dapi-client 0.24.0-dev.1 → 0.24.0-dev.10

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.
@@ -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.24.0-dev.1",
3
+ "version": "0.24.0-dev.10",
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": "~0.24.0-dev.1",
30
- "@dashevo/dash-spv": "~0.24.0-dev.1",
29
+ "@dashevo/dapi-grpc": "0.24.0-dev.10",
30
+ "@dashevo/dash-spv": "0.24.0-dev.10",
31
31
  "@dashevo/dashcore-lib": "~0.19.44",
32
- "@dashevo/dpp": "~0.24.0-dev.1",
33
- "@dashevo/grpc-common": "~0.24.0-dev.1",
34
- "@grpc/grpc-js": "^1.3.7",
32
+ "@dashevo/dpp": "0.24.0-dev.10",
33
+ "@dashevo/grpc-common": "0.24.0-dev.10",
35
34
  "axios": "^0.27.2",
36
35
  "bs58": "^4.0.1",
37
36
  "cbor": "^8.0.0",
38
- "lodash.sample": "^4.2.1",
39
- "node-inspect-extracted": "^1.0.8"
37
+ "lodash": "^4.17.21",
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,6 +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
+ "google-protobuf": "^3.12.2",
57
58
  "karma": "^6.4.1",
58
59
  "karma-chai": "^0.1.0",
59
60
  "karma-chrome-launcher": "^3.1.0",