diff --git a/apps/playground/src/App.tsx b/apps/playground/src/App.tsx index ac36b618..0ba00ce0 100644 --- a/apps/playground/src/App.tsx +++ b/apps/playground/src/App.tsx @@ -49,7 +49,7 @@ function App() { }); })} -
+
diff --git a/apps/playground/src/pages/superviz-room.tsx b/apps/playground/src/pages/superviz-room.tsx index e998512c..081b9396 100644 --- a/apps/playground/src/pages/superviz-room.tsx +++ b/apps/playground/src/pages/superviz-room.tsx @@ -1,4 +1,4 @@ -import { createRoom, type Room, ParticipantEvent, RoomEvent } from '@superviz/room' +import { createRoom, type Room, ParticipantEvent, RoomEvent, Participant } from '@superviz/room' import { v4 as generateId } from "uuid"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -13,6 +13,10 @@ export function SuperVizRoom() { const room = useRef(null); const loaded = useRef(false); const [subscribed, setSubscribed] = useState(false); + const [participants, setParticipants] = useState([]); + const [roomState, setRoomState] = useState("Not connected"); + const [observerState, setObserverState] = useState("Not subscribed"); + const [events, setEvents] = useState([]); const initializeSuperViz = useCallback(async () => { const uuid = generateId(); @@ -33,6 +37,7 @@ export function SuperVizRoom() { }); room.current = newRoom; + setRoomState("Connected"); subscribeToEvents(); }, []); @@ -49,16 +54,19 @@ export function SuperVizRoom() { Object.values(ParticipantEvent).forEach(event => { room.current?.subscribe(event, (data) => { console.log('New event from room, eventName:', event, 'data:', data); + setEvents(prevEvents => [...prevEvents, { eventName: event, data }]); }) }); Object.values(RoomEvent).forEach(event => { room.current?.subscribe(event, (data) => { console.log('New event from room, eventName:', event, 'data:', data); + setEvents(prevEvents => [...prevEvents, { eventName: event, data }]); }) }); setSubscribed(true); + setObserverState("Subscribed to events"); } const unsubscribeFromEvents = () => { @@ -73,17 +81,62 @@ export function SuperVizRoom() { }); setSubscribed(false); + setObserverState("Unsubscribed from events"); } const leaveRoom = () => { room.current?.leave(); + setRoomState("Left the room"); + setObserverState("Not subscribed"); + } + + const getParticipants = () => { + room.current?.getParticipants().then((participants) => { + setParticipants(participants); + console.log('Participants:', participants); + }); } return ( -
- - - +
+
+
+

Room State: {roomState}

+

Observer State: {observerState}

+
+ + + + +
+
+

Participants:

+
{JSON.stringify(participants, null, 2)}
+

Events:

+
{JSON.stringify(events, null, 2)}
+
) } diff --git a/packages/room/src/core/index.test.ts b/packages/room/src/core/index.test.ts index c23df061..b6ed355c 100644 --- a/packages/room/src/core/index.test.ts +++ b/packages/room/src/core/index.test.ts @@ -1,3 +1,4 @@ +import { PresenceEvent } from '@superviz/socket-client'; import { Subject } from 'rxjs'; import { Logger } from '../common/utils/logger'; @@ -15,6 +16,7 @@ jest.mock('../services/io', () => ({ createRoom: jest.fn(() => ({ disconnect: jest.fn(), presence: { + get: jest.fn(), off: jest.fn(), on: jest.fn(), update: jest.fn(), @@ -57,6 +59,7 @@ describe('Room', () => { room.leave(); expect(room['subscriptions'].size).toBe(0); + expect(room['observers'].size).toBe(0); }); it('should subscribe to an event', () => { @@ -95,7 +98,13 @@ describe('Room', () => { room['onParticipantJoinedRoom'](data); - expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_JOINED, expected); + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_JOINED, { + ...expected, + slot: { + ...expected.slot, + timestamp: expect.any(Number), + }, + }); }); it('should handle local participant joined room event', () => { @@ -103,12 +112,24 @@ describe('Room', () => { const emitSpy = jest.spyOn(room as any, 'emit'); const updateSpy = jest.spyOn(room['room'].presence, 'update'); const emitExpected = room['transfromSocketMesssageToParticipant'](data); - const updateExpcted = room['createParticipant'](params.participant); + const updateExpected = room['createParticipant'](params.participant); room['onLocalParticipantJoinedRoom'](data); - expect(updateSpy).toHaveBeenCalledWith(updateExpcted); - expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_JOINED, emitExpected); + expect(updateSpy).toHaveBeenCalledWith({ + ...updateExpected, + slot: { + ...updateExpected.slot, + timestamp: expect.any(Number), + }, + }); + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_JOINED, { + ...emitExpected, + slot: { + ...emitExpected.slot, + timestamp: expect.any(Number), + }, + }); }); it('should handle participant leaves room event', () => { @@ -118,7 +139,13 @@ describe('Room', () => { room['onParticipantLeavesRoom'](data); - expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_LEFT, expected); + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_LEFT, { + ...expected, + slot: { + ...expected.slot, + timestamp: expect.any(Number), + }, + }); }); it('should handle participant updates event', () => { @@ -179,6 +206,48 @@ describe('Room', () => { room['onConnectionStateChange'](state); + expect(room['state']).toBe(state); expect(emitSpy).toHaveBeenCalledWith('room.update', { status: state }); }); + + it('should get participants when room is connected', async () => { + room['state'] = IOCState.CONNECTED; + + const date = Date.now(); + + const mockParticipants: PresenceEvent[] = [ + { + id: '1', + name: 'Participant 1', + data: [], + connectionId: 'conn-1', + timestamp: date, + }, + ]; + room['room'].presence.get = jest.fn((callback) => callback(mockParticipants)); + + const participants = await room.getParticipants(); + + expect(participants).toEqual([{ + id: '1', + name: 'Participant 1', + slot: { + index: null, + color: '#878291', + textColor: '#fff', + colorName: 'gray', + timestamp: expect.any(Number), + }, + activeComponents: [], + }]); + expect(room['participants'].size).toBe(mockParticipants.length); + }); + + it('should return empty array when room is not connected', async () => { + room['state'] = IOCState.DISCONNECTED; + + const participants = await room.getParticipants(); + + expect(participants).toEqual([]); + }); }); diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index b5c8949e..8c0a30f2 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -14,6 +14,8 @@ export class Room { private io: IOC; private room: SocketRoomType; + private participants: Map = new Map(); + private state: IOCState = IOCState.DISCONNECTED; private logger: Logger; private subscriptions: Map, Subscription> = new Map(); @@ -32,6 +34,7 @@ export class Room { * @description leave the room, destroy the socket connnection and all attached components */ public leave() { + this.state = IOCState.DISCONNECTED; this.unsubscribeFromRoomEvents(); this.emit(ParticipantEvent.PARTICIPANT_LEFT, this.participant); @@ -50,6 +53,7 @@ export class Room { this.subscriptions.clear(); this.observers.clear(); + this.participants.clear(); } /** @@ -95,6 +99,35 @@ export class Room { this.subscriptions.delete(callback); } + /** + * Retrieves the list of participants in the room. + * + * @returns {Promise} A promise that resolves to an array of participants. + * + * @remarks + * - If the room is not connected or the state is not `IOCState.CONNECTED`, + an empty array is returned. + */ + public async getParticipants(): Promise { + if (!this.room || this.state !== IOCState.CONNECTED) { + return []; + } + + const participants = await new Promise((resolve) => { + this.room.presence.get((presences) => { + const mapped = presences.map((presence) => { + return this.transfromSocketMesssageToParticipant(presence); + }); + + this.participants = new Map(mapped.map((participant) => [participant.id, participant])); + + resolve(mapped); + }); + }); + + return participants; + } + /** * @description Initializes the room features */ @@ -217,6 +250,9 @@ export class Room { private onParticipantJoinedRoom = (data: PresenceEvent<{}>) => { if (this.participant.id === data.id) { this.onLocalParticipantJoinedRoom(data); + } else { + this.participants.set(data.id, this.transfromSocketMesssageToParticipant(data)); + this.logger.log('participant joined room @ update participants', this.participants); } this.emit(ParticipantEvent.PARTICIPANT_JOINED, this.transfromSocketMesssageToParticipant(data)); @@ -229,13 +265,16 @@ export class Room { * @fires ParticipantEvent.MY_PARTICIPANT_JOINED - Emitted when the local participant joins * the room. */ - private onLocalParticipantJoinedRoom = (data: PresenceEvent<{}>) => { + private onLocalParticipantJoinedRoom = async (data: PresenceEvent<{}>) => { this.room.presence.update(this.participant); this.emit( ParticipantEvent.MY_PARTICIPANT_JOINED, this.transfromSocketMesssageToParticipant(data), ); + + await this.getParticipants(); + this.logger.log('local participant joined room @ update participants', this.participants); }; /** @@ -245,7 +284,10 @@ export class Room { * @fires ParticipantEvent.PARTICIPANT_LEFT - Emitted when a participant leaves the room. */ private onParticipantLeavesRoom = (data: PresenceEvent) => { + this.participants.delete(data.id); + this.emit(ParticipantEvent.PARTICIPANT_LEFT, this.transfromSocketMesssageToParticipant(data)); + this.logger.log('participant leaves room @ update participants', this.participants); }; /** @@ -259,6 +301,9 @@ export class Room { this.onLocalParticipantUpdates(data); } + this.participants.set(data.data.id, data.data); + this.logger.log('participant updates @ update participants', this.participants); + this.emit(ParticipantEvent.PARTICIPANT_UPDATED, data.data); }; @@ -319,6 +364,7 @@ export class Room { const common = () => { this.emit(RoomEvent.UPDATE, { status: state }); + this.state = state; }; const map = { @@ -334,6 +380,6 @@ export class Room { [IOCState.SAME_ACCOUNT_ERROR]: () => this.onSameAccountError(), }; - map[state](); + map[state]?.(); }; }