Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/games/plugins/sync-clients.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fp from 'fastify-plugin'
import { GameState } from '../../database/models/game.model'
import { GameState, type GameNumber } from '../../database/models/game.model'
import { events } from '../../events'
import { ConnectInfo } from '../views/html/connect-info'
import { DemoLink } from '../views/html/demo-link'
Expand All @@ -19,9 +19,32 @@ import { errors } from '../../errors'
import { AdminToolbox } from '../views/html/admin-toolbox'
import { players } from '../../players'
import { PlayerRole } from '../../database/models/player.model'
import { findOne } from '../find-one'
import type { WebSocket } from 'ws'

// eslint-disable-next-line @typescript-eslint/require-await
export default fp(async app => {
// Sync game page state when user navigates to a game page
app.gateway.on('navigated', async (socket: WebSocket, url: string) => {
const match = /^\/games\/(\d+)$/.exec(url)
if (!match) {
return
}

const gameNumber = parseInt(match[1]!, 10) as GameNumber
try {
const game = await findOne({ number: gameNumber })
const actor = socket.player?.steamId

// Send critical dynamic components that may have changed during navigation
socket.send(await ConnectInfo({ game, actor }))
socket.send(await GameStateIndicator({ game }))
socket.send(await GameSlotList.refreshAll({ game, actor }))
} catch {
// Game not found, ignore
}
})

events.on('game:updated', ({ before, after }) => {
if (before.state !== after.state) {
app.gateway
Expand Down
33 changes: 13 additions & 20 deletions src/html/@client/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import htmx from './htmx.js'

interface SocketWrapper {
send: (message: string) => void
}

let socket: SocketWrapper

function reportNavigation(path: string) {
const msg = JSON.stringify({ navigated: path })
socket.send(msg)
}

export async function goTo(path: string) {
await htmx.ajax('get', path, document.body)
history.pushState({}, '', path)
reportNavigation(path)
}

htmx.on('htmx:wsOpen', event => {
socket = (event as CustomEvent<{ socketWrapper: SocketWrapper }>).detail.socketWrapper
reportNavigation(window.location.pathname)
})

htmx.on('htmx:pushedIntoHistory', event => {
const path = (event as CustomEvent<{ path: string }>).detail.path.split('?')[0]!
reportNavigation(path)
htmx.on('htmx:wsAfterMessage', event => {
const message = (event as CustomEvent<{ message: string }>).detail.message
try {
const parsed = JSON.parse(message) as unknown
if (parsed && typeof parsed === 'object' && 'socketId' in parsed) {
const socketId = (parsed as { socketId: string }).socketId
// Set up hx-headers on body to include socket ID in all HTMX requests
document.body.setAttribute('hx-headers', JSON.stringify({ 'x-ws-id': socketId }))
// htmx.process(document.body)
}
} catch {
// Not JSON, ignore (probably HTML content)
}
})
30 changes: 30 additions & 0 deletions src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,33 @@ export default fp(
const gateway = new Gateway(app)
app.decorate('gateway', gateway)

// Update socket currentUrl based on HTTP requests with x-ws-id header
app.addHook('onResponse', async (req, reply) => {
const wsId = req.headers['x-ws-id']
if (typeof wsId !== 'string') {
return
}
// Only track successful HTML page responses (not API calls, assets, etc.)
if (reply.statusCode < 200 || reply.statusCode >= 300) {
return
}

const url = req.url.split('?')[0]!
const client = [...app.websocketServer.clients].find(c => c.id === wsId)
if (!client) {
return
}

const previousUrl = client.currentUrl
client.currentUrl = url

if (!previousUrl) {
gateway.emit('ready', client)
} else if (previousUrl !== url) {
gateway.emit('navigated', client, url)
}
})

app.get('/ws', { config: { otel: false }, websocket: true }, (socket, req) => {
socket.id = nanoid()

Expand Down Expand Up @@ -100,6 +127,9 @@ export default fp(
const ipAddress = extractClientIp(req.headers) ?? req.socket.remoteAddress
const userAgent = req.headers['user-agent']
gateway.emit('connected', socket, ipAddress, userAgent)

// Send socket ID to client for HTTP header correlation
socket.send(JSON.stringify({ socketId: socket.id }))
})
},
{ name: 'websockets' },
Expand Down
Loading