From e8957047852b971f7ed923179c0ba8723bea8641 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sat, 27 Sep 2025 14:36:06 -0400 Subject: [PATCH] Enhance WebSocket server with new HTTP server implementation and type assertions * Updated package.json to specify the entry point for the development script and added morgan as a dependency. * Introduced HttpServer class for handling HTTP requests with Express and integrated logging. * Added new assertion methods in the Assert class for better type validation. * Created IRoutes interface to define route handling structure. * Added optional and nullable type definitions for improved type safety. * Implemented initial server setup in src/index.ts. --- Web/WebSocket/websocket/server/package.json | 6 +- Web/WebSocket/websocket/server/src/index.ts | 18 +++++ .../websocket/server/src/lang/Assert.ts | 43 +++++++++++- .../server/src/net/http/HttpServer.ts | 68 +++++++++++++++++++ .../websocket/server/src/net/http/IRoutes.ts | 8 +++ .../websocket/server/src/types/optional.ts | 2 + 6 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 Web/WebSocket/websocket/server/src/index.ts create mode 100644 Web/WebSocket/websocket/server/src/net/http/HttpServer.ts create mode 100644 Web/WebSocket/websocket/server/src/net/http/IRoutes.ts create mode 100644 Web/WebSocket/websocket/server/src/types/optional.ts diff --git a/Web/WebSocket/websocket/server/package.json b/Web/WebSocket/websocket/server/package.json index 244525c..1ab2d9e 100644 --- a/Web/WebSocket/websocket/server/package.json +++ b/Web/WebSocket/websocket/server/package.json @@ -9,7 +9,7 @@ "lint": "eslint --max-warnings 0 .", "prelint:fix": "npm run format", "lint:fix": "eslint --fix .", - "dev": "tsx watch --clear-screen=false", + "dev": "tsx watch --clear-screen=false src/index.ts", "typecheck": "tsc" }, "author": "Alexander Zinn", @@ -20,6 +20,7 @@ "@eslint/js": "9.36.0", "@eslint/json": "0.13.2", "@eslint/markdown": "7.3.0", + "@types/morgan": "1.9.10", "@types/node": "24.5.2", "eslint": "9.36.0", "globals": "16.4.0", @@ -32,6 +33,7 @@ }, "dependencies": { "@techniker-me/logger": "0.0.15", - "@techniker-me/tools": "2025.0.16" + "@techniker-me/tools": "2025.0.16", + "morgan": "1.10.1" } } diff --git a/Web/WebSocket/websocket/server/src/index.ts b/Web/WebSocket/websocket/server/src/index.ts new file mode 100644 index 0000000..3e62f52 --- /dev/null +++ b/Web/WebSocket/websocket/server/src/index.ts @@ -0,0 +1,18 @@ +import HttpServer from './net/http/HttpServer'; +import {LoggerFactory} from '@techniker-me/logger'; + +const logger = LoggerFactory.getLogger('Server'); + +const httpServer = new HttpServer('http', 3000, { + getGETRoutes: () => ({}), + getPOSTRoutes: () => ({}), + getPUTRoutes: () => ({}), + getPATCHRoutes: () => ({}), + getDELETERoutes: () => ({}), +}, {}, '', [], '', {}); + +httpServer.on('error', () => { + logger.error('[HttpServer] Error'); +}); + +httpServer.start(); \ No newline at end of file diff --git a/Web/WebSocket/websocket/server/src/lang/Assert.ts b/Web/WebSocket/websocket/server/src/lang/Assert.ts index c36b5a0..4ba255d 100644 --- a/Web/WebSocket/websocket/server/src/lang/Assert.ts +++ b/Web/WebSocket/websocket/server/src/lang/Assert.ts @@ -11,7 +11,7 @@ export default class Assert { } } - public static isDefined(name: string, value: unknown): asserts value is undefined | null { + public static isDefined(name: string, value: unknown): asserts value is NonNullable { if (value === undefined || value === null) { throw new Error(`[${name}] must be defined instead received [${typeof value}]`); } @@ -73,6 +73,37 @@ export default class Assert { } } + public static isFunction(name: string, value: unknown): asserts value is Function { + if (typeof value !== 'function') { + throw new Error(`[${name}] must be a function, instead received [${typeof value}]`); + } + } + public static satisfiesInterface(name: string, obj: unknown, requiredProps: (keyof T)[]): asserts obj is T { + Assert.isObject(name, obj); + + for (const prop of requiredProps) { + if (!(prop in (obj as any))) { + throw new Error(`[${name}] missing required property: ${String(prop)}`); + } + } + } + + public static isArray(name: string, value: unknown): asserts value is T[] { + if (!Array.isArray(value)) { + throw new Error(`[${name}] must be an array, instead received [${typeof value}]`); + } + } + + public static isStringArray(name: string, value: unknown): asserts value is string[] { + if (!Array.isArray(value)) { + throw new Error(`[${name}] must be an array, instead received [${typeof value}]`); + } + + for (const item of value) { + Assert.isString(name, item); + } + } + public static isObject(name: string, value: unknown): asserts value is T { if (value === null || typeof value !== 'object') { throw new Error(`[${name}] must be an object, instead received [${typeof value}]`); @@ -85,6 +116,16 @@ export default class Assert { } } + public static isInstance(name: string, parentClass: new (...args: any[]) => T, object: unknown): asserts object is T { + if (object === null || object === undefined || typeof object !== 'object') { + throw new Error(`[${name}] must be an instance of [${parentClass.constructor.name}], instead received [${typeof object}]`); + } + + if (!(object instanceof parentClass)) { + throw new Error(`[${name}] must be an instance of [${parentClass.constructor.name}], instead received [${object.constructor.name}]`); + } + } + private static _isBoolean(value: unknown): value is boolean { return typeof value === 'boolean'; } diff --git a/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts b/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts new file mode 100644 index 0000000..aa22a34 --- /dev/null +++ b/Web/WebSocket/websocket/server/src/net/http/HttpServer.ts @@ -0,0 +1,68 @@ +import { Nullable } from '../../types/optional'; +import Assert from '../../lang/Assert'; +import type {Server} from 'node:http'; +import http from 'node:http'; +import {EventEmitter} from 'node:events'; +import type {ILogger} from '@techniker-me/logger'; +import {LoggerFactory} from '@techniker-me/logger'; +import IRoutes from './IRoutes'; +import {Subject} from '@techniker-me/tools'; +import express from 'express'; +import morgan from 'morgan'; + +export default class HttpServer { + private readonly _logger: ILogger = LoggerFactory.getLogger('HttpServer'); + private readonly _protocol: 'http' | 'https'; + private readonly _port: number; + private readonly _routes: IRoutes; + private readonly _viewsPath: object; + private readonly _viewParameters: string; + private readonly _resourcesPaths: string[]; + private readonly _favicon: string; + private readonly _cors: object; + private readonly _app: Subject>; + private readonly _eventEmitter: EventEmitter; + private readonly _server: Subject>; + + constructor(protocol: 'https' | 'http', port: number, routes: IRoutes, viewsPath: object, viewParameters: string, resourcesPaths: string[], favicon: string, cors: object) { + Assert.isString('protocol', protocol); + Assert.isNumber('port', port); + Assert.satisfiesInterface ('routes', routes, ['getGETRoutes', 'getPOSTRoutes', 'getPUTRoutes', 'getPATCHRoutes', 'getDELETERoutes']); + Assert.isObject('viewsPath', viewsPath); + Assert.isString('viewParameters', viewParameters); + Assert.isStringArray('resourcesPaths', resourcesPaths); + Assert.isString('favicon', favicon); + Assert.isObject('cors', cors); + + this._protocol = protocol; + this._port = port; + this._routes = routes; + this._viewsPath = viewsPath; + this._viewParameters = viewParameters; + this._resourcesPaths = resourcesPaths; + this._favicon = favicon; + this._cors = cors; + this._app = new Subject>(null); + this._eventEmitter = new EventEmitter(); + this._server = new Subject>(null); + } + + public async start() { + const app = this._app.value = express(); + + app.use(morgan('common')); + this._server.value = http.createServer(app); + + this._server.value.listen(this._port, () => { + this._logger.info(`Server is running on port ${this._port}`); + }); + } + + + public on(event: string, handler: () => void): void { + Assert.isNonEmptyString('event', event); + Assert.isFunction('handler', handler); + + this._eventEmitter.on(event, handler); + } +} diff --git a/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts b/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts new file mode 100644 index 0000000..bacd489 --- /dev/null +++ b/Web/WebSocket/websocket/server/src/net/http/IRoutes.ts @@ -0,0 +1,8 @@ + +export default interface IRoutes { + getGETRoutes(): Record (Promise | void); + getPOSTRoutes(): Record (Promise | void); + getPUTRoutes(): Record (Promise | void); + getPATCHRoutes(): Record (Promise | void); + getDELETERoutes(): Record (Promise | void); +} diff --git a/Web/WebSocket/websocket/server/src/types/optional.ts b/Web/WebSocket/websocket/server/src/types/optional.ts new file mode 100644 index 0000000..164bc2e --- /dev/null +++ b/Web/WebSocket/websocket/server/src/types/optional.ts @@ -0,0 +1,2 @@ +export type Optional = T | undefined | null; +export type Nullable = T | null; \ No newline at end of file