| 31 | } |
| 32 | |
| 33 | export class WsUpstream implements Upstream { |
| 34 | private ws: WebSocket | null = null; |
| 35 | private connected = false; |
| 36 | private closed = false; |
| 37 | private reconnectTimer: ReturnType<typeof setTimeout> | null = null; |
| 38 | private config: Config = DEFAULT_CONFIG; |
| 39 | private aircraft = new Map<string, Aircraft>(); |
| 40 | |
| 41 | constructor( |
| 42 | private url: string, |
| 43 | private events: UpstreamEvents = {}, |
| 44 | ) {} |
| 45 | |
| 46 | start(): void { |
| 47 | this.closed = false; |
| 48 | this.open(); |
| 49 | } |
| 50 | |
| 51 | stop(): void { |
| 52 | this.closed = true; |
| 53 | if (this.reconnectTimer) clearTimeout(this.reconnectTimer); |
| 54 | this.ws?.close(); |
| 55 | } |
| 56 | |
| 57 | isConnected(): boolean { |
| 58 | return this.connected; |
| 59 | } |
| 60 | getConfig(): Config { |
| 61 | return this.config; |
| 62 | } |
| 63 | getAircraft(): Aircraft[] { |
| 64 | return [...this.aircraft.values()]; |
| 65 | } |
| 66 | find(hex: string): Aircraft | undefined { |
| 67 | return this.aircraft.get(hex); |
| 68 | } |
| 69 | |
| 70 | patchConfig(patch: Partial<Config>): void { |
| 71 | const msg: ClientMessage = { type: "patchConfig", patch }; |
| 72 | if (this.ws?.readyState === WebSocket.OPEN) { |
| 73 | this.ws.send(JSON.stringify(msg)); |
| 74 | } |
| 75 | // Optimistic local merge so the tracker reacts immediately. |
| 76 | this.config = mergeConfig(this.config, patch); |
| 77 | this.events.onConfig?.(this.config); |
| 78 | } |
| 79 | |
| 80 | private open(): void { |
| 81 | const ws = new WebSocket(this.url); |
| 82 | this.ws = ws; |
| 83 | ws.on("open", () => { |
| 84 | this.connected = true; |
| 85 | ws.send(JSON.stringify({ type: "hello", role: "control" } satisfies ClientMessage)); |
| 86 | }); |
| 87 | ws.on("close", () => { |
| 88 | this.connected = false; |
| 89 | this.scheduleReconnect(); |
| 90 | }); |
nothing calls this directly
no outgoing calls
no test coverage detected