add websocket server to backend, client connects to it

This commit is contained in:
Vivian Lim 2024-04-04 02:42:12 -07:00
parent 98b06181bf
commit ba74f0a430
10 changed files with 307 additions and 81 deletions

2
.vscode/launch.json vendored
View File

@ -7,7 +7,7 @@
"request": "launch",
// Debug current file in VSCode
"program": "${workspaceFolder}/server/bin/www.js",
"program": "${workspaceFolder}/server/app.ts",
"cwd": "${workspaceFolder}/server/",
/*

19
common/index.ts Normal file
View File

@ -0,0 +1,19 @@
export { RoomClient } from "./room";
export const PatchMessageKind: string = "patch";
export class PatchMessage {
public static readonly Kind = "patch";
public readonly kind = PatchMessage.Kind;
public constructor() {}
}
export const ErrorMessageKind: string = "error";
export class ErrorMessage {
public static readonly Kind = "error";
public readonly kind = ErrorMessage.Kind;
public constructor(public readonly message: string) {}
}
export type TaggedMessage = PatchMessage | ErrorMessage;

46
common/room.ts Normal file
View File

@ -0,0 +1,46 @@
import {
ErrorMessage,
ErrorMessageKind,
PatchMessage,
PatchMessageKind,
TaggedMessage,
} from ".";
export class RoomClient {
public constructor(private sender: (tm: TaggedMessage) => void) {}
public handleMessage(messageStr: Buffer | ArrayBuffer | Buffer[]) {
try {
const message = JSON.parse(messageStr.toString());
switch (message.kind) {
case PatchMessageKind:
this.handlePatch(message);
case ErrorMessageKind:
this.handleError(message);
default:
console.log(`unhandled message kind: ${message.kind}`);
}
} catch (e) {
this.sendError(`Failed to handle message: ${e}`);
console.error(`Failed to handle message '${messageStr}': ${e}`);
}
}
public handleError(error: Error) {
console.log(`received error: ${error}`);
}
public handleClose() {
console.log(`received close`);
}
private handlePatch(patch: PatchMessage) {
console.log("received patch");
}
private handleErrorMessage(error: ErrorMessage) {
console.log(`received error: `);
}
private sendError(message: string) {
this.sender(new ErrorMessage(message));
}
}

View File

@ -1,22 +1,22 @@
{
"name": "graphthing-fe",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@types/cytoscape": "^3.21.0",
"typescript": "^5.4.3",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"cytoscape": "^3.28.1",
"ts-loader": "^9.5.1"
}
"name": "graphthing-fe",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"build": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@types/cytoscape": "^3.21.0",
"typescript": "^5.4.3",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"cytoscape": "^3.28.1",
"ts-loader": "^9.5.1"
}
}

View File

@ -1,7 +1,10 @@
import * as cytoscape from "cytoscape";
import { RoomClient } from "../../common";
export class ThingGraph {
private cy;
private client: RoomClient;
private socket: WebSocket;
public constructor() {
this.cy = cytoscape({
container: document.getElementById("cy"),
@ -17,5 +20,13 @@ export class ThingGraph {
},
],
});
this.socket = new WebSocket("ws://localhost:3000");
this.client = new RoomClient((tm) => {
this.socket.send(JSON.stringify(tm));
});
this.socket.addEventListener("message", (event) => {
this.client.handleMessage(event.data.toString());
});
}
}

View File

@ -41,6 +41,9 @@ importers:
express:
specifier: ^4.19.2
version: 4.19.2
graphthing-common:
specifier: workspace:../common
version: link:../common
graphthing-fe:
specifier: workspace:../frontend
version: link:../frontend
@ -50,6 +53,9 @@ importers:
morgan:
specifier: ~1.9.1
version: 1.9.1
nanoid:
specifier: ^5.0.6
version: 5.0.6
pug:
specifier: 2.0.0-beta11
version: 2.0.0-beta11
@ -59,6 +65,9 @@ importers:
tsx:
specifier: ^4.7.1
version: 4.7.1
ws:
specifier: ^8.16.0
version: 8.16.0
devDependencies:
'@types/cookie-parser':
specifier: ^1.4.7
@ -66,6 +75,9 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
'@types/express-ws':
specifier: ^3.0.4
version: 3.0.4
'@types/http-errors':
specifier: ^2.0.4
version: 2.0.4
@ -78,6 +90,9 @@ importers:
'@types/webpack-dev-middleware':
specifier: ^5.3.0
version: 5.3.0(webpack@5.91.0)
'@types/ws':
specifier: ^8.5.10
version: 8.5.10
typescript:
specifier: ^5.4.3
version: 5.4.3
@ -392,6 +407,14 @@ packages:
'@types/send': 0.17.4
dev: true
/@types/express-ws@3.0.4:
resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==}
dependencies:
'@types/express': 4.17.21
'@types/express-serve-static-core': 4.17.43
'@types/ws': 8.5.10
dev: true
/@types/express@4.17.21:
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
dependencies:
@ -462,6 +485,12 @@ packages:
- webpack-cli
dev: true
/@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies:
'@types/node': 20.12.4
dev: true
/@webassemblyjs/ast@1.12.1:
resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==}
dependencies:
@ -1508,6 +1537,12 @@ packages:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/nanoid@5.0.6:
resolution: {integrity: sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==}
engines: {node: ^18 || >=20}
hasBin: true
dev: false
/negotiator@0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
@ -2254,6 +2289,19 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/ws@8.16.0:
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
dev: false
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: false

View File

@ -1,54 +1,119 @@
import createError from 'http-errors';
import express from 'express';
import path from 'path';
import cookieParser from 'cookie-parser';
import logger from 'morgan';
import WebSocket from "ws";
import path from "path";
import cookieParser from "cookie-parser";
import logger from "morgan";
import http from "http";
import webpack from 'webpack';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpack from "webpack";
import webpackDevMiddleware from "webpack-dev-middleware";
import { RoomManager } from "./src/rooms";
let webpackConfig = require("./node_modules/graphthing-fe/webpack.config");
const webpackCompiler = webpack(webpackConfig);
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var app = express();
const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const rooms = new RoomManager(wss);
server.listen(port);
server.on("error", onError);
server.on("listening", onListening);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(logger('dev'));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, "public")));
app.use(
'/dist/',
webpackDevMiddleware(webpackCompiler, {
publicPath: webpackConfig.output.publicPath,
})
"/dist/",
webpackDevMiddleware(webpackCompiler, {
publicPath: webpackConfig.output.publicPath,
})
);
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/", indexRouter);
app.use("/users", usersRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
// render the error page
res.status(err.status || 500);
res.render("error");
});
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== "listen") {
throw error;
}
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case "EACCES":
console.error(bind + " requires elevated privileges");
process.exit(1);
break;
case "EADDRINUSE":
console.error(bind + " is already in use");
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr?.port;
console.log("Listening on " + bind);
}
module.exports = app;

View File

@ -1,35 +1,40 @@
{
"name": "graphthing",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "tsx ./bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/http-errors": "^2.0.4",
"@types/node": "^20.12.4",
"@types/webpack": "^5.28.5",
"@types/webpack-dev-middleware": "^5.3.0",
"typescript": "^5.4.3",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-middleware": "^7.2.1"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "^4.19.2",
"graphthing-fe": "workspace:../frontend",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"pug": "2.0.0-beta11",
"save-dev": "0.0.1-security",
"tsx": "^4.7.1"
}
"name": "graphthing",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "tsx ./bin/www",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "UNLICENSED",
"devDependencies": {
"@types/cookie-parser": "^1.4.7",
"@types/express": "^4.17.21",
"@types/express-ws": "^3.0.4",
"@types/http-errors": "^2.0.4",
"@types/node": "^20.12.4",
"@types/webpack": "^5.28.5",
"@types/webpack-dev-middleware": "^5.3.0",
"@types/ws": "^8.5.10",
"typescript": "^5.4.3",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-middleware": "^7.2.1"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "^4.19.2",
"graphthing-fe": "workspace:../frontend",
"graphthing-common": "workspace:../common",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"nanoid": "^5.0.6",
"pug": "2.0.0-beta11",
"save-dev": "0.0.1-security",
"tsx": "^4.7.1",
"ws": "^8.16.0"
}
}

View File

@ -1 +0,0 @@
console.log("hi");

33
server/src/rooms.ts Normal file
View File

@ -0,0 +1,33 @@
import WebSocket from "ws";
import { RoomClient } from "../../common";
export class RoomManager {
private clients: RoomClient[] = [];
public constructor(wss: WebSocket.Server) {
wss.on("connection", (ws: WebSocket) => {
const c = new RoomClient((tm) => {
ws.send(JSON.stringify(tm));
});
ws.on("message", (msg: WebSocket.RawData) => {
c.handleMessage(msg);
});
ws.on("error", (e) => {
c.handleError(e);
});
ws.on("close", () => {
c.handleClose();
const closingClientIndex = this.clients.indexOf(c);
if (closingClientIndex !== -1) {
this.clients.splice(closingClientIndex, 1);
console.log("removed a client");
} else {
console.error(
`unexpected, closed a client that isn't in the list`
);
}
});
this.clients.push(c);
console.log("added a client");
});
}
}