mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
d845057f84
- Fix backup restore: try/finally ensures DB always reopens after closeDb - Fix EBUSY on uploads during restore (in-place overwrite instead of rmSync) - Add DB proxy null guard for clearer errors during restore window - Add red warning modal before backup restore (DE/EN, dark mode support) - JWT secret: empty docker-compose default so auto-generation kicks in - OIDC: pass token via URL fragment instead of query param (no server logs) - Block SVG uploads on photos, files and covers (stored XSS prevention) - Add helmet for security headers (HSTS, X-Frame, nosniff, etc.) - Explicit express.json body size limit (100kb) - Fix XSS in Leaflet map markers (escape image_url in HTML) - Remove verbose WebSocket debug logging from client
142 lines
3.4 KiB
JavaScript
142 lines
3.4 KiB
JavaScript
// Singleton WebSocket manager for real-time collaboration
|
|
|
|
let socket = null
|
|
let reconnectTimer = null
|
|
let reconnectDelay = 1000
|
|
const MAX_RECONNECT_DELAY = 30000
|
|
const listeners = new Set()
|
|
const activeTrips = new Set()
|
|
let currentToken = null
|
|
let refetchCallback = null
|
|
let mySocketId = null
|
|
|
|
export function getSocketId() {
|
|
return mySocketId
|
|
}
|
|
|
|
export function setRefetchCallback(fn) {
|
|
refetchCallback = fn
|
|
}
|
|
|
|
function getWsUrl(token) {
|
|
const protocol = location.protocol === 'https:' ? 'wss' : 'ws'
|
|
return `${protocol}://${location.host}/ws?token=${token}`
|
|
}
|
|
|
|
function handleMessage(event) {
|
|
try {
|
|
const parsed = JSON.parse(event.data)
|
|
// Store our socket ID from welcome message
|
|
if (parsed.type === 'welcome') {
|
|
mySocketId = parsed.socketId
|
|
return
|
|
}
|
|
listeners.forEach(fn => {
|
|
try { fn(parsed) } catch (err) { console.error('WebSocket listener error:', err) }
|
|
})
|
|
} catch (err) {
|
|
console.error('WebSocket message parse error:', err)
|
|
}
|
|
}
|
|
|
|
function scheduleReconnect() {
|
|
if (reconnectTimer) return
|
|
reconnectTimer = setTimeout(() => {
|
|
reconnectTimer = null
|
|
if (currentToken) {
|
|
connectInternal(currentToken, true)
|
|
}
|
|
}, reconnectDelay)
|
|
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY)
|
|
}
|
|
|
|
function connectInternal(token, isReconnect = false) {
|
|
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
|
return
|
|
}
|
|
|
|
const url = getWsUrl(token)
|
|
socket = new WebSocket(url)
|
|
|
|
socket.onopen = () => {
|
|
// connection established
|
|
reconnectDelay = 1000
|
|
// Join active trips on any connect (initial or reconnect)
|
|
if (activeTrips.size > 0) {
|
|
activeTrips.forEach(tripId => {
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.send(JSON.stringify({ type: 'join', tripId }))
|
|
// joined trip room
|
|
}
|
|
})
|
|
// Refetch trip data for active trips
|
|
if (refetchCallback) {
|
|
activeTrips.forEach(tripId => {
|
|
try { refetchCallback(tripId) } catch (err) {
|
|
console.error('Failed to refetch trip data on reconnect:', err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
socket.onmessage = handleMessage
|
|
|
|
socket.onclose = () => {
|
|
socket = null
|
|
if (currentToken) {
|
|
scheduleReconnect()
|
|
}
|
|
}
|
|
|
|
socket.onerror = () => {
|
|
// onclose will fire after onerror, reconnect handled there
|
|
}
|
|
}
|
|
|
|
export function connect(token) {
|
|
currentToken = token
|
|
reconnectDelay = 1000
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
connectInternal(token, false)
|
|
}
|
|
|
|
export function disconnect() {
|
|
currentToken = null
|
|
if (reconnectTimer) {
|
|
clearTimeout(reconnectTimer)
|
|
reconnectTimer = null
|
|
}
|
|
activeTrips.clear()
|
|
if (socket) {
|
|
socket.onclose = null // prevent reconnect
|
|
socket.close()
|
|
socket = null
|
|
}
|
|
}
|
|
|
|
export function joinTrip(tripId) {
|
|
activeTrips.add(String(tripId))
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.send(JSON.stringify({ type: 'join', tripId: String(tripId) }))
|
|
}
|
|
}
|
|
|
|
export function leaveTrip(tripId) {
|
|
activeTrips.delete(String(tripId))
|
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
socket.send(JSON.stringify({ type: 'leave', tripId: String(tripId) }))
|
|
}
|
|
}
|
|
|
|
export function addListener(fn) {
|
|
listeners.add(fn)
|
|
}
|
|
|
|
export function removeListener(fn) {
|
|
listeners.delete(fn)
|
|
}
|