Skip to content

Commit 143c137

Browse files
committed
chore: add useRoomActivity hook for WebSocket room activity subscription
1 parent ef62aa8 commit 143c137

2 files changed

Lines changed: 201 additions & 0 deletions

File tree

src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useRoomActivity';

src/hooks/useRoomActivity.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import {
3+
ICE_SERVERS,
4+
ICEServer,
5+
MESHERY_CLOUD_PROD,
6+
MESHERY_CLOUD_STAGING,
7+
MESHERY_CLOUD_WS_PROD,
8+
MESHERY_CLOUD_WS_STAGING
9+
} from '../constants/constants';
10+
11+
interface UserProfile {
12+
[key: string]: unknown;
13+
}
14+
15+
interface CollaborationConfigParams {
16+
provider_url: string;
17+
getUserProfile: () => Promise<{ data: UserProfile }>;
18+
getUserAccessToken: () => Promise<{ data: string }>;
19+
}
20+
21+
interface CollaborationConfig {
22+
signaling: string[];
23+
user: UserProfile;
24+
authToken: string;
25+
refreshAuthToken: () => Promise<string>;
26+
websocketCallbacks: string[];
27+
peerOpts: {
28+
config: {
29+
iceServers: ICEServer[];
30+
};
31+
};
32+
}
33+
34+
interface SubscribeToRoomsActivityMessage {
35+
type: string;
36+
}
37+
38+
interface UserMapChangeMessage {
39+
type: string;
40+
user_map?: UserMapping;
41+
}
42+
43+
interface UserMapping {
44+
[roomId: string]: {
45+
[userId: string]: unknown;
46+
};
47+
}
48+
49+
interface UseRoomActivityParams {
50+
provider_url: string;
51+
getUserProfile: () => Promise<{ data: UserProfile }>;
52+
getUserAccessToken: () => Promise<{ data: string }>;
53+
}
54+
55+
const SUBSCRIBE_TO_ROOMS_ACTIVITY_MSG: SubscribeToRoomsActivityMessage = {
56+
type: 'subscribe_to_rooms_activity'
57+
};
58+
const USER_MAP_CHANGE_MSG = 'user_map';
59+
60+
/**
61+
* Determines the appropriate websocket host based on the provider host
62+
* @param {string} providerHost - The provider host
63+
* @returns {string} - The websocket host
64+
*/
65+
const getWebsocketHost = (providerHost: string): string => {
66+
if (providerHost === MESHERY_CLOUD_PROD) {
67+
return MESHERY_CLOUD_WS_PROD;
68+
} else if (providerHost === MESHERY_CLOUD_STAGING) {
69+
return MESHERY_CLOUD_WS_STAGING;
70+
}
71+
72+
return providerHost;
73+
};
74+
75+
/**
76+
* Determines the appropriate WebSocket protocol based on current page protocol
77+
* @returns {string} - WebSocket protocol ('ws://' or 'wss://')
78+
*/
79+
export function getWebSocketProtocol(): string {
80+
const isSecure = window.location.protocol === 'https:'; // https only accepts secure websockets
81+
return isSecure ? 'wss://' : 'ws://';
82+
}
83+
84+
/**
85+
* Constructs a signaling URL from a provider URL
86+
* @param {string} providerUrl - The provider URL
87+
* @returns {string} - The signaling URL
88+
*/
89+
const getSignalingUrlFromProviderUrl = (providerUrl: string): string => {
90+
const parsedUrl = new URL(providerUrl);
91+
const websocketHost = getWebsocketHost(parsedUrl.host);
92+
const protocol = websocketHost === MESHERY_CLOUD_WS_PROD ? 'wss://' : getWebSocketProtocol();
93+
return `${protocol}${websocketHost}/collaboration`;
94+
};
95+
96+
/**
97+
* Gets collaboration configuration for WebRTC
98+
*/
99+
export const getCollaborationConfig = async ({
100+
provider_url,
101+
getUserProfile,
102+
getUserAccessToken
103+
}: CollaborationConfigParams): Promise<CollaborationConfig> => {
104+
const { data: userProfile } = await getUserProfile();
105+
106+
const websocketCallbacks = ['user_info', 'user_left', 'user_joined', 'user_map'];
107+
108+
// Fetch token after fetching provider and user so that it
109+
// gets refreshed if necessary.
110+
const { data: accessToken } = await getUserAccessToken();
111+
const refreshToken = async () => {
112+
return (await getUserAccessToken()).data;
113+
};
114+
115+
return {
116+
signaling: [getSignalingUrlFromProviderUrl(provider_url)],
117+
user: userProfile,
118+
authToken: accessToken,
119+
refreshAuthToken: refreshToken,
120+
websocketCallbacks,
121+
peerOpts: {
122+
config: {
123+
iceServers: ICE_SERVERS
124+
}
125+
}
126+
};
127+
};
128+
129+
/**
130+
* Subscribes to room activity via WebSocket
131+
*/
132+
const subscribeToRoomActivity = async (
133+
wsRef: React.MutableRefObject<WebSocket | null>,
134+
onUserMapChange: (userMap: UserMapping) => void,
135+
provider_url: string,
136+
getUserProfile: () => Promise<{ data: UserProfile }>,
137+
getUserAccessToken: () => Promise<{ data: string }>
138+
): Promise<void> => {
139+
const config = await getCollaborationConfig({
140+
provider_url,
141+
getUserProfile,
142+
getUserAccessToken
143+
});
144+
145+
// Create the websocket connection with proper headers
146+
const ws = new WebSocket(config.signaling[0], ['auth', config.authToken]);
147+
wsRef.current = ws;
148+
149+
ws.addEventListener('open', () => {
150+
console.log('[RoomActivity] connected to room activity');
151+
ws.send(JSON.stringify(SUBSCRIBE_TO_ROOMS_ACTIVITY_MSG));
152+
});
153+
154+
ws.addEventListener('message', (event: MessageEvent) => {
155+
const data = JSON.parse(event.data) as UserMapChangeMessage;
156+
if (data.type === USER_MAP_CHANGE_MSG && data.user_map) {
157+
onUserMapChange(data.user_map);
158+
}
159+
});
160+
161+
ws.addEventListener('close', () => {
162+
console.log('[RoomActivity] subscription to room activity closed');
163+
});
164+
165+
ws.addEventListener('error', (err: Event) => {
166+
console.error('[RoomActivity] error in room activity subscription', err);
167+
});
168+
};
169+
170+
/**
171+
* Hook to subscribe to and get room activity data
172+
*/
173+
export const useRoomActivity = ({
174+
provider_url,
175+
getUserProfile,
176+
getUserAccessToken
177+
}: UseRoomActivityParams): UserMapping => {
178+
const [allRoomsUserMapping, setAllRoomsUserMapping] = useState<UserMapping>({});
179+
const wsRef = useRef<WebSocket | null>(null);
180+
181+
useEffect(() => {
182+
subscribeToRoomActivity(
183+
wsRef,
184+
setAllRoomsUserMapping,
185+
provider_url,
186+
getUserProfile,
187+
getUserAccessToken
188+
);
189+
190+
const ws = wsRef.current;
191+
192+
return () => {
193+
if (ws) {
194+
ws.close();
195+
}
196+
};
197+
}, [provider_url, getUserProfile, getUserAccessToken]);
198+
199+
return allRoomsUserMapping;
200+
};

0 commit comments

Comments
 (0)