configurapi-runner-ws 1.13.0 → 1.15.0

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/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "configurapi-runner-ws",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "Websocket runner for configurapi.",
5
5
  "bin": {
6
6
  "configurapi-runner-ws": "src/app.mjs"
7
7
  },
8
8
  "main": "src/index.js",
9
9
  "scripts": {
10
- "test": "cd test && mocha ./"
10
+ "test": "node --test --test-reporter=spec spec/**/*.spec.js",
11
+ "test-watch": "node --test --watch --test-reporter=spec spec/**/*.spec.js"
11
12
  },
12
13
  "files": [
13
14
  "src/*"
@@ -33,5 +34,9 @@
33
34
  "dotenv": "^17.2.3",
34
35
  "one-liner": "^1.3.0",
35
36
  "ws": "^8.18.3"
37
+ },
38
+ "devDependencies": {
39
+ "@types/chai": "^5.2.3",
40
+ "chai": "^6.2.2"
36
41
  }
37
42
  }
package/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  require('dotenv').config();
2
2
  const http = require('http');
3
3
  const https = require('https');
4
- const WsAdapter = require('./wsAdapter');
5
4
  const events = require('events');
6
5
  const Configurapi = require('configurapi');
7
6
  const { WebSocketServer } = require('ws');
@@ -11,7 +10,7 @@ const oneliner = require('one-liner');
11
10
  const wsAdapter = require('./wsAdapter');
12
11
  const URL = require('url');
13
12
 
14
- function addInternalRoute(self, config, routeName,)
13
+ function addInternalRoute(self, config, routeName)
15
14
  {
16
15
  let route = new Route();
17
16
  route.name = routeName;
@@ -27,7 +26,7 @@ function addInternalRoute(self, config, routeName,)
27
26
  config.events.set(route.name, route);
28
27
  }
29
28
 
30
- const connections = []
29
+ const connections = new Map()
31
30
 
32
31
  module.exports = class HttpRunner extends events.EventEmitter
33
32
  {
@@ -55,7 +54,7 @@ module.exports = class HttpRunner extends events.EventEmitter
55
54
  loader.handlers.set('list_@connections', (e)=>
56
55
  {
57
56
  e.response.headers['Content-Type'] = 'application/json';
58
- e.response.body = Object.keys(connections);
57
+ e.response.body = Array.from(connections.keys());
59
58
  });
60
59
  loader.handlers.set('post_@connection', async (e)=>
61
60
  {
@@ -63,10 +62,10 @@ module.exports = class HttpRunner extends events.EventEmitter
63
62
 
64
63
  const connectionId = e.params?.['@connection'];
65
64
 
66
- if(connectionId && connections[connectionId])
65
+ if(connectionId && connections.has(connectionId))
67
66
  {
68
67
  let wsResponse = new Response(e.payload, 200, {...e.request.headers, 'connection-id': connectionId})
69
- await wsAdapter.write(connections[connectionId].ws, wsResponse)
68
+ await wsAdapter.write(connections.get(connectionId).ws, wsResponse)
70
69
  e.response.statusCode = 204
71
70
  }
72
71
  else
@@ -80,9 +79,9 @@ module.exports = class HttpRunner extends events.EventEmitter
80
79
  addInternalRoute(this, config, 'post_@connection')
81
80
 
82
81
  this.service = new Configurapi.Service(config, loader);
83
- this.service.on("trace", (s) => this.emit("trace", s));
84
- this.service.on("debug", (s) => this.emit("debug", s));
85
- this.service.on("error", (s) => this.emit("error", s));
82
+ this.service.on(LogLevel.Trace, (s) => this.emit(LogLevel.Trace, s));
83
+ this.service.on(LogLevel.Debug, (s) => this.emit(LogLevel.Debug, s));
84
+ this.service.on(LogLevel.Error, (s) => this.emit(LogLevel.Error, s));
86
85
  await this.service.init();
87
86
 
88
87
  let httpServer;
@@ -94,7 +93,7 @@ module.exports = class HttpRunner extends events.EventEmitter
94
93
  httpServer.keepAliveTimeout = this.keepAliveTimeout;
95
94
  httpServer.listen(this.sPort);
96
95
 
97
- this.emit("trace", "Secure Server is listening...");
96
+ this.emit(LogLevel.Trace, "Secure Server is listening...");
98
97
  }
99
98
  else
100
99
  {
@@ -103,7 +102,7 @@ module.exports = class HttpRunner extends events.EventEmitter
103
102
  httpServer.keepAliveTimeout = this.keepAliveTimeout;
104
103
  httpServer.listen(this.port);
105
104
 
106
- this.emit("trace", "Server is listening..."+this.port);
105
+ this.emit(LogLevel.Trace, "Server is listening..."+this.port);
107
106
  }
108
107
 
109
108
  let managementServer = http.createServer({key: this.key, cert: this.cert}, (req, resp) => {
@@ -112,7 +111,7 @@ module.exports = class HttpRunner extends events.EventEmitter
112
111
  managementServer.keepAliveTimeout = this.keepAliveTimeout;
113
112
  managementServer.listen(this.mPort);
114
113
 
115
- this.emit('trace', 'Management server is listening...'+this.mPort);
114
+ this.emit(LogLevel.Trace, 'Management server is listening...'+this.mPort);
116
115
 
117
116
  const wss = new WebSocketServer({ server: httpServer, path: "/ws", handleProtocols: (protocols, req) => {
118
117
  if(protocols === undefined)
@@ -136,38 +135,52 @@ module.exports = class HttpRunner extends events.EventEmitter
136
135
  const connectionId = randomUUID();
137
136
  ws.connectionId = connectionId;
138
137
 
139
- connections[connectionId] = {
140
- connectionId,
141
- ws
142
- }
143
- this.emit("trace", `[connect] ${connectionId}`);
138
+ connections.set(connectionId, { connectionId, ws });
139
+
140
+ this.emit(LogLevel.Trace, `[connect] ${connectionId}`);
144
141
 
145
142
  ws.on("message", async (data) =>
146
143
  {
147
144
  const incomingMessage = typeof data === "string" ? data : data.toString("utf8");
148
145
 
149
- await this._requestListener(ws, incomingMessage);
146
+ try
147
+ {
148
+ await this._requestListener(ws, incomingMessage);
149
+ }
150
+ catch(err)
151
+ {
152
+ this.emit(LogLevel.Error, err);
153
+ }
150
154
  });
151
155
 
152
156
  ws.on("close", async(code, reason) =>
153
157
  {
154
- await (config.events.has('on_disconnect') ? this._requestListener(ws, undefined, 'on_disconnect') : undefined);
158
+ try
159
+ {
160
+ await (config.events.has('on_disconnect') ? this._requestListener(ws, undefined, 'on_disconnect') : undefined);
161
+ }
162
+ catch(err)
163
+ {
164
+ this.emit(LogLevel.Error, err);
165
+ }
155
166
 
156
167
  const reasonMessage = reason && reason.length ? reason.toString("utf8") : "";
157
168
 
158
- delete connections[connectionId];
169
+ connections.delete(connectionId);
159
170
 
160
- this.emit("trace", `[disconnect] ${connectionId} (${code}${reasonMessage ? ` - ${reasonMessage}` : ""})`);
171
+ this.emit(LogLevel.Trace, `[disconnect] ${connectionId} (${code}${reasonMessage ? ` - ${reasonMessage}` : ""})`);
161
172
  });
162
173
 
163
174
  ws.on("error", (err) =>
164
175
  {
165
176
  // Note: 'error' will often be followed by 'close'
166
177
  const message = err instanceof Error ? err.message : err;
167
- this.emit("error", `${connectionId} - ${message}`);
178
+ this.emit(LogLevel.Error, `${connectionId} - ${message}`);
168
179
  });
169
180
 
170
181
  if (config.events.has('on_connect'))
182
+ {
183
+ try
171
184
  {
172
185
  const response = await this._requestListener(ws, undefined, 'on_connect');
173
186
 
@@ -176,6 +189,11 @@ module.exports = class HttpRunner extends events.EventEmitter
176
189
  return ws.close(1008, 'Policy Violation');
177
190
  }
178
191
  }
192
+ catch(err)
193
+ {
194
+ this.emit(LogLevel.Error, err);
195
+ }
196
+ }
179
197
  });
180
198
  }
181
199
 
@@ -198,7 +216,7 @@ module.exports = class HttpRunner extends events.EventEmitter
198
216
 
199
217
  try
200
218
  {
201
- event = new Configurapi.Event(await WsAdapter.toRequest(ws, incomingMessage));
219
+ event = new Configurapi.Event(await wsAdapter.toRequest(ws, incomingMessage));
202
220
 
203
221
  if(customEventName)
204
222
  {
@@ -217,7 +235,7 @@ module.exports = class HttpRunner extends events.EventEmitter
217
235
  event.response.headers['Sec-WebSocket-Protocol'] = protocol;
218
236
  }
219
237
 
220
- await WsAdapter.write(ws, event.response);
238
+ await wsAdapter.write(ws, event.response);
221
239
  }
222
240
  else
223
241
  {
@@ -229,7 +247,7 @@ module.exports = class HttpRunner extends events.EventEmitter
229
247
  }
230
248
  event.response.headers['message-id'] = event.request.headers?.['message-id'];
231
249
 
232
- await WsAdapter.write(ws, event.response); // Send response back to the caller.
250
+ await wsAdapter.write(ws, event.response); // Send response back to the caller.
233
251
  }
234
252
 
235
253
  return event.response;
@@ -246,13 +264,13 @@ module.exports = class HttpRunner extends events.EventEmitter
246
264
 
247
265
  try
248
266
  {
249
- await WsAdapter.write(ws, response);
267
+ await wsAdapter.write(ws, response);
250
268
  }
251
269
  catch(err)
252
270
  {
253
271
  try
254
272
  {
255
- this.emit("error", JSON.stringify(err));
273
+ this.emit(LogLevel.Error, JSON.stringify(err));
256
274
  }
257
275
  catch(errorFromListner)
258
276
  {
package/src/wsAdapter.js CHANGED
@@ -12,6 +12,26 @@ module.exports = {
12
12
 
13
13
  await new Promise((resolve, reject)=>
14
14
  {
15
+ let settled = false;
16
+
17
+ const safeResolve = () =>
18
+ {
19
+ if(!settled)
20
+ {
21
+ settled = true;
22
+ resolve();
23
+ }
24
+ };
25
+
26
+ const safeReject = (err) =>
27
+ {
28
+ if(!settled)
29
+ {
30
+ settled = true;
31
+ reject(err);
32
+ }
33
+ };
34
+
15
35
  try
16
36
  {
17
37
  if(message instanceof StreamResponse)
@@ -22,7 +42,7 @@ module.exports = {
22
42
  // Guard: need a readable stream
23
43
  if(!stream || typeof stream.on !== 'function')
24
44
  {
25
- return reject(new Error('StreamResponse.stream is not a readable stream.'));
45
+ return safeReject(new Error('StreamResponse.stream is not a readable stream.'));
26
46
  }
27
47
 
28
48
  const HIGH_WATER = 64 * 1024;
@@ -40,7 +60,11 @@ module.exports = {
40
60
  {
41
61
  stream.pause();
42
62
  const check = () => {
43
- if (ws.readyState !== WebSocket.OPEN) return cleanup();
63
+ if (ws.readyState !== WebSocket.OPEN)
64
+ {
65
+ cleanup();
66
+ return safeResolve();
67
+ }
44
68
  if (ws.bufferedAmount <= HIGH_WATER) return stream.resume();
45
69
  setTimeout(check, 100);
46
70
  };
@@ -52,13 +76,13 @@ module.exports = {
52
76
 
53
77
  ws.send(JSON.stringify(response), (err) =>
54
78
  {
55
- if (err) { cleanup(); return reject(err); }
79
+ if (err) { cleanup(); return safeReject(err); }
56
80
  });
57
81
 
58
82
  bucket = '';
59
83
  bucketBytes = 0;
60
84
  };
61
-
85
+
62
86
  const cleanup = () =>
63
87
  {
64
88
  try {
@@ -66,9 +90,31 @@ module.exports = {
66
90
  stream.removeListener('data', onData);
67
91
  stream.removeListener('end', onEnd);
68
92
  stream.removeListener('error', onErr);
93
+ stream.removeListener('close', onClose);
94
+
95
+ ws.removeListener('close', onWsClose);
96
+ ws.removeListener('error', onWsError);
97
+
98
+ // safeResolve();
69
99
  } catch(_) {}
70
100
  };
71
101
 
102
+ // If ws connection closes before the stream does, cleanup the connection.
103
+ const onWsClose = () =>
104
+ {
105
+ cleanup();
106
+ safeResolve();
107
+ };
108
+
109
+ const onWsError = () =>
110
+ {
111
+ cleanup();
112
+ safeResolve();
113
+ };
114
+
115
+ ws.on('close', onWsClose);
116
+ ws.on('error', onWsError);
117
+
72
118
  const onData = (chunk) =>
73
119
  {
74
120
  if (ws.readyState !== WebSocket.OPEN) { cleanup(); return; }
@@ -99,60 +145,76 @@ module.exports = {
99
145
  ws.send(JSON.stringify(response), (err) =>
100
146
  {
101
147
  cleanup();
102
- return err ? reject(err) : resolve();
148
+ return err ? safeReject(err) : safeResolve();
103
149
  });
104
150
  }
105
151
  else
106
152
  {
107
153
  cleanup();
108
- return resolve();
154
+ safeResolve();
109
155
  }
110
156
  };
111
157
 
112
158
  const onErr = (e) =>
113
159
  {
114
160
  cleanup();
115
- return reject(e);
161
+ safeReject(e);
116
162
  };
117
163
 
164
+ const onClose = () =>
165
+ {
166
+ cleanup();
167
+ safeResolve()
168
+ }
169
+
118
170
  stream.on('data', onData);
119
171
  stream.on('end', onEnd);
120
172
  stream.on('error', onErr);
173
+ stream.on('close', onClose);
121
174
 
122
175
  // Important: stop here; promise resolves on 'end' or rejects on 'error'
123
176
  return;
124
177
  }
125
178
  else
126
179
  {
127
- ws.send(JSON.stringify(message), (err) => err ? reject(err) : resolve());
180
+ ws.send(JSON.stringify(message), (err) => err ? safeReject(err) : safeResolve());
128
181
  }
129
182
  }
130
183
  catch(e)
131
184
  {
132
- reject(e)
185
+ safeReject(e)
133
186
  }
134
187
  });
135
188
  },
136
189
 
137
190
  toRequest: async function(ws, incomingMessage)
138
- {
139
- let data = incomingMessage ? JSON.parse(incomingMessage) : {};
191
+ {
192
+ let data = {};
193
+
194
+ if(incomingMessage)
195
+ {
196
+ try
197
+ {
198
+ data = JSON.parse(incomingMessage);
199
+ }
200
+ catch(e)
201
+ {
202
+ data.payload = incomingMessage;
203
+ }
204
+ }
140
205
 
141
206
  let request = new Configurapi.Request();
142
207
 
143
208
  request.method = '';
144
209
  request.headers = {};
145
- request.name = data.name || undefined;
210
+ request.name = data.name || data?.action || undefined;
146
211
  request.query = data.query || {};
147
212
  request.params = data.params || {};
148
213
  request.payload = data.payload || undefined;
149
214
 
150
- if(data.headers)
215
+ for (const key of Object.keys(data.headers || {}))
151
216
  {
152
- for(let key of Object.keys(data.headers))
153
- {
154
- request.headers[key.toLowerCase()] = data.headers[key];
155
- }
217
+ request.headers[key.toLowerCase()] = data.headers[key];
156
218
  }
157
219
 
158
220
  request.headers['connection-id'] = ws.connectionId;