| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- /** Host-side WebSocket carrier for the two server-to-browser event streams. */
- import { randomUUID } from 'node:crypto'
- import type { IncomingMessage } from 'node:http'
- import type { Duplex } from 'node:stream'
- import WebSocket, { WebSocketServer } from 'ws'
- import type {
- ApiProxy, HostFrame, MuxFrame, RpcRequest, ServerRequest,
- } from '@deepseek-ai/dsh-host-apiproxy/api'
- import { RpcId } from '@deepseek-ai/dsh-host-apiproxy/api'
- type Frame = MuxFrame | HostFrame
- function serverRequest(frame: RpcRequest<Frame>): ServerRequest {
- return {
- type: 'server-request',
- rpcId: frame.rpcId,
- method: frame.payload.type,
- payload: frame.payload,
- }
- }
- function send(socket: WebSocket, frame: RpcRequest<Frame>): Promise<void> {
- return new Promise((resolve, reject) => {
- if (socket.readyState !== WebSocket.OPEN) {
- reject(new Error('websocket downlink closed before frame delivery'))
- return
- }
- socket.send(JSON.stringify(serverRequest(frame)), (error) => {
- if (error) reject(error)
- else resolve()
- })
- })
- }
- function failureFrame(error: unknown): RpcRequest<Frame> {
- return {
- rpcId: RpcId(randomUUID()),
- payload: {
- type: 'stream/error',
- error: { code: 'internal', message: String(error), details: {} },
- },
- }
- }
- /**
- * Owns WebSocket negotiation and frame pumping for the connection plugin's
- * two downlinks. Client messages are a protocol violation: upstream traffic
- * remains on HTTP.
- */
- export class WebSocketDownlinks {
- private readonly server = new WebSocketServer({ noServer: true })
- private readonly pumps = new Set<Promise<void>>()
- /** @param api - host API supplying the typed event streams. */
- constructor(private readonly api: ApiProxy) {}
- /**
- * Upgrade one socket and pump the mux stream until either side closes.
- * @param req - HTTP upgrade request.
- * @param socket - Raw socket transferred by the HTTP server.
- * @param head - Bytes already read after the upgrade headers.
- */
- handleMux(req: IncomingMessage, socket: Duplex, head: Buffer): void {
- this.upgrade(req, socket, head, signal => this.api.events.mux({
- rpcId: RpcId(randomUUID()),
- payload: {},
- }, signal))
- }
- /**
- * Upgrade one socket and pump the host stream until either side closes.
- * @param req - HTTP upgrade request.
- * @param socket - Raw socket transferred by the HTTP server.
- * @param head - Bytes already read after the upgrade headers.
- */
- handleHost(req: IncomingMessage, socket: Duplex, head: Buffer): void {
- this.upgrade(req, socket, head, signal => this.api.events.host({
- rpcId: RpcId(randomUUID()),
- payload: {},
- }, signal))
- }
- /**
- * Terminate owned sockets and await the no-server acceptor plus frame pumps.
- * @returns A promise resolving after every socket and source iterator stops.
- */
- async close(): Promise<void> {
- for (const socket of this.server.clients) socket.terminate()
- await new Promise<void>((resolve, reject) => {
- this.server.close((error) => {
- if (error === undefined) resolve()
- else reject(error)
- })
- })
- await Promise.all(this.pumps)
- }
- private upgrade<F extends Frame>(
- req: IncomingMessage,
- socket: Duplex,
- head: Buffer,
- open: (signal: AbortSignal) => AsyncIterable<RpcRequest<F>>,
- ): void {
- this.server.handleUpgrade(req, socket, head, (websocket) => {
- const abort = new AbortController()
- websocket.once('close', () => { abort.abort() })
- websocket.once('error', () => { abort.abort() })
- websocket.once('message', () => {
- websocket.close(1008, 'downlink only')
- })
- const pump = this.pump(websocket, open(abort.signal), abort)
- this.pumps.add(pump)
- void pump.then(() => { this.pumps.delete(pump) })
- })
- }
- private async pump<F extends Frame>(
- socket: WebSocket,
- frames: AsyncIterable<RpcRequest<F>>,
- abort: AbortController,
- ): Promise<void> {
- try {
- for await (const frame of frames) await send(socket, frame)
- } catch (error) {
- if (!abort.signal.aborted) {
- try {
- await send(socket, failureFrame(error))
- } catch {
- // Socket loss won the race; no downstream remains to receive the failure frame.
- }
- }
- } finally {
- abort.abort()
- if (socket.readyState === WebSocket.OPEN) socket.close()
- }
- }
- }
- /**
- * Reject an untrusted upgrade before protocol negotiation.
- * @param socket - Raw HTTP socket that remains owned by the caller.
- */
- export function rejectWebSocketUpgrade(socket: Duplex): void {
- socket.end([
- 'HTTP/1.1 403 Forbidden',
- 'Connection: close',
- 'Content-Type: text/plain; charset=utf-8',
- 'Content-Length: 9',
- '',
- 'forbidden',
- ].join('\r\n'))
- }
|