import type { Ref } from 'vue' import type { SmartTileLayer } from '~/lib/SmartTileLayer' import { TileSize } from '~/lib/LeafletCustomTypes' import type L from 'leaflet' type SmartTileLayerInstance = InstanceType export type SseConnectionState = 'connecting' | 'open' | 'error' interface TileUpdate { M: number X: number Y: number Z: number T: number } interface MergeEvent { From: number To: number Shift: { x: number; y: number } } export interface UseMapUpdatesOptions { backendBase: string layer: SmartTileLayerInstance overlayLayer: SmartTileLayerInstance map: L.Map getCurrentMapId: () => number onMerge: (mapTo: number, shift: { x: number; y: number }) => void /** Optional ref updated with SSE connection state for reconnection indicator. */ connectionStateRef?: Ref } export interface UseMapUpdatesReturn { cleanup: () => void } export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesReturn { const { backendBase, layer, overlayLayer, map, getCurrentMapId, onMerge, connectionStateRef } = options const updatesPath = `${backendBase}/updates` const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath const source = new EventSource(updatesUrl) if (connectionStateRef) { connectionStateRef.value = 'connecting' } source.onopen = () => { if (connectionStateRef) connectionStateRef.value = 'open' } source.onerror = () => { if (connectionStateRef) connectionStateRef.value = 'error' } source.onmessage = (event: MessageEvent) => { if (connectionStateRef) connectionStateRef.value = 'open' try { const raw: unknown = event?.data if (raw == null || typeof raw !== 'string' || raw.trim() === '') return const updates: unknown = JSON.parse(raw) if (!Array.isArray(updates)) return for (const u of updates as TileUpdate[]) { const key = `${u.M}:${u.X}:${u.Y}:${u.Z}` layer.cache[key] = u.T overlayLayer.cache[key] = u.T if (layer.map === u.M) layer.refresh(u.X, u.Y, u.Z) if (overlayLayer.map === u.M) overlayLayer.refresh(u.X, u.Y, u.Z) } } catch { // Ignore parse errors from SSE } } source.onerror = () => {} source.addEventListener('merge', (e: MessageEvent) => { try { const merge: MergeEvent = JSON.parse((e?.data as string) ?? '{}') if (getCurrentMapId() === merge.From) { const point = map.project(map.getCenter(), 6) const shift = { x: Math.floor(point.x / TileSize) + merge.Shift.x, y: Math.floor(point.y / TileSize) + merge.Shift.y, } onMerge(merge.To, shift) } } catch { // Ignore merge parse errors } }) function cleanup() { source.close() } return { cleanup } }