mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
fix: ws connection duplication + disconnecting during browse
This commit is contained in:
@@ -9,6 +9,7 @@ export type WebSocketEventHandler<
|
||||
export interface WebSocketConnection {
|
||||
serverId: string
|
||||
socket: WebSocket
|
||||
authenticated: boolean
|
||||
reconnectAttempts: number
|
||||
reconnectTimer?: ReturnType<typeof setTimeout>
|
||||
isReconnecting: boolean
|
||||
@@ -31,6 +32,7 @@ export abstract class AbstractWebSocketClient {
|
||||
protected readonly MAX_RECONNECT_ATTEMPTS = 10
|
||||
protected readonly RECONNECT_BASE_DELAY = 1000
|
||||
protected readonly RECONNECT_MAX_DELAY = 30000
|
||||
protected readonly AUTHENTICATION_TIMEOUT = 30000
|
||||
|
||||
constructor(
|
||||
protected client: {
|
||||
@@ -58,6 +60,7 @@ export abstract class AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
if (status && !status.connected && !options?.force) {
|
||||
await this.waitForAuthentication(serverId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -69,6 +72,28 @@ export abstract class AbstractWebSocketClient {
|
||||
await this.connect(serverId, auth)
|
||||
}
|
||||
|
||||
protected async waitForAuthentication(serverId: string): Promise<void> {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let unsubscribe = () => {}
|
||||
const timeout = setTimeout(() => {
|
||||
unsubscribe()
|
||||
reject(new Error(`WebSocket authentication timed out for server ${serverId}`))
|
||||
}, this.AUTHENTICATION_TIMEOUT)
|
||||
|
||||
unsubscribe = this.on(serverId, 'auth-ok', () => {
|
||||
clearTimeout(timeout)
|
||||
unsubscribe()
|
||||
resolve()
|
||||
})
|
||||
|
||||
if (this.getStatus(serverId)?.connected) {
|
||||
clearTimeout(timeout)
|
||||
unsubscribe()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
on<E extends Archon.Websocket.v0.WSEventType>(
|
||||
serverId: string,
|
||||
eventType: E,
|
||||
@@ -88,7 +113,7 @@ export abstract class AbstractWebSocketClient {
|
||||
if (!connection) return null
|
||||
|
||||
return {
|
||||
connected: connection.socket.readyState === WebSocket.OPEN,
|
||||
connected: connection.socket.readyState === WebSocket.OPEN && connection.authenticated,
|
||||
reconnecting: connection.isReconnecting,
|
||||
reconnectAttempts: connection.reconnectAttempts,
|
||||
}
|
||||
|
||||
@@ -1152,9 +1152,12 @@ export namespace Archon {
|
||||
|
||||
export type InstallProgressFileKey = {
|
||||
type: 'file'
|
||||
parent_directory: string
|
||||
filename: string
|
||||
install_type: 'install' | 'update'
|
||||
project_id: string
|
||||
version_id: string
|
||||
parent_directory: string
|
||||
source_filename: string | null
|
||||
target_filename?: string | null
|
||||
}
|
||||
|
||||
export type InstallProgressModrinthModpackKey = {
|
||||
|
||||
@@ -19,12 +19,14 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false
|
||||
try {
|
||||
const ws = new WebSocket(getNodeWebSocketUrl(auth.url))
|
||||
|
||||
const connection: WebSocketConnection = {
|
||||
serverId,
|
||||
socket: ws,
|
||||
authenticated: false,
|
||||
reconnectAttempts: 0,
|
||||
reconnectTimer: undefined,
|
||||
isReconnecting: false,
|
||||
@@ -37,18 +39,26 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
|
||||
connection.reconnectAttempts = 0
|
||||
connection.isReconnecting = false
|
||||
|
||||
resolve()
|
||||
}
|
||||
|
||||
ws.onmessage = (messageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(messageEvent.data) as Archon.Websocket.v0.WSEvent
|
||||
if (data.event === 'auth-ok') {
|
||||
connection.authenticated = true
|
||||
} else if (data.event === 'auth-incorrect') {
|
||||
connection.authenticated = false
|
||||
}
|
||||
|
||||
const eventKey = `${serverId}:${data.event}` as keyof WSEventMap
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.emitter.emit(eventKey, data as any)
|
||||
|
||||
if (data.event === 'auth-ok' && !settled) {
|
||||
settled = true
|
||||
resolve()
|
||||
}
|
||||
|
||||
if (data.event === 'auth-expiring' || data.event === 'auth-incorrect') {
|
||||
this.handleAuthExpiring(serverId).catch(console.error)
|
||||
}
|
||||
@@ -58,11 +68,20 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
connection.authenticated = false
|
||||
console.debug(`[WebSocket] Closed for server ${serverId}:`, {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean,
|
||||
})
|
||||
if (!settled) {
|
||||
settled = true
|
||||
reject(
|
||||
new Error(
|
||||
`WebSocket closed before authentication for server ${serverId} (code: ${event.code})`,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (event.code !== NORMAL_CLOSURE) {
|
||||
this.scheduleReconnect(serverId, auth)
|
||||
}
|
||||
@@ -77,13 +96,17 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
readyStateLabel: ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][readyState],
|
||||
type: (event as Event).type,
|
||||
})
|
||||
reject(
|
||||
new Error(
|
||||
`WebSocket connection failed for server ${serverId} (readyState: ${readyState})`,
|
||||
),
|
||||
)
|
||||
if (!settled) {
|
||||
settled = true
|
||||
reject(
|
||||
new Error(
|
||||
`WebSocket connection failed for server ${serverId} (readyState: ${readyState})`,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
settled = true
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user