Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Websocket watcher #517

Merged
merged 30 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
220a589
add reconnecting websocket
capyq Jul 4, 2024
9043926
add common Websocket listener. We need 3items ( context / component /…
capyq Jul 11, 2024
30737dd
fix import for build
capyq Jul 11, 2024
61a197b
fix typo
capyq Jul 12, 2024
3cf1fe4
fix PR review, add author , update license, remove callback ref, remo…
capyq Jul 19, 2024
a04d7a8
add test
capyq Jul 19, 2024
7a1edfd
change test dep to devDependencies
capyq Aug 1, 2024
2312265
fix ref when urls changed; change random generator to uuid
capyq Aug 2, 2024
e7e369b
fix: wait for callback to be sure test pass
capyq Aug 2, 2024
80923a2
fix: wait for callback to be sure test pass
capyq Aug 2, 2024
3bd6b9e
feat(websocket)!:addlisterner on open when WS connexion open
capyq Aug 8, 2024
c14a8b4
fix:update after merge
capyq Aug 8, 2024
a6f6f10
fix: add license
capyq Aug 8, 2024
6b28775
merge:init barel for Websocket
capyq Oct 11, 2024
921d249
Merge branch 'main' into websocket-watcher
sBouzols Oct 11, 2024
3115be7
Update src/components/websocket/index.ts
capyq Oct 11, 2024
b29e20e
Merge remote-tracking branch 'origin/main' into websocket-watcher
sBouzols Nov 22, 2024
06d4067
some renamings
sBouzols Nov 22, 2024
e8dac57
Merge remote-tracking branch 'origin/main' into websocket-watcher
sBouzols Nov 29, 2024
d2ea113
refactor(Websocket): rename Websocket component to WebsocketsProvider
sBouzols Nov 29, 2024
5e53904
refactor(NotificationsProvider): rename Websocket* to Notifications* …
sBouzols Nov 29, 2024
36d150c
rename websocket folder
sBouzols Nov 29, 2024
63fd17f
Merge branch 'main' into websocket-watcher
sBouzols Dec 2, 2024
d1e0324
Merge branch 'main' into websocket-watcher
sBouzols Dec 12, 2024
52cacc8
fix(useListenerManager): Allow to register Listeners before urls init…
sBouzols Dec 16, 2024
cc8ce94
Merge branch 'main' into websocket-watcher
sBouzols Dec 16, 2024
7778b00
Merge branch 'main' into websocket-watcher
sBouzols Dec 17, 2024
f01d851
keep listeners registered when `urls` state changes
sBouzols Dec 17, 2024
e9443a3
fix(useListenerManager): init urlsListenersRef to empty object
sBouzols Dec 17, 2024
3c3b4f2
sonar code smell remove never type here
sBouzols Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
68 changes: 68 additions & 0 deletions src/components/notifications/NotificationsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* 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).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: () => {},
});
67 changes: 67 additions & 0 deletions src/components/notifications/hooks/useListenerManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* 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 | never
>(
urls: Record<string, string>
) => {
const urlsListenersRef = useRef(
Object.keys(urls).reduce((acc, urlKey) => {
acc[urlKey] = [];
return acc;
}, {} as Record<string, TListener[]>)
);
sBouzols marked this conversation as resolved.
Show resolved Hide resolved

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
Loading