Skip to content

Commit

Permalink
Merge pull request #196 from SuperViz/feat/new-room-oackage-params
Browse files Browse the repository at this point in the history
Introducing getParticipants method
  • Loading branch information
carlossantos74 authored Jan 6, 2025
2 parents 70c0ecf + f74b7b4 commit dc13d4c
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 13 deletions.
2 changes: 1 addition & 1 deletion apps/playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function App() {
});
})}
</aside>
<section className="flex-1">
<section className="flex-1 h-full overflow-hidden">
<Outlet />
</section>
</main>
Expand Down
63 changes: 58 additions & 5 deletions apps/playground/src/pages/superviz-room.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,6 +13,10 @@ export function SuperVizRoom() {
const room = useRef<Room | null>(null);
const loaded = useRef<boolean>(false);
const [subscribed, setSubscribed] = useState<boolean>(false);
const [participants, setParticipants] = useState<Participant[]>([]);
const [roomState, setRoomState] = useState<string>("Not connected");
const [observerState, setObserverState] = useState<string>("Not subscribed");
const [events, setEvents] = useState<any[]>([]);

const initializeSuperViz = useCallback(async () => {
const uuid = generateId();
Expand All @@ -33,6 +37,7 @@ export function SuperVizRoom() {
});

room.current = newRoom;
setRoomState("Connected");
subscribeToEvents();
}, []);

Expand All @@ -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 = () => {
Expand All @@ -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 (
<div className='w-full h-full flex items-center justify-center gap-2'>
<button onClick={leaveRoom}> Leave </button>
<button onClick={subscribeToEvents} disabled={subscribed}> Subscribe to Events </button>
<button onClick={unsubscribeFromEvents} disabled={!subscribed}> Unsubscribe from Events </button>
<div className='w-full h-full flex justify-between gap-2 p-10 overflow-hidden'>
<div className='flex gap-2 flex-col'>
<div>
<h2>Room State: {roomState}</h2>
<h2>Observer State: {observerState}</h2>
</div>
<button
onClick={leaveRoom}
className='px-4 py-2 bg-red-500 text-white rounded hover:bg-red-700'
>
Leave
</button>
<button
onClick={subscribeToEvents}
disabled={subscribed}
className={`px-4 py-2 rounded ${subscribed ? 'bg-gray-500' : 'bg-green-500'} text-white hover:bg-green-700`}
>
Subscribe to Events
</button>
<button
onClick={unsubscribeFromEvents}
disabled={!subscribed}
className={`px-4 py-2 rounded ${!subscribed ? 'bg-gray-500' : 'bg-yellow-500'} text-white hover:bg-yellow-700`}
>
Unsubscribe from Events
</button>
<button
onClick={getParticipants}
className='px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700'
>
Get Participants
</button>
</div>
<div className='p-4 border rounded shadow-md h-full overflow-auto flex-1'>
<h3>Participants:</h3>
<pre>{JSON.stringify(participants, null, 2)}</pre>
<h3>Events:</h3>
<pre>{JSON.stringify(events, null, 2)}</pre>
</div>
</div>
)
}
79 changes: 74 additions & 5 deletions packages/room/src/core/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PresenceEvent } from '@superviz/socket-client';
import { Subject } from 'rxjs';

import { Logger } from '../common/utils/logger';
Expand All @@ -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(),
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -95,20 +98,38 @@ 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', () => {
const data = { id: '123' } as any;
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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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([]);
});
});
50 changes: 48 additions & 2 deletions packages/room/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export class Room {
private io: IOC;
private room: SocketRoomType;

private participants: Map<string, Participant> = new Map();
private state: IOCState = IOCState.DISCONNECTED;
private logger: Logger;

private subscriptions: Map<Callback<GeneralEvent>, Subscription> = new Map();
Expand All @@ -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);
Expand All @@ -50,6 +53,7 @@ export class Room {

this.subscriptions.clear();
this.observers.clear();
this.participants.clear();
}

/**
Expand Down Expand Up @@ -95,6 +99,35 @@ export class Room {
this.subscriptions.delete(callback);
}

/**
* Retrieves the list of participants in the room.
*
* @returns {Promise<Participant[]>} 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<Participant[]> {
if (!this.room || this.state !== IOCState.CONNECTED) {
return [];
}

const participants = await new Promise<Participant[]>((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
*/
Expand Down Expand Up @@ -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));
Expand All @@ -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);
};

/**
Expand All @@ -245,7 +284,10 @@ export class Room {
* @fires ParticipantEvent.PARTICIPANT_LEFT - Emitted when a participant leaves the room.
*/
private onParticipantLeavesRoom = (data: PresenceEvent<Participant>) => {
this.participants.delete(data.id);

this.emit(ParticipantEvent.PARTICIPANT_LEFT, this.transfromSocketMesssageToParticipant(data));
this.logger.log('participant leaves room @ update participants', this.participants);
};

/**
Expand All @@ -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);
};

Expand Down Expand Up @@ -319,6 +364,7 @@ export class Room {

const common = () => {
this.emit(RoomEvent.UPDATE, { status: state });
this.state = state;
};

const map = {
Expand All @@ -334,6 +380,6 @@ export class Room {
[IOCState.SAME_ACCOUNT_ERROR]: () => this.onSameAccountError(),
};

map[state]();
map[state]?.();
};
}

0 comments on commit dc13d4c

Please sign in to comment.