mirror of
https://github.com/jcreek/EstimationPoker.git
synced 2026-07-13 02:53:46 +00:00
Merge pull request #37 from jcreek/35-replace-custom-websockets-with-partykit
35 replace custom websockets with partykit
This commit is contained in:
@@ -4,11 +4,18 @@ A web app to allow scrum teams to easily perform estimations quickly and without
|
|||||||
There are two components to this:
|
There are two components to this:
|
||||||
|
|
||||||
1. Frontend, built using SvelteKit
|
1. Frontend, built using SvelteKit
|
||||||
2. WebSockets server, built using Node.js
|
2. Realtime server, built using PartyKit (WebSocket-compatible)
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
For full functionality, ensure there is a copy of the WebSockets server running.
|
For full functionality, ensure there is a PartyKit server running.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
The frontend uses PartyKit defaults unless overridden:
|
||||||
|
|
||||||
|
- `PUBLIC_PARTYKIT_HOST`: PartyKit host (defaults to `localhost:1999` in dev, `websocket.jcreek.co.uk` in production)
|
||||||
|
- `PUBLIC_PARTYKIT_PARTY`: Party name (defaults to `main`)
|
||||||
|
|
||||||
### Developing
|
### Developing
|
||||||
|
|
||||||
@@ -44,12 +51,14 @@ This favicon was [generated](https://favicon.io/favicon-generator/) using the fo
|
|||||||
|
|
||||||
### Developing
|
### Developing
|
||||||
|
|
||||||
Once you've installed dependencies with `npm install`, start a development server:
|
Install dependencies in `src/server`, then run the PartyKit dev server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
cd src/server
|
||||||
|
npm install
|
||||||
|
npx partykit dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploying
|
### Deploying
|
||||||
|
|
||||||
See the README in `src/server` for details.
|
See the README in `src/server` for PartyKit deployment details.
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fireworks-js/svelte": "^2.10.7",
|
"@fireworks-js/svelte": "^2.10.7",
|
||||||
"@joeattardi/emoji-button": "^4.6.4",
|
"@joeattardi/emoji-button": "^4.6.4",
|
||||||
|
"partysocket": "1.0.2",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-14
@@ -1,10 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { connectToWebSocket, sendMessage, generateId } from './estimation/estimation.js';
|
import { connectToWebSocket, sendMessage, generateId } from './estimation/estimation.js';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
let roomId = '';
|
let roomId = '';
|
||||||
let socket;
|
|
||||||
const cardSets = [
|
const cardSets = [
|
||||||
{ name: 'Fibonacci', values: [1, 2, 3, 5, 8, 13, 21, '?'], example: '(1, 2, 3, 5)' },
|
{ name: 'Fibonacci', values: [1, 2, 3, 5, 8, 13, 21, '?'], example: '(1, 2, 3, 5)' },
|
||||||
{
|
{
|
||||||
@@ -18,9 +16,39 @@
|
|||||||
|
|
||||||
let selectedCardSet = cardSets[0];
|
let selectedCardSet = cardSets[0];
|
||||||
|
|
||||||
function startARoom() {
|
function waitForSocketOpen(socket) {
|
||||||
|
if (socket.readyState === WebSocket.OPEN) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const handleOpen = () => {
|
||||||
|
socket.removeEventListener('open', handleOpen);
|
||||||
|
socket.removeEventListener('error', handleError);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const handleError = (event) => {
|
||||||
|
socket.removeEventListener('open', handleOpen);
|
||||||
|
socket.removeEventListener('error', handleError);
|
||||||
|
reject(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.addEventListener('open', handleOpen);
|
||||||
|
socket.addEventListener('error', handleError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startARoom() {
|
||||||
const roomId = generateId();
|
const roomId = generateId();
|
||||||
sendMessage(socket, { roomId: roomId, type: 'create-room', cardSetName: selectedCardSet.name });
|
const socket = connectToWebSocket(roomId, onMessageReceived);
|
||||||
|
try {
|
||||||
|
await waitForSocketOpen(socket);
|
||||||
|
sendMessage(socket, { roomId: roomId, type: 'create-room', cardSetName: selectedCardSet.name });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to connect to PartyKit:', error);
|
||||||
|
alert('Unable to create the room right now. Please try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
goto(`/estimation/${roomId}`);
|
goto(`/estimation/${roomId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,16 +61,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onMessageReceived(message) {}
|
function onMessageReceived(message) {}
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
socket = connectToWebSocket(null, onMessageReceived);
|
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(JSON.stringify({ type: 'ping' }));
|
|
||||||
}
|
|
||||||
}, 45000); // send a ping every 45 seconds
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import PartySocket from 'partysocket';
|
||||||
|
|
||||||
function generateId() {
|
function generateId() {
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectToWebSocket(roomId, onMessageReceived) {
|
const partyHost = import.meta.env.PUBLIC_PARTYKIT_HOST;
|
||||||
let socket;
|
const partyName = import.meta.env.PUBLIC_PARTYKIT_PARTY || 'main';
|
||||||
const isDevelopment = import.meta.env.DEV;
|
|
||||||
if (isDevelopment) {
|
function getPartyKitHost() {
|
||||||
socket = new WebSocket(`ws://localhost:8080`);
|
if (partyHost) {
|
||||||
} else {
|
return partyHost;
|
||||||
socket = new WebSocket(`wss://websocket.jcreek.co.uk`);
|
|
||||||
}
|
}
|
||||||
|
return import.meta.env.DEV ? 'localhost:1999' : 'websocket.jcreek.co.uk';
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectToWebSocket(roomId, onMessageReceived) {
|
||||||
|
const host = getPartyKitHost();
|
||||||
|
const socket = new PartySocket({
|
||||||
|
host,
|
||||||
|
room: roomId ?? 'lobby',
|
||||||
|
party: partyName,
|
||||||
|
...(import.meta.env.DEV ? { protocol: 'ws' } : {})
|
||||||
|
});
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
console.log(`Connected to WebSocket server from roomId: ${roomId}`);
|
console.log(`Connected to WebSocket server from roomId: ${roomId}`);
|
||||||
|
|||||||
+19
-20
@@ -1,5 +1,5 @@
|
|||||||
import { WebSocket } from 'ws';
|
import type * as Party from 'partykit/server';
|
||||||
import { User } from './User';
|
import { User } from './User.js';
|
||||||
|
|
||||||
class CardSet {
|
class CardSet {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,7 +27,7 @@ export const cardSets: Array<CardSet> = [
|
|||||||
export class Room {
|
export class Room {
|
||||||
id: string;
|
id: string;
|
||||||
cardSet: CardSet;
|
cardSet: CardSet;
|
||||||
users: Map<WebSocket, User>;
|
users: Map<Party.Connection, User>;
|
||||||
|
|
||||||
constructor(id: string, cardSetName: string) {
|
constructor(id: string, cardSetName: string) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -37,49 +37,48 @@ export class Room {
|
|||||||
} else {
|
} else {
|
||||||
this.cardSet = cardSets[0];
|
this.cardSet = cardSets[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
this.users = new Map();
|
this.users = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
addUser(ws: WebSocket, user: User) {
|
addUser(connection: Party.Connection, user: User) {
|
||||||
this.users.set(ws, user);
|
this.users.set(connection, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUser(ws: WebSocket) {
|
removeUser(connection: Party.Connection) {
|
||||||
this.users.delete(ws);
|
this.users.delete(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsers() {
|
getUsers() {
|
||||||
return Array.from(this.users.values());
|
return Array.from(this.users.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserByWebSocket(ws: WebSocket) {
|
getUserByConnection(connection: Party.Connection): User | undefined {
|
||||||
return this.users.get(ws);
|
return this.users.get(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
getWebSocketByUser(user: User): WebSocket | null {
|
getConnectionByUser(user: User): Party.Connection | null {
|
||||||
for (const [ws, u] of this.users) {
|
for (const [connection, u] of this.users) {
|
||||||
if (u === user) {
|
if (u === user) {
|
||||||
return ws;
|
return connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getWebSocketByUserId(userId: String): WebSocket | null {
|
getConnectionByUserId(userId: string): Party.Connection | null {
|
||||||
for (const [ws, u] of this.users) {
|
for (const [connection, u] of this.users) {
|
||||||
if (u.userId === userId) {
|
if (u.userId === userId) {
|
||||||
return ws;
|
return connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast(message: any) {
|
broadcast(message: any) {
|
||||||
this.users.forEach((user, client) => {
|
const messageString = JSON.stringify(message);
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
this.users.forEach((user, connection) => {
|
||||||
client.send(JSON.stringify(message));
|
connection.send(messageString);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"tsc-watch": "^5.0.3",
|
"tsc-watch": "^5.0.3",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "^4.6.3",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1",
|
||||||
|
"partysocket": "1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"partykit": "0.0.111"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type * as Party from "partykit/server";
|
||||||
|
|
||||||
|
export default class Server implements Party.Server {
|
||||||
|
constructor(readonly room: Party.Room) {}
|
||||||
|
|
||||||
|
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
|
||||||
|
// A websocket just connected!
|
||||||
|
console.log(
|
||||||
|
`Connected:
|
||||||
|
id: ${conn.id}
|
||||||
|
room: ${this.room.id}
|
||||||
|
url: ${new URL(ctx.request.url).pathname}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// let's send a message to the connection
|
||||||
|
conn.send("hello from server");
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(message: string, sender: Party.Connection) {
|
||||||
|
// let's log the message
|
||||||
|
console.log(`connection ${sender.id} sent message: ${message}`);
|
||||||
|
// as well as broadcast it to all the other connections in the room...
|
||||||
|
this.room.broadcast(
|
||||||
|
`${sender.id}: ${message}`,
|
||||||
|
// ...except for the connection it came from
|
||||||
|
[sender.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Server satisfies Party.Worker;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://www.partykit.io/schema.json",
|
||||||
|
"name": "websocket-server-party",
|
||||||
|
"main": "websocket.ts",
|
||||||
|
"compatibilityDate": "2024-11-07"
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ES2022",
|
"module": "nodenext",
|
||||||
"esModuleInterop": true,
|
"moduleResolution": "nodenext",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"skipLibCheck": true
|
"strict": true,
|
||||||
}
|
"skipLibCheck": true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+199
-193
@@ -1,233 +1,239 @@
|
|||||||
import express from 'express';
|
import type * as Party from 'partykit/server';
|
||||||
import { WebSocketServer, WebSocket } from 'ws';
|
import { Room } from './classes/Room.js';
|
||||||
import { Room, cardSets } from './classes/Room.js';
|
|
||||||
import { User } from './classes/User.js';
|
import { User } from './classes/User.js';
|
||||||
import { JoinRoomMessage } from './classes/messages/JoinRoomMessage.js';
|
import { JoinRoomMessage } from './classes/messages/JoinRoomMessage.js';
|
||||||
import { ChangeEstimateMessage } from './classes/messages/ChangeEstimateMessage.js';
|
|
||||||
import { SelectEstimateMessage } from './classes/messages/SelectEstimateMessage.js';
|
import { SelectEstimateMessage } from './classes/messages/SelectEstimateMessage.js';
|
||||||
|
|
||||||
const app = express();
|
type StoredRoom = {
|
||||||
const port = process.env.PORT || 8080;
|
id: string;
|
||||||
|
cardSetName: string;
|
||||||
|
};
|
||||||
|
|
||||||
function onSocketPreError(e: Error) {
|
export default class EstimationParty implements Party.Server {
|
||||||
console.log(e);
|
room: Room | null = null;
|
||||||
}
|
private nudgeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
function onSocketPostError(e: Error) {
|
constructor(public party: Party.Room) {}
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupEstimates(users: Array<User>) {
|
async onStart() {
|
||||||
let estimateGroups: { [key: number | string]: string[] } = {};
|
const savedRoom = (await this.party.storage.get('room')) as StoredRoom | undefined;
|
||||||
users.forEach((user) => {
|
if (savedRoom?.cardSetName) {
|
||||||
if (user.estimate !== null) {
|
this.room = new Room(savedRoom.id ?? this.party.id, savedRoom.cardSetName);
|
||||||
if (estimateGroups[user.estimate]) {
|
|
||||||
estimateGroups[user.estimate].push(user.name);
|
|
||||||
} else {
|
|
||||||
estimateGroups[user.estimate] = [user.name];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return estimateGroups;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Attempting to run server on port ${port}`);
|
|
||||||
|
|
||||||
const server = app.listen(port, () => {
|
|
||||||
console.log(`Listening on port ${port}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const wss = new WebSocketServer({ noServer: true });
|
|
||||||
const rooms = new Set<Room>();
|
|
||||||
|
|
||||||
server.on('upgrade', (req, socket, head) => {
|
|
||||||
socket.on('error', onSocketPreError);
|
|
||||||
|
|
||||||
// perform auth
|
|
||||||
if (!!req.headers['BadAuth']) {
|
|
||||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wss.handleUpgrade(req, socket, head, (ws) => {
|
async onConnect(_connection: Party.Connection) {}
|
||||||
socket.removeListener('error', onSocketPreError);
|
|
||||||
wss.emit('connection', ws, req);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
wss.on('connection', (ws: WebSocket, req) => {
|
async onClose(connection: Party.Connection) {
|
||||||
let room: Room | null = null;
|
if (!this.room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ws.on('error', onSocketPostError);
|
const user = this.room.getUserByConnection(connection);
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function createRoom(roomId: string, cardSetName: string) {
|
this.room.removeUser(connection);
|
||||||
room = new Room(roomId, cardSetName);
|
await this.updateRoomState();
|
||||||
rooms.add(room);
|
this.party.broadcast(JSON.stringify({ type: 'user-left', userId: user.userId }));
|
||||||
|
|
||||||
|
if (this.room.getUsers().length === 0) {
|
||||||
|
this.clearNudgeTimeout();
|
||||||
|
await this.party.storage.delete('room');
|
||||||
|
this.room = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.on('message', (message) => {
|
async onMessage(message: string | ArrayBuffer | ArrayBufferView, connection: Party.Connection) {
|
||||||
const data = JSON.parse(message.toString());
|
if (typeof message !== 'string') {
|
||||||
|
console.log('Unsupported message payload type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
|
||||||
if (data.type === 'create-room') {
|
switch (data.type) {
|
||||||
const roomId = data.roomId;
|
case 'create-room':
|
||||||
const cardSetName = data.cardSetName;
|
this.room = new Room(this.party.id, data.cardSetName ?? '');
|
||||||
createRoom(roomId, cardSetName);
|
await this.updateRoomState();
|
||||||
} else if (data.type === 'join-room') {
|
break;
|
||||||
const joinRoomMessage: JoinRoomMessage = data as JoinRoomMessage;
|
|
||||||
const { roomId, userId, name } = joinRoomMessage;
|
|
||||||
room = getRoomById(roomId);
|
|
||||||
|
|
||||||
// Create room if it doesn't already exist
|
case 'join-room':
|
||||||
if (!room) {
|
{
|
||||||
createRoom(roomId, data.cardSetName);
|
this.room = await this.getOrCreateRoom(data.cardSetName);
|
||||||
}
|
if (!this.room) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const joinRoomMessage: JoinRoomMessage = data as JoinRoomMessage;
|
||||||
|
const { userId, name } = joinRoomMessage;
|
||||||
|
const user = new User(userId, name);
|
||||||
|
this.room.addUser(connection, user);
|
||||||
|
await this.updateRoomState();
|
||||||
|
this.party.broadcast(
|
||||||
|
JSON.stringify({ type: 'user-joined', message: `${name} has joined the room` })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Create the user in the room
|
case 'select-estimate':
|
||||||
const user = new User(userId, name);
|
{
|
||||||
room!.addUser(ws, user);
|
this.room = await this.getOrCreateRoom();
|
||||||
|
if (!this.room) {
|
||||||
broadcastToRoom(roomId, { type: 'user-joined', message: `${name} has joined the room` });
|
break;
|
||||||
} else if (data.type === 'select-estimate') {
|
}
|
||||||
const selectEstimateMessage: SelectEstimateMessage = data as SelectEstimateMessage;
|
const selectEstimateMessage: SelectEstimateMessage = data as SelectEstimateMessage;
|
||||||
const { userId, estimate } = selectEstimateMessage;
|
const { userId, estimate } = selectEstimateMessage;
|
||||||
|
const user = this.room.getUserByConnection(connection);
|
||||||
if (room) {
|
if (!user) {
|
||||||
const user = room.getUserByWebSocket(ws);
|
break;
|
||||||
if (user) {
|
}
|
||||||
user.estimate = estimate;
|
user.estimate = estimate;
|
||||||
broadcastToRoom(room.id, {
|
await this.updateRoomState();
|
||||||
type: 'estimate-selected',
|
this.party.broadcast(
|
||||||
userId,
|
JSON.stringify({
|
||||||
name: user.name,
|
type: 'estimate-selected',
|
||||||
estimate
|
userId,
|
||||||
});
|
name: user.name,
|
||||||
|
estimate
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const users = room.getUsers();
|
const users = this.room.getUsers();
|
||||||
let timeout = undefined;
|
const allEstimates = this.room.getAllEstimates();
|
||||||
|
|
||||||
const allEstimates = room.getAllEstimates();
|
|
||||||
if (allEstimates.size === users.length) {
|
if (allEstimates.size === users.length) {
|
||||||
// If all users have estimated, clear the timeout
|
this.clearNudgeTimeout();
|
||||||
clearTimeout(timeout);
|
this.party.broadcast(
|
||||||
timeout = undefined;
|
JSON.stringify({
|
||||||
|
type: 'estimation-closed',
|
||||||
broadcastToRoom(room.id, {
|
groupedEstimates: this.groupEstimates()
|
||||||
type: 'estimation-closed',
|
})
|
||||||
groupedEstimates: groupEstimates(users)
|
);
|
||||||
});
|
|
||||||
} else if (allEstimates.size === users.length - 1) {
|
} else if (allEstimates.size === users.length - 1) {
|
||||||
// The last user has 30 seconds to add their estimation, otherwise send the nudge message
|
const notEstimatedUser = users.find((candidate) => !allEstimates.has(candidate.name));
|
||||||
let notEstimatedUser: User | undefined = undefined;
|
if (notEstimatedUser) {
|
||||||
for (let i = 0; i < users.length; i++) {
|
this.scheduleNudge(notEstimatedUser);
|
||||||
if (!allEstimates.has(users[i].name)) {
|
|
||||||
notEstimatedUser = users[i];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (notEstimatedUser !== undefined) {
|
|
||||||
const ws = room.getWebSocketByUser(notEstimatedUser);
|
|
||||||
if (ws) {
|
|
||||||
// Cancel the old timeout if it's still running
|
|
||||||
if (timeout) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
// Set a new timeout
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
if (!room!.getAllEstimates().has(notEstimatedUser!.name)) {
|
|
||||||
ws.send(JSON.stringify({ type: 'nudge' }));
|
|
||||||
}
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
} else if (data.type === 'restart-estimation') {
|
|
||||||
if (room) {
|
|
||||||
room.clearEstimates();
|
|
||||||
broadcastToRoom(room.id, { type: 'estimation-restarted' });
|
|
||||||
}
|
|
||||||
} else if (data.type === 'get-user-estimates') {
|
|
||||||
if (room) {
|
|
||||||
const users = room.getUsers();
|
|
||||||
broadcastToRoom(room.id, { type: 'user-estimates', users, selectedCardSet: room.cardSet });
|
|
||||||
}
|
|
||||||
} else if (data.type === 'trigger-emoji') {
|
|
||||||
const { cardId, emoji } = data;
|
|
||||||
if (room) {
|
|
||||||
broadcastToRoom(room.id, {
|
|
||||||
type: 'trigger-emoji',
|
|
||||||
cardId,
|
|
||||||
emoji
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (data.type === 'ping') {
|
|
||||||
if (room) {
|
|
||||||
broadcastToRoom(room.id, 'pong');
|
|
||||||
}
|
|
||||||
} else if (data.type === 'kick-user') {
|
|
||||||
console.log('kick user message received');
|
|
||||||
if (room) {
|
|
||||||
const userIdToKick = data.userId;
|
|
||||||
|
|
||||||
const ws = room.getWebSocketByUserId(userIdToKick);
|
case 'restart-estimation':
|
||||||
console.log(userIdToKick, ws);
|
if (this.room) {
|
||||||
if (ws) {
|
this.clearNudgeTimeout();
|
||||||
console.log('closing ws');
|
this.room.clearEstimates();
|
||||||
ws.close();
|
await this.updateRoomState();
|
||||||
|
this.party.broadcast(JSON.stringify({ type: 'estimation-restarted' }));
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
// Remove them from the room even if the active websocket can't be found or can't be closed
|
case 'get-user-estimates':
|
||||||
room.removeUser(userIdToKick);
|
if (this.room) {
|
||||||
}
|
const users = this.room.getUsers();
|
||||||
}
|
connection.send(
|
||||||
});
|
JSON.stringify({ type: 'user-estimates', users, selectedCardSet: this.room.cardSet })
|
||||||
|
);
|
||||||
ws.on('close', () => {
|
|
||||||
if (room) {
|
|
||||||
const user = room.getUserByWebSocket(ws);
|
|
||||||
if (user) {
|
|
||||||
room.removeUser(ws);
|
|
||||||
|
|
||||||
if (room.getUsers().length === 0) {
|
|
||||||
rooms.delete(room);
|
|
||||||
broadcastToRoom(room.id, {
|
|
||||||
type: 'room-deleted',
|
|
||||||
message: 'Room deleted'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
broadcastToRoom(room.id, {
|
case 'trigger-emoji':
|
||||||
type: 'user-left',
|
if (this.room) {
|
||||||
userId: user.userId
|
const { cardId, emoji } = data;
|
||||||
});
|
this.party.broadcast(JSON.stringify({ type: 'trigger-emoji', cardId, emoji }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
connection.send(JSON.stringify({ type: 'pong' }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'kick-user':
|
||||||
|
if (this.room) {
|
||||||
|
const userIdToKick = data.userId;
|
||||||
|
const connectionToKick = this.room.getConnectionByUserId(userIdToKick);
|
||||||
|
if (connectionToKick) {
|
||||||
|
connectionToKick.close();
|
||||||
|
this.room.removeUser(connectionToKick);
|
||||||
|
}
|
||||||
|
await this.updateRoomState();
|
||||||
|
this.party.broadcast(JSON.stringify({ type: 'user-left', userId: userIdToKick }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('Unknown message type:', data.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to broadcast the current room state to all users in the room
|
||||||
|
private async updateRoomState() {
|
||||||
|
if (this.room) {
|
||||||
|
await this.party.storage.put('room', {
|
||||||
|
id: this.room.id,
|
||||||
|
cardSetName: this.room.cardSet.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group user estimates into a map of estimates -> list of user names
|
||||||
|
private groupEstimates() {
|
||||||
|
const users = this.room ? this.room.getUsers() : [];
|
||||||
|
const estimateGroups: { [key: number | string]: string[] } = {};
|
||||||
|
users.forEach((user) => {
|
||||||
|
if (user.estimate !== null) {
|
||||||
|
if (estimateGroups[user.estimate]) {
|
||||||
|
estimateGroups[user.estimate].push(user.name);
|
||||||
|
} else {
|
||||||
|
estimateGroups[user.estimate] = [user.name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return estimateGroups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getOrCreateRoom(cardSetName?: string): Promise<Room | null> {
|
||||||
|
if (this.room) {
|
||||||
|
return this.room;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function broadcastToRoom(roomId: string, message: any) {
|
const savedRoom = (await this.party.storage.get('room')) as StoredRoom | undefined;
|
||||||
const room = getRoomById(roomId);
|
if (savedRoom?.cardSetName) {
|
||||||
if (room) {
|
this.room = new Room(savedRoom.id ?? this.party.id, savedRoom.cardSetName);
|
||||||
room.broadcast(message);
|
return this.room;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.room = new Room(this.party.id, cardSetName ?? '');
|
||||||
|
await this.updateRoomState();
|
||||||
|
return this.room;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function getUsersInRoom(roomId: string) {
|
private scheduleNudge(user: User) {
|
||||||
const room = getRoomById(roomId);
|
this.clearNudgeTimeout();
|
||||||
if (room) {
|
|
||||||
return room.getUsers();
|
if (!this.room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection = this.room.getConnectionByUser(user);
|
||||||
|
if (!connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nudgeTimeout = setTimeout(() => {
|
||||||
|
if (!this.room) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allEstimates = this.room.getAllEstimates();
|
||||||
|
if (!allEstimates.has(user.name)) {
|
||||||
|
connection.send(JSON.stringify({ type: 'nudge' }));
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRoomById(roomId: string) {
|
private clearNudgeTimeout() {
|
||||||
for (const room of rooms) {
|
if (this.nudgeTimeout) {
|
||||||
if (room.id === roomId) {
|
clearTimeout(this.nudgeTimeout);
|
||||||
return room;
|
this.nudgeTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user