[web] ensure that only a single socket.io transport is connected (#24422)

GitOrigin-RevId: 9397b0c85f0a889385d4761945e976ada7aa537b
This commit is contained in:
Jakob Ackermann
2025-03-26 09:36:41 +00:00
committed by Copybot
parent e754ee9cb4
commit dda94cdfbc
3 changed files with 62 additions and 8 deletions

View File

@@ -25,8 +25,6 @@ const MAX_RECONNECT_GRACEFULLY_INTERVAL_MS = getMeta(
'ol-maxReconnectGracefullyIntervalMs'
)
const BEFORE_RECONNECT = 'beforeReconnect'
const MAX_RETRY_CONNECT = 5
const RETRY_WEBSOCKET = 3
@@ -117,7 +115,7 @@ export class ConnectionManager extends EventTarget {
}
socket.on('connect', () => this.onConnect())
socket.on('disconnect', (reason: string) => this.onDisconnect(reason))
socket.on('disconnect', () => this.onDisconnect())
socket.on('error', err => this.onConnectError(err))
socket.on('connect_failed', err => this.onConnectError(err))
socket.on('joinProjectResponse', body => this.onJoinProjectResponse(body))
@@ -276,8 +274,7 @@ export class ConnectionManager extends EventTarget {
this.websocketFailureCount = 0
}
private onDisconnect(reason: string) {
if (reason === BEFORE_RECONNECT) return // triggered from reconnect, ignore.
private onDisconnect() {
this.connectionAttempt = null
if (this.externalHeartbeatInterval) {
window.clearInterval(this.externalHeartbeatInterval)
@@ -446,7 +443,7 @@ export class ConnectionManager extends EventTarget {
if (this.socket.socket.connecting || this.socket.socket.connected) {
// Ensure the old transport has been cleaned up.
// Socket.disconnect() does not accept a parameter. Go one level deeper.
this.socket.socket.onDisconnect(BEFORE_RECONNECT)
this.socket.forceDisconnectWithoutEvent()
}
this.socket.socket.connect()
}

View File

@@ -34,7 +34,6 @@ export type Socket = {
connected: boolean
connecting: boolean
connect(): void
onDisconnect(reason: string): void
disconnect(): void
sessionid: string
transport?: {
@@ -43,4 +42,5 @@ export type Socket = {
transports: string[]
}
disconnect(): void
forceDisconnectWithoutEvent(): void
}

View File

@@ -11,11 +11,12 @@ class SocketShimBase {
constructor(socket) {
this._socket = socket
}
forceDisconnectWithoutEvent() {}
}
const transparentMethods = [
'connect',
'disconnect',
'onDisconnect',
'emit',
'on',
'removeListener',
@@ -64,6 +65,62 @@ class SocketShimV0 extends SocketShimBase {
constructor(socket) {
super(socket)
this.socket = this._socket.socket
const self = this
Object.defineProperty(this.socket, 'transport', {
get() {
return self._transport
},
set(v) {
self.forceDisconnectWithoutEvent()
self._transport = v
},
})
}
forceDisconnectWithoutEvent() {
clearTimeout(this.socket.heartbeatTimeoutTimer)
if (this._transport) this.forceCloseTransport(this._transport)
}
forceCloseTransport(transport) {
transport.clearTimeouts()
if (transport instanceof io.Transport.websocket) {
// retry closing
transport.websocket.onopen = transport.websocket.onmessage = () =>
transport.websocket.close()
// mute close/error handler
transport.websocket.onclose = transport.websocket.onerror = () => {}
// disconnect
try {
transport.websocket.close()
} catch {}
} else if (transport instanceof io.Transport['xhr-polling']) {
// mute data/close handler and block new polling GET requests
transport.onData = transport.onClose = transport.get = () => {}
// abort pending long-polling/POST request
for (const xhr of [transport.xhr, transport.sendXHR]) {
if (!xhr) continue // not pending
// mute xhr callbacks
xhr.onreadystatechange = xhr.onload = xhr.onerror = () => {}
try {
xhr.abort()
} catch {}
}
transport.xhr = transport.sendXHR = null
// Mark long-polling client as disconnected to avoid "ghost" connected client.
fetch(transport.prepareUrl() + '/?disconnect=1', {
// Let the request continue after navigating away from or reloading the page.
keepalive: true,
})
// Avoid leaving a dangling response on the wire.
.then(res => res.text())
.catch(() => {})
} else {
try {
transport.close()
} catch {}
debugConsole.warn('unexpected socket.io transport', transport)
}
}
}