blob: b90f609029e8b5dc424fa6e61173e3dff677c891 [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 */
simona5c54ef2022-11-18 05:26:06 -050018import {
19 buildWebSocketCallbacks,
20 WebSocketCallbacks,
21 WebSocketMessage,
22 WebSocketMessageTable,
23 WebSocketMessageType,
24} from 'jami-web-common';
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000025import { createContext, useCallback, useEffect, useRef, useState } from 'react';
26
27import { apiUrl } from '../utils/constants';
28import { WithChildren } from '../utils/utils';
29import { useAuthContext } from './AuthProvider';
30
simona5c54ef2022-11-18 05:26:06 -050031type BindFunction = <T extends WebSocketMessageType>(
32 type: T,
33 callback: (data: WebSocketMessageTable[T]) => void
34) => void;
35type SendFunction = <T extends WebSocketMessageType>(type: T, data: WebSocketMessageTable[T]) => void;
36
Issam E. Maghni0432cb72022-11-12 06:09:26 +000037export interface IWebSocketContext {
simona5c54ef2022-11-18 05:26:06 -050038 bind: BindFunction;
39 unbind: BindFunction;
40 send: SendFunction;
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000041}
42
43export const WebSocketContext = createContext<IWebSocketContext | undefined>(undefined);
44
45export default ({ children }: WithChildren) => {
46 const [isConnected, setIsConnected] = useState(false);
47 const webSocketRef = useRef<WebSocket>();
simona5c54ef2022-11-18 05:26:06 -050048 const callbacksRef = useRef<WebSocketCallbacks>(buildWebSocketCallbacks());
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000049
50 const { token: accessToken } = useAuthContext();
51
simona5c54ef2022-11-18 05:26:06 -050052 const bind: BindFunction = useCallback((type, callback) => {
53 const callbacks = callbacksRef.current[type];
54 callbacks.add(callback);
55 }, []);
56
57 const unbind: BindFunction = useCallback((type, callback) => {
58 const callbacks = callbacksRef.current[type];
59 callbacks.delete(callback);
60 }, []);
61
62 const send: SendFunction = useCallback(
63 (type, data) => {
64 if (isConnected) {
65 webSocketRef.current?.send(JSON.stringify({ type, data }));
66 }
67 },
68 [isConnected]
69 );
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000070
Issam E. Maghni0432cb72022-11-12 06:09:26 +000071 const connect = useCallback(() => {
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +000072 const url = new URL(apiUrl);
73 url.protocol = 'ws:';
74 url.searchParams.set('accessToken', accessToken);
75
76 const webSocket = new WebSocket(url);
Issam E. Maghni0432cb72022-11-12 06:09:26 +000077
78 webSocket.onopen = () => {
79 console.debug('WebSocket connected');
80 setIsConnected(true);
81 };
82
83 webSocket.onclose = () => {
84 console.debug('WebSocket disconnected');
85 setIsConnected(false);
86 for (const callbacks of Object.values(callbacksRef.current)) {
simona5c54ef2022-11-18 05:26:06 -050087 callbacks.clear();
Issam E. Maghni0432cb72022-11-12 06:09:26 +000088 }
89 setTimeout(connect, 1000);
90 };
91
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050092 webSocket.onmessage = <T extends WebSocketMessageType>(event: MessageEvent<string>) => {
93 const messageString = event.data;
94 console.debug('WebSocket received message', messageString);
95
96 const message: WebSocketMessage<T> = JSON.parse(messageString);
Issam E. Maghni0432cb72022-11-12 06:09:26 +000097 if (!message.type || !message.data) {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -050098 console.warn('WebSocket message is not a valid WebSocketMessage (missing type or data fields)');
Issam E. Maghni0432cb72022-11-12 06:09:26 +000099 return;
100 }
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500101
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000102 if (!Object.values(WebSocketMessageType).includes(message.type)) {
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500103 console.warn(`Invalid WebSocket message type: ${message.type}`);
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000104 return;
105 }
Misha Krieger-Raynauldb933fbb2022-11-15 15:11:09 -0500106
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000107 const callbacks = callbacksRef.current[message.type];
108 for (const callback of callbacks) {
109 callback(message.data);
110 }
111 };
112
113 webSocket.onerror = (event: Event) => {
114 console.error('Closing WebSocket due to an error:', event);
115 webSocketRef.current?.close();
116 };
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000117
118 webSocketRef.current = webSocket;
119
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000120 return () => {
121 switch (webSocket.readyState) {
122 case webSocket.CONNECTING:
123 webSocket.onopen = () => webSocket.close();
124 break;
125 case webSocket.OPEN:
126 webSocket.close();
127 break;
128 }
129 };
130 }, [accessToken]);
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000131
Issam E. Maghni0432cb72022-11-12 06:09:26 +0000132 useEffect(connect, [connect]);
133
simona5c54ef2022-11-18 05:26:06 -0500134 return (
135 <WebSocketContext.Provider
136 value={
137 isConnected
138 ? {
139 bind,
140 unbind,
141 send,
142 }
143 : undefined
144 }
145 >
146 {children}
147 </WebSocketContext.Provider>
148 );
Issam E. Maghni09a3a1f2022-11-02 04:56:21 +0000149};