fix: PWA icon, robust socket.io reconnect, faster real-time sync

This commit is contained in:
Pepe Ziberi
2026-02-21 23:37:36 +01:00
parent e3f8f14f6a
commit 2432e9a17f
5 changed files with 53 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -20,7 +20,13 @@
"src": "/logo-icon.png", "src": "/logo-icon.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png", "type": "image/png",
"purpose": "any maskable" "purpose": "any"
},
{
"src": "/logo-icon-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
} }
], ],
"screenshots": [], "screenshots": [],

View File

@@ -30,6 +30,9 @@ self.addEventListener('fetch', (event) => {
// Skip non-GET requests // Skip non-GET requests
if (event.request.method !== 'GET') return if (event.request.method !== 'GET') return
// Never intercept Socket.IO — let it pass through directly
if (pathname.startsWith('/socket.io')) return
// Cacheable API routes: Network First with cache fallback (icons, hose-types, dictionary) // Cacheable API routes: Network First with cache fallback (icons, hose-types, dictionary)
if (CACHEABLE_API.some(p => pathname.startsWith(p))) { if (CACHEABLE_API.some(p => pathname.startsWith(p))) {
event.respondWith( event.respondWith(

View File

@@ -20,7 +20,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
import { JournalView } from '@/components/journal/journal-view' import { JournalView } from '@/components/journal/journal-view'
import { jsPDF } from 'jspdf' import { jsPDF } from 'jspdf'
import { Lock, Unlock, Eye, AlertTriangle, WifiOff } from 'lucide-react' import { Lock, Unlock, Eye, AlertTriangle, WifiOff } from 'lucide-react'
import { getSocket } from '@/lib/socket' import { getSocket, setSocketRoom } from '@/lib/socket'
import { CustomDragLayer } from '@/components/map/custom-drag-layer' import { CustomDragLayer } from '@/components/map/custom-drag-layer'
import { addToSyncQueue, flushSyncQueue, getSyncQueue, isOnline as checkOnline } from '@/lib/offline-sync' import { addToSyncQueue, flushSyncQueue, getSyncQueue, isOnline as checkOnline } from '@/lib/offline-sync'
@@ -507,27 +507,31 @@ export default function AppPage() {
const socketRef = useRef<any>(null) const socketRef = useRef<any>(null)
const prevProjectIdRef = useRef<string | null>(null) const prevProjectIdRef = useRef<string | null>(null)
// Throttled socket broadcast for near-real-time sync (1.5s instead of 10s auto-save) // Throttled socket broadcast for near-real-time sync
const lastEmitRef = useRef(0) const lastEmitRef = useRef(0)
const emitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null) const emitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const currentProjectRef = useRef(currentProject)
useEffect(() => { currentProjectRef.current = currentProject }, [currentProject])
const broadcastFeatures = useCallback((feats: DrawFeature[]) => { const broadcastFeatures = useCallback((feats: DrawFeature[]) => {
if (!socketRef.current || !currentProject?.id || !isEditingByMe) return const proj = currentProjectRef.current
if (!socketRef.current || !proj?.id || !isEditingByMeRef.current) return
const now = Date.now() const now = Date.now()
const emit = () => { const emit = () => {
socketRef.current?.emit('features-updated', { socketRef.current?.emit('features-updated', {
projectId: currentProject!.id, projectId: proj!.id,
features: feats, features: feats,
}) })
lastEmitRef.current = Date.now() lastEmitRef.current = Date.now()
} }
// Throttle: emit at most every 1.5 seconds // Throttle: emit at most every 800ms for snappier sync
if (now - lastEmitRef.current > 1500) { if (now - lastEmitRef.current > 800) {
emit() emit()
} else { } else {
if (emitTimerRef.current) clearTimeout(emitTimerRef.current) if (emitTimerRef.current) clearTimeout(emitTimerRef.current)
emitTimerRef.current = setTimeout(emit, 1500 - (now - lastEmitRef.current)) emitTimerRef.current = setTimeout(emit, 800 - (now - lastEmitRef.current))
} }
}, [currentProject?.id, isEditingByMe]) }, [])
const isEditingByMeRef = useRef(false) const isEditingByMeRef = useRef(false)
// Keep ref in sync with state // Keep ref in sync with state
@@ -546,6 +550,7 @@ export default function AppPage() {
socket.emit('leave-project', prevProjectIdRef.current) socket.emit('leave-project', prevProjectIdRef.current)
} }
socket.emit('join-project', currentProject.id) socket.emit('join-project', currentProject.id)
setSocketRoom(currentProject.id)
prevProjectIdRef.current = currentProject.id prevProjectIdRef.current = currentProject.id
// Listen for features changes from other clients (only apply if NOT the editor) // Listen for features changes from other clients (only apply if NOT the editor)

View File

@@ -3,23 +3,50 @@
import { io, Socket } from 'socket.io-client' import { io, Socket } from 'socket.io-client'
let socket: Socket | null = null let socket: Socket | null = null
let currentRoom: string | null = null
export function getSocket(): Socket { export function getSocket(): Socket {
if (!socket) { if (!socket) {
socket = io({ socket = io({
path: '/socket.io', path: '/socket.io',
transports: ['polling', 'websocket'], transports: ['websocket', 'polling'],
upgrade: true, upgrade: true,
reconnectionAttempts: 10, reconnection: true,
reconnectionDelay: 2000, reconnectionAttempts: Infinity,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 10000, timeout: 10000,
forceNew: false,
}) })
socket.on('connect', () => { socket.on('connect', () => {
console.log('[Socket.io] Connected:', socket?.id) console.log('[Socket.io] Connected:', socket?.id)
// Re-join project room after reconnect
if (currentRoom) {
console.log('[Socket.io] Re-joining room:', currentRoom)
socket?.emit('join-project', currentRoom)
}
})
socket.on('disconnect', (reason) => {
console.warn('[Socket.io] Disconnected:', reason)
if (reason === 'io server disconnect') {
// Server disconnected us, need to manually reconnect
socket?.connect()
}
}) })
socket.on('connect_error', (err) => { socket.on('connect_error', (err) => {
console.warn('[Socket.io] Connection error:', err.message) console.warn('[Socket.io] Connection error:', err.message)
}) })
socket.io.on('reconnect', (attempt) => {
console.log('[Socket.io] Reconnected after', attempt, 'attempts')
})
socket.io.on('reconnect_attempt', (attempt) => {
console.log('[Socket.io] Reconnect attempt', attempt)
})
} }
return socket return socket
} }
/** Track which room the socket should be in (for auto-rejoin on reconnect) */
export function setSocketRoom(projectId: string | null): void {
currentRoom = projectId
}