This commit is contained in:
Alexander Zinn
2025-08-25 13:36:17 -04:00
commit 705274c4ba
34 changed files with 1130 additions and 0 deletions

15
server/README.md Normal file
View File

@@ -0,0 +1,15 @@
# chatroom
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

9
server/eslint.config.ts Normal file
View File

@@ -0,0 +1,9 @@
import js from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import {defineConfig} from 'eslint/config';
export default defineConfig([
{files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], plugins: {js}, extends: ['js/recommended'], languageOptions: {globals: globals.node}},
tseslint.configs.recommended
]);

29
server/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "chatroom",
"version": "0.0.0",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"format": "prettier --write ./",
"prelint": "bun run format",
"lint": "eslint --max-warnings 0 ./",
"lint:fix": "eslint --fix ./",
"dev": "bun run --watch src"
},
"devDependencies": {
"@eslint/js": "9.34.0",
"@types/bun": "latest",
"@types/node": "24.3.0",
"eslint": "9.34.0",
"globals": "16.3.0",
"jiti": "2.5.1",
"prettier": "3.6.2",
"typescript": "5.9.2",
"typescript-eslint": "8.40.0"
},
"dependencies": {
"@techniker-me/logger": "0.0.15",
"@techniker-me/tools": "2025.0.16"
}
}

7
server/src/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import Servers from './net/Servers';
const server = Servers.createServerWithWebSockets();
Bun.serve(server);
console.log(`Server is running on [:${server.port}]`);

View File

@@ -0,0 +1,39 @@
import type {Server, WebSocketHandler} from 'bun';
import {LoggerFactory, type ILogger} from '@techniker-me/logger';
import WebSocketManager from './sockets/WebsocketManager';
export default class ServerWithWebSockets {
private readonly _logger: ILogger = LoggerFactory.getLogger('ServerWithWebSockets');
private readonly _webSocketManager: WebSocketManager = new WebSocketManager();
private readonly _port: number = 8080;
constructor() {
this.fetch = this.fetch.bind(this);
}
get port(): number {
return this._port;
}
get websocket(): WebSocketHandler {
return this._webSocketManager.websocketHandler;
}
public fetch(request: Request, server: Server) {
const requestUrl = new URL(request.url);
const requestCookies = new Bun.CookieMap(request.headers.get('cookie') ?? '');
if (requestUrl.pathname === '/ws') {
server.upgrade(request, {
data: {
receivedAt: new Date(),
cookies: requestCookies
}
});
return;
}
return new Response('Not found', {status: 404});
}
}

11
server/src/net/Servers.ts Normal file
View File

@@ -0,0 +1,11 @@
import ServerWithWebSockets from './ServerWithWebSockets';
export default class Servers {
public static createServerWithWebSockets() {
return new ServerWithWebSockets();
}
private constructor() {
throw new Error('Servers is a static class and cannot be instantiated');
}
}

View File

@@ -0,0 +1,28 @@
import type {ServerWebSocket} from 'bun';
import type {IDisposable} from '@techniker-me/tools';
export default class WebSocketClient implements IDisposable {
private readonly _id: string;
private readonly _socket: ServerWebSocket;
constructor(id: string, socket: ServerWebSocket) {
this._id = id;
this._socket = socket;
}
get id(): string {
return this._id;
}
get socket(): ServerWebSocket {
return this._socket;
}
public send<T>(message: T) {
this._socket.send(JSON.stringify(message));
}
public dispose(): void {
this._socket.close();
}
}

View File

@@ -0,0 +1,47 @@
import type {ServerWebSocket} from 'bun';
import WebSocketClient from './WebSocketClient';
export default class WebSocketHandler<T = unknown> {
private readonly _sockets: Map<string, WebSocketClient> = new Map();
constructor(sockets: Map<string, WebSocketClient>) {
this._sockets = sockets;
this.open = this.open.bind(this);
this.message = this.message.bind(this);
this.drain = this.drain.bind(this);
this.close = this.close.bind(this);
this.ping = this.ping.bind(this);
this.pong = this.pong.bind(this);
}
public open(ws: ServerWebSocket<T>) {
const websocketClient = new WebSocketClient('id', ws);
this._sockets.set(websocketClient.id, websocketClient);
}
public message(ws: ServerWebSocket<T>, message: string) {
console.log('WebSocket message:', message);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public drain(_ws: ServerWebSocket<T>) {
console.log('WebSocket drained');
}
public close(ws: ServerWebSocket<T>) {
console.log('WebSocket closed');
this._sockets.delete(ws.id);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public ping(ws: ServerWebSocket<T>) {
console.log('WebSocket ping');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public pong(ws: ServerWebSocket<T>) {
console.log('WebSocket pong');
}
}

View File

@@ -0,0 +1,22 @@
import {type ILogger, LoggerFactory} from '@techniker-me/logger';
import type {IDisposable} from '@techniker-me/tools';
import WebSocketClient from './WebSocketClient';
import WebSocketHandler from './WebSocketHandler';
export default class WebsocketManager implements IDisposable {
private readonly _logger: ILogger = LoggerFactory.getLogger('WebsocketManager');
private readonly _clients: Map<string, WebSocketClient> = new Map();
private readonly _websocketHandler: WebSocketHandler;
constructor() {
this._websocketHandler = new WebSocketHandler(this._clients);
}
get websocketHandler(): WebSocketHandler {
return this._websocketHandler;
}
public dispose(): void {
this._clients.forEach(client => client.dispose());
}
}

29
server/tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}