|
| 1 | +/** |
| 2 | + * @file A WebSocket that can only receive messages from the server, and cannot |
| 3 | + * ever send anything. |
| 4 | + * |
| 5 | + * This should ALWAYS be favored in favor of using Server-Sent Events and the |
| 6 | + * built-in EventSource class for doing one-way communication. SSEs have a hard |
| 7 | + * limitation on HTTP/1.1 and below where there is a maximum number of 6 ports |
| 8 | + * that can ever be used for a domain (sometimes less depending on the browser). |
| 9 | + * Not only is this limit shared with short-lived REST requests, but it also |
| 10 | + * applies across tabs and windows. So if a user opens Coder in multiple tabs, |
| 11 | + * there is a very real possibility that parts of the app will start to lock up |
| 12 | + * without it being clear why. |
| 13 | + * |
| 14 | + * WebSockets do not have this limitation, even on HTTP/1.1 – all modern |
| 15 | + * browsers implement at least some degree of multiplexing for them. This file |
| 16 | + * just provides a wrapper to make it harder to use WebSockets for two-way |
| 17 | + * communication by accident. |
| 18 | + */ |
| 19 | + |
| 20 | +// Not bothering with trying to borrow methods from the base WebSocket type |
| 21 | +// because it's a mess of inheritance and generics. |
| 22 | +type WebSocketEventType = "close" | "error" | "message" | "open"; |
| 23 | + |
| 24 | +type WebSocketEventPayloadMap = { |
| 25 | + close: CloseEvent; |
| 26 | + error: Event; |
| 27 | + message: MessageEvent; |
| 28 | + open: Event; |
| 29 | +}; |
| 30 | + |
| 31 | +// The generics need to apply on a per-method-invocation basis; they cannot be |
| 32 | +// generalized to the top-level interface definition |
| 33 | +interface OneWayWebSocketApi { |
| 34 | + addEventListener: <T extends WebSocketEventType>( |
| 35 | + eventType: T, |
| 36 | + callback: (payload: WebSocketEventPayloadMap[T]) => void, |
| 37 | + ) => void; |
| 38 | + |
| 39 | + removeEventListener: <T extends WebSocketEventType>( |
| 40 | + eventType: T, |
| 41 | + callback: (payload: WebSocketEventPayloadMap[T]) => void, |
| 42 | + ) => void; |
| 43 | + |
| 44 | + close: (closeCode?: number, reason?: string) => void; |
| 45 | +} |
| 46 | + |
| 47 | +// Implementing wrapper around the base WebSocket class instead of doing fancy |
| 48 | +// compile-time type-casts so that we have more runtime assurance that we won't |
| 49 | +// accidentally send a message from the client to the server |
| 50 | +export class OneWayWebSocket implements OneWayWebSocketApi { |
| 51 | + #socket: WebSocket; |
| 52 | + |
| 53 | + constructor(url: string | URL, protocols?: string | string[]) { |
| 54 | + this.#socket = new WebSocket(url, protocols); |
| 55 | + } |
| 56 | + |
| 57 | + // Just because this is a React project, all public methods are defined as |
| 58 | + // arrow functions so they can be passed around as values without losing |
| 59 | + // their `this` context |
| 60 | + addEventListener = <T extends WebSocketEventType>( |
| 61 | + message: T, |
| 62 | + callback: (payload: WebSocketEventPayloadMap[T]) => void, |
| 63 | + ): void => { |
| 64 | + this.#socket.addEventListener(message, callback); |
| 65 | + }; |
| 66 | + |
| 67 | + removeEventListener = <T extends WebSocketEventType>( |
| 68 | + message: T, |
| 69 | + callback: (payload: WebSocketEventPayloadMap[T]) => void, |
| 70 | + ): void => { |
| 71 | + this.#socket.removeEventListener(message, callback); |
| 72 | + }; |
| 73 | + |
| 74 | + close = (closeCode?: number, reason?: string): void => { |
| 75 | + this.#socket.close(closeCode, reason); |
| 76 | + }; |
| 77 | +} |
0 commit comments