diff --git a/app/durableObjects/ChatRoom.server.ts b/app/durableObjects/ChatRoom.server.ts index 4864b41b..16c3f32f 100644 --- a/app/durableObjects/ChatRoom.server.ts +++ b/app/durableObjects/ChatRoom.server.ts @@ -34,9 +34,9 @@ export class ChatRoom extends Server { ) ) - if (!this.ctx.storage.getAlarm()) { + if (!(await this.ctx.storage.getAlarm())) { // start the alarm to broadcast state every 30 seconds - this.ctx.storage.setAlarm(30000) + this.ctx.storage.setAlarm(Date.now() + 30000) } // cleaning out storage used by older versions of this code @@ -74,16 +74,29 @@ export class ChatRoom extends Server { } broadcastState() { - this.broadcast( - JSON.stringify({ - type: 'roomState', - state: { - users: [...this.getConnections()] - .map((connection) => connection.state) - .filter((x) => !!x), - }, - } satisfies ServerMessage) - ) + let didSomeoneQuit = false + const roomStateMessage = { + type: 'roomState', + state: { + users: [...this.getConnections()] + .map((connection) => connection.state) + .filter((x) => !!x), + }, + } satisfies ServerMessage + + for (const connection of this.getConnections()) { + try { + connection.send(JSON.stringify(roomStateMessage)) + } catch (err) { + connection.close(1011, 'Failed to broadcast state') + didSomeoneQuit = true + } + } + + if (didSomeoneQuit) { + // broadcast again to remove the user who quit + this.broadcastState() + } } async onMessage( @@ -100,7 +113,8 @@ export class ChatRoom extends Server { switch (data.type) { case 'userLeft': - // TODO: ?? + connection.close(1000, 'User left') + this.broadcastState() break case 'userUpdate': connection.setState(data.user) @@ -176,7 +190,11 @@ export class ChatRoom extends Server { } onClose() { - this.broadcastState() + // while it makes sense to broadcast immediately on close, + // it's possible that the websocket just closed for an instant + // and will reconnect momentarily. + // so let's just let the alarm handler do the broadcasting. + // this.broadcastState() } onError(): void | Promise { @@ -187,6 +205,6 @@ export class ChatRoom extends Server { // technically we don't need to broadcast state on an alarm, // but let's keep it for a while and see if it's useful this.broadcastState() - this.ctx.storage.setAlarm(30000) + this.ctx.storage.setAlarm(Date.now() + 30000) } } diff --git a/app/hooks/useRoom.ts b/app/hooks/useRoom.ts index de3f4473..8edd4a23 100644 --- a/app/hooks/useRoom.ts +++ b/app/hooks/useRoom.ts @@ -44,13 +44,25 @@ export default function useRoom({ }, }) + useEffect(() => { + function onBeforeUnload() { + websocket.send( + JSON.stringify({ type: 'userLeft' } satisfies ClientMessage) + ) + } + window.addEventListener('beforeunload', onBeforeUnload) + return () => { + window.removeEventListener('beforeunload', onBeforeUnload) + } + }, [websocket]) + // setup a simple ping pong useEffect(() => { const interval = setInterval(() => { websocket.send( JSON.stringify({ type: 'partyserver-ping' } satisfies ClientMessage) ) - }, 10000) + }, 5000) return () => clearInterval(interval) }, [websocket])