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 +7 -2
- package/src/index.js +45 -27
- package/src/wsAdapter.js +79 -17
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configurapi-runner-ws",
|
|
3
|
-
"version": "1.
|
|
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": "
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
84
|
-
this.service.on(
|
|
85
|
-
this.service.on(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
169
|
+
connections.delete(connectionId);
|
|
159
170
|
|
|
160
|
-
this.emit(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
267
|
+
await wsAdapter.write(ws, response);
|
|
250
268
|
}
|
|
251
269
|
catch(err)
|
|
252
270
|
{
|
|
253
271
|
try
|
|
254
272
|
{
|
|
255
|
-
this.emit(
|
|
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
|
|
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)
|
|
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
|
|
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 ?
|
|
148
|
+
return err ? safeReject(err) : safeResolve();
|
|
103
149
|
});
|
|
104
150
|
}
|
|
105
151
|
else
|
|
106
152
|
{
|
|
107
153
|
cleanup();
|
|
108
|
-
|
|
154
|
+
safeResolve();
|
|
109
155
|
}
|
|
110
156
|
};
|
|
111
157
|
|
|
112
158
|
const onErr = (e) =>
|
|
113
159
|
{
|
|
114
160
|
cleanup();
|
|
115
|
-
|
|
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 ?
|
|
180
|
+
ws.send(JSON.stringify(message), (err) => err ? safeReject(err) : safeResolve());
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
183
|
catch(e)
|
|
131
184
|
{
|
|
132
|
-
|
|
185
|
+
safeReject(e)
|
|
133
186
|
}
|
|
134
187
|
});
|
|
135
188
|
},
|
|
136
189
|
|
|
137
190
|
toRequest: async function(ws, incomingMessage)
|
|
138
|
-
{
|
|
139
|
-
let data =
|
|
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
|
-
|
|
215
|
+
for (const key of Object.keys(data.headers || {}))
|
|
151
216
|
{
|
|
152
|
-
|
|
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;
|