Skip to content

Commit

Permalink
Websocket watcher (#517)
Browse files Browse the repository at this point in the history
* add reconnecting websocket
* add common Websocket listener. We need 3items ( context / component / custom hook )
* feat(websocket)!:addlisterner on open when WS connexion open

Signed-off-by: capyq <quentin.capy@rte-france.com>
Co-authored-by: Sylvain Bouzols <sylvain.bouzols@gmail.com>
  • Loading branch information
capyq and sBouzols authored Dec 17, 2024
1 parent 51b33fe commit ce8496a
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 1 deletion.
4 changes: 3 additions & 1 deletion jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const config: Config = {
'^.+\\.(css|less|scss)$': 'identity-obj-proxy',
},
// see https://github.com/react-dnd/react-dnd/issues/3443
transformIgnorePatterns: ['node_modules/(?!react-dnd|dnd-core|@react-dnd)'], // transform from ESM
transformIgnorePatterns: [
'node_modules/(?!react-dnd|dnd-core|@react-dnd)',
], // transform from ESM
globals: {
IS_REACT_ACT_ENVIRONMENT: true,
},
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-querybuilder": "^7.2.0",
"react-virtualized": "^9.22.5",
"reconnecting-websocket": "^4.4.0",
"uuid": "^9.0.1"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './overflowableText';
export * from './snackbarProvider';
export * from './topBar';
export * from './treeViewFinder';
export * from './notifications';
70 changes: 70 additions & 0 deletions src/components/notifications/NotificationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// @author Quentin CAPY

import { PropsWithChildren, useEffect, useMemo } from 'react';
import ReconnectingWebSocket from 'reconnecting-websocket';
import { ListenerEventWS, ListenerOnReopen, NotificationsContext } from './contexts/NotificationsContext';
import { useListenerManager } from './hooks/useListenerManager';

// the delay before we consider the WS truly connected
const DELAY_BEFORE_WEBSOCKET_CONNECTED = 12000;

export type NotificationsProviderProps = { urls: Record<string, string> };
export function NotificationsProvider({ urls, children }: PropsWithChildren<NotificationsProviderProps>) {
const {
broadcast: broadcastMessage,
addListener: addListenerMessage,
removeListener: removeListenerMessage,
} = useListenerManager<ListenerEventWS, MessageEvent>(urls);
const {
broadcast: broadcastOnReopen,
addListener: addListenerOnReopen,
removeListener: removeListenerOnReopen,
} = useListenerManager<ListenerOnReopen, never>(urls);

useEffect(() => {
const connections = Object.keys(urls)
.filter((u) => urls[u] != null)
.map((urlKey) => {
const rws = new ReconnectingWebSocket(() => urls[urlKey], [], {
// this option set the minimum duration being connected before reset the retry count to 0
minUptime: DELAY_BEFORE_WEBSOCKET_CONNECTED,
});

rws.onmessage = broadcastMessage(urlKey);

rws.onclose = (event) => {
console.error(`Unexpected ${urlKey} Notification WebSocket closed`, event);
};
rws.onerror = (event) => {
console.error(`Unexpected ${urlKey} Notification WebSocket error`, event);
};

rws.onopen = () => {
console.info(`${urlKey} Notification Websocket connected`);
broadcastOnReopen(urlKey);
};
return rws;
});

return () => {
connections.forEach((c) => c.close());
};
}, [broadcastMessage, broadcastOnReopen, urls]);

const contextValue = useMemo(
() => ({
addListenerEvent: addListenerMessage,
removeListenerEvent: removeListenerMessage,
addListenerOnReopen,
removeListenerOnReopen,
}),
[addListenerMessage, removeListenerMessage, addListenerOnReopen, removeListenerOnReopen]
);
return <NotificationsContext.Provider value={contextValue}>{children}</NotificationsContext.Provider>;
}
35 changes: 35 additions & 0 deletions src/components/notifications/contexts/NotificationsContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// @author Quentin CAPY

import { createContext } from 'react';

export type ListenerEventWS = {
id: string;
callback: (event: MessageEvent) => void;
};

export type ListenerOnReopen = {
id: string;
callback: () => void;
};

export type NotificationsContextType = {
addListenerEvent: (urlKey: string, l: ListenerEventWS) => void;
removeListenerEvent: (urlKey: string, idListener: string) => void;
addListenerOnReopen: (urlKey: string, l: ListenerOnReopen) => void;
removeListenerOnReopen: (urlKey: string, idListener: string) => void;
};

export type NotificationsContextRecordType = Record<string, NotificationsContextType>;

export const NotificationsContext = createContext<NotificationsContextType>({
addListenerEvent: () => {},
removeListenerEvent: () => {},
addListenerOnReopen: () => {},
removeListenerOnReopen: () => {},
});
59 changes: 59 additions & 0 deletions src/components/notifications/hooks/useListenerManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// @author Quentin CAPY
import { useCallback, useEffect, useRef } from 'react';
import { ListenerEventWS, ListenerOnReopen } from '../contexts/NotificationsContext';

export const useListenerManager = <TListener extends ListenerEventWS | ListenerOnReopen, TMessage extends MessageEvent>(
urls: Record<string, string>
) => {
const urlsListenersRef = useRef<Record<string, TListener[]>>({});

useEffect(() => {
urlsListenersRef.current = Object.keys(urls).reduce((acc, urlKey) => {
acc[urlKey] = urlsListenersRef.current[urlKey] ?? [];
return acc;
}, {} as Record<string, TListener[]>);
}, [urls]);

const addListenerEvent = useCallback((urlKey: string, listener: TListener) => {
const urlsListeners = urlsListenersRef.current;
if (urlKey in urlsListeners) {
urlsListeners[urlKey].push(listener);
} else {
urlsListeners[urlKey] = [listener];
}
urlsListenersRef.current = urlsListeners;
}, []);
const removeListenerEvent = useCallback((urlKey: string, id: string) => {
const listeners = urlsListenersRef.current?.[urlKey];
if (listeners) {
const newListerners = listeners.filter((l) => l.id !== id);
urlsListenersRef.current = {
...urlsListenersRef.current,
[urlKey]: newListerners,
};
}
}, []);
const broadcast = useCallback(
(urlKey: string) => (event: TMessage) => {
const listeners = urlsListenersRef.current?.[urlKey];
if (listeners) {
listeners.forEach(({ callback }) => {
callback(event);
});
}
},
[]
);

return {
addListener: addListenerEvent,
removeListener: removeListenerEvent,
broadcast,
};
};
49 changes: 49 additions & 0 deletions src/components/notifications/hooks/useNotificationsListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2024, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// @author Quentin CAPY

import { useContext, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { NotificationsContext } from '../contexts/NotificationsContext';

export const useNotificationsListener = (
listenerKey: string,
{
listenerCallbackMessage,
listenerCallbackOnReopen,
propsId,
}: {
listenerCallbackMessage?: (event: MessageEvent<any>) => void;
listenerCallbackOnReopen?: () => void;
propsId?: string;
}
) => {
const { addListenerEvent, removeListenerEvent, addListenerOnReopen, removeListenerOnReopen } =
useContext(NotificationsContext);

useEffect(() => {
const id = propsId ?? uuidv4();
if (listenerCallbackMessage) {
addListenerEvent(listenerKey, {
id,
callback: listenerCallbackMessage,
});
}
return () => removeListenerEvent(listenerKey, id);
}, [addListenerEvent, removeListenerEvent, listenerKey, listenerCallbackMessage, propsId]);

useEffect(() => {
const id = propsId ?? uuidv4();
if (listenerCallbackOnReopen) {
addListenerOnReopen(listenerKey, {
id,
callback: listenerCallbackOnReopen,
});
}
return () => removeListenerOnReopen(listenerKey, id);
}, [addListenerOnReopen, removeListenerOnReopen, listenerKey, listenerCallbackOnReopen, propsId]);
};
10 changes: 10 additions & 0 deletions src/components/notifications/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

export * from './NotificationsProvider';
export * from './contexts/NotificationsContext';
export * from './hooks/useNotificationsListener';
export * from './hooks/useListenerManager';
Loading

0 comments on commit ce8496a

Please sign in to comment.