* Wire WebSocket → PTY (input, resize, ping). * On close/error, start the grace period instead of immediately destroying * the session — this keeps the PTY alive for reconnection. * Called once per WebSocket connection (safe to call again on reconnect).
(token: string, ws: WebSocket, pty: IPty)
| 271 | * Called once per WebSocket connection (safe to call again on reconnect). |
| 272 | */ |
| 273 | private wireWsEvents(token: string, ws: WebSocket, pty: IPty): void { |
| 274 | ws.on("message", (data: Buffer | string) => { |
| 275 | const str = data.toString(); |
| 276 | if (str.startsWith("{")) { |
| 277 | try { |
| 278 | const msg = JSON.parse(str) as Record<string, unknown>; |
| 279 | if ( |
| 280 | msg.type === "resize" && |
| 281 | typeof msg.cols === "number" && |
| 282 | typeof msg.rows === "number" |
| 283 | ) { |
| 284 | pty.resize(msg.cols as number, msg.rows as number); |
| 285 | return; |
| 286 | } |
| 287 | if (msg.type === "ping") { |
| 288 | if (ws.readyState === 1 /* OPEN */) { |
| 289 | ws.send(JSON.stringify({ type: "pong" })); |
| 290 | } |
| 291 | return; |
| 292 | } |
| 293 | } catch { |
| 294 | // Not JSON — treat as terminal input |
| 295 | } |
| 296 | } |
| 297 | pty.write(str); |
| 298 | }); |
| 299 | |
| 300 | const handleClose = () => { |
| 301 | console.log(`[session ${token.slice(0, 8)}] WebSocket closed`); |
| 302 | const session = this.store.get(token); |
| 303 | // Only start grace if this WS is still the one attached to the session |
| 304 | if (session && session.ws === ws) { |
| 305 | this.store.startGrace(token, () => { |
| 306 | /* logged inside startGrace */ |
| 307 | }); |
| 308 | } |
| 309 | }; |
| 310 | |
| 311 | ws.on("close", handleClose); |
| 312 | ws.on("error", (err) => { |
| 313 | console.error(`[session ${token.slice(0, 8)}] WebSocket error:`, err.message); |
| 314 | handleClose(); |
| 315 | }); |
| 316 | } |
| 317 | |
| 318 | /** |
| 319 | * Force-kill a session immediately (used by the REST API). |