@qoretechnologies/reqraft 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ import { forEach } from 'lodash';
2
+ import shortid from 'shortid';
3
+ import { fetchConfig, query } from './fetch';
4
+
5
+ export interface IReqraftWebSocketConfig {
6
+ url: string;
7
+ reconnect?: boolean;
8
+ reconnectInterval?: number;
9
+ maxReconnectTries?: number;
10
+ useHeartbeat?: boolean;
11
+ onOpen?: (ev?: Event) => void;
12
+ onMessage?: (ev: MessageEvent) => void;
13
+ onClose?: (ev?: CloseEvent) => void;
14
+ onError?: (ev?: Event) => void;
15
+ onReconnecting?: (reconnectNumber?: number) => void;
16
+ onReconnectFailed?: () => void;
17
+ }
18
+
19
+ export class ReqraftWebSocketsManager {
20
+ public static defaultConfig: IReqraftWebSocketConfig = {
21
+ reconnect: true,
22
+ url: '',
23
+ maxReconnectTries: 10,
24
+ reconnectInterval: 5000,
25
+ useHeartbeat: true,
26
+ };
27
+ public static connections: Record<
28
+ string,
29
+ { socket: WebSocket; using: number; heartbeat?: NodeJS.Timeout }
30
+ > = {};
31
+
32
+ public static closeAll() {
33
+ forEach(this.connections, (connection) => {
34
+ connection.socket.close(4999);
35
+ });
36
+ }
37
+
38
+ public static addHandler(
39
+ url: string,
40
+ event: keyof WebSocketEventMap,
41
+ handler: (ev: Event | MessageEvent | CloseEvent) => void
42
+ ) {
43
+ this.connections[url]?.socket.addEventListener(event, handler);
44
+
45
+ return () => {
46
+ this.connections[url]?.socket.removeEventListener(event, handler);
47
+ };
48
+ }
49
+
50
+ public static removeHandler(
51
+ url: string,
52
+ event: keyof WebSocketEventMap,
53
+ handler: (ev: Event | MessageEvent | CloseEvent) => void
54
+ ) {
55
+ this.connections[url]?.socket.removeEventListener(event, handler);
56
+ }
57
+ }
58
+
59
+ export class ReqraftWebSocket {
60
+ public isConnected: boolean;
61
+ public reconnectTries: number = 0;
62
+ public reconnectInterval: NodeJS.Timeout;
63
+ public options: IReqraftWebSocketConfig;
64
+ public readonly handlers: Record<
65
+ string,
66
+ { type: keyof WebSocketEventMap; event: (ev: Event) => void }
67
+ > = {};
68
+ private socket: WebSocket;
69
+
70
+ constructor(config: IReqraftWebSocketConfig) {
71
+ this.isConnected = true;
72
+ this.reconnectTries = 0;
73
+ this.reconnectInterval = null;
74
+ this.options = { ...ReqraftWebSocketsManager.defaultConfig, ...config };
75
+
76
+ this.connect();
77
+ }
78
+
79
+ public addHandler(
80
+ event: keyof WebSocketEventMap,
81
+ handler: (ev: Event | MessageEvent | CloseEvent) => void
82
+ ) {
83
+ const id = shortid.generate();
84
+
85
+ this.handlers[id] = { type: event, event: handler };
86
+
87
+ ReqraftWebSocketsManager.addHandler(this.options.url, event, handler);
88
+
89
+ return id;
90
+ }
91
+
92
+ public removeHandler(id: string) {
93
+ ReqraftWebSocketsManager.removeHandler(
94
+ this.options.url,
95
+ this.handlers[id].type,
96
+ this.handlers[id].event
97
+ );
98
+
99
+ delete this.handlers[id];
100
+ }
101
+
102
+ private getSocketUrl() {
103
+ let wsUrl = fetchConfig.instance.replace('http', 'ws');
104
+
105
+ if (wsUrl.endsWith('/')) {
106
+ wsUrl = wsUrl.slice(0, -1);
107
+ }
108
+
109
+ return `${wsUrl}/${this.options.url}?token=${fetchConfig.instanceToken}`;
110
+ }
111
+
112
+ public connect() {
113
+ if (!ReqraftWebSocketsManager.connections[this.options.url]) {
114
+ this.socket = new WebSocket(this.getSocketUrl());
115
+ this.socket.onopen = (ev) => {
116
+ this.reconnectTries = 0;
117
+ this.isConnected = true;
118
+
119
+ clearInterval(this.reconnectInterval);
120
+
121
+ this.reconnectInterval = undefined;
122
+
123
+ if (this.options.useHeartbeat) {
124
+ this.startHeartbeat();
125
+ }
126
+
127
+ this.options?.onOpen?.(ev);
128
+ };
129
+
130
+ ReqraftWebSocketsManager.connections[this.options.url] = {
131
+ socket: this.socket,
132
+ using: 0,
133
+ };
134
+ } else {
135
+ this.socket = ReqraftWebSocketsManager.connections[this.options.url].socket;
136
+ this.options?.onOpen?.(null);
137
+ }
138
+
139
+ // Increment the number of parts using this connection
140
+ ReqraftWebSocketsManager.connections[this.options.url].using++;
141
+
142
+ if (this.options?.onMessage) {
143
+ this.addHandler('message', (event) => {
144
+ if ((<MessageEvent>event).data === 'pong') {
145
+ return;
146
+ }
147
+
148
+ this.options?.onMessage?.(<MessageEvent>event);
149
+ });
150
+ }
151
+
152
+ this.addHandler('close', (event) => {
153
+ this.options?.onClose?.(<CloseEvent>event);
154
+ this.stopHeartbeat();
155
+ this.removeAllHandlers();
156
+
157
+ delete ReqraftWebSocketsManager.connections[this.options.url];
158
+
159
+ // Start the reconnect process
160
+ this.maybeReconnect((<CloseEvent>event).code);
161
+ });
162
+
163
+ this.addHandler('error', (event) => {
164
+ this.options?.onError?.(event);
165
+ });
166
+ }
167
+
168
+ public send(data: string) {
169
+ this.socket.send(data);
170
+ }
171
+
172
+ public async checkServerStatus() {
173
+ const check = await query({ url: '/system/pid', cache: false });
174
+
175
+ if (check.code === 401) {
176
+ // Qorus is back up again but we need to re-authenticate
177
+ // Get the current pathname and redirect to the login page
178
+ window.location.href = fetchConfig.unauthorizedRedirect(window.location.pathname);
179
+ }
180
+ }
181
+
182
+ private async tryReconnect() {
183
+ this.reconnectTries++;
184
+ this.options?.onReconnecting?.(this.reconnectTries);
185
+
186
+ await this.checkServerStatus();
187
+
188
+ this.connect();
189
+ }
190
+
191
+ public maybeReconnect(closeCode: number) {
192
+ if (this.options.reconnect && closeCode !== 4999) {
193
+ // If this is the first reconnect try
194
+ if (this.reconnectTries === 0) {
195
+ this.tryReconnect();
196
+
197
+ return;
198
+ }
199
+
200
+ // If we haven't reached the max number of reconnect tries
201
+ if (this.reconnectTries < this.options.maxReconnectTries) {
202
+ this.reconnectInterval = setTimeout(
203
+ this.tryReconnect.bind(this),
204
+ this.options.reconnectInterval
205
+ );
206
+
207
+ return;
208
+ }
209
+
210
+ // Reconnect failed
211
+ this.isConnected = false;
212
+ this.reconnectInterval = undefined;
213
+ this.reconnectTries = 0;
214
+
215
+ this.options.onReconnectFailed?.();
216
+ }
217
+ }
218
+
219
+ public remove() {
220
+ if (!ReqraftWebSocketsManager.connections[this.options.url]) return;
221
+ // Decrement the number of parts using this connection
222
+ ReqraftWebSocketsManager.connections[this.options.url].using--;
223
+ // If this is the last part using the connection, close it
224
+ if (ReqraftWebSocketsManager.connections[this.options.url].using === 0) {
225
+ this.close();
226
+ return;
227
+ }
228
+
229
+ // Remove all handlers
230
+ this.removeAllHandlers();
231
+ }
232
+
233
+ public close() {
234
+ this.socket.close(4999);
235
+ }
236
+
237
+ public removeAllHandlers() {
238
+ forEach(this.handlers, (_handler, id) => {
239
+ this.removeHandler(id);
240
+ });
241
+ }
242
+
243
+ private startHeartbeat() {
244
+ // Start the heartbeat
245
+ clearInterval(ReqraftWebSocketsManager.connections[this.options.url].heartbeat);
246
+ ReqraftWebSocketsManager.connections[this.options.url].heartbeat = null;
247
+ ReqraftWebSocketsManager.connections[this.options.url].heartbeat = setInterval(() => {
248
+ this.socket.send('ping');
249
+ }, 3000);
250
+ }
251
+
252
+ private stopHeartbeat() {
253
+ if (!ReqraftWebSocketsManager.connections[this.options.url]) return;
254
+
255
+ clearInterval(ReqraftWebSocketsManager.connections[this.options.url].heartbeat);
256
+ ReqraftWebSocketsManager.connections[this.options.url].heartbeat = null;
257
+ }
258
+ }