Break the server down in different files and change fetch all party lines behavior
This commit is contained in:
parent
cf70e11fe8
commit
f4258fb35f
8 changed files with 284 additions and 233 deletions
98
server/src/routes/connectionRoutes.ts
Normal file
98
server/src/routes/connectionRoutes.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { query, validationResult } from 'express-validator';
|
||||||
|
import { partyLines } from '../stores/dataStore';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { Client } from "../types/partyLine";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Route to verify if a party line exists before connecting
|
||||||
|
router.get('/joinPartyLine', [
|
||||||
|
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
||||||
|
], (req: any, res: any) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const currentPartyLine = req.query.partyLine;
|
||||||
|
const partyLine = partyLines[currentPartyLine];
|
||||||
|
if (!partyLine) {
|
||||||
|
return res.status(404).send({ status: 'Party line not found' });
|
||||||
|
}
|
||||||
|
res.status(200).send({ status: 'Connection to party line authorized' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR: Failed to join party line', error);
|
||||||
|
res.status(500).send({ status: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to connect to party line and receive rumors
|
||||||
|
router.get('/connectPartyLine', [
|
||||||
|
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
||||||
|
], (req: any, res: any) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const connectedPartyLine = req.query.partyLine;
|
||||||
|
const partyLine = partyLines[connectedPartyLine];
|
||||||
|
if (!partyLine) {
|
||||||
|
return res.status(404).send({ status: 'Party line not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers for Server-Sent Events (SSE)
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
|
||||||
|
const clientId = `${uuidv4()}`;
|
||||||
|
const client: Client = { clientId, response: res, ipAddress: res.socket.remoteAddress };
|
||||||
|
|
||||||
|
// Add client to the party line
|
||||||
|
partyLine.clients.push(client);
|
||||||
|
partyLine.lastActivity = Date.now();
|
||||||
|
|
||||||
|
console.log(`JOIN: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
||||||
|
|
||||||
|
// Send the last event to the client
|
||||||
|
res.write(`data: ${partyLine.lastEvent}\n\n`);
|
||||||
|
|
||||||
|
// Keep-alive mechanism to keep the connection open
|
||||||
|
const keepAliveId = setInterval(() => {
|
||||||
|
res.write(': keep-alive\n\n');
|
||||||
|
}, 15000);
|
||||||
|
|
||||||
|
// Function to remove the client from the party line
|
||||||
|
const removeClient = () => {
|
||||||
|
clearInterval(keepAliveId);
|
||||||
|
const index = partyLine.clients.findIndex((client: any) => client.clientId === clientId);
|
||||||
|
if (index !== -1) {
|
||||||
|
partyLine.clients.splice(index, 1);
|
||||||
|
console.log(`REMOVE: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
||||||
|
} else {
|
||||||
|
console.log(`CLIENT_NOT_FOUND: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
res.write('event: close\ndata: finished\n\n');
|
||||||
|
res.end();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`ERROR: Error sending close event for client ${clientId}:`, err);
|
||||||
|
}
|
||||||
|
console.log(`DISCONNECT: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle client disconnection
|
||||||
|
req.on('close', removeClient);
|
||||||
|
req.on('error', (err: any) => {
|
||||||
|
console.error(`ERROR: Error in client connection in party line ${connectedPartyLine}:`, err);
|
||||||
|
removeClient();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR: Failed to connect to party line', error);
|
||||||
|
res.status(500).send({ status: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
115
server/src/routes/managementRoutes.ts
Normal file
115
server/src/routes/managementRoutes.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { MAX_PARTY_LINES, INITIAL_RUMOR } from '../stores/configStore';
|
||||||
|
import { partyLines } from '../stores/dataStore';
|
||||||
|
import { broadcast } from '../services/broadcastService';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Route to get all party lines
|
||||||
|
router.get('/partyLines', (_req: any, res: any) => {
|
||||||
|
const allPartyLines = Object.keys(partyLines).map(key => ({
|
||||||
|
name: key,
|
||||||
|
...partyLines[key],
|
||||||
|
clients: partyLines[key].clients.map(({ response, ...client }) => client)
|
||||||
|
}));
|
||||||
|
res.status(200).send(allPartyLines);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to create a new party line
|
||||||
|
router.post('/createPartyLine', [
|
||||||
|
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
||||||
|
], (req: any, res: any) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { partyLine: currentPartyLine } = req.body;
|
||||||
|
if (Object.keys(partyLines).length >= MAX_PARTY_LINES) {
|
||||||
|
return res.status(400).send({ status: 'Maximum number of party lines reached' });
|
||||||
|
}
|
||||||
|
if (partyLines[currentPartyLine]) {
|
||||||
|
return res.status(400).send({ status: 'Party line already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new party line
|
||||||
|
partyLines[currentPartyLine] = {
|
||||||
|
clients: [],
|
||||||
|
lastEvent: INITIAL_RUMOR,
|
||||||
|
lastActivity: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`CREATE: { partyLine: ${currentPartyLine} }`);
|
||||||
|
res.status(200).send({ status: 'Party line created', currentPartyLine });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR: Failed to create party line', error);
|
||||||
|
res.status(500).send({ status: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to delete a party line
|
||||||
|
router.delete('/deletePartyLine', [
|
||||||
|
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
||||||
|
], (req: any, res: any) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { partyLine: currentPartyLine } = req.body;
|
||||||
|
const partyLine = partyLines[currentPartyLine];
|
||||||
|
if (!partyLine) {
|
||||||
|
return res.status(404).send({ status: 'Party line not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast deletion message to all clients
|
||||||
|
broadcast(currentPartyLine, 'PARTY_LINE_DELETED');
|
||||||
|
|
||||||
|
// Disconnect all clients
|
||||||
|
partyLine.clients.forEach((client: any) => {
|
||||||
|
console.log(`DISCONNECTING: { partyLine: ${currentPartyLine}, clientId: ${client.clientId} }`);
|
||||||
|
client.response.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete the party line
|
||||||
|
delete partyLines[currentPartyLine];
|
||||||
|
console.log(`DELETE: { partyLine: ${currentPartyLine} }`);
|
||||||
|
res.status(200).send({ status: 'Party line deleted', currentPartyLine });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR: Failed to delete party line', error);
|
||||||
|
res.status(500).send({ status: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route to send an event to a party line
|
||||||
|
router.post('/rumor', [
|
||||||
|
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name'),
|
||||||
|
body('rumor').isString().trim().escape().notEmpty().withMessage('Invalid rumor')
|
||||||
|
], (req: any, res: any) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(400).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { partyLine: connectedPartyLine, rumor } = req.body;
|
||||||
|
const partyLine = partyLines[connectedPartyLine];
|
||||||
|
if (!partyLine) {
|
||||||
|
return res.status(404).send({ status: 'Party line not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`RECEIVE: { partyLine: ${connectedPartyLine}, rumor: ${rumor} }`);
|
||||||
|
|
||||||
|
// Update the last event and broadcast it to all clients
|
||||||
|
partyLine.lastEvent = rumor;
|
||||||
|
partyLine.clients.forEach((client: any) => {
|
||||||
|
client.response.write(`data: ${rumor}\n\n`);
|
||||||
|
});
|
||||||
|
res.status(200).send({ status: 'Rumor broadcast' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ERROR: Failed to send event', error);
|
||||||
|
res.status(500).send({ status: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
11
server/src/routes/navigationRoutes.ts
Normal file
11
server/src/routes/navigationRoutes.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Navigate to the admin route in the client
|
||||||
|
router.get('/admin', (_req: any, res: any) => {
|
||||||
|
res.sendFile(path.resolve(__dirname, '../../static', 'index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -1,21 +1,21 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { body, query, validationResult } from 'express-validator';
|
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { partyLines } from './stores/dataStore';
|
||||||
dotenv.config();
|
import { ENVIRONMENT, PORT, CLIENT_URL, DOCKER } from './stores/configStore';
|
||||||
|
import { broadcast } from './services/broadcastService';
|
||||||
|
import connectionRoutes from './routes/connectionRoutes';
|
||||||
|
import managementRoutes from './routes/managementRoutes';
|
||||||
|
import navigationRoutes from './routes/navigationRoutes';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = Number(process.env.PORT);
|
|
||||||
|
|
||||||
// CORS configuration options
|
// CORS configuration options
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: process.env.CLIENT_URL,
|
origin: CLIENT_URL,
|
||||||
optionsSuccessStatus: 200,
|
optionsSuccessStatus: 200,
|
||||||
methods: ['GET', 'POST', 'DELETE'],
|
methods: ['GET', 'POST', 'DELETE'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
|
@ -38,7 +38,7 @@ app.use(limiter);
|
||||||
app.use('/', express.static(path.join(__dirname, '../static')));
|
app.use('/', express.static(path.join(__dirname, '../static')));
|
||||||
|
|
||||||
// Force HTTPS redirection and CSP in production
|
// Force HTTPS redirection and CSP in production
|
||||||
if (process.env.ENVIRONMENT === 'production') {
|
if (ENVIRONMENT === 'production') {
|
||||||
app.use((req: any, res: any, next: any) => {
|
app.use((req: any, res: any, next: any) => {
|
||||||
if (req.headers['x-forwarded-proto'] !== 'https') {
|
if (req.headers['x-forwarded-proto'] !== 'https') {
|
||||||
return res.redirect(`https://${req.headers.host}${req.url}`);
|
return res.redirect(`https://${req.headers.host}${req.url}`);
|
||||||
|
@ -57,7 +57,7 @@ if (process.env.ENVIRONMENT === 'production') {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Disable https preference for non-prod
|
// Disable CSP for non-prod
|
||||||
app.use(
|
app.use(
|
||||||
helmet({
|
helmet({
|
||||||
contentSecurityPolicy: false,
|
contentSecurityPolicy: false,
|
||||||
|
@ -65,131 +65,17 @@ if (process.env.ENVIRONMENT === 'production') {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the admin route in the client
|
// Routes
|
||||||
app.get('/admin', (_req: any, res: any) => {
|
app.use(navigationRoutes);
|
||||||
res.sendFile(path.resolve(__dirname, '../static', 'index.html'));
|
app.use(managementRoutes);
|
||||||
});
|
app.use(connectionRoutes);
|
||||||
|
|
||||||
// Maximum number of party lines allowed
|
// Periodically resend the last rumor and clean up inactive party lines
|
||||||
const MAX_PARTY_LINES = Number(process.env.MAX_PARTY_LINES);
|
|
||||||
|
|
||||||
// In-memory storage for party lines
|
|
||||||
const partyLines = {};
|
|
||||||
|
|
||||||
// Function to broadcast a rumor to all clients at a party line
|
|
||||||
const broadcast = (currentPartyLine: string, rumor: string) => {
|
|
||||||
const partyLine = partyLines[currentPartyLine];
|
|
||||||
if (partyLine) {
|
|
||||||
console.log(`BROADCAST: { partyLine: ${currentPartyLine}, rumor: ${rumor} }`);
|
|
||||||
partyLine.clients.forEach((client: any) => {
|
|
||||||
if (client.response && typeof client.response.write === 'function') {
|
|
||||||
try {
|
|
||||||
client.response.write(`data: ${rumor}\n\n`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`ERROR: Failed to write to client ${client.clientId}`, err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error(`ERROR: Response object is invalid for client ${client.clientId}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Route to verify if a party line exists before connecting
|
|
||||||
app.get('/joinPartyLine', [
|
|
||||||
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
|
||||||
], (req: any, res: any) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const currentPartyLine = req.query.partyLine;
|
|
||||||
const partyLine = partyLines[currentPartyLine];
|
|
||||||
if (!partyLine) {
|
|
||||||
return res.status(404).send({ status: 'Party line not found' });
|
|
||||||
}
|
|
||||||
res.status(200).send({ status: 'Connection to party line authorized' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR: Failed to join party line', error);
|
|
||||||
res.status(500).send({ status: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route to connect to party line and receive rumors
|
|
||||||
app.get('/connectPartyLine', [
|
|
||||||
query('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
|
||||||
], (req: any, res: any) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const connectedPartyLine = req.query.partyLine;
|
|
||||||
const partyLine = partyLines[connectedPartyLine];
|
|
||||||
if (!partyLine) {
|
|
||||||
return res.status(404).send({ status: 'Party line not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers for Server-Sent Events (SSE)
|
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
|
||||||
res.setHeader('Connection', 'keep-alive');
|
|
||||||
|
|
||||||
const clientId = `${uuidv4()}`;
|
|
||||||
const client = { clientId, response: res };
|
|
||||||
|
|
||||||
// Add client to the party line
|
|
||||||
partyLine.clients.push(client);
|
|
||||||
partyLine.lastActivity = Date.now();
|
|
||||||
|
|
||||||
console.log(`JOIN: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
|
||||||
|
|
||||||
// Send the last event to the client
|
|
||||||
res.write(`data: ${partyLine.lastEvent}\n\n`);
|
|
||||||
|
|
||||||
// Keep-alive mechanism to keep the connection open
|
|
||||||
const keepAliveId = setInterval(() => {
|
|
||||||
res.write(': keep-alive\n\n');
|
|
||||||
}, 15000);
|
|
||||||
|
|
||||||
// Function to remove the client from the party line
|
|
||||||
const removeClient = () => {
|
|
||||||
clearInterval(keepAliveId);
|
|
||||||
const index = partyLine.clients.findIndex((client: any) => client.clientId === clientId);
|
|
||||||
if (index !== -1) {
|
|
||||||
partyLine.clients.splice(index, 1);
|
|
||||||
console.log(`REMOVE: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
|
||||||
} else {
|
|
||||||
console.log(`CLIENT_NOT_FOUND: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
res.write('event: close\ndata: finished\n\n');
|
|
||||||
res.end();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`ERROR: Error sending close event for client ${clientId}:`, err);
|
|
||||||
}
|
|
||||||
console.log(`DISCONNECT: { partyLine: ${connectedPartyLine}, clientId: ${clientId} }`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle client disconnection
|
|
||||||
req.on('close', removeClient);
|
|
||||||
req.on('error', (err: any) => {
|
|
||||||
console.error(`ERROR: Error in client connection in party line ${connectedPartyLine}:`, err);
|
|
||||||
removeClient();
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR: Failed to connect to party line', error);
|
|
||||||
res.status(500).send({ status: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Periodically resend the last event and clean up inactive party lines
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
Object.keys(partyLines).forEach((currentPartyLine) => {
|
Object.keys(partyLines).forEach((currentPartyLine) => {
|
||||||
const partyLine = partyLines[currentPartyLine];
|
const partyLine = partyLines[currentPartyLine];
|
||||||
if (partyLine.lastEvent) {
|
if (partyLine.lastEvent && partyLine.clients.length > 0) {
|
||||||
broadcast(currentPartyLine, partyLine.lastEvent);
|
broadcast(currentPartyLine, partyLine.lastEvent);
|
||||||
}
|
}
|
||||||
if (now - partyLine.lastActivity > 3600000) { // 1 hour in milliseconds
|
if (now - partyLine.lastActivity > 3600000) { // 1 hour in milliseconds
|
||||||
|
@ -199,113 +85,11 @@ setInterval(() => {
|
||||||
});
|
});
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
// Route to create a new party line
|
|
||||||
app.post('/createPartyLine', [
|
|
||||||
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
|
||||||
], (req: any, res: any) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { partyLine: currentPartyLine } = req.body;
|
|
||||||
if (Object.keys(partyLines).length >= MAX_PARTY_LINES) {
|
|
||||||
return res.status(400).send({ status: 'Maximum number of party lines reached' });
|
|
||||||
}
|
|
||||||
if (partyLines[currentPartyLine]) {
|
|
||||||
return res.status(400).send({ status: 'Party line already exists' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new party line
|
|
||||||
partyLines[currentPartyLine] = {
|
|
||||||
clients: [],
|
|
||||||
lastEvent: process.env.INITIAL_RUMOR,
|
|
||||||
lastActivity: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`CREATE: { partyLine: ${currentPartyLine} }`);
|
|
||||||
res.status(200).send({ status: 'Party line created', currentPartyLine });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR: Failed to create party line', error);
|
|
||||||
res.status(500).send({ status: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route to send an event to a party line
|
|
||||||
app.post('/rumor', [
|
|
||||||
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name'),
|
|
||||||
body('rumor').isString().trim().escape().notEmpty().withMessage('Invalid rumor')
|
|
||||||
], (req: any, res: any) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { partyLine: connectedPartyLine, rumor } = req.body;
|
|
||||||
const partyLine = partyLines[connectedPartyLine];
|
|
||||||
if (!partyLine) {
|
|
||||||
return res.status(404).send({ status: 'Party line not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`RECEIVE: { partyLine: ${connectedPartyLine}, rumor: ${rumor} }`);
|
|
||||||
|
|
||||||
// Update the last event and broadcast it to all clients
|
|
||||||
partyLine.lastEvent = rumor;
|
|
||||||
partyLine.clients.forEach((client: any) => {
|
|
||||||
client.response.write(`data: ${rumor}\n\n`);
|
|
||||||
});
|
|
||||||
res.status(200).send({ status: 'Rumor broadcast' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR: Failed to send event', error);
|
|
||||||
res.status(500).send({ status: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route to get all party lines
|
|
||||||
app.get('/partyLines', (_req: any, res: any) => {
|
|
||||||
const allPartyLines = Object.keys(partyLines);
|
|
||||||
res.status(200).send({ allPartyLines });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route to delete a party line
|
|
||||||
app.delete('/deletePartyLine', [
|
|
||||||
body('partyLine').isString().trim().escape().notEmpty().withMessage('Invalid party line name')
|
|
||||||
], (req: any, res: any) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(400).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { partyLine: currentPartyLine } = req.body;
|
|
||||||
const partyLine = partyLines[currentPartyLine];
|
|
||||||
if (!partyLine) {
|
|
||||||
return res.status(404).send({ status: 'Party line not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast deletion message to all clients
|
|
||||||
broadcast(currentPartyLine, 'PARTY_LINE_DELETED');
|
|
||||||
|
|
||||||
// Disconnect all clients
|
|
||||||
partyLine.clients.forEach((client: any) => {
|
|
||||||
console.log(`DISCONNECTING: { partyLine: ${currentPartyLine}, clientId: ${client.clientId} }`);
|
|
||||||
client.response.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete the party line
|
|
||||||
delete partyLines[currentPartyLine];
|
|
||||||
console.log(`DELETE: { partyLine: ${currentPartyLine} }`);
|
|
||||||
res.status(200).send({ status: 'Party line deleted', currentPartyLine });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('ERROR: Failed to delete party line', error);
|
|
||||||
res.status(500).send({ status: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Rumor Party Line`);
|
console.log(`Rumor Party Line`);
|
||||||
console.log(`Internally running on http://localhost:${PORT} in ${process.env.ENVIRONMENT} mode.`);
|
console.log(`Internally running on http://localhost:${PORT} in ${ENVIRONMENT} mode.`);
|
||||||
if (process.env.DOCKER) {
|
if (DOCKER) {
|
||||||
console.log(`You're running on Docker!\n↳ Check your external port in the compose file to avoid confusion!`);
|
console.log(`You're running on Docker!\n↳ Check your external port in the compose file to avoid confusion!`);
|
||||||
}
|
}
|
||||||
});
|
});
|
20
server/src/services/broadcastService.ts
Normal file
20
server/src/services/broadcastService.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { partyLines } from '../stores/dataStore';
|
||||||
|
|
||||||
|
// Function to broadcast a rumor to all clients at a party line
|
||||||
|
export const broadcast = (currentPartyLine: string, rumor: string) => {
|
||||||
|
const partyLine = partyLines[currentPartyLine];
|
||||||
|
if (partyLine) {
|
||||||
|
console.log(`BROADCAST: { partyLine: ${currentPartyLine}, rumor: ${rumor} }`);
|
||||||
|
partyLine.clients.forEach((client: any) => {
|
||||||
|
if (client.response && typeof client.response.write === 'function') {
|
||||||
|
try {
|
||||||
|
client.response.write(`data: ${rumor}\n\n`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`ERROR: Failed to write to client ${client.clientId}`, err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`ERROR: Response object is invalid for client ${client.clientId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
9
server/src/stores/configStore.ts
Normal file
9
server/src/stores/configStore.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const ENVIRONMENT = process.env.ENVIRONMENT || 'development';
|
||||||
|
export const PORT = Number(process.env.PORT) || 3000;
|
||||||
|
export const CLIENT_URL = process.env.CLIENT_URL || 'http://localhost:3000';
|
||||||
|
export const MAX_PARTY_LINES = Number(process.env.MAX_PARTY_LINES) || 1;
|
||||||
|
export const INITIAL_RUMOR = process.env.INITIAL_RUMOR || 'Lorem Ipsum.';
|
||||||
|
export const DOCKER = Boolean(process.env.DOCKER);
|
3
server/src/stores/dataStore.ts
Normal file
3
server/src/stores/dataStore.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { PartyLine } from "../types/partyLine";
|
||||||
|
|
||||||
|
export const partyLines: PartyLine[] = [];
|
11
server/src/types/partyLine.ts
Normal file
11
server/src/types/partyLine.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export type PartyLine = {
|
||||||
|
clients: Client[];
|
||||||
|
lastActivity: number;
|
||||||
|
lastEvent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Client = {
|
||||||
|
clientId: string;
|
||||||
|
response: any;
|
||||||
|
ipAddress: string;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue