@bjoernboss/mws 1.0.0 → 1.1.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/dist/server.js CHANGED
@@ -13,232 +13,306 @@ export class Server extends libLog.Logger {
13
13
  _stop;
14
14
  _cache;
15
15
  _config;
16
- _nextId;
16
+ _nextEndpoint;
17
17
  constructor(config) {
18
- super('server');
18
+ super(config?.name ?? 'server');
19
19
  this.info(`Server created`);
20
20
  this._config = new BurntServerConfig(config);
21
21
  if (config?.cache instanceof libCache.CacheHost)
22
22
  this._cache = config.cache;
23
23
  else
24
24
  this._cache = libCache.createCache(config?.cache);
25
- this._nextId = 0;
25
+ this._nextEndpoint = 0;
26
26
  let stoppedResolver = () => { };
27
- this._stop = { listener: [], stoppedPromise: new Promise((res) => stoppedResolver = res), stoppedResolver: () => { }, stopping: false };
27
+ this._stop = { list: [], stoppedPromise: new Promise((res) => stoppedResolver = res), stoppedResolver: () => { }, stopping: false };
28
28
  this._stop.stoppedResolver = stoppedResolver;
29
29
  }
30
- async handleClient(request, client, handler, id) {
31
- this.log(`Listener[${id}]: Client [${client.logIdentity}] connected using [method: ${request.method ?? '_'}] from [${request.socket.remoteAddress}]:${request.socket.remotePort} to [${client.url.hostname}]:[${request.url}] (user-agent: [${request.headers['user-agent'] ?? ''}])`);
30
+ /** listener is automatically stopped when the server is stopped or the handler stops itself */
31
+ listen(handler, options) {
32
+ return Listener._fromParams(this, handler, ++this._nextEndpoint, this._stop, options ?? {});
33
+ }
34
+ /** shutdown the server and unlink all modules (immediately kills all open connections and listener; can be called multiple times) */
35
+ stop() {
36
+ if (this._stop.stopping)
37
+ return this._stop.stoppedPromise;
38
+ this._stop.stopping = true;
39
+ (async () => {
40
+ /* stop all connections and listener */
41
+ this.info('Stopping server connections and modules');
42
+ const promises = [];
43
+ for (const cb of this._stop.list)
44
+ promises.push(cb());
45
+ await Promise.all(promises);
46
+ this.info('Server stopped');
47
+ this._stop.stoppedResolver();
48
+ })();
49
+ return this._stop.stoppedPromise;
50
+ }
51
+ /** cache host used by this server */
52
+ get cache() {
53
+ return this._cache;
54
+ }
55
+ /** configuration used by this server */
56
+ get config() {
57
+ return this._config;
58
+ }
59
+ /** resolves once the server has stopped */
60
+ get stopped() {
61
+ return this._stop.stoppedPromise;
62
+ }
63
+ /** check if the server is still running */
64
+ get running() {
65
+ return !this._stop.stopping;
66
+ }
67
+ /** link the given module to the server (automatically unlinked upon server stop) */
68
+ linkModule(module, unlinked) {
69
+ const cleanup = () => attached.unlink();
70
+ this._stop.list.push(cleanup);
71
+ const attached = module._rootAttachToServer(this, () => {
72
+ if (unlinked != null)
73
+ unlinked();
74
+ if (!this._stop.stopping)
75
+ this._stop.list = this._stop.list.filter((v) => v != cleanup);
76
+ });
77
+ return attached;
78
+ }
79
+ }
80
+ /**
81
+ * Either 'listening' or 'failed' is fired, followed at some point by a 'stopped' event.
82
+ * 'address' of 'listening' event is null for serverless listener.
83
+ */
84
+ export class Listener {
85
+ _host;
86
+ _self;
87
+ _stop;
88
+ _native;
89
+ _handling;
90
+ _emitter;
91
+ _config;
92
+ constructor(server, id, hostStop, clientConfig, handler) {
93
+ this._host = { self: server, stop: hostStop };
94
+ this._self = { endpoint: `endpoint!${id}`, listening: null, protocol: '' };
95
+ this._emitter = new libEvents.EventEmitter();
96
+ this._config = clientConfig;
97
+ this._handling = { count: 0, promise: null, resolver: () => { } };
98
+ let stoppedResolver = () => { };
99
+ this._stop = { stoppedPromise: new Promise((res) => stoppedResolver = res), stoppedResolver: () => { }, stopping: false };
100
+ this._stop.stoppedResolver = stoppedResolver;
101
+ /* register the handler and the cleanup callback */
102
+ const attached = handler._rootAttachToServer(this._host.self, () => this.stop());
103
+ const wss = new libWs.WebSocketServer({ noServer: true });
104
+ this._native = { wss, attached, server: null, cleanup: () => this.stop() };
105
+ this._host.stop.list.push(this._native.cleanup);
106
+ }
107
+ emitEventSync(event, ...args) {
32
108
  try {
33
- await handler.handle(client);
109
+ this._emitter.emit(event, ...args);
34
110
  }
35
111
  catch (err) {
36
- client.respondInternalError(`Uncaught exception: ${err.message}`);
112
+ this._host.self.error(`Unhandled exception in ${event} listener: ${err.message}`);
37
113
  }
38
- /* kill the connection on any errors, as the finalizing normally ensures that the response is completed */
114
+ }
115
+ performServerListening(address) {
116
+ if (typeof address == 'string')
117
+ address = { address, port: 0, family: 'unix' };
118
+ if (address == null)
119
+ this._self.listening = 'serverless';
120
+ else
121
+ this._self.listening = `[${address.address}]:${address.port} [family: ${address.family}]`;
122
+ this._host.self.info(`Successfully started ${this._self.endpoint} on ${this._self.listening} with handler [${this._native.attached.module.identity}]`);
123
+ this.emitEventSync('listening', address);
124
+ }
125
+ async handleClient(request, client) {
126
+ if (this._handling.count++ == 0)
127
+ this._handling.promise = new Promise((res) => this._handling.resolver = res);
128
+ const endpoint = `${this._host.self.identity}.${this._self.endpoint}`;
129
+ /* register the completed log immediately to ensure it is logged as the first thing before the other completed awaits execute */
130
+ client.log(`Connected to [${endpoint}] using [method: ${request.method ?? '_'}] from [${request.socket.remoteAddress}]:${request.socket.remotePort} to [${client.url.hostname}]:[${request.url}] (user-agent: [${request.headers['user-agent'] ?? ''}])`);
131
+ client.completed.then(() => client.log(`Completed on [${endpoint}]`));
39
132
  try {
40
- await client.finalizeConnection();
133
+ await this._native.attached.handle(client);
41
134
  }
42
135
  catch (err) {
43
- this.error(`Fatal error while finalizing client: ${err.message}`);
44
- request.destroy(new Error('Unhandled exception'));
45
- }
46
- this.log(`Listener[${id}]: Client [${client.logIdentity}] completed`);
47
- }
48
- fetchAddress(server) {
49
- const raw = server.address();
50
- if (raw == null)
51
- return null;
52
- if (typeof raw == 'string')
53
- return { address: raw, port: 0, family: 'unix' };
54
- return raw;
55
- }
56
- async performServerCleanup(server, id, who, attached, wss) {
57
- /* close the server and any existing connections within it */
58
- const address = this.fetchAddress(server);
59
- if (address != null && id != null)
60
- this.info(`Stopping ${who} on [${address.address}]:${address.port} [family: ${address.family}] as listener [${id}] with handler [${attached.module.logIdentity}]`);
61
- const serverStopped = new Promise((res) => server.close(() => res()));
62
- server.closeAllConnections();
63
- /* close all of the web-sockets (after unlinking the module to
64
- * ensure it has a chance to clean the connections itself) */
65
- await attached.unlink();
66
- const sockets = [];
67
- for (const ws of [...wss.clients]) {
68
- if (ws.readyState == libWs.WebSocket.CLOSED)
69
- continue;
70
- sockets.push(new Promise((res) => ws.on('close', () => res())));
71
- ws.terminate();
136
+ client.respondInternalError(`Uncaught exception: ${err.message}`);
72
137
  }
73
- await Promise.all(sockets);
74
- /* await the server being fully stopped */
75
- await serverStopped;
76
- }
77
- emitEventSync(emitter, event, ...args) {
138
+ /* kill the connection on any errors, as the finalizing normally ensures that the response is completed */
78
139
  try {
79
- emitter.emit(event, ...args);
140
+ await client._finalizeConnection();
80
141
  }
81
142
  catch (err) {
82
- this.error(`Unhandled exception in ${event} listener: ${err.message}`);
143
+ client.error(`Fatal error while finalizing [${client.identity}]: ${err.message}`);
144
+ request.destroy(new Error('Unhandled exception'));
83
145
  }
84
- }
85
- makeServer(options) {
86
- if (options?.tls != null) {
87
- const config = {
88
- requireHostHeader: true,
89
- key: libFs.readFileSync(options.tls.key),
90
- cert: libFs.readFileSync(options.tls.cert),
91
- connectionsCheckingInterval: CONNECTION_TIMEOUT_CHECKING
92
- };
93
- return libHttps.createServer(config);
146
+ if (--this._handling.count == 0) {
147
+ this._handling.promise = null;
148
+ this._handling.resolver();
94
149
  }
95
- if (options?.server != null)
96
- return options.server.server;
97
- return libHttp.createServer({ requireHostHeader: true, connectionsCheckingInterval: CONNECTION_TIMEOUT_CHECKING });
98
150
  }
99
- /* listener is automatically stopped when the server is stopped or the handler stops itself */
100
- listen(handler, options) {
101
- const protocol = ((options?.tls != null || options?.server?.secure === true) ? 'https' : 'http');
102
- const who = `${protocol}:${options?.hostname ?? ''}:${options?.port ?? 0}`, idListener = this._nextId++;
103
- const emitter = new libEvents.EventEmitter();
104
- /* setup the listener interface to be returned */
105
- let stopping = null, listenLogged = false;
106
- let server = null;
107
- const listener = {
108
- on: function (event, cb) { emitter.on(event, cb); return this; },
109
- once: function (event, cb) { emitter.once(event, cb); return this; },
110
- off: function (event, cb) { emitter.off(event, cb); return this; },
111
- stop: () => {
112
- if (stopping != null)
113
- return stopping;
114
- /* already setup the promise to ensure nested stop-calls will already see it set */
115
- let resolver = () => { };
116
- stopping = new Promise((res) => resolver = res);
117
- (async () => {
118
- if (server != null)
119
- await this.performServerCleanup(server, (listenLogged ? idListener : null), who, attached, wss);
120
- /* check if the cleanup can be removed from the stop list (only if stopping is not already in progress) */
121
- if (!this._stop.stopping)
122
- this._stop.listener = this._stop.listener.filter((v) => v != listener.stop);
123
- this.emitEventSync(emitter, 'stopped');
124
- resolver();
125
- })();
126
- return stopping;
127
- }
128
- };
151
+ configure(options) {
152
+ this._self.protocol = ((options.tls != null || options.server?.secure === true || (options.server == null && options.serverless?.secure === true)) ? 'https' : 'http');
153
+ const who = (options.tls == null && options.server == null && options.serverless != null ? 'serverless' : `${this._self.protocol}|${options.hostname ?? ''}:${options.port ?? 0}`);
154
+ /* defer the failure to allow the caller to attach listeners */
129
155
  const performFailure = (err) => {
130
- /* defer the failure to allow the caller to attach listeners */
131
156
  process.nextTick(() => {
132
- if (stopping == null)
133
- this.emitEventSync(emitter, 'failed', err);
134
- listener.stop();
157
+ if (!this._stop.stopping)
158
+ this.emitEventSync('failed', err);
159
+ this.stop();
135
160
  });
136
161
  };
162
+ this._host.self.trace(`Setting up listening to [${who}] and handler [${this._native.attached.module.identity}]`);
137
163
  /* check if the server is being stopped, in which case nothing will be listened to */
138
- if (this._stop.stopping) {
139
- this.error(`Stopped server cannot listen to ${who}`);
140
- performFailure(new Error('Server already stopped'));
141
- return listener;
164
+ if (this._host.stop.stopping) {
165
+ this._host.self.error(`Stopped server cannot listen to [${who}]`);
166
+ return performFailure(new Error('Server already stopped'));
142
167
  }
143
- /* setup the origin server */
168
+ let server = null;
144
169
  try {
145
- server = this.makeServer(options);
170
+ /* setup the actual server server */
171
+ if (options.tls != null) {
172
+ const config = {
173
+ requireHostHeader: true,
174
+ key: libFs.readFileSync(options.tls.key),
175
+ cert: libFs.readFileSync(options.tls.cert),
176
+ connectionsCheckingInterval: CONNECTION_TIMEOUT_CHECKING
177
+ };
178
+ server = libHttps.createServer(config);
179
+ }
180
+ else if (options.server != null)
181
+ server = options.server.server;
182
+ else if (options.serverless == null)
183
+ server = libHttp.createServer({ requireHostHeader: true, connectionsCheckingInterval: CONNECTION_TIMEOUT_CHECKING });
146
184
  }
147
185
  catch (err) {
148
- this.error(`Error creating server ${who}: ${err.message}`);
149
- performFailure(err);
150
- return listener;
186
+ this._host.self.error(`Error creating server [${who}]: ${err.message}`);
187
+ return performFailure(err);
151
188
  }
152
- /* register the handler properly and register the cleanup callback */
153
- const attached = handler._rootAttachToServer(this, () => listener.stop());
154
- const wss = new libWs.WebSocketServer({ noServer: true });
155
- this._stop.listener.push(listener.stop);
189
+ /* check if this is a serverless run, in which case the server does not need to be configured further */
190
+ if (server == null)
191
+ return this.performServerListening(null);
192
+ this._native.server = server;
156
193
  /* register the corresponding connection handlers and error handlers */
157
- const clientConfig = (options?.client != null ? libClient.BurntClientConfig.from(options.client) : this._config.client);
158
- server.on('request', (req, resp) => {
159
- const client = libClient.ClientRequest.fromRequest(protocol, req, resp, { cache: this._cache, config: clientConfig });
160
- this.handleClient(req, client, attached, idListener);
161
- });
162
- server.on('upgrade', (req, sock, head) => {
163
- const client = libClient.ClientRequest.fromUpgrade(protocol, req, sock, head, { cache: this._cache, config: clientConfig, wss });
164
- this.handleClient(req, client, attached, idListener);
165
- });
194
+ server.on('request', (req, resp) => this.handleRequest(req, resp));
195
+ server.on('upgrade', (req, sock, head) => this.handleUpgrade(req, sock, head));
166
196
  server.once('error', (err) => {
167
- if (stopping != null)
197
+ if (this._stop.stopping)
168
198
  return;
169
- this.error(`Error while listening to ${who}: ${err.message}`);
170
- this.emitEventSync(emitter, 'failed', err);
171
- listener.stop();
199
+ this._host.self.error(`Error while listening to [${who}]: ${err.message}`);
200
+ this.emitEventSync('failed', err);
201
+ this.stop();
172
202
  });
173
203
  server.on('listening', () => {
174
- if (stopping != null)
175
- return;
176
- const address = this.fetchAddress(server);
177
- this.info(`Successfully started ${who} on [${address.address}]:${address.port} [family: ${address.family}] as listener [${idListener}] with handler [${handler.logIdentity}]`);
178
- listenLogged = true;
179
- this.emitEventSync(emitter, 'listening', address);
204
+ if (!this._stop.stopping)
205
+ this.performServerListening(server.address());
180
206
  });
181
207
  /* configure the server to have a minimum header receive timeout, overall connection-loss timeout,
182
208
  * and keep-alive timeout (no request-timeout, as this is handled manually by the throughput control) */
183
- server.headersTimeout = this._config.headerTimeout;
184
- server.timeout = this._config.connectionTimeout;
185
- server.keepAliveTimeout = this._config.keepAliveTimeout;
209
+ server.headersTimeout = this._host.self.config.headerTimeout;
210
+ server.timeout = this._host.self.config.connectionTimeout;
211
+ server.keepAliveTimeout = this._host.self.config.keepAliveTimeout;
186
212
  server.requestTimeout = 0;
187
213
  /* start the actual server listening */
188
214
  try {
189
- server.listen(options?.port, options?.hostname);
215
+ server.listen(options.port, options.hostname);
190
216
  }
191
217
  catch (err) {
192
- this.error(`Error starting listener ${who}: ${err.message}`);
193
- performFailure(err);
218
+ this._host.self.error(`Error starting listener [${who}]: ${err.message}`);
219
+ return performFailure(err);
194
220
  }
221
+ }
222
+ static _fromParams(server, handler, id, hostStop, options) {
223
+ const clientConfig = (options.client != null ? libClient.BurntClientConfig.from(options.client) : server.config.client);
224
+ const listener = new Listener(server, id, hostStop, clientConfig, handler);
225
+ listener.configure(options);
195
226
  return listener;
196
227
  }
197
- /* shutdown the server and unlink all modules (immediately kills all open connections and listener; can be called multiple times) */
198
- async stop() {
199
- if (this._stop.stopping)
200
- return this._stop.stoppedPromise;
201
- this._stop.stopping = true;
202
- /* stop all connections and listener */
203
- this.info('Stopping server connections and modules');
204
- const promises = [];
205
- for (const cb of this._stop.listener)
206
- promises.push(cb());
207
- await Promise.all(promises);
208
- this.info('Server stopped');
209
- this._stop.stoppedResolver();
210
- return this._stop.stoppedPromise;
228
+ /** manually pass a request through the listener (takes ownership of the request; will kill the connection if the listener is not running anymore) */
229
+ async handleRequest(request, response) {
230
+ if (this._stop.stopping) {
231
+ request.destroy(new Error('Listener not running anymore'));
232
+ return;
233
+ }
234
+ const client = libClient.ClientRequest._fromRequest(this._self.protocol, request, response, this._config, this._host.self);
235
+ await this.handleClient(request, client);
211
236
  }
212
- /* cache host used by this server */
213
- get cache() {
214
- return this._cache;
237
+ /** manually pass an upgrade through the listener (takes ownership of the connection; immediately closes the connection if the listener is not running anymore) */
238
+ async handleUpgrade(request, socket, head) {
239
+ if (this._stop.stopping) {
240
+ request.destroy(new Error('Listener not running anymore'));
241
+ return;
242
+ }
243
+ const client = libClient.ClientRequest._fromUpgrade(this._self.protocol, request, socket, head, this._config, this._host.self, this._native.wss);
244
+ await this.handleClient(request, client);
245
+ }
246
+ /** server this listener belongs to */
247
+ get server() {
248
+ return this._host.self;
215
249
  }
216
- /* configuration used by this server */
250
+ /** client configuration used for this listener */
217
251
  get config() {
218
252
  return this._config;
219
253
  }
220
- /* resolves once the server has stopped */
254
+ /** stop the listener and return promise which resolves once fully stopped */
255
+ stop() {
256
+ if (this._stop.stopping)
257
+ return this._stop.stoppedPromise;
258
+ this._stop.stopping = true;
259
+ (async () => {
260
+ /* close the server and any existing connections within it */
261
+ let serverStopped = null;
262
+ if (this._native.server != null) {
263
+ serverStopped = new Promise((res) => this._native.server.close(() => res()));
264
+ this._native.server.closeAllConnections();
265
+ }
266
+ else
267
+ serverStopped = Promise.resolve();
268
+ await this._native.attached.unlink();
269
+ /* close all of the web-sockets (after unlinking the module to
270
+ * ensure it has a chance to clean the connections itself) */
271
+ const sockets = [];
272
+ for (const ws of [...this._native.wss.clients]) {
273
+ if (ws.readyState == libWs.WebSocket.CLOSED)
274
+ continue;
275
+ sockets.push(new Promise((res) => ws.on('close', () => res())));
276
+ ws.terminate();
277
+ }
278
+ await Promise.all(sockets);
279
+ /* wait for any handled connections to be over and for the server to be fully stopped */
280
+ while (this._handling.promise != null)
281
+ await this._handling.promise;
282
+ await serverStopped;
283
+ if (this._self.listening != null)
284
+ this._host.self.info(`Stopped ${this._self.endpoint} on ${this._self.listening} with handler [${this._native.attached.module.identity}]`);
285
+ /* check if the cleanup can be removed from the stop list (only if stopping is not already in progress) */
286
+ if (!this._host.stop.stopping)
287
+ this._host.stop.list = this._host.stop.list.filter((v) => v != this._native.cleanup);
288
+ this.emitEventSync('stopped');
289
+ this._stop.stoppedResolver();
290
+ })();
291
+ return this._stop.stoppedPromise;
292
+ }
293
+ /** resolves once the server has stopped */
221
294
  get stopped() {
222
295
  return this._stop.stoppedPromise;
223
296
  }
224
- /* check if the server is still running */
297
+ /** check if the server is still running */
225
298
  get running() {
226
299
  return !this._stop.stopping;
227
300
  }
228
- /* link the given module to the server (automatically unlinked upon server stop) */
229
- linkModule(module, unlinked) {
230
- const cleanup = () => attached.unlink();
231
- this._stop.listener.push(cleanup);
232
- const attached = module._rootAttachToServer(this, () => {
233
- if (unlinked != null)
234
- unlinked();
235
- if (!this._stop.stopping)
236
- this._stop.listener = this._stop.listener.filter((v) => v != cleanup);
237
- });
238
- return attached;
301
+ /* -------- event handler interfaces -------- */
302
+ on(event, listener) {
303
+ this._emitter.on(event, listener);
304
+ return this;
305
+ }
306
+ once(event, listener) {
307
+ this._emitter.once(event, listener);
308
+ return this;
309
+ }
310
+ off(event, listener) {
311
+ this._emitter.off(event, listener);
312
+ return this;
239
313
  }
240
314
  }
241
- /* wrapper to create a simple server */
315
+ /** wrapper to create a simple server */
242
316
  export function createServer(config) {
243
317
  return new Server(config);
244
318
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjoernboss/mws",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Modular Web Server — a TypeScript framework for hosting isolated modules behind HTTP/HTTPS+WebSocket endpoints",
6
6
  "license": "BSD-3-Clause",
@@ -16,12 +16,12 @@
16
16
  "types": "dist/index.d.ts",
17
17
  "exports": {
18
18
  ".": {
19
- "types": "./dist/index.d.ts",
20
- "default": "./dist/index.js"
19
+ "default": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
21
  },
22
22
  "./*.js": {
23
- "types": "./dist/*.d.ts",
24
- "default": "./dist/*.js"
23
+ "default": "./dist/*.js",
24
+ "types": "./dist/*.d.ts"
25
25
  },
26
26
  "./package.json": "./package.json"
27
27
  },