Download desktop/src/api/websocket.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 5.21 kB
-
https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/api/websocket.ts
- Command line
-
hf download hf://chenbhao/codev@e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/api/websocket.ts
-
curl -L -o websocket.ts https://huggingface.co/chenbhao/codev/resolve/e40073cc959cd4729bf4009662bca374c5c4e266/desktop/src/api/websocket.ts
5.21 kB
| import type { ClientMessage, ServerMessage } from '../types/chat' | |
| import { getAuthToken, getBaseUrl } from './client' | |
| type MessageHandler = (msg: ServerMessage) => void | |
| type Connection = { | |
| ws: WebSocket | |
| handlers: Set<MessageHandler> | |
| reconnectTimer: ReturnType<typeof setTimeout> | null | |
| reconnectAttempt: number | |
| pingInterval: ReturnType<typeof setInterval> | null | |
| intentionalClose: boolean | |
| pendingMessages: ClientMessage[] | |
| } | |
| class WebSocketManager { | |
| private connections = new Map<string, Connection>() | |
| isConnected(sessionId: string): boolean { | |
| const conn = this.connections.get(sessionId) | |
| return conn?.ws.readyState === WebSocket.OPEN | |
| } | |
| getConnectedSessionIds(): string[] { | |
| return [...this.connections.keys()] | |
| } | |
| connect(sessionId: string) { | |
| const existing = this.connections.get(sessionId) | |
| if ( | |
| existing && | |
| !existing.intentionalClose && | |
| ( | |
| existing.ws.readyState === WebSocket.OPEN || | |
| existing.ws.readyState === WebSocket.CONNECTING || | |
| existing.reconnectTimer !== null | |
| ) | |
| ) { | |
| return | |
| } | |
| const ws = new WebSocket(buildSessionWebSocketUrl(sessionId)) | |
| const conn: Connection = { | |
| ws, | |
| handlers: existing?.handlers ?? new Set(), | |
| reconnectTimer: null, | |
| reconnectAttempt: existing?.reconnectAttempt ?? 0, | |
| pingInterval: null, | |
| intentionalClose: false, | |
| pendingMessages: existing?.pendingMessages ?? [], | |
| } | |
| this.connections.set(sessionId, conn) | |
| ws.onopen = () => { | |
| conn.reconnectAttempt = 0 | |
| this.startPingLoop(sessionId) | |
| while (conn.pendingMessages.length > 0) { | |
| const msg = conn.pendingMessages.shift()! | |
| ws.send(JSON.stringify(msg)) | |
| } | |
| } | |
| ws.onmessage = (event) => { | |
| try { | |
| const msg = JSON.parse(event.data as string) as ServerMessage | |
| for (const handler of conn.handlers) { | |
| handler(msg) | |
| } | |
| } catch { | |
| // Ignore malformed messages | |
| } | |
| } | |
| ws.onclose = () => { | |
| this.stopPingLoop(sessionId) | |
| if (!conn.intentionalClose && this.connections.get(sessionId) === conn) { | |
| this.scheduleReconnect(sessionId, conn) | |
| } | |
| } | |
| ws.onerror = () => { | |
| // onclose will fire after onerror | |
| } | |
| } | |
| disconnect(sessionId: string) { | |
| const conn = this.connections.get(sessionId) | |
| if (!conn) return | |
| conn.intentionalClose = true | |
| this.stopPingLoop(sessionId) | |
| if (conn.reconnectTimer) { | |
| clearTimeout(conn.reconnectTimer) | |
| conn.reconnectTimer = null | |
| } | |
| conn.pendingMessages = [] | |
| conn.ws.close() | |
| this.connections.delete(sessionId) | |
| } | |
| disconnectAll() { | |
| for (const sessionId of [...this.connections.keys()]) { | |
| this.disconnect(sessionId) | |
| } | |
| } | |
| send(sessionId: string, message: ClientMessage) { | |
| let conn = this.connections.get(sessionId) | |
| if (!conn) { | |
| this.connect(sessionId) | |
| conn = this.connections.get(sessionId) | |
| if (!conn) return | |
| } | |
| if (conn.ws.readyState === WebSocket.OPEN) { | |
| conn.ws.send(JSON.stringify(message)) | |
| return | |
| } | |
| conn.pendingMessages.push(message) | |
| if ( | |
| conn.ws.readyState === WebSocket.CLOSED || | |
| conn.ws.readyState === WebSocket.CLOSING | |
| ) { | |
| if (!conn.intentionalClose && !conn.reconnectTimer) { | |
| this.scheduleReconnect(sessionId, conn) | |
| } | |
| } | |
| } | |
| onMessage(sessionId: string, handler: MessageHandler): () => void { | |
| const conn = this.connections.get(sessionId) | |
| if (!conn) return () => {} | |
| conn.handlers.add(handler) | |
| return () => { conn.handlers.delete(handler) } | |
| } | |
| clearHandlers(sessionId: string) { | |
| const conn = this.connections.get(sessionId) | |
| if (conn) conn.handlers.clear() | |
| } | |
| private startPingLoop(sessionId: string) { | |
| this.stopPingLoop(sessionId) | |
| const conn = this.connections.get(sessionId) | |
| if (!conn) return | |
| conn.pingInterval = setInterval(() => { | |
| this.send(sessionId, { type: 'ping' }) | |
| }, 30_000) | |
| } | |
| private stopPingLoop(sessionId: string) { | |
| const conn = this.connections.get(sessionId) | |
| if (conn?.pingInterval) { | |
| clearInterval(conn.pingInterval) | |
| conn.pingInterval = null | |
| } | |
| } | |
| private scheduleReconnect(sessionId: string, conn: Connection) { | |
| if (conn.reconnectTimer) { | |
| clearTimeout(conn.reconnectTimer) | |
| } | |
| const delay = Math.min(1000 * 2 ** conn.reconnectAttempt, 30_000) | |
| conn.reconnectAttempt++ | |
| conn.reconnectTimer = setTimeout(() => { | |
| if (this.connections.get(sessionId) === conn && !conn.intentionalClose) { | |
| conn.reconnectTimer = null | |
| this.connect(sessionId) | |
| } | |
| }, delay) | |
| } | |
| } | |
| export function buildSessionWebSocketUrl(sessionId: string) { | |
| const url = new URL(getBaseUrl()) | |
| url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:' | |
| const basePath = url.pathname === '/' ? '' : url.pathname.replace(/\/$/, '') | |
| url.pathname = `${basePath}/ws/${encodeURIComponent(sessionId)}` | |
| const token = getAuthToken() | |
| if (token) { | |
| url.searchParams.set('token', token) | |
| } else { | |
| url.searchParams.delete('token') | |
| } | |
| return url.toString() | |
| } | |
| export const wsManager = new WebSocketManager() | |