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

Handling room events and IO connections #195

Merged
merged 10 commits into from
Jan 3, 2025
55 changes: 49 additions & 6 deletions apps/playground/src/pages/superviz-room.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createRoom } from '@superviz/room'
import { createRoom, type Room, ParticipantEvent, RoomEvent } from '@superviz/room'
import { v4 as generateId } from "uuid";

import { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { getConfig } from "../config";

const SUPERVIZ_KEY = getConfig<string>("keys.superviz");
Expand All @@ -10,8 +10,9 @@ const SUPERVIZ_ROOM_PREFIX = getConfig<string>("roomPrefix");
const componentName = "new-room";

export function SuperVizRoom() {
const room = useRef<any>();
const room = useRef<Room | null>(null);
const loaded = useRef<boolean>(false);
const [subscribed, setSubscribed] = useState<boolean>(false);

const initializeSuperViz = useCallback(async () => {
const uuid = generateId();
Expand All @@ -32,15 +33,57 @@ export function SuperVizRoom() {
});

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

useEffect(() => {
if (loaded.current) return;
loaded.current = true;


initializeSuperViz();
}, []);
}, [initializeSuperViz]);

const subscribeToEvents = () => {
if (!room.current) return;

Object.values(ParticipantEvent).forEach(event => {
room.current?.subscribe(event, (data) => {
console.log('New event from room, eventName:', event, 'data:', data);
})
});

Object.values(RoomEvent).forEach(event => {
room.current?.subscribe(event, (data) => {
console.log('New event from room, eventName:', event, 'data:', data);
})
});

setSubscribed(true);
}

const unsubscribeFromEvents = () => {
if (!room.current) return;

Object.values(ParticipantEvent).forEach(event => {
room.current?.unsubscribe(event);
});

Object.values(RoomEvent).forEach(event => {
room.current?.unsubscribe(event)
});

setSubscribed(false);
}

const leaveRoom = () => {
room.current?.leave();
}

return <></>
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>
)
}
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@superviz/react-sdk",
"private": false,
"version": "1.15.0-lab.2",
"version": "1.15.0",
"type": "module",
"scripts": {
"watch": "./node_modules/typescript/bin/tsc && vite build --watch",
Expand Down
2 changes: 1 addition & 1 deletion packages/realtime/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@superviz/realtime",
"version": "1.3.0-lab.3",
"version": "1.3.0",
"description": "SuperViz Real-Time",
"main": "./dist/node/index.cjs.js",
"module": "./dist/browser/index.js",
Expand Down
15 changes: 14 additions & 1 deletion packages/room/src/common/types/participant.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
export type Participant = {
export type InitialParticipant = {
id: string
name: string
}

export type Participant = InitialParticipant & {
slot: Slot
activeComponents: string[]
}

export type Slot = {
index: number;
color: string;
textColor: string;
colorName: string;
timestamp: number;
}
184 changes: 184 additions & 0 deletions packages/room/src/core/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { Subject } from 'rxjs';

import { Logger } from '../common/utils/logger';
import { IOC } from '../services/io';
import { IOCState } from '../services/io/types';

import { ParticipantEvent, RoomParams } from './types';

import { Room } from './index';

jest.mock('../services/io', () => ({
IOC: jest.fn().mockImplementation(() => ({
stateSubject: new Subject(),
destroy: jest.fn(),
createRoom: jest.fn(() => ({
disconnect: jest.fn(),
presence: {
off: jest.fn(),
on: jest.fn(),
update: jest.fn(),
},
})),
})),
}));
jest.mock('../common/utils/logger');

describe('Room', () => {
let room: Room;
let params: RoomParams;

beforeEach(() => {
params = {
participant: { id: '123', name: 'Test Participant' },
};
room = new Room(params);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should create a room and initialize it', () => {
expect(IOC).toHaveBeenCalledWith(params.participant);
expect(Logger).toHaveBeenCalledWith('@superviz/room/room');
});

it('should leave the room and destroy the socket connection', () => {
room.leave();

expect(room['room'].disconnect).toHaveBeenCalled();
expect(room['io'].destroy).toHaveBeenCalled();
});

it('should remove all subscriptions and observers from the room when it\'s destroyed', () => {
room.subscribe('my-participant.joined', () => {});

room.leave();

expect(room['subscriptions'].size).toBe(0);
});

it('should subscribe to an event', () => {
const callback = jest.fn();
const event = 'participant.joined';

room.subscribe(event, callback);

expect(room['observers'].get(event)).toBeInstanceOf(Subject);
expect(room['subscriptions'].get(callback)).toBeDefined();
});

it('should unsubscribe from an event', () => {
const callback = jest.fn();
const event = 'participant.joined';

room.subscribe(event, callback);
room.unsubscribe(event, callback);

expect(room['subscriptions'].get(callback)).toBeUndefined();
});

it('should unsubscribe from all callbacks of an event', () => {
const event = 'participant.joined';

room.subscribe(event, jest.fn());
room.unsubscribe(event);

expect(room['observers'].get(event)).toBeUndefined();
});

it('should handle participant joined room event', () => {
const data = { id: '123' } as any;
const emitSpy = jest.spyOn(room as any, 'emit');
const expected = room['transfromSocketMesssageToParticipant'](data);

room['onParticipantJoinedRoom'](data);

expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_JOINED, expected);
});

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);

room['onLocalParticipantJoinedRoom'](data);

expect(updateSpy).toHaveBeenCalledWith(updateExpcted);
expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_JOINED, emitExpected);
});

it('should handle participant leaves room event', () => {
const data = { id: '123' } as any;
const emitSpy = jest.spyOn(room as any, 'emit');
const expected = room['transfromSocketMesssageToParticipant'](data);

room['onParticipantLeavesRoom'](data);

expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_LEFT, expected);
});

it('should handle participant updates event', () => {
const data = { data: { id: '123' } } as any;
const emitSpy = jest.spyOn(room as any, 'emit');

room['onParticipantUpdates'](data);

expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_UPDATED, data.data);
});

it('should handle local participant updates event', () => {
const data = { data: { id: '123' } } as any;
const emitSpy = jest.spyOn(room as any, 'emit');

room['onLocalParticipantUpdates'](data);

expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_UPDATED, data.data);
});

it('should handle the same account error', () => {
const emitSpy = jest.spyOn(room as any, 'emit');
const leaveSpy = jest.spyOn(room, 'leave');

room['onConnectionStateChange'](IOCState.SAME_ACCOUNT_ERROR);

expect(leaveSpy).toHaveBeenCalled();
expect(emitSpy).toHaveBeenCalledWith(
'room.error',
{
code: 'same_account_error',
message: '[SuperViz] Room initialization failed: the user is already connected to the room. Please verify if the user is connected with the same account and try again.',
},
);
});

it('should handle the authentication error', () => {
const emitSpy = jest.spyOn(room as any, 'emit');
const leaveSpy = jest.spyOn(room, 'leave');

room['onConnectionStateChange'](IOCState.AUTH_ERROR);

expect(emitSpy).toHaveBeenCalledWith(
'room.error',
{
code: 'auth_error',
message: "[SuperViz] Room initialization failed: this website's domain is not whitelisted. If you are the developer, please add your domain in https://dashboard.superviz.com/developer",
},
);

expect(leaveSpy).toHaveBeenCalled();
expect(room['room'].disconnect).toHaveBeenCalled();
});

it('should update the room state', () => {
const state = IOCState.CONNECTED;
const emitSpy = jest.spyOn(room as any, 'emit');

room['onConnectionStateChange'](state);

expect(emitSpy).toHaveBeenCalledWith('room.update', { status: state });
});
});
Loading
Loading