Skip to content

Commit

Permalink
Merge pull request #2893 from Hyperkid123/ws-subs
Browse files Browse the repository at this point in the history
Expose WS subscription chrome API.
  • Loading branch information
Hyperkid123 authored Jul 11, 2024
2 parents eb7eae8 + 2167305 commit 46ed661
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 28 deletions.
49 changes: 49 additions & 0 deletions docs/wsSubscription.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Websocket subscription API

> This API is experimental and is restricted only to the notification drawer and other internal chrome APIs. If you are interested in using the WS API, contact the platform experience services team.
## Subscribing to an event

To consume events, the following information is necessary
- the event type
- the event payload shape

Once this information is know, you can subscribe using the chrome API:

```tsx
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import { ChromeWsPayload, ChromeWsEventTypes } from '@redhat-cloud-services/types';
// depends on the event type
type EventPayload = {
description: string;
id: string;
read: boolean;
title: string;
};

const eventType: ChromeWsEventTypes = 'foo.bar';

const ConsumerComponent = () => {
const { addWsEventListener } = useChrome();
const [data, setData] = useState<ChromeWsPayload<EventPayload>[]>([])

function handleWsEvent(event: ChromeWsPayload<EventPayload>) {
// handle the event according to requirements
setData(prev => [...prev, event])
}

useEffect(() => {
const unRegister = addWsEventListener(eventType, handleWsEvent)
return () => {
// Do not forget to clean the listener once the component is removed from VDOM
unRegister()
}
}, [])

return (
// somehow use the data
)
}


```
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@redhat-cloud-services/eslint-config-redhat-cloud-services": "^1.3.0",
"@redhat-cloud-services/frontend-components-config-utilities": "^3.0.7",
"@redhat-cloud-services/types": "^1.0.11",
"@redhat-cloud-services/types": "^1.0.12",
"@simonsmith/cypress-image-snapshot": "^8.1.2",
"@swc/core": "^1.6.5",
"@swc/jest": "^0.2.36",
Expand Down
1 change: 1 addition & 0 deletions src/chrome/create-chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ describe('create chrome', () => {
};

const chromeContextOptionsMock = {
addWsEventListener: jest.fn(),
store: createStore(() => ({})) as Store<ReduxState>,
// getUser: () => Promise.resolve(mockUser),
chromeAuth: chromeAuthMock,
Expand Down
5 changes: 4 additions & 1 deletion src/chrome/create-chrome.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFetchPermissionsWatcher } from '../auth/fetchPermissions';
import { AppNavigationCB, ChromeAPI, GenericCB } from '@redhat-cloud-services/types';
import { AddChromeWsEventListener, AppNavigationCB, ChromeAPI, GenericCB } from '@redhat-cloud-services/types';
import { Store } from 'redux';
import { AnalyticsBrowser } from '@segment/analytics-next';
import get from 'lodash/get';
Expand Down Expand Up @@ -49,6 +49,7 @@ export type CreateChromeContextConfig = {
isPreview: boolean;
addNavListener: (cb: NavListener) => number;
deleteNavListener: (id: number) => void;
addWsEventListener: AddChromeWsEventListener;
};

export const createChromeContext = ({
Expand All @@ -63,6 +64,7 @@ export const createChromeContext = ({
isPreview,
addNavListener,
deleteNavListener,
addWsEventListener,
}: CreateChromeContextConfig): ChromeAPI => {
const fetchPermissions = createFetchPermissionsWatcher(chromeAuth.getUser);
const visibilityFunctions = getVisibilityFunctions();
Expand Down Expand Up @@ -108,6 +110,7 @@ export const createChromeContext = ({

const api: ChromeAPI = {
...actions,
addWsEventListener,
auth: {
getRefreshToken: chromeAuth.getRefreshToken,
getToken: chromeAuth.getToken,
Expand Down
3 changes: 2 additions & 1 deletion src/components/RootApp/ScalprumRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const ScalprumRoot = memo(
const mutableChromeApi = useRef<ChromeAPI>();

// initialize WS event handling
useChromeServiceEvents();
const addWsEventListener = useChromeServiceEvents();
// track pendo usage
useTrackPendoUsage();
// setting default tab title
Expand Down Expand Up @@ -166,6 +166,7 @@ const ScalprumRoot = memo(
isPreview,
addNavListener,
deleteNavListener,
addWsEventListener,
});
// reset chrome object after token (user) updates/changes
}, [chromeAuth.token, isPreview]);
Expand Down
75 changes: 54 additions & 21 deletions src/hooks/useChromeServiceEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,59 @@ import { useFlag } from '@unleash/proxy-client-react';
import { setCookie } from '../auth/setCookie';
import ChromeAuthContext from '../auth/ChromeAuthContext';
import { useSetAtom } from 'jotai';
import { NotificationData, NotificationsPayload, addNotificationAtom } from '../state/atoms/notificationDrawerAtom';
import { NotificationData, addNotificationAtom } from '../state/atoms/notificationDrawerAtom';
import { AddChromeWsEventListener, ChromeWsEventListener, ChromeWsEventTypes, ChromeWsPayload } from '@redhat-cloud-services/types';

const NOTIFICATION_DRAWER = 'com.redhat.console.notifications.drawer';
const SAMPLE_EVENT = 'sample.type';
const NOTIFICATION_DRAWER: ChromeWsEventTypes = 'com.redhat.console.notifications.drawer';
const ALL_TYPES: ChromeWsEventTypes[] = [NOTIFICATION_DRAWER];
type Payload = NotificationData;

const ALL_TYPES = [NOTIFICATION_DRAWER, SAMPLE_EVENT] as const;
type EventTypes = (typeof ALL_TYPES)[number];
function isGenericEvent(event: unknown): event is ChromeWsPayload<Payload> {
return typeof event === 'object' && event !== null && ALL_TYPES.includes((event as Record<string, never>).type);
}

type SamplePayload = {
foo: string;
type WsEventListenersRegistry = {
[type in ChromeWsEventTypes]: Map<symbol, ChromeWsEventListener<Payload>>;
};

type Payload = NotificationsPayload | SamplePayload;
interface GenericEvent<T extends Payload = Payload> {
type: EventTypes;
data: T;
}

function isGenericEvent(event: unknown): event is GenericEvent {
return typeof event === 'object' && event !== null && ALL_TYPES.includes((event as Record<string, never>).type);
}
// needs to be outside rendring cycle to preserver clients when chrome API changes
const wsEventListenersRegistry: WsEventListenersRegistry = {
[NOTIFICATION_DRAWER]: new Map(),
};

const useChromeServiceEvents = () => {
const useChromeServiceEvents = (): AddChromeWsEventListener => {
const connection = useRef<WebSocket | undefined>();
const addNotification = useSetAtom(addNotificationAtom);
const isNotificationsEnabled = useFlag('platform.chrome.notifications-drawer');
const { token, tokenExpires } = useContext(ChromeAuthContext);

const handlerMap: { [key in EventTypes]: (payload: GenericEvent<Payload>) => void } = useMemo(
const removeEventListener = (id: symbol) => {
const type = id.description as ChromeWsEventTypes;
wsEventListenersRegistry[type].delete(id);
};

const addEventListener: AddChromeWsEventListener = (type: ChromeWsEventTypes, listener: ChromeWsEventListener<any>) => {
const id = Symbol(type);
wsEventListenersRegistry[type].set(id, listener);
return () => removeEventListener(id);
};

const triggerListeners = (type: ChromeWsEventTypes, data: ChromeWsPayload<Payload>) => {
wsEventListenersRegistry[type].forEach((cb) => cb(data));
};

const handlerMap: { [key in ChromeWsEventTypes]: (payload: ChromeWsPayload<Payload>) => void } = useMemo(
() => ({
[NOTIFICATION_DRAWER]: (data: GenericEvent<Payload>) => {
[NOTIFICATION_DRAWER]: (data: ChromeWsPayload<Payload>) => {
triggerListeners(NOTIFICATION_DRAWER, data);
// TODO: Move away from chrome once the portal content is moved to notifications
addNotification(data.data as unknown as NotificationData);
},
[SAMPLE_EVENT]: (data: GenericEvent<Payload>) => console.log('Received sample payload', data),
}),
[]
);

function handleEvent(type: EventTypes, data: GenericEvent<Payload>): void {
function handleEvent(type: ChromeWsEventTypes, data: ChromeWsPayload<Payload>): void {
handlerMap[type](data);
}

Expand Down Expand Up @@ -69,6 +83,23 @@ const useChromeServiceEvents = () => {
console.error('Handler failed when processing WS payload: ', data, error);
}
};

socket.onclose = () => {
// renew connection on close
// pod was restarted or network issue
setTimeout(() => {
createConnection();
}, 2000);
};

socket.onerror = (error) => {
console.error('WS connection error: ', error);
// renew connection on error
// data was unable to be sent
setTimeout(() => {
createConnection();
}, 2000);
};
}
};

Expand All @@ -82,6 +113,8 @@ const useChromeServiceEvents = () => {
console.error('Unable to establish WS connection');
}
}, [isNotificationsEnabled]);

return addEventListener;
};

export default useChromeServiceEvents;

0 comments on commit 46ed661

Please sign in to comment.