Enhance map update handling and connection stability

- Introduced mechanisms to detect stale connections in the map updates, allowing for automatic reconnection if no messages are received within a specified timeframe.
- Updated the `refresh` method in `SmartTileLayer` to return a boolean indicating whether the tile was refreshed, improving the handling of tile updates.
- Enhanced the `Send` method in the `Topic` struct to drop messages for full subscribers while keeping them subscribed, ensuring continuous delivery of future updates.
- Added a keepalive mechanism in the `WatchGridUpdates` handler to maintain the connection and prevent timeouts.
This commit is contained in:
2026-03-04 21:29:53 +03:00
parent 179357bc93
commit dc53b79d84
6 changed files with 92 additions and 19 deletions

View File

@@ -38,6 +38,9 @@ export interface UseMapUpdatesReturn {
const RECONNECT_INITIAL_MS = 1000
const RECONNECT_MAX_MS = 30000
/** If no SSE message received for this long, treat connection as stale and reconnect. */
const STALE_CONNECTION_MS = 65000
const STALE_CHECK_INTERVAL_MS = 30000
export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesReturn {
const { backendBase, layer, overlayLayer, map, getCurrentMapId, onMerge, connectionStateRef } = options
@@ -50,6 +53,8 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
let batchScheduled = false
let source: EventSource | null = null
let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null
let staleCheckIntervalId: ReturnType<typeof setInterval> | null = null
let lastMessageTime = 0
let reconnectDelayMs = RECONNECT_INITIAL_MS
let destroyed = false
@@ -79,15 +84,22 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
overlayLayer.cache[key] = u.T
}
const visible = getVisibleTileBounds()
// u.Z is backend storage zoom (0..5); visible.zoom is map zoom (1..6). With zoomReverse, current backend Z = maxZoom - mapZoom.
const currentBackendZ = visible ? layer.options.maxZoom - visible.zoom : null
let needRedraw = false
for (const u of updates) {
if (visible && u.Z !== visible.zoom) continue
if (visible && currentBackendZ != null && u.Z !== currentBackendZ) continue
if (
visible &&
(u.X < visible.minX || u.X > visible.maxX || u.Y < visible.minY || u.Y > visible.maxY)
)
continue
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)
if (layer.map === u.M && !layer.refresh(u.X, u.Y, u.Z)) needRedraw = true
if (overlayLayer.map === u.M && !overlayLayer.refresh(u.X, u.Y, u.Z)) needRedraw = true
}
if (needRedraw) {
layer.redraw()
overlayLayer.redraw()
}
}
@@ -99,12 +111,30 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
function connect() {
if (destroyed || !import.meta.client) return
if (staleCheckIntervalId != null) {
clearInterval(staleCheckIntervalId)
staleCheckIntervalId = null
}
source = new EventSource(updatesUrl)
if (connectionStateRef) connectionStateRef.value = 'connecting'
source.onopen = () => {
if (connectionStateRef) connectionStateRef.value = 'open'
lastMessageTime = Date.now()
reconnectDelayMs = RECONNECT_INITIAL_MS
staleCheckIntervalId = setInterval(() => {
if (destroyed || !source) return
if (Date.now() - lastMessageTime > STALE_CONNECTION_MS) {
if (staleCheckIntervalId != null) {
clearInterval(staleCheckIntervalId)
staleCheckIntervalId = null
}
source.close()
source = null
if (connectionStateRef) connectionStateRef.value = 'error'
connect()
}
}, STALE_CHECK_INTERVAL_MS)
}
source.onerror = () => {
@@ -121,6 +151,7 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
}
source.onmessage = (event: MessageEvent) => {
lastMessageTime = Date.now()
if (connectionStateRef) connectionStateRef.value = 'open'
try {
const raw: unknown = event?.data
@@ -157,6 +188,10 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
function cleanup() {
destroyed = true
if (staleCheckIntervalId != null) {
clearInterval(staleCheckIntervalId)
staleCheckIntervalId = null
}
if (reconnectTimeoutId != null) {
clearTimeout(reconnectTimeoutId)
reconnectTimeoutId = null