@bjoernboss/mws 1.0.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/log.js ADDED
@@ -0,0 +1,288 @@
1
+ /* SPDX-License-Identifier: BSD-3-Clause */
2
+ /* Copyright (c) 2024-2026 Bjoern Boss Henrichsen */
3
+ import * as libFs from "fs";
4
+ const DEFAULT_FILE_FLUSHING_DELAY = 2_000;
5
+ const DEFAULT_FILE_BUF_MAXIMUM_LINES = 1_000;
6
+ const DEFAULT_FILE_SIZE_SWAP_FILE = 10_000_000;
7
+ const LoggerIdMap = {};
8
+ const GlobalLogConsumers = new Set();
9
+ function formatLevel(level) {
10
+ switch (level) {
11
+ case 'log':
12
+ return 'Log ';
13
+ case 'trace':
14
+ return 'Trace';
15
+ case 'error':
16
+ return 'Error';
17
+ case 'info':
18
+ return 'Info ';
19
+ case 'warning':
20
+ return 'Warn ';
21
+ }
22
+ }
23
+ function formatLine(level, date, identity, msg, lineBreak) {
24
+ let printLevel = formatLevel(level);
25
+ if (lineBreak)
26
+ return `[${date}] ${printLevel}: [${identity}] ${msg}\n`;
27
+ return `[${date}] ${printLevel}: [${identity}] ${msg}`;
28
+ }
29
+ /* implementation of a console logger */
30
+ export function createConsoleLogger() {
31
+ return (level, date, identity, msg) => {
32
+ if (level == null)
33
+ return;
34
+ let levelPrint = formatLevel(level);
35
+ if (process.stdout?.hasColors == null || !process.stdout.hasColors())
36
+ return console.log(formatLine(level, date, identity, msg, false));
37
+ let levelColor = '';
38
+ switch (level) {
39
+ case 'log':
40
+ levelColor = '\x1b[37m';
41
+ break;
42
+ case 'trace':
43
+ levelColor = '\x1b[94m';
44
+ break;
45
+ case 'error':
46
+ levelColor = '\x1b[31m';
47
+ break;
48
+ case 'info':
49
+ levelColor = '\x1b[92m';
50
+ break;
51
+ case 'warning':
52
+ levelColor = '\x1b[33m';
53
+ break;
54
+ }
55
+ console.log(`\x1b[90m[${date}] ${levelColor}${levelPrint}\x1b[0m: [\x1b[93m${identity}\x1b[0m] ${msg}`);
56
+ };
57
+ }
58
+ /* implementation of a logger which receives a well formatted line */
59
+ export function createLineLogger(cb) {
60
+ return (level, date, identity, msg) => {
61
+ if (level != null)
62
+ cb(formatLine(level, date, identity, msg, false));
63
+ };
64
+ }
65
+ /* file overwriting preservation mode */
66
+ export var PreserveMode;
67
+ (function (PreserveMode) {
68
+ /* simply clear the log file once full */
69
+ PreserveMode[PreserveMode["none"] = 0] = "none";
70
+ /* preserve the last log file to 'filePath.old' */
71
+ PreserveMode[PreserveMode["last"] = 1] = "last";
72
+ /* preserve all last log files to 'filePath.%time%.old */
73
+ PreserveMode[PreserveMode["all"] = 2] = "all";
74
+ })(PreserveMode || (PreserveMode = {}));
75
+ /* implementation of a file logger, which logs into the file-path and optionally preserves old logs
76
+ * (Note: contains a timer, server shutdown should clear all loggers to ensure fast shutdown) */
77
+ export function createFileLogger(filePath, options) {
78
+ /* setup the logging state (ignore any errors, as they cannot be logged) */
79
+ let fileHandle = null;
80
+ let logFileSize = 0;
81
+ try {
82
+ fileHandle = libFs.openSync(filePath, 'a');
83
+ logFileSize = libFs.fstatSync(fileHandle).size;
84
+ }
85
+ catch (_) { }
86
+ /* setup the file-flushing helper function */
87
+ let logBuffer = [];
88
+ let flushId = null;
89
+ const flushToFile = () => {
90
+ /* write the buffer to the file */
91
+ if (logBuffer.length > 0 && fileHandle != null) {
92
+ const content = Buffer.from(logBuffer.join(""), 'utf-8');
93
+ try {
94
+ libFs.writeFileSync(fileHandle, content);
95
+ }
96
+ catch (_) { }
97
+ logFileSize += content.length;
98
+ }
99
+ logBuffer = [];
100
+ /* clear any currently queued flushes */
101
+ if (flushId != null) {
102
+ clearTimeout(flushId);
103
+ flushId = null;
104
+ }
105
+ };
106
+ const flushingDelayMs = (options?.flushingDelayMs == null ? DEFAULT_FILE_FLUSHING_DELAY : options.flushingDelayMs);
107
+ const bufMaxLineCount = (options?.bufMaxLineCount == null ? DEFAULT_FILE_BUF_MAXIMUM_LINES : options.bufMaxLineCount);
108
+ const sizeSwapFile = (options?.sizeSwapFile == null ? DEFAULT_FILE_SIZE_SWAP_FILE : options.sizeSwapFile);
109
+ /* setup the actual closure handler */
110
+ return (level, date, identity, msg) => {
111
+ /* check if the logger is being disabled and reset the file state */
112
+ if (level == null) {
113
+ flushToFile();
114
+ if (fileHandle != null)
115
+ try {
116
+ libFs.closeSync(fileHandle);
117
+ }
118
+ catch (_) { }
119
+ fileHandle = null;
120
+ return;
121
+ }
122
+ /* write the log to the buffer and check if the data need to be flushed inplace, or if the flushing can be delayed */
123
+ logBuffer.push(formatLine(level, date, identity, msg, true));
124
+ if (logBuffer.length >= bufMaxLineCount)
125
+ flushToFile();
126
+ else {
127
+ if (flushId != null)
128
+ clearTimeout(flushId);
129
+ flushId = setTimeout(flushToFile, flushingDelayMs);
130
+ }
131
+ /* check if the log-files need to be swapped */
132
+ if (logFileSize < sizeSwapFile)
133
+ return;
134
+ /* flush the buffered entries and close the current file */
135
+ flushToFile();
136
+ if (fileHandle != null) {
137
+ try {
138
+ libFs.closeSync(fileHandle);
139
+ }
140
+ catch (_) { }
141
+ fileHandle = null;
142
+ }
143
+ /* preserve the old file to be used or remove it */
144
+ if (options?.preserve == PreserveMode.all)
145
+ try {
146
+ libFs.renameSync(filePath, `${filePath}.${Date.now()}.old`);
147
+ }
148
+ catch (_) { }
149
+ else if (options?.preserve == PreserveMode.last)
150
+ try {
151
+ libFs.renameSync(filePath, `${filePath}.old`);
152
+ }
153
+ catch (_) { }
154
+ else
155
+ try {
156
+ libFs.unlinkSync(filePath);
157
+ }
158
+ catch (_) { }
159
+ try {
160
+ fileHandle = libFs.openSync(filePath, 'w');
161
+ }
162
+ catch (_) { }
163
+ logFileSize = 0;
164
+ };
165
+ }
166
+ /* implementation of a log filter, which can filter by matching level and identity including a given sub-string, and then forwards these logs to the target logger */
167
+ export function createLogFilter(target, options) {
168
+ const filterLevel = (options?.level == null ? null : new Set(Array.isArray(options.level) ? options.level : [options.level]));
169
+ const filterIdentity = (options?.identity == null ? null : (Array.isArray(options.identity) ? options.identity : [options.identity]));
170
+ return (level, date, identity, msg) => {
171
+ if (level == null)
172
+ return target(null, '', '', '');
173
+ /* check if the level is supported */
174
+ if (filterLevel != null && !filterLevel.has(level))
175
+ return;
176
+ if (filterIdentity != null && !filterIdentity.some((value) => identity.includes(value)))
177
+ return;
178
+ target(level, date, identity, msg);
179
+ };
180
+ }
181
+ /* logger class to extend, supporting various logging classes, and writing to the registered log consumer */
182
+ export class Logger {
183
+ _rootIdentity;
184
+ _logIdentity;
185
+ _logTagList;
186
+ constructor(identity) {
187
+ const id = (LoggerIdMap[identity] ?? 0) + 1;
188
+ LoggerIdMap[identity] = id;
189
+ this._rootIdentity = `${identity}!${id}`;
190
+ this._logIdentity = this._rootIdentity;
191
+ this._logTagList = [];
192
+ }
193
+ _updateLogIdentity() {
194
+ this._logIdentity = this._rootIdentity;
195
+ for (const tag of this._logTagList) {
196
+ if (tag.value != '')
197
+ this._logIdentity += `.${tag.value}`;
198
+ }
199
+ }
200
+ _performActualLog(level, msg, options) {
201
+ const identity = (options?.extension == null ? this._logIdentity : this._rootIdentity + (options.extension == '' ? '' : `.${options.extension}`));
202
+ logGlobal(level, identity, msg);
203
+ }
204
+ /* root identity tagged with unique id */
205
+ get logRoot() {
206
+ return this._rootIdentity;
207
+ }
208
+ /* tag extension appended to root identity to form full logging identity */
209
+ get logExtension() {
210
+ return this._logIdentity.substring(this._rootIdentity.length + 1);
211
+ }
212
+ /* full logging identity as shown in logs */
213
+ get logIdentity() {
214
+ return this._logIdentity;
215
+ }
216
+ /* tag the logging with the given identifier and return a callback to update the tag */
217
+ tagLog(identifier) {
218
+ let tag = { value: identifier };
219
+ this._logTagList.push(tag);
220
+ if (tag.value != '')
221
+ this._updateLogIdentity();
222
+ /* setup the handler responsible to update the logging */
223
+ return (value) => {
224
+ if (tag == null)
225
+ return;
226
+ /* check if the tag should be removed or if the value should just be updated */
227
+ if (value == null) {
228
+ this._logTagList = this._logTagList.filter((v) => v != tag);
229
+ tag = null;
230
+ }
231
+ else if (value != tag.value)
232
+ tag.value = value;
233
+ this._updateLogIdentity();
234
+ };
235
+ }
236
+ error(msg, options) {
237
+ this._performActualLog('error', msg, options);
238
+ }
239
+ info(msg, options) {
240
+ this._performActualLog('info', msg, options);
241
+ }
242
+ warning(msg, options) {
243
+ this._performActualLog('warning', msg, options);
244
+ }
245
+ log(msg, options) {
246
+ this._performActualLog('log', msg, options);
247
+ }
248
+ trace(msg, options) {
249
+ this._performActualLog('trace', msg, options);
250
+ }
251
+ }
252
+ /* create a logger class to create associated logs */
253
+ export function createLogger(identity) {
254
+ return new Logger(identity);
255
+ }
256
+ /* register another global logger to receive the logs (returned detacher can be invoked to remove the log) */
257
+ export function addLogger(cb) {
258
+ let detached = false;
259
+ const wrapped = (level, date, identity, msg) => {
260
+ if (detached)
261
+ return;
262
+ if (level == null) {
263
+ GlobalLogConsumers.delete(wrapped);
264
+ detached = true;
265
+ }
266
+ try {
267
+ if (detached)
268
+ cb(null, '', '', '');
269
+ else
270
+ cb(level, date, identity, msg);
271
+ }
272
+ catch (err) {
273
+ console.error(`Logger failed: ${err.message}`);
274
+ wrapped(null, '', '', '');
275
+ }
276
+ };
277
+ GlobalLogConsumers.add(wrapped);
278
+ return () => {
279
+ wrapped(null, '', '', '');
280
+ };
281
+ }
282
+ /* perform a global log to all registered loggers */
283
+ export function logGlobal(level, identity, msg) {
284
+ const date = new Date().toUTCString();
285
+ for (const cb of GlobalLogConsumers)
286
+ cb(level, date, identity, msg);
287
+ }
288
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,66 @@
1
+ import * as libLog from "./log.js";
2
+ import * as libClient from "./client.js";
3
+ import * as libHandler from "./handler.js";
4
+ import * as libCache from "./cache.js";
5
+ import * as libHttp from "http";
6
+ import * as libNet from "net";
7
+ export declare class Server extends libLog.Logger {
8
+ private _stop;
9
+ private _cache;
10
+ private _config;
11
+ private _nextId;
12
+ constructor(config?: ServerConfig);
13
+ private handleClient;
14
+ private fetchAddress;
15
+ private performServerCleanup;
16
+ private emitEventSync;
17
+ private makeServer;
18
+ listen(handler: libHandler.ModuleHandler, options?: ListenOptions): Listener;
19
+ stop(): Promise<void>;
20
+ get cache(): libCache.CacheHost;
21
+ get config(): BurntServerConfig;
22
+ get stopped(): Promise<void>;
23
+ get running(): boolean;
24
+ linkModule(module: libHandler.ModuleHandler, unlinked?: () => void): libHandler.AttachedModule;
25
+ }
26
+ export interface Listener {
27
+ stop(): Promise<void>;
28
+ on<K extends keyof ListenerEvents>(event: K, listener: ListenerEvents[K]): Listener;
29
+ off<K extends keyof ListenerEvents>(event: K, listener: ListenerEvents[K]): Listener;
30
+ once<K extends keyof ListenerEvents>(event: K, listener: ListenerEvents[K]): Listener;
31
+ }
32
+ type ListenerEvents = {
33
+ 'listening': (address: libNet.AddressInfo) => void;
34
+ 'failed': (err: Error) => void;
35
+ 'stopped': () => void;
36
+ };
37
+ export interface ListenOptions {
38
+ port?: number;
39
+ hostname?: string;
40
+ client?: libClient.ClientConfig | libClient.BurntClientConfig;
41
+ tls?: {
42
+ key: string;
43
+ cert: string;
44
+ };
45
+ server?: {
46
+ server: libHttp.Server;
47
+ secure: boolean;
48
+ };
49
+ }
50
+ export declare function createServer(config?: ServerConfig): Server;
51
+ export interface ServerConfig {
52
+ headerTimeout?: number;
53
+ connectionTimeout?: number;
54
+ keepAliveTimeout?: number;
55
+ client?: libClient.ClientConfig | libClient.BurntClientConfig;
56
+ cache?: libCache.CacheHost | libCache.CacheConfig | libCache.BurntCacheConfig;
57
+ }
58
+ export declare class BurntServerConfig {
59
+ readonly headerTimeout: number;
60
+ readonly connectionTimeout: number;
61
+ readonly keepAliveTimeout: number;
62
+ readonly client: libClient.BurntClientConfig;
63
+ constructor(config?: ServerConfig);
64
+ }
65
+ export {};
66
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,257 @@
1
+ /* SPDX-License-Identifier: BSD-3-Clause */
2
+ /* Copyright (c) 2024-2026 Bjoern Boss Henrichsen */
3
+ import * as libLog from "./log.js";
4
+ import * as libClient from "./client.js";
5
+ import * as libCache from "./cache.js";
6
+ import * as libHttps from "https";
7
+ import * as libHttp from "http";
8
+ import * as libWs from "ws";
9
+ import * as libFs from "fs";
10
+ import * as libEvents from "events";
11
+ const CONNECTION_TIMEOUT_CHECKING = 10_000;
12
+ export class Server extends libLog.Logger {
13
+ _stop;
14
+ _cache;
15
+ _config;
16
+ _nextId;
17
+ constructor(config) {
18
+ super('server');
19
+ this.info(`Server created`);
20
+ this._config = new BurntServerConfig(config);
21
+ if (config?.cache instanceof libCache.CacheHost)
22
+ this._cache = config.cache;
23
+ else
24
+ this._cache = libCache.createCache(config?.cache);
25
+ this._nextId = 0;
26
+ let stoppedResolver = () => { };
27
+ this._stop = { listener: [], stoppedPromise: new Promise((res) => stoppedResolver = res), stoppedResolver: () => { }, stopping: false };
28
+ this._stop.stoppedResolver = stoppedResolver;
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'] ?? ''}])`);
32
+ try {
33
+ await handler.handle(client);
34
+ }
35
+ catch (err) {
36
+ client.respondInternalError(`Uncaught exception: ${err.message}`);
37
+ }
38
+ /* kill the connection on any errors, as the finalizing normally ensures that the response is completed */
39
+ try {
40
+ await client.finalizeConnection();
41
+ }
42
+ 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();
72
+ }
73
+ await Promise.all(sockets);
74
+ /* await the server being fully stopped */
75
+ await serverStopped;
76
+ }
77
+ emitEventSync(emitter, event, ...args) {
78
+ try {
79
+ emitter.emit(event, ...args);
80
+ }
81
+ catch (err) {
82
+ this.error(`Unhandled exception in ${event} listener: ${err.message}`);
83
+ }
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);
94
+ }
95
+ if (options?.server != null)
96
+ return options.server.server;
97
+ return libHttp.createServer({ requireHostHeader: true, connectionsCheckingInterval: CONNECTION_TIMEOUT_CHECKING });
98
+ }
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
+ };
129
+ const performFailure = (err) => {
130
+ /* defer the failure to allow the caller to attach listeners */
131
+ process.nextTick(() => {
132
+ if (stopping == null)
133
+ this.emitEventSync(emitter, 'failed', err);
134
+ listener.stop();
135
+ });
136
+ };
137
+ /* 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;
142
+ }
143
+ /* setup the origin server */
144
+ try {
145
+ server = this.makeServer(options);
146
+ }
147
+ catch (err) {
148
+ this.error(`Error creating server ${who}: ${err.message}`);
149
+ performFailure(err);
150
+ return listener;
151
+ }
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);
156
+ /* 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
+ });
166
+ server.once('error', (err) => {
167
+ if (stopping != null)
168
+ return;
169
+ this.error(`Error while listening to ${who}: ${err.message}`);
170
+ this.emitEventSync(emitter, 'failed', err);
171
+ listener.stop();
172
+ });
173
+ 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);
180
+ });
181
+ /* configure the server to have a minimum header receive timeout, overall connection-loss timeout,
182
+ * 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;
186
+ server.requestTimeout = 0;
187
+ /* start the actual server listening */
188
+ try {
189
+ server.listen(options?.port, options?.hostname);
190
+ }
191
+ catch (err) {
192
+ this.error(`Error starting listener ${who}: ${err.message}`);
193
+ performFailure(err);
194
+ }
195
+ return listener;
196
+ }
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;
211
+ }
212
+ /* cache host used by this server */
213
+ get cache() {
214
+ return this._cache;
215
+ }
216
+ /* configuration used by this server */
217
+ get config() {
218
+ return this._config;
219
+ }
220
+ /* resolves once the server has stopped */
221
+ get stopped() {
222
+ return this._stop.stoppedPromise;
223
+ }
224
+ /* check if the server is still running */
225
+ get running() {
226
+ return !this._stop.stopping;
227
+ }
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;
239
+ }
240
+ }
241
+ /* wrapper to create a simple server */
242
+ export function createServer(config) {
243
+ return new Server(config);
244
+ }
245
+ export class BurntServerConfig {
246
+ headerTimeout;
247
+ connectionTimeout;
248
+ keepAliveTimeout;
249
+ client;
250
+ constructor(config) {
251
+ this.headerTimeout = config?.headerTimeout ?? 30_000;
252
+ this.connectionTimeout = config?.connectionTimeout ?? 90_000;
253
+ this.keepAliveTimeout = config?.keepAliveTimeout ?? 10_000;
254
+ this.client = libClient.BurntClientConfig.from(config?.client);
255
+ }
256
+ }
257
+ //# sourceMappingURL=server.js.map
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@bjoernboss/mws",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Modular Web Server — a TypeScript framework for hosting isolated modules behind HTTP/HTTPS+WebSocket endpoints",
6
+ "license": "BSD-3-Clause",
7
+ "author": "Bjoern Boss Henrichsen <bjoernbossdev@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/BjoernBoss/mws.git"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./*.js": {
23
+ "types": "./dist/*.d.ts",
24
+ "default": "./dist/*.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist/**/*.js",
30
+ "dist/**/*.d.ts"
31
+ ],
32
+ "scripts": {
33
+ "prepare": "tsc"
34
+ },
35
+ "dependencies": {
36
+ "ws": "^8.18.1",
37
+ "@types/ws": "^8.18.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.6.0",
41
+ "typescript": "^6.0.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=22.0.0"
45
+ }
46
+ }