-
Notifications
You must be signed in to change notification settings - Fork 133
/
Copy pathindex.ts
206 lines (196 loc) · 6.78 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { type Accessor, onCleanup, createSignal } from "solid-js";
export type WSMessage = string | ArrayBufferLike | ArrayBufferView | Blob;
/**
* opens a web socket connection with a queued send
* ```ts
* const ws = makeWS("ws://localhost:5000");
* createEffect(() => ws.send(serverMessage()));
* onCleanup(() => ws.close());
* ```
* Will not throw if you attempt to send messages before the connection opened; instead, it will enqueue the message to be sent when the connection opens.
*
* It will not close the connection on cleanup. To do that, use `createWS`.
*/
export const makeWS = (
url: string,
protocols?: string | string[],
sendQueue: WSMessage[] = [],
): WebSocket => {
const ws: WebSocket = new WebSocket(url, protocols);
const _send = ws.send.bind(ws);
ws.send = (msg: WSMessage) => (ws.readyState == 1 ? _send(msg) : sendQueue.push(msg));
ws.addEventListener("open", () => {
while (sendQueue.length) _send(sendQueue.shift()!);
});
return ws;
};
/**
* opens a web socket connection with a queued send that closes on cleanup
* ```ts
* const ws = makeWS("ws://localhost:5000");
* createEffect(() => ws.send(serverMessage()));
* ```
* Will not throw if you attempt to send messages before the connection opened; instead, it will enqueue the message to be sent when the connection opens.
*/
export const createWS = (url: string, protocols?: string | string[]): WebSocket => {
const ws = makeWS(url, protocols);
onCleanup(() => ws.close());
return ws;
};
/**
* Returns a reactive state signal for the web socket's readyState:
*
* WebSocket.CONNECTING = 0
* WebSocket.OPEN = 1
* WebSocket.CLOSING = 2
* WebSocket.CLOSED = 3
*
* ```ts
* const ws = createWS('ws://localhost:5000');
* const state = createWSState(ws);
* const states = ["Connecting", "Open", "Closing", "Closed"] as const;
* return <div>{states[state()]}</div>
* ```
*/
export const createWSState = (ws: WebSocket): Accessor<0 | 1 | 2 | 3> => {
const [state, setState] = createSignal(ws.readyState as 0 | 1 | 2 | 3);
const _close = ws.close.bind(ws);
ws.addEventListener("open", () => setState(1));
ws.close = (...args) => {
_close(...args);
setState(2);
};
ws.addEventListener("close", () => setState(3));
return state;
};
export type WSReconnectOptions = {
delay?: number;
retries?: number;
};
export type ReconnectingWebSocket = WebSocket & {
reconnect: () => void;
/** required for the heartbeat implementation; do not overwrite if you want to use this with heartbeat */
send: WebSocket["send"] & { before?: () => void };
};
/**
* Returns a WebSocket-like object that under the hood opens new connections on disconnect:
* ```ts
* const ws = makeReconnectingWS("ws:localhost:5000");
* createEffect(() => ws.send(serverMessage()));
* onCleanup(() => ws.close());
* ```
* Will not throw if you attempt to send messages before the connection opened; instead, it will enqueue the message to be sent when the connection opens.
*
* It will not close the connection on cleanup. To do that, use `createReconnectingWS`.
*/
export const makeReconnectingWS = (
url: string,
protocols?: string | string[],
options: WSReconnectOptions = {},
) => {
let retries = options.retries || Infinity;
let ws: ReconnectingWebSocket;
const queue: WSMessage[] = [];
let events: Parameters<WebSocket["addEventListener"]>[] = [
[
"close",
() => {
retries-- > 0 && setTimeout(getWS, options.delay || 3000);
},
],
];
const getWS = () => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ws && ws.readyState < 2) ws.close();
ws = Object.assign(makeWS(url, protocols, queue), {
reconnect: getWS,
});
events.forEach(args => ws.addEventListener(...args));
};
getWS();
const wws: Partial<ReconnectingWebSocket> = {
close: (...args: Parameters<WebSocket["close"]>) => {
retries = 0;
return ws.close(...args);
},
addEventListener: (...args: Parameters<WebSocket["addEventListener"]>) => {
events.push(args);
return ws.addEventListener(...args);
},
removeEventListener: (...args: Parameters<WebSocket["removeEventListener"]>) => {
events = events.filter(ev => args[0] !== ev[0] || args[1] !== ev[1]);
return ws.removeEventListener(...args);
},
send: (msg: WSMessage) => {
wws.send!.before?.();
return ws.send(msg);
},
};
for (const name in ws!)
wws[name as keyof typeof wws] == null &&
Object.defineProperty(wws, name, {
enumerable: true,
get: () =>
typeof ws[name as keyof typeof ws] === "function"
? (ws[name as keyof typeof ws] as Function).bind(ws)
: ws[name as keyof typeof ws],
});
return wws as ReconnectingWebSocket;
};
/**
* Returns a WebSocket-like object that under the hood opens new connections on disconnect and closes on cleanup:
* ```ts
* const ws = makeReconnectingWS("ws:localhost:5000");
* createEffect(() => ws.send(serverMessage()));
* ```
* Will not throw if you attempt to send messages before the connection opened; instead, it will enqueue the message to be sent when the connection opens.
*/
export const createReconnectingWS: typeof makeReconnectingWS = (url, protocols, options) => {
const ws = makeReconnectingWS(url, protocols, options);
onCleanup(() => ws.close());
return ws;
};
export type WSHeartbeatOptions = {
/**
* Heartbeat message being sent to the server in order to validate the connection
* @default "ping"
*/
message?: WSMessage;
/**
* The time between messages being sent in milliseconds
* @default 1000
*/
interval?: number;
/**
* The time after the heartbeat message being sent to wait for the next message in milliseconds
* @default 1500
*/
wait?: number;
};
/**
* Wraps a reconnecting WebSocket to send a heartbeat to check the connection
* ```ts
* const ws = makeHeartbeatWS(createReconnectingWS('ws://localhost:5000'))
* ```
* Dispatches a close event to initiate the reconnection of the defunct web socket.
*/
export const makeHeartbeatWS = (
ws: ReconnectingWebSocket,
options: WSHeartbeatOptions = {},
): WebSocket & { reconnect: () => void } => {
let pingtimer: ReturnType<typeof setTimeout> | undefined;
let pongtimer: ReturnType<typeof setTimeout> | undefined;
const clearTimers = () => (clearTimeout(pingtimer), clearTimeout(pongtimer));
ws.send.before = () => {
clearTimers();
pongtimer = setTimeout(ws.reconnect, options.wait || 1500);
};
const receiveMessage = () => {
clearTimers();
pingtimer = setTimeout(() => ws.send(options.message || "ping"), options.interval || 1000);
};
ws.addEventListener("close", clearTimers);
ws.addEventListener("message", receiveMessage);
ws.addEventListener("open", () => setTimeout(receiveMessage, options.interval || 1000));
return ws;
};