node-red-contrib-ntrip 0.2.3 → 0.2.7

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.
Files changed (39) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/node.js.yml +22 -21
  3. package/.github/workflows/release.yml +65 -0
  4. package/.prettierignore +20 -0
  5. package/.prettierrc.json +9 -0
  6. package/CHANGELOG.md +61 -0
  7. package/CLAUDE.md +73 -0
  8. package/README.md +145 -18
  9. package/doc/architecture/README.md +19 -0
  10. package/doc/architecture/adr/0001-single-registration-file.md +64 -0
  11. package/doc/architecture/adr/0002-ntrip-uploader-extension.md +71 -0
  12. package/doc/architecture/adr/0003-two-output-decoder-design.md +73 -0
  13. package/doc/architecture/adr/0004-stateful-handshake-interception.md +74 -0
  14. package/doc/architecture/adr/0005-rtcm-partial-frame-buffer.md +87 -0
  15. package/doc/architecture/adr/0006-nmea-multi-sentence-split.md +66 -0
  16. package/doc/architecture/adr/0007-mocha-test-helper-stack.md +67 -0
  17. package/doc/architecture/adr/0008-coordinate-gating-sentinel.md +76 -0
  18. package/doc/architecture/adr/README.md +18 -0
  19. package/doc/architecture/architecture-decisions.md +60 -0
  20. package/doc/architecture/behavioural-design.md +226 -0
  21. package/doc/architecture/errors-and-weaknesses.md +71 -0
  22. package/doc/architecture/future-improvements.md +130 -0
  23. package/doc/architecture/overview.md +77 -0
  24. package/doc/architecture/refactoring-recommendations.md +114 -0
  25. package/doc/architecture/statistics.md +118 -0
  26. package/doc/architecture/structural-design.md +141 -0
  27. package/eslint.config.js +36 -0
  28. package/ntrip/99-ntrip.html +207 -31
  29. package/ntrip/99-ntrip.js +12 -12
  30. package/ntrip/lib/ntrip-client.js +23 -18
  31. package/ntrip/nodes/nmea-decoder-node.js +77 -34
  32. package/ntrip/nodes/nmea-encoder-node.js +68 -47
  33. package/ntrip/nodes/ntrip-client-node.js +143 -123
  34. package/ntrip/nodes/rtcm-decoder-node.js +65 -47
  35. package/package.json +20 -2
  36. package/test/nmea-decoder.spec.js +146 -0
  37. package/test/nmea-encoder.spec.js +104 -0
  38. package/test/rtcm-decoder.spec.js +116 -0
  39. package/.github/workflows/npm-publish.yml +0 -33
@@ -1,158 +1,178 @@
1
1
  module.exports = function (RED) {
2
2
  'use strict';
3
3
 
4
- const NtripClient = require("../lib/ntrip-client.js");
4
+ const NtripClient = require('../lib/ntrip-client.js');
5
5
  const NtripServerOkReply = 'ICY 200 OK';
6
6
  const NtripServerNotOkReply = 'ICY 406';
7
7
  const NtripServerMissingMountpoint = 'SOURCETABLE 200 OK';
8
+ const HandshakePrefixBytes = 64;
8
9
 
9
10
  function NtripClientNode(config) {
10
11
  RED.nodes.createNode(this, config);
11
12
  let node = this;
12
13
  node.messagesReceived = 0;
14
+ node.messagesSent = 0;
13
15
  node.passthrough = config.passthrough || false;
14
-
15
- let host = config.host;
16
- let port = config.port || 2101;
16
+
17
+ let host = typeof config.host === 'string' ? config.host.trim() : '';
18
+ let port = parseInt(config.port, 10) || 2101;
17
19
  let mountpoint = config.mountpoint;
18
20
  let mode = config.mode || 'download';
19
21
  let authmode = config.authmode || 'legacy';
20
-
22
+ let interval = parseInt(config.interval, 10) || 1000;
23
+
24
+ if (host === '') {
25
+ node.status({ fill: 'red', shape: 'ring', text: 'Host not configured' });
26
+ return;
27
+ }
28
+
29
+ let options = {
30
+ host: host,
31
+ port: port,
32
+ mountpoint: mountpoint,
33
+ username: node.credentials.username,
34
+ password: node.credentials.password,
35
+ interval: interval,
36
+ };
37
+
38
+ // Depending on the caster the clients need to provide the location via GGA sentence.
39
+ let x = parseFloat(config.xcoordinate);
40
+ let y = parseFloat(config.ycoordinate);
41
+ let z = parseFloat(config.zcoordinate);
42
+
43
+ if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z) && !(x === 0 && y === 0 && z === 0)) {
44
+ options.xyz = [x, y, z];
45
+ }
46
+
21
47
  let client;
22
- if(host !== undefined ) {
23
-
24
- let options = {
25
- host: host,
26
- port: port,
27
- mountpoint: mountpoint,
28
- username: node.credentials.username,
29
- password: node.credentials.password,
30
- interval: config.interval,
31
- };
32
-
33
- // Depending on the caster the clients need to provide the location via GGA sentence.
34
- let x = parseFloat(config.xcoordinate || 0);
35
- let y = parseFloat(config.ycoordinate || 0);
36
- let z = parseFloat(config.zcoordinate || 0);
37
-
38
- if (x !== 0 && y !== 0 && z !== 0) {
39
- options.xyz = [x, y, z];
40
- }
41
-
42
- if (mode === 'download') {
43
- // Clients only consume ntrip data.
44
- client = NtripClient.createDownloader(options);
45
- }
46
- else if (mode === 'upload') {
47
- // Uploaders send RTCM data to a mountpoint so that clients can consume it.
48
- options.authmode = authmode;
48
+ if (mode === 'download') {
49
+ client = NtripClient.createDownloader(options);
50
+ } else if (mode === 'upload') {
51
+ options.authmode = authmode;
52
+ try {
49
53
  client = NtripClient.createUploader(options);
54
+ } catch (error) {
55
+ node.status({ fill: 'red', shape: 'ring', text: String(error.message || error) });
56
+ node.error('Failed to create uploader: ' + (error.message || error));
57
+ return;
50
58
  }
51
- else {
52
- node.error('Mode not supported ' + mode);
53
- }
59
+ } else {
60
+ node.status({ fill: 'red', shape: 'ring', text: 'Unsupported mode: ' + mode });
61
+ node.error('Mode not supported: ' + mode);
62
+ return;
63
+ }
54
64
 
55
- client.on('data', (data) => {
56
- node.messagesReceived++;
57
- node.status({
58
- fill: 'green',
59
- shape: 'ring',
60
- text: 'Messages ' + node.messagesReceived,
61
- });
65
+ // Track handshake state so we only intercept control replies during the handshake,
66
+ // not on every subsequent data packet. Reset on socket close so reconnects work.
67
+ let connected = false;
62
68
 
63
- let msg = {
64
- payload : data
65
- }
69
+ function updateStatus() {
70
+ node.status({
71
+ fill: 'green',
72
+ shape: 'ring',
73
+ text: 'Rx ' + node.messagesReceived + ' Tx ' + node.messagesSent,
74
+ });
75
+ }
66
76
 
67
- // check if ICY 200 OK
68
- const response = data.toString();
69
- if (response.startsWith(NtripServerOkReply)) {
70
- node.status({
71
- fill: 'green',
72
- shape: 'ring',
73
- text: 'NTRIP server connected.',
74
- });
77
+ client.on('data', (data) => {
78
+ if (!connected) {
79
+ let prefix = data.toString('utf8', 0, Math.min(data.length, HandshakePrefixBytes));
80
+ if (prefix.startsWith(NtripServerOkReply)) {
81
+ connected = true;
82
+ node.status({ fill: 'green', shape: 'ring', text: 'NTRIP server connected.' });
83
+ // Forward any RTCM bytes that arrived in the same TCP segment as the handshake reply.
84
+ let headerEnd = data.indexOf('\r\n\r\n');
85
+ if (headerEnd !== -1 && headerEnd + 4 < data.length) {
86
+ node.messagesReceived++;
87
+ node.send({ payload: data.slice(headerEnd + 4) });
88
+ updateStatus();
89
+ }
90
+ return;
75
91
  }
76
- // check if ICY 406 Not Acceptable
77
- else if (response.startsWith(NtripServerNotOkReply)) {
78
- node.status({
79
- fill: 'red',
80
- shape: 'ring',
81
- text: 'NTRIP server rejected connection.',
82
- });
83
- node.error('Server rejected connect: ' + response);
84
- }
85
- else if (response.startsWith(NtripServerMissingMountpoint)) {
86
- node.status({
87
- fill: 'red',
88
- shape: 'ring',
89
- text: 'No mountpont ' + mountpoint,
90
- });
91
- node.error('No mountpoint: ' + response);
92
+ if (prefix.startsWith(NtripServerNotOkReply)) {
93
+ node.status({ fill: 'red', shape: 'ring', text: 'NTRIP server rejected connection.' });
94
+ node.error('Server rejected connect: ' + prefix);
95
+ return;
92
96
  }
93
- else {
94
- node.send(msg);
97
+ if (prefix.startsWith(NtripServerMissingMountpoint)) {
98
+ node.status({ fill: 'red', shape: 'ring', text: 'No mountpoint ' + mountpoint });
99
+ node.error('No mountpoint: ' + prefix);
100
+ return;
95
101
  }
96
- });
102
+ // Unexpected: data arrived before any recognised handshake reply.
103
+ // Treat as connected so we don't drop further frames.
104
+ connected = true;
105
+ }
97
106
 
98
- client.on('close', () => {
99
- node.log('Closed NTRIP connection.');
100
- });
107
+ node.messagesReceived++;
108
+ node.send({ payload: data });
109
+ updateStatus();
110
+ });
101
111
 
102
- client.on('error', (error) => {
103
- if (error instanceof AggregateError) {
104
- error.errors.forEach((e, i) => {
105
- node.log(`Error ${i + 1}: ${e.message}`);
106
- node.error('Error: ' + e.message);
107
- });
108
- } else {
109
- node.log(error);
110
- node.error('Error: ' + error);
111
- }
112
- });
112
+ client.on('close', () => {
113
+ connected = false;
114
+ node.log('Closed NTRIP connection.');
115
+ });
113
116
 
117
+ client.on('error', (error) => {
118
+ connected = false;
119
+ if (typeof AggregateError !== 'undefined' && error instanceof AggregateError) {
120
+ error.errors.forEach((e, i) => {
121
+ node.log('Error ' + (i + 1) + ': ' + e.message);
122
+ node.error('Error: ' + e.message);
123
+ });
124
+ } else {
125
+ let text = error && error.message ? error.message : String(error);
126
+ node.log(text);
127
+ node.error('Error: ' + text);
128
+ }
129
+ });
130
+
131
+ try {
114
132
  client.run();
115
- node.status({
116
- fill: 'green',
117
- shape: 'ring',
118
- text: 'ok',
119
- });
133
+ } catch (error) {
134
+ node.status({ fill: 'red', shape: 'ring', text: String(error.message || error) });
135
+ node.error('Failed to start client: ' + (error.message || error));
136
+ return;
137
+ }
138
+
139
+ node.status({ fill: 'yellow', shape: 'ring', text: 'connecting...' });
140
+
141
+ this.on('input', function (msg) {
142
+ let payload = msg.payload;
143
+ if (payload === undefined || payload === null) {
144
+ return;
145
+ }
120
146
 
121
- this.on('input', async function (msg) {
122
- if (msg.payload) {
123
- node.messagesReceived++;
124
- node.status({
125
- fill: 'green',
126
- shape: 'ring',
127
- text: 'Messages ' + node.messagesReceived,
128
- });
129
-
130
- try {
131
- let data = msg.payload;
132
- if (Array.isArray(data)) {
133
- client.setXYZ(data);
134
- }
135
- else {
136
- client.write(data);
137
-
138
- if(node.passthrough) {
139
- node.send(msg);
140
- }
141
- }
142
- } catch (error) {
143
- node.status({ fill: 'red', shape: 'ring', text: error });
144
- node.error('Failed to write data: ' + error, error);
147
+ try {
148
+ if (Array.isArray(payload)) {
149
+ client.setXYZ(payload);
150
+ } else {
151
+ client.write(payload);
152
+ node.messagesSent++;
153
+ if (node.passthrough) {
154
+ node.send(msg);
145
155
  }
156
+ updateStatus();
146
157
  }
147
- });
158
+ } catch (error) {
159
+ let text = error && error.message ? error.message : String(error);
160
+ node.status({ fill: 'red', shape: 'ring', text: text });
161
+ node.error('Failed to write data: ' + text, msg);
162
+ }
163
+ });
148
164
 
149
- this.on('close', function(done) {
165
+ this.on('close', function (done) {
166
+ try {
167
+ client.removeAllListeners();
150
168
  client.close();
151
- node.status({});
152
- done();
153
- });
154
- }
169
+ } catch {
170
+ // ignore — node is shutting down anyway
171
+ }
172
+ node.status({});
173
+ done();
174
+ });
155
175
  }
156
-
176
+
157
177
  return NtripClientNode;
158
- };
178
+ };
@@ -1,71 +1,89 @@
1
1
  module.exports = function (RED) {
2
2
  'use strict';
3
3
 
4
- const { RtcmTransport, RtcmMessage, RtcmMessageType } = require('@gnss/rtcm');
5
-
4
+ const { RtcmTransport } = require('@gnss/rtcm');
5
+
6
+ // Cap on pending bytes when a decode keeps failing — guards against runaway memory
7
+ // if upstream feeds us garbage rather than truly-partial RTCM frames.
8
+ const MaxPendingBytes = 65536;
9
+
6
10
  function RtcmDecoderNode(config) {
7
11
  RED.nodes.createNode(this, config);
8
12
  let node = this;
9
13
  node.rtcmMessagesReceived = 0;
10
14
  node.invalidMessagesReceived = 0;
15
+ node.pendingBuffer = Buffer.alloc(0);
16
+
17
+ function updateStatus() {
18
+ node.status({
19
+ fill: 'green',
20
+ shape: 'ring',
21
+ text: 'RTCM: ' + node.rtcmMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
22
+ });
23
+ }
11
24
 
12
25
  this.on('input', function (msg) {
26
+ if (msg.payload === undefined || msg.payload === null) {
27
+ return;
28
+ }
13
29
 
14
- let done = false;
15
- let buffer = msg.payload;
16
- try
17
- {
18
- do {
19
- let message;
20
- let length;
21
- [message, length] = RtcmTransport.decode(buffer);
22
-
23
- let messageType = message.constructor.name.replace('RtcmMessage', '');
30
+ let chunk = Buffer.isBuffer(msg.payload) ? msg.payload : Buffer.from(msg.payload);
31
+ let buffer = node.pendingBuffer.length > 0 ? Buffer.concat([node.pendingBuffer, chunk]) : chunk;
24
32
 
25
- let msg = {
26
- payload : {
27
- rtcm : message.messageType,
28
- messageType : messageType,
29
- message : message,
30
- input : buffer
31
- }
32
- };
33
+ while (buffer.length > 0) {
34
+ let message;
35
+ let length;
36
+ try {
37
+ [message, length] = RtcmTransport.decode(buffer);
38
+ } catch (ex) {
39
+ // Likely a partial frame at the tail of this chunk — keep the bytes
40
+ // for the next input event. If we've accumulated too much without a
41
+ // successful decode, drop and emit an error.
42
+ if (buffer.length > MaxPendingBytes) {
43
+ let errMsg = {
44
+ payload: {
45
+ error: ex,
46
+ input: buffer,
47
+ inputString: buffer.toString(),
48
+ },
49
+ };
50
+ node.send([null, errMsg]);
51
+ node.invalidMessagesReceived++;
52
+ buffer = Buffer.alloc(0);
53
+ }
54
+ break;
55
+ }
33
56
 
34
- node.send([msg, null]);
35
- node.rtcmMessagesReceived++;
57
+ if (!Number.isInteger(length) || length <= 0 || length > buffer.length) {
58
+ // Defensive — guarantees forward progress even if the decoder ever
59
+ // returns a non-positive length.
60
+ break;
61
+ }
36
62
 
37
- // find next message in buffer.
38
- buffer = buffer.slice(length);
39
- if(buffer.length == 0){
40
- done = true;
41
- }
42
- } while(!done);
43
- }
44
- catch(ex)
45
- {
46
- let msg = {
47
- payload : {
48
- error : ex,
49
- input : buffer,
50
- inputString : buffer.toString()
51
- }
63
+ let messageType = message.constructor.name.replace('RtcmMessage', '');
64
+ let outMsg = {
65
+ payload: {
66
+ rtcm: message.messageType,
67
+ messageType: messageType,
68
+ message: message,
69
+ input: buffer.slice(0, length),
70
+ },
52
71
  };
53
- node.send([null, msg]);
54
- node.invalidMessagesReceived++;
72
+ node.send([outMsg, null]);
73
+ node.rtcmMessagesReceived++;
74
+ buffer = buffer.slice(length);
55
75
  }
56
76
 
57
- node.status({
58
- fill: 'green',
59
- shape: 'ring',
60
- text: 'RTCM: ' + node.rtcmMessagesReceived + ' Invalid: ' + node.invalidMessagesReceived,
61
- });
77
+ node.pendingBuffer = buffer;
78
+ updateStatus();
62
79
  });
63
80
 
64
- this.on('close', function(done) {
81
+ this.on('close', function (done) {
82
+ node.pendingBuffer = Buffer.alloc(0);
65
83
  node.status({});
66
84
  done();
67
85
  });
68
86
  }
69
-
87
+
70
88
  return RtcmDecoderNode;
71
- };
89
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ntrip",
3
- "version": "0.2.3",
3
+ "version": "0.2.7",
4
4
  "description": "Node for ntrip and rtcm handling.",
5
5
  "node-red": {
6
6
  "version": ">=0.1.0",
@@ -9,7 +9,7 @@
9
9
  }
10
10
  },
11
11
  "engines": {
12
- "node": ">=7.6.0"
12
+ "node": ">=18.0.0"
13
13
  },
14
14
  "keywords": [
15
15
  "node-red",
@@ -24,9 +24,27 @@
24
24
  },
25
25
  "author": "Karl-Heinz Wind",
26
26
  "license": "MIT",
27
+ "scripts": {
28
+ "test": "mocha test/**/*.spec.js --timeout 5000",
29
+ "lint": "eslint .",
30
+ "lint:fix": "eslint . --fix",
31
+ "format": "prettier --write .",
32
+ "format:check": "prettier --check ."
33
+ },
27
34
  "dependencies": {
28
35
  "@gnss/nmea": "^0.1.2",
29
36
  "@gnss/rtcm": "^0.1.5",
30
37
  "ntrip-client": "^1.1.1"
38
+ },
39
+ "devDependencies": {
40
+ "@eslint/js": "^10.0.1",
41
+ "chai": "^4.5.0",
42
+ "eslint": "^10.3.0",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "globals": "^17.6.0",
45
+ "mocha": "^11.7.5",
46
+ "node-red": "^4.1.10",
47
+ "node-red-node-test-helper": "^0.3.6",
48
+ "prettier": "^3.8.3"
31
49
  }
32
50
  }
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ const helper = require('node-red-node-test-helper');
4
+ const { expect } = require('chai');
5
+ const ntripModule = require('../ntrip/99-ntrip.js');
6
+
7
+ helper.init(require.resolve('node-red'));
8
+
9
+ const GGA = '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47';
10
+ const GLL = '$GPGLL,4916.45,N,12311.12,W,225444,A,*1D';
11
+
12
+ function flow() {
13
+ return [
14
+ { id: 'n1', type: 'NmeaDecoder', wires: [['ok'], ['err']] },
15
+ { id: 'ok', type: 'helper' },
16
+ { id: 'err', type: 'helper' },
17
+ ];
18
+ }
19
+
20
+ describe('NmeaDecoder', function () {
21
+ before(() => helper.startServer());
22
+ after(() => helper.stopServer());
23
+ afterEach(() => helper.unload());
24
+
25
+ it('decodes a valid GGA sentence on the success output', function () {
26
+ return new Promise((resolve, reject) => {
27
+ helper.load(ntripModule, flow(), () => {
28
+ const n1 = helper.getNode('n1');
29
+ const ok = helper.getNode('ok');
30
+ ok.on('input', (msg) => {
31
+ try {
32
+ expect(msg.payload.messageType).to.equal('GGA');
33
+ expect(msg.payload.nmeaMessage).to.have.property('latitude');
34
+ expect(msg.payload.input).to.equal(GGA);
35
+ resolve();
36
+ } catch (e) {
37
+ reject(e);
38
+ }
39
+ });
40
+ n1.receive({ payload: GGA });
41
+ });
42
+ });
43
+ });
44
+
45
+ it('splits a multi-sentence chunk into separate output messages', function () {
46
+ return new Promise((resolve, reject) => {
47
+ helper.load(ntripModule, flow(), () => {
48
+ const n1 = helper.getNode('n1');
49
+ const ok = helper.getNode('ok');
50
+ const types = [];
51
+ ok.on('input', (msg) => {
52
+ types.push(msg.payload.messageType);
53
+ if (types.length === 2) {
54
+ try {
55
+ expect(types).to.deep.equal(['GGA', 'GLL']);
56
+ resolve();
57
+ } catch (e) {
58
+ reject(e);
59
+ }
60
+ }
61
+ });
62
+ n1.receive({ payload: GGA + '\r\n' + GLL + '\r\n' });
63
+ });
64
+ });
65
+ });
66
+
67
+ it('accepts a Buffer payload and decodes it as UTF-8', function () {
68
+ return new Promise((resolve, reject) => {
69
+ helper.load(ntripModule, flow(), () => {
70
+ const n1 = helper.getNode('n1');
71
+ const ok = helper.getNode('ok');
72
+ ok.on('input', (msg) => {
73
+ try {
74
+ expect(msg.payload.messageType).to.equal('GGA');
75
+ resolve();
76
+ } catch (e) {
77
+ reject(e);
78
+ }
79
+ });
80
+ n1.receive({ payload: Buffer.from(GGA, 'utf8') });
81
+ });
82
+ });
83
+ });
84
+
85
+ it('reads payload.nmeaMessage when payload is an object', function () {
86
+ return new Promise((resolve, reject) => {
87
+ helper.load(ntripModule, flow(), () => {
88
+ const n1 = helper.getNode('n1');
89
+ const ok = helper.getNode('ok');
90
+ ok.on('input', (msg) => {
91
+ try {
92
+ expect(msg.payload.messageType).to.equal('GGA');
93
+ resolve();
94
+ } catch (e) {
95
+ reject(e);
96
+ }
97
+ });
98
+ n1.receive({ payload: { nmeaMessage: GGA } });
99
+ });
100
+ });
101
+ });
102
+
103
+ it('error output preserves Buffer input as `input`, utf8 as `inputString`', function () {
104
+ return new Promise((resolve, reject) => {
105
+ helper.load(ntripModule, flow(), () => {
106
+ const n1 = helper.getNode('n1');
107
+ const err = helper.getNode('err');
108
+ const garbage = Buffer.from([0xd3, 0x00, 0x13, 0x3e, 0xd0, 0x00]);
109
+ err.on('input', (msg) => {
110
+ try {
111
+ expect(Buffer.isBuffer(msg.payload.input)).to.equal(true);
112
+ expect(msg.payload.input).to.deep.equal(garbage);
113
+ expect(msg.payload.inputString).to.be.a('string');
114
+ resolve();
115
+ } catch (e) {
116
+ reject(e);
117
+ }
118
+ });
119
+ n1.receive({ payload: garbage });
120
+ });
121
+ });
122
+ });
123
+
124
+ it('does not call node.error on decode failure (regression: log flood)', function () {
125
+ return new Promise((resolve, reject) => {
126
+ helper.load(ntripModule, flow(), () => {
127
+ const n1 = helper.getNode('n1');
128
+ let errorCalled = false;
129
+ const originalError = n1.error.bind(n1);
130
+ n1.error = function (...args) {
131
+ errorCalled = true;
132
+ return originalError(...args);
133
+ };
134
+ n1.receive({ payload: Buffer.from([0xd3, 0x00, 0x13, 0x3e]) });
135
+ setTimeout(() => {
136
+ try {
137
+ expect(errorCalled).to.equal(false);
138
+ resolve();
139
+ } catch (e) {
140
+ reject(e);
141
+ }
142
+ }, 50);
143
+ });
144
+ });
145
+ });
146
+ });