fix: PWA icon, robust socket.io reconnect, faster real-time sync
This commit is contained in:
BIN
public/logo-icon-maskable.png
Normal file
BIN
public/logo-icon-maskable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
@@ -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": [],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user