blob: 0d153dc8b16b1d775c8714d9bc193a4b1fbb87cd [file] [log] [blame]
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +00001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050018import { WebSocketMessage, WebSocketMessageTable, WebSocketMessageType } from 'jami-web-common';
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000019import { createContext, useCallback, useEffect, useRef, useState } from 'react';
20
21import { apiUrl } from '../utils/constants';
22import { WithChildren } from '../utils/utils';
23import { useAuthContext } from './AuthProvider';
24
Misha Krieger-Raynauld20cf1c82022-11-23 20:26:50 -050025type WebSocketCallback<T extends WebSocketMessageType> = (data: WebSocketMessageTable[T]) => void;
26
27type WebSocketCallbacks = {
28 [key in WebSocketMessageType]: Set<WebSocketCallback<key>>;
29};
30
31const buildWebSocketCallbacks = (): WebSocketCallbacks => {
32 const webSocketCallback = {} as WebSocketCallbacks;
33 for (const messageType of Object.values(WebSocketMessageType)) {
34 webSocketCallback[messageType] = new Set<WebSocketCallback<typeof messageType>>();
35 }
36 return webSocketCallback;
37};
38
simona5c54ef2022-11-18 05:26:06 -050039type BindFunction = <T extends WebSocketMessageType>(
40 type: T,
41 callback: (data: WebSocketMessageTable[T]) => void
42) => void;
43type SendFunction = <T extends WebSocketMessageType>(type: T, data: WebSocketMessageTable[T]) => void;
44
Issam E. Maghni0432cb72022-11-12 06:09:26 +000045export interface IWebSocketContext {
simona5c54ef2022-11-18 05:26:06 -050046 bind: BindFunction;
47 unbind: BindFunction;
48 send: SendFunction;
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000049}
50
51export const WebSocketContext = createContext<IWebSocketContext | undefined>(undefined);
52
53export default ({ children }: WithChildren) => {
54 const [isConnected, setIsConnected] = useState(false);
55 const webSocketRef = useRef<WebSocket>();
simona5c54ef2022-11-18 05:26:06 -050056 const callbacksRef = useRef<WebSocketCallbacks>(buildWebSocketCallbacks());
Issam E. Maghnib2d465c2022-11-27 18:57:03 +000057 const reconnectionTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000058
59 const { token: accessToken } = useAuthContext();
60
simona5c54ef2022-11-18 05:26:06 -050061 const bind: BindFunction = useCallback((type, callback) => {
62 const callbacks = callbacksRef.current[type];
63 callbacks.add(callback);
64 }, []);
65
66 const unbind: BindFunction = useCallback((type, callback) => {
67 const callbacks = callbacksRef.current[type];
68 callbacks.delete(callback);
69 }, []);
70
71 const send: SendFunction = useCallback(
72 (type, data) => {
73 if (isConnected) {
74 webSocketRef.current?.send(JSON.stringify({ type, data }));
75 }
76 },
77 [isConnected]
78 );
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000079
Issam E. Maghni0432cb72022-11-12 06:09:26 +000080 const connect = useCallback(() => {
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000081 const url = new URL(apiUrl);
82 url.protocol = 'ws:';
83 url.searchParams.set('accessToken', accessToken);
84
85 const webSocket = new WebSocket(url);
Issam E. Maghni0432cb72022-11-12 06:09:26 +000086
Issam E. Maghnib2d465c2022-11-27 18:57:03 +000087 const close = (reconnect = false) => {
Issam E. Maghni0432cb72022-11-12 06:09:26 +000088 console.debug('WebSocket disconnected');
89 setIsConnected(false);
90 for (const callbacks of Object.values(callbacksRef.current)) {
simona5c54ef2022-11-18 05:26:06 -050091 callbacks.clear();
Issam E. Maghni0432cb72022-11-12 06:09:26 +000092 }
Issam E. Maghnib2d465c2022-11-27 18:57:03 +000093 if (reconnect) {
94 reconnectionTimeoutRef.current = setTimeout(connect, 2000);
95 }
Issam E. Maghni0432cb72022-11-12 06:09:26 +000096 };
97
Issam E. Maghnib2d465c2022-11-27 18:57:03 +000098 webSocket.onopen = () => {
99 console.debug('WebSocket connected');
100 setIsConnected(true);
101 };
102
103 webSocket.onclose = () => close(true);
104
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500105 webSocket.onmessage = <T extends WebSocketMessageType>(event: MessageEvent<string>) => {
106 const messageString = event.data;
107 console.debug('WebSocket received message', messageString);
108
109 const message: WebSocketMessage<T> = JSON.parse(messageString);
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000110 if (!message.type || !message.data) {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500111 console.warn('WebSocket message is not a valid WebSocketMessage (missing type or data fields)');
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000112 return;
113 }
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500114
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000115 if (!Object.values(WebSocketMessageType).includes(message.type)) {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500116 console.warn(`Invalid WebSocket message type: ${message.type}`);
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000117 return;
118 }
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500119
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000120 const callbacks = callbacksRef.current[message.type];
121 for (const callback of callbacks) {
122 callback(message.data);
123 }
124 };
125
126 webSocket.onerror = (event: Event) => {
Issam E. Maghnib2d465c2022-11-27 18:57:03 +0000127 console.error('WebSocket errored', event);
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000128 };
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000129
130 webSocketRef.current = webSocket;
131
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000132 return () => {
Issam E. Maghnib2d465c2022-11-27 18:57:03 +0000133 // Cancel any previous reconnection attempt
134 if (reconnectionTimeoutRef.current !== undefined) {
135 clearTimeout(reconnectionTimeoutRef.current);
136 reconnectionTimeoutRef.current = undefined;
137 }
138
139 // Setup a closure without reconnection
140 webSocket.onclose = () => close();
141
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000142 switch (webSocket.readyState) {
143 case webSocket.CONNECTING:
144 webSocket.onopen = () => webSocket.close();
145 break;
146 case webSocket.OPEN:
147 webSocket.close();
148 break;
149 }
150 };
151 }, [accessToken]);
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000152
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000153 useEffect(connect, [connect]);
154
simonf929a362022-11-18 16:53:45 -0500155 const value: IWebSocketContext | undefined = isConnected
156 ? {
157 bind,
158 unbind,
159 send,
simona5c54ef2022-11-18 05:26:06 -0500160 }
simonf929a362022-11-18 16:53:45 -0500161 : undefined;
162
163 return <WebSocketContext.Provider value={value}>{children}</WebSocketContext.Provider>;
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000164};