configurapi-runner-ws 1.12.0 → 1.14.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 +55 -36
- package/src/wsAdapter.js +76 -11
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configurapi-runner-ws",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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,45 +135,65 @@ 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
|
-
if (config.events.has('on_connect'))
|
|
146
|
-
{
|
|
147
|
-
const response = await this._requestListener(ws, undefined, 'on_connect');
|
|
148
|
-
|
|
149
|
-
if (response.statusCode >= 400)
|
|
150
|
-
{
|
|
151
|
-
return ws.close(1008, 'Policy Violation');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
142
|
ws.on("message", async (data) =>
|
|
155
143
|
{
|
|
156
144
|
const incomingMessage = typeof data === "string" ? data : data.toString("utf8");
|
|
157
145
|
|
|
158
|
-
|
|
146
|
+
try
|
|
147
|
+
{
|
|
148
|
+
await this._requestListener(ws, incomingMessage);
|
|
149
|
+
}
|
|
150
|
+
catch(err)
|
|
151
|
+
{
|
|
152
|
+
this.emit(LogLevel.Error, err);
|
|
153
|
+
}
|
|
159
154
|
});
|
|
160
155
|
|
|
161
156
|
ws.on("close", async(code, reason) =>
|
|
162
157
|
{
|
|
163
|
-
|
|
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
|
+
}
|
|
164
166
|
|
|
165
167
|
const reasonMessage = reason && reason.length ? reason.toString("utf8") : "";
|
|
166
168
|
|
|
167
|
-
delete
|
|
169
|
+
connections.delete(connectionId);
|
|
168
170
|
|
|
169
|
-
this.emit(
|
|
171
|
+
this.emit(LogLevel.Trace, `[disconnect] ${connectionId} (${code}${reasonMessage ? ` - ${reasonMessage}` : ""})`);
|
|
170
172
|
});
|
|
171
173
|
|
|
172
174
|
ws.on("error", (err) =>
|
|
173
175
|
{
|
|
174
176
|
// Note: 'error' will often be followed by 'close'
|
|
175
177
|
const message = err instanceof Error ? err.message : err;
|
|
176
|
-
this.emit(
|
|
178
|
+
this.emit(LogLevel.Error, `${connectionId} - ${message}`);
|
|
177
179
|
});
|
|
180
|
+
|
|
181
|
+
if (config.events.has('on_connect'))
|
|
182
|
+
{
|
|
183
|
+
try
|
|
184
|
+
{
|
|
185
|
+
const response = await this._requestListener(ws, undefined, 'on_connect');
|
|
186
|
+
|
|
187
|
+
if (response.statusCode >= 400)
|
|
188
|
+
{
|
|
189
|
+
return ws.close(1008, 'Policy Violation');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch(err)
|
|
193
|
+
{
|
|
194
|
+
this.emit(LogLevel.Error, err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
178
197
|
});
|
|
179
198
|
}
|
|
180
199
|
|
|
@@ -197,7 +216,7 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
197
216
|
|
|
198
217
|
try
|
|
199
218
|
{
|
|
200
|
-
event = new Configurapi.Event(await
|
|
219
|
+
event = new Configurapi.Event(await wsAdapter.toRequest(ws, incomingMessage));
|
|
201
220
|
|
|
202
221
|
if(customEventName)
|
|
203
222
|
{
|
|
@@ -216,7 +235,7 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
216
235
|
event.response.headers['Sec-WebSocket-Protocol'] = protocol;
|
|
217
236
|
}
|
|
218
237
|
|
|
219
|
-
await
|
|
238
|
+
await wsAdapter.write(ws, event.response);
|
|
220
239
|
}
|
|
221
240
|
else
|
|
222
241
|
{
|
|
@@ -228,7 +247,7 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
228
247
|
}
|
|
229
248
|
event.response.headers['message-id'] = event.request.headers?.['message-id'];
|
|
230
249
|
|
|
231
|
-
await
|
|
250
|
+
await wsAdapter.write(ws, event.response); // Send response back to the caller.
|
|
232
251
|
}
|
|
233
252
|
|
|
234
253
|
return event.response;
|
|
@@ -245,13 +264,13 @@ module.exports = class HttpRunner extends events.EventEmitter
|
|
|
245
264
|
|
|
246
265
|
try
|
|
247
266
|
{
|
|
248
|
-
await
|
|
267
|
+
await wsAdapter.write(ws, response);
|
|
249
268
|
}
|
|
250
269
|
catch(err)
|
|
251
270
|
{
|
|
252
271
|
try
|
|
253
272
|
{
|
|
254
|
-
this.emit(
|
|
273
|
+
this.emit(LogLevel.Error, JSON.stringify(err));
|
|
255
274
|
}
|
|
256
275
|
catch(errorFromListner)
|
|
257
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,44 +145,63 @@ 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
|
+
throw new SyntaxError(`Invalid JSON payload: '${incomingMessage}'`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
140
205
|
|
|
141
206
|
let request = new Configurapi.Request();
|
|
142
207
|
|