Skip to content

Commit bb96c7f

Browse files
committed
chore: add OneWayWebSocket class
1 parent 10326b4 commit bb96c7f

File tree

1 file changed

+77
-0
lines changed

1 file changed

+77
-0
lines changed

site/src/utils/OneWayWebSocket.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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

Comments
 (0)