| 52 | } |
| 53 | |
| 54 | export class RuntimeClient { |
| 55 | private socket: WebSocket | null = null |
| 56 | private nextId = 1 |
| 57 | private retryMs = 1000 |
| 58 | private stopped = false |
| 59 | private connecting: Promise<void> | null = null |
| 60 | private lastStatus: RuntimeClientStatus | null = null |
| 61 | private lastCursor: string | null = null |
| 62 | private readonly pending = new Map<RuntimeRequestId, { resolve: (value: unknown) => void; reject: (error: Error) => void }>() |
| 63 | private readonly listeners = new Set<NotificationListener>() |
| 64 | |
| 65 | constructor(private readonly options: RuntimeClientOptions) {} |
| 66 | |
| 67 | async request<T>(method: string, params?: unknown): Promise<T> { |
| 68 | await this.connect() |
| 69 | |
| 70 | const socket = this.socket |
| 71 | if (!socket || socket.readyState !== WebSocket.OPEN) throw new Error("Runtime socket is not connected") |
| 72 | |
| 73 | return this.sendRequest<T>(socket, method, params) |
| 74 | } |
| 75 | |
| 76 | private sendRequest<T>(socket: WebSocket, method: string, params?: unknown): Promise<T> { |
| 77 | const id = this.nextId++ |
| 78 | const message = params === undefined ? { id, method } : { id, method, params } |
| 79 | |
| 80 | return new Promise<T>((resolve, reject) => { |
| 81 | this.pending.set(id, { |
| 82 | resolve: (value) => resolve(value as T), |
| 83 | reject, |
| 84 | }) |
| 85 | try { |
| 86 | socket.send(JSON.stringify(message)) |
| 87 | } catch (error) { |
| 88 | this.pending.delete(id) |
| 89 | reject(error instanceof Error ? error : new Error("Runtime socket send failed")) |
| 90 | } |
| 91 | }) |
| 92 | } |
| 93 | |
| 94 | subscribe(listener: NotificationListener): () => void { |
| 95 | this.listeners.add(listener) |
| 96 | this.connectQuietly() |
| 97 | return () => { |
| 98 | this.listeners.delete(listener) |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | close(): void { |
| 103 | this.stopped = true |
| 104 | this.socket?.close() |
| 105 | this.socket = null |
| 106 | for (const pending of this.pending.values()) { |
| 107 | pending.reject(new Error("Runtime socket closed")) |
| 108 | } |
| 109 | this.pending.clear() |
| 110 | this.emitStatus("disconnected") |
| 111 | } |
nothing calls this directly
no outgoing calls
no test coverage detected