From d32973280bc6e654f5db518426cb0a88dd622258 Mon Sep 17 00:00:00 2001 From: SuperViz Date: Thu, 2 Jan 2025 14:20:25 +0000 Subject: [PATCH 1/8] chore(release): update package versions [skip ci] --- packages/react/package.json | 2 +- packages/realtime/package.json | 2 +- packages/sdk/package.json | 2 +- packages/socket-client/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index a7d787a8..63d537e2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@superviz/react-sdk", "private": false, - "version": "1.15.0-lab.2", + "version": "1.15.0-beta.1", "type": "module", "scripts": { "watch": "./node_modules/typescript/bin/tsc && vite build --watch", diff --git a/packages/realtime/package.json b/packages/realtime/package.json index e4f19a6c..15b852ea 100644 --- a/packages/realtime/package.json +++ b/packages/realtime/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/realtime", - "version": "1.3.0-lab.3", + "version": "1.3.0-beta.1", "description": "SuperViz Real-Time", "main": "./dist/node/index.cjs.js", "module": "./dist/browser/index.js", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b5f44ce7..fcdafeff 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/sdk", - "version": "6.8.0-lab.2", + "version": "6.8.0-beta.1", "description": "SuperViz SDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/socket-client/package.json b/packages/socket-client/package.json index b69c016a..58fddd4a 100644 --- a/packages/socket-client/package.json +++ b/packages/socket-client/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/socket-client", - "version": "1.14.0-lab.7", + "version": "1.14.0-beta.1", "description": "SuperViz Socket Client", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 2af14894fd7b4a5bbbae5495c33547ee2fac409e Mon Sep 17 00:00:00 2001 From: SuperViz Date: Thu, 2 Jan 2025 15:02:02 +0000 Subject: [PATCH 2/8] chore(release): update package versions [skip ci] --- packages/react/package.json | 2 +- packages/realtime/package.json | 2 +- packages/sdk/package.json | 2 +- packages/socket-client/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 63d537e2..fef97355 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "@superviz/react-sdk", "private": false, - "version": "1.15.0-beta.1", + "version": "1.15.0", "type": "module", "scripts": { "watch": "./node_modules/typescript/bin/tsc && vite build --watch", diff --git a/packages/realtime/package.json b/packages/realtime/package.json index 15b852ea..354bf1b4 100644 --- a/packages/realtime/package.json +++ b/packages/realtime/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/realtime", - "version": "1.3.0-beta.1", + "version": "1.3.0", "description": "SuperViz Real-Time", "main": "./dist/node/index.cjs.js", "module": "./dist/browser/index.js", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index fcdafeff..06d8fae9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/sdk", - "version": "6.8.0-beta.1", + "version": "6.8.0", "description": "SuperViz SDK", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/socket-client/package.json b/packages/socket-client/package.json index 58fddd4a..b9e458b5 100644 --- a/packages/socket-client/package.json +++ b/packages/socket-client/package.json @@ -1,6 +1,6 @@ { "name": "@superviz/socket-client", - "version": "1.14.0-beta.1", + "version": "1.14.0", "description": "SuperViz Socket Client", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 85a2a8a540cbdbc9b088d1c3bb6cbfa1f4b6c183 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 2 Jan 2025 18:27:07 -0300 Subject: [PATCH 3/8] feat: intialize the IOC room --- packages/room/src/core/index.ts | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index 1e51592c..5e12ca3e 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -1,18 +1,78 @@ +import type { Callback, Room as SocketRoomType } from '@superviz/socket-client'; import { Logger } from '../common/utils/logger'; import { IOC } from '../services/io'; +import { IOCState } from '../services/io/types'; -import { RoomParams } from './types'; +import { RoomEventsArg, RoomParams } from './types'; export class Room { private participant: RoomParams['participant']; private io: IOC; private logger: Logger; + private room: SocketRoomType; constructor(params: RoomParams) { this.io = new IOC(params.participant); this.participant = params.participant; this.logger = new Logger('@superviz/room/room'); - this.logger.log('Room created', this.participant); + this.logger.log('room created', this.participant); + this.init(); } + + /** + * @description leave the room, destroy the socket connnection and all attached components + */ + public leave() { + this.unsubscribeFromRoomEvents(); + + this.room.disconnect(); + this.io.destroy(); + } + /** + * @description Initializes the room features + */ + private init() { + this.io.stateSubject.subscribe(this.onConnectionStateChange); + this.room = this.io.createRoom('room', 'unlimited'); + + this.subscribeToRoomEvents(); + } + private subscribeToRoomEvents() { + this.room.presence.on('presence.joined-room', this.onParticipantJoinedRoom); + this.room.presence.on('presence.leave', this.onParticipantLeavesRoom); + this.room.presence.on('presence.update', this.onParticipantUpdates); + } + + private unsubscribeFromRoomEvents() { + this.room.presence.off('presence.joined-room'); + this.room.presence.off('presence.leave'); + this.room.presence.off('presence.update'); + } + /** + * + * Callbacks + * + */ + + private onParticipantJoinedRoom = (data) => { + console.log('room joined', data); + }; + + private onParticipantLeavesRoom = (data) => { + console.log('room left', data); + }; + + private onParticipantUpdates = (data) => { + console.log('room update', data); + }; + + /** + * @description Handles changes in the connection state. + * + * @param {IOCState} state - The current state of the connection. + */ + private onConnectionStateChange = (state: IOCState): void => { + this.logger.log('connection state changed', state); + }; } From 9e2a22e69cec11ee57d0aab3ed5c7a708b726c59 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 2 Jan 2025 18:36:32 -0300 Subject: [PATCH 4/8] feat: add support to emit events --- apps/playground/src/pages/superviz-room.tsx | 10 +++- packages/room/src/core/index.ts | 51 +++++++++++++++++++++ packages/room/src/core/types.ts | 3 ++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/apps/playground/src/pages/superviz-room.tsx b/apps/playground/src/pages/superviz-room.tsx index 3896fba4..4d6cd219 100644 --- a/apps/playground/src/pages/superviz-room.tsx +++ b/apps/playground/src/pages/superviz-room.tsx @@ -42,5 +42,13 @@ export function SuperVizRoom() { initializeSuperViz(); }, []); - return <> + const leaveRoom = () => { + room.current.leave(); + } + + return ( +
+ +
+ ) } diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index 5e12ca3e..e11cf62f 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -1,4 +1,6 @@ import type { Callback, Room as SocketRoomType } from '@superviz/socket-client'; +import { Subject, Subscription } from 'rxjs'; + import { Logger } from '../common/utils/logger'; import { IOC } from '../services/io'; import { IOCState } from '../services/io/types'; @@ -10,6 +12,8 @@ export class Room { private io: IOC; private logger: Logger; private room: SocketRoomType; + private subscriptions: Map, Subscription> = new Map(); + private observers: Map> = new Map(); constructor(params: RoomParams) { this.io = new IOC(params.participant); @@ -29,6 +33,43 @@ export class Room { this.room.disconnect(); this.io.destroy(); } + + /** + * @description Listen to an event + * @param event - The event to listen to + * @param callback - The callback to execute when the event is emitted + * @returns {void} + */ + public subscribe(event: RoomEventsArg, callback: Callback): void { + this.logger.log('room @ subscribe', event); + + let subject = this.observers.get(event); + + if (!subject) { + subject = new Subject(); + this.observers.set(event, subject); + } + + this.subscriptions.set(callback, subject.subscribe(callback)); + } + + /** + * @description Stop listening to an event + * @param event - The event to stop listening to + * @param callback - The callback to remove from the event + * @returns {void} + */ + public unsubscribe(event: string, callback?: Callback): void { + this.logger.log('room @ unsubscribe', event); + + if (!callback) { + this.observers.delete(event); + return; + } + + this.subscriptions.get(callback)?.unsubscribe(); + } + /** * @description Initializes the room features */ @@ -38,6 +79,7 @@ export class Room { this.subscribeToRoomEvents(); } + private subscribeToRoomEvents() { this.room.presence.on('presence.joined-room', this.onParticipantJoinedRoom); this.room.presence.on('presence.leave', this.onParticipantLeavesRoom); @@ -49,6 +91,15 @@ export class Room { this.room.presence.off('presence.leave'); this.room.presence.off('presence.update'); } + + private emit(event: string, data: T) { + const subject = this.observers.get(event); + + if (!subject) return; + + subject.next(data); + } + /** * * Callbacks diff --git a/packages/room/src/core/types.ts b/packages/room/src/core/types.ts index 10978b14..ea3c6d37 100644 --- a/packages/room/src/core/types.ts +++ b/packages/room/src/core/types.ts @@ -3,3 +3,6 @@ import { Participant } from '../common/types/participant.types'; export interface RoomParams { participant: Participant } + +export type RoomEventsArg = string +export type Callback = (event: T) => void; From 1625cfd0ecb48063a08d85c195a3691217e728bf Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 3 Jan 2025 12:00:27 -0300 Subject: [PATCH 5/8] feat: introducing participants events to the room --- apps/playground/src/pages/superviz-room.tsx | 38 +++++- packages/room/src/core/index.test.ts | 137 ++++++++++++++++++++ packages/room/src/core/index.ts | 118 +++++++++++++++-- packages/room/src/core/types.ts | 44 ++++++- packages/room/src/index.ts | 12 ++ 5 files changed, 325 insertions(+), 24 deletions(-) create mode 100644 packages/room/src/core/index.test.ts diff --git a/apps/playground/src/pages/superviz-room.tsx b/apps/playground/src/pages/superviz-room.tsx index 4d6cd219..b2775d6b 100644 --- a/apps/playground/src/pages/superviz-room.tsx +++ b/apps/playground/src/pages/superviz-room.tsx @@ -1,7 +1,7 @@ -import { createRoom } from '@superviz/room' +import { createRoom, type Room, ParticipantEvent } 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("keys.superviz"); @@ -10,8 +10,9 @@ const SUPERVIZ_ROOM_PREFIX = getConfig("roomPrefix"); const componentName = "new-room"; export function SuperVizRoom() { - const room = useRef(); + const room = useRef(null); const loaded = useRef(false); + const [subscribed, setSubscribed] = useState(false); const initializeSuperViz = useCallback(async () => { const uuid = generateId(); @@ -38,17 +39,40 @@ export function SuperVizRoom() { 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); + }) + }); + + setSubscribed(true); + } + + const unsubscribeFromEvents = () => { + if (!room.current) return; + + Object.values(ParticipantEvent).forEach(event => { + room.current?.unsubscribe(event); + }); + + setSubscribed(false); + } const leaveRoom = () => { - room.current.leave(); + room.current?.leave(); } return ( -
+
+ +
) } diff --git a/packages/room/src/core/index.test.ts b/packages/room/src/core/index.test.ts new file mode 100644 index 00000000..36b3a53f --- /dev/null +++ b/packages/room/src/core/index.test.ts @@ -0,0 +1,137 @@ +import { Subject } from 'rxjs'; + +import { Logger } from '../common/utils/logger'; +import { IOC } from '../services/io'; + +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 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'); + + room['onParticipantJoinedRoom'](data); + + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_JOINED, data); + }); + + 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'); + + room['onLocalParticipantJoinedRoom'](data); + + expect(updateSpy).toHaveBeenCalledWith(params.participant); + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_JOINED, data); + }); + + it('should handle participant leaves room event', () => { + const data = { id: '123' } as any; + const emitSpy = jest.spyOn(room as any, 'emit'); + + room['onParticipantLeavesRoom'](data); + + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_LEFT, data); + }); + + 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 connection state change', () => { + const state = { connected: true } as any; + const logSpy = jest.spyOn(room['logger'], 'log'); + + room['onConnectionStateChange'](state); + + expect(logSpy).toHaveBeenCalledWith('connection state changed', state); + }); +}); diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index e11cf62f..27d50ed7 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -1,18 +1,19 @@ -import type { Callback, Room as SocketRoomType } from '@superviz/socket-client'; +import type { PresenceEvent, Room as SocketRoomType } from '@superviz/socket-client'; import { Subject, Subscription } from 'rxjs'; +import { Participant } from '../common/types/participant.types'; import { Logger } from '../common/utils/logger'; import { IOC } from '../services/io'; import { IOCState } from '../services/io/types'; -import { RoomEventsArg, RoomParams } from './types'; +import { GeneralEvent, ParticipantEvent, RoomEventPayload, RoomParams, Callback, EventOptions } from './types'; export class Room { private participant: RoomParams['participant']; private io: IOC; private logger: Logger; private room: SocketRoomType; - private subscriptions: Map, Subscription> = new Map(); + private subscriptions: Map, Subscription> = new Map(); private observers: Map> = new Map(); constructor(params: RoomParams) { @@ -40,13 +41,16 @@ export class Room { * @param callback - The callback to execute when the event is emitted * @returns {void} */ - public subscribe(event: RoomEventsArg, callback: Callback): void { + public subscribe( + event: EventOptions, + callback: Callback, + ): void { this.logger.log('room @ subscribe', event); let subject = this.observers.get(event); if (!subject) { - subject = new Subject(); + subject = new Subject>(); this.observers.set(event, subject); } @@ -59,15 +63,19 @@ export class Room { * @param callback - The callback to remove from the event * @returns {void} */ - public unsubscribe(event: string, callback?: Callback): void { + public unsubscribe( + event: EventOptions, + callback?: Callback, + ): void { this.logger.log('room @ unsubscribe', event); if (!callback) { - this.observers.delete(event); + this.observers.delete(event as string); return; } this.subscriptions.get(callback)?.unsubscribe(); + this.subscriptions.delete(callback); } /** @@ -80,19 +88,51 @@ export class Room { this.subscribeToRoomEvents(); } + /** + * Subscribes to room events such as participant joining, leaving, and updating. + * + * This method sets up listeners for the following events: + * - `presence.joined-room`: Triggered when a participant joins the room. + * - `presence.leave`: Triggered when a participant leaves the room. + * - `presence.update`: Triggered when a participant updates their presence. + * + * The corresponding event handlers are: + * - `onParticipantJoinedRoom` + * - `onParticipantLeavesRoom` + * - `onParticipantUpdates` + */ private subscribeToRoomEvents() { this.room.presence.on('presence.joined-room', this.onParticipantJoinedRoom); this.room.presence.on('presence.leave', this.onParticipantLeavesRoom); this.room.presence.on('presence.update', this.onParticipantUpdates); } + /** + * Unsubscribes from room presence events. + * + * This method removes the event listeners for the following room presence events: + * - 'presence.joined-room': Triggered when a user joins the room. + * - 'presence.leave': Triggered when a user leaves the room. + * - 'presence.update': Triggered when a user's presence is updated. + */ private unsubscribeFromRoomEvents() { this.room.presence.off('presence.joined-room'); this.room.presence.off('presence.leave'); this.room.presence.off('presence.update'); } - private emit(event: string, data: T) { + /** + * Emits an event to the observers. + * + * @template E - The type of the event. + * @param event - The event options containing the event type. + * @param data - The payload data associated with the event. + * @returns void + */ + private emit( + event: EventOptions, + data: RoomEventPayload, + ): void { const subject = this.observers.get(event); if (!subject) return; @@ -106,16 +146,66 @@ export class Room { * */ - private onParticipantJoinedRoom = (data) => { - console.log('room joined', data); + /** + * Handles the event when a participant joins the room. + * + * @param data - The event data containing information about the participant. + * @fires ParticipantEvent.PARTICIPANT_JOINED - Emitted when a participant joins the room. + */ + private onParticipantJoinedRoom = (data: PresenceEvent<{}>) => { + if (this.participant.id === data.id) { + this.onLocalParticipantJoinedRoom(data); + } + + this.emit(ParticipantEvent.PARTICIPANT_JOINED, data); + }; + + /** + * Handles the event when a local participant joins the room. + * + * @param data - The presence event data associated with the participant joining. + * @fires ParticipantEvent.MY_PARTICIPANT_JOINED - Emitted when the local participant joins + * the room. + */ + private onLocalParticipantJoinedRoom = (data: PresenceEvent<{}>) => { + this.room.presence.update(this.participant); + + this.emit(ParticipantEvent.MY_PARTICIPANT_JOINED, data); }; - private onParticipantLeavesRoom = (data) => { - console.log('room left', data); + /** + * Handles the event when a participant leaves the room. + * + * @param data - The presence event data containing the participant information. + * @fires ParticipantEvent.PARTICIPANT_LEFT - Emitted when a participant leaves the room. + */ + private onParticipantLeavesRoom = (data: PresenceEvent) => { + this.emit(ParticipantEvent.PARTICIPANT_LEFT, data); }; - private onParticipantUpdates = (data) => { - console.log('room update', data); + /** + * Handles participant updates received from a presence event. + * + * @param data - The presence event containing participant data. + * @fires ParticipantEvent.PARTICIPANT_UPDATED - Emitted when a participant's data is updated. + */ + private onParticipantUpdates = (data: PresenceEvent) => { + if (this.participant.id === data.data.id) { + this.onLocalParticipantUpdates(data); + } + + this.emit(ParticipantEvent.PARTICIPANT_UPDATED, data.data); + }; + + /** + * Handles updates to the local participant's presence. + * + * @param data - The presence event containing the updated participant data. + * @fires ParticipantEvent.MY_PARTICIPANT_UPDATED - Emitted when the local participant's data + * is updated. + */ + private onLocalParticipantUpdates = (data: PresenceEvent) => { + this.emit(ParticipantEvent.MY_PARTICIPANT_UPDATED, data.data); }; /** diff --git a/packages/room/src/core/types.ts b/packages/room/src/core/types.ts index ea3c6d37..7d23cd76 100644 --- a/packages/room/src/core/types.ts +++ b/packages/room/src/core/types.ts @@ -1,8 +1,46 @@ import { Participant } from '../common/types/participant.types'; export interface RoomParams { - participant: Participant + participant: Participant; } -export type RoomEventsArg = string -export type Callback = (event: T) => void; +type RoomError = { + code: string, + message: string +} + +export enum ParticipantEvent { + MY_PARTICIPANT_JOINED = 'my-participant.joined', + MY_PARTICIPANT_LEFT = 'my-participant.left', + MY_PARTICIPANT_UPDATED = 'my-participant.updated', + PARTICIPANT_JOINED = 'participant.joined', + PARTICIPANT_LEFT = 'participant.left', + PARTICIPANT_UPDATED = 'participant.updated', +} + +export enum RoomEvent { + ERROR = 'room.error' +} + +export interface RoomEventPayloads { + [ParticipantEvent.MY_PARTICIPANT_JOINED]: Participant; + [ParticipantEvent.MY_PARTICIPANT_LEFT]: Participant; + [ParticipantEvent.MY_PARTICIPANT_UPDATED]: Participant; + [ParticipantEvent.PARTICIPANT_JOINED]: Participant; + [ParticipantEvent.PARTICIPANT_LEFT]: Participant; + [ParticipantEvent.PARTICIPANT_UPDATED]: Participant; + [RoomEvent.ERROR]: RoomError; +} + +export type EventOptions = T | `${T}` + +export type GeneralEvent = ParticipantEvent | RoomEvent; + +export type RoomEventPayload = + T extends keyof RoomEventPayloads ? RoomEventPayloads[T] : never; + +export type RoomEventPair = { + [K in keyof RoomEventPayloads]: { event: K; payload: RoomEventPayloads[K] }; + }[keyof RoomEventPayloads]; + +export type Callback = (event: RoomEventPayload) => void; diff --git a/packages/room/src/index.ts b/packages/room/src/index.ts index 4c3e6a7a..fe59ac57 100644 --- a/packages/room/src/index.ts +++ b/packages/room/src/index.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { Participant } from './common/types/participant.types'; import { Room } from './core'; +import { Callback, ParticipantEvent, RoomEvent } from './core/types'; import { ApiService } from './services/api'; import config from './services/config'; import { InitializeRoomParams, InitializeRoomSchema } from './types'; @@ -81,3 +82,14 @@ export async function createRoom(params: InitializeRoomParams): Promise { throw error; } } + +export type { + Room, + Participant, +}; + +export { + RoomEvent, + ParticipantEvent, + Callback, +}; From c446f75ba137b5a36699516aa52db47e429abc6e Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 3 Jan 2025 13:04:42 -0300 Subject: [PATCH 6/8] feat: format the participant object from the events payloads --- .../src/common/types/participant.types.ts | 15 ++++- packages/room/src/core/index.test.ts | 12 ++-- packages/room/src/core/index.ts | 62 ++++++++++++++++--- packages/room/src/core/types.ts | 4 +- packages/room/src/services/io/index.ts | 4 +- 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/packages/room/src/common/types/participant.types.ts b/packages/room/src/common/types/participant.types.ts index e5c05ecd..84f235c7 100644 --- a/packages/room/src/common/types/participant.types.ts +++ b/packages/room/src/common/types/participant.types.ts @@ -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; } diff --git a/packages/room/src/core/index.test.ts b/packages/room/src/core/index.test.ts index 36b3a53f..6396d9a1 100644 --- a/packages/room/src/core/index.test.ts +++ b/packages/room/src/core/index.test.ts @@ -82,30 +82,34 @@ describe('Room', () => { 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, 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(params.participant); - expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_JOINED, 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, data); + expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.PARTICIPANT_LEFT, expected); }); it('should handle participant updates event', () => { diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index 27d50ed7..d4dd40c2 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -1,7 +1,7 @@ import type { PresenceEvent, Room as SocketRoomType } from '@superviz/socket-client'; -import { Subject, Subscription } from 'rxjs'; +import { Subject, Subscription, timestamp } from 'rxjs'; -import { Participant } from '../common/types/participant.types'; +import { InitialParticipant, Participant } from '../common/types/participant.types'; import { Logger } from '../common/utils/logger'; import { IOC } from '../services/io'; import { IOCState } from '../services/io/types'; @@ -9,7 +9,7 @@ import { IOCState } from '../services/io/types'; import { GeneralEvent, ParticipantEvent, RoomEventPayload, RoomParams, Callback, EventOptions } from './types'; export class Room { - private participant: RoomParams['participant']; + private participant: Participant; private io: IOC; private logger: Logger; private room: SocketRoomType; @@ -18,7 +18,7 @@ export class Room { constructor(params: RoomParams) { this.io = new IOC(params.participant); - this.participant = params.participant; + this.participant = this.createParticipant(params.participant); this.logger = new Logger('@superviz/room/room'); this.logger.log('room created', this.participant); @@ -88,6 +88,51 @@ export class Room { this.subscribeToRoomEvents(); } + /** + * Transforms a socket message into a Participant object. + * + * @param message - The presence event containing the participant data. + * @returns The transformed Participant object. + */ + private transfromSocketMesssageToParticipant( + message: PresenceEvent, + ): Participant { + const participant = message.data as Participant; + + return { + id: message.id, + name: participant?.name ? participant.name : message.name, + activeComponents: participant?.activeComponents ?? [], + slot: participant?.slot ?? { + index: null, + color: '#878291', + textColor: '#fff', + colorName: 'gray', + timestamp: Date.now(), + }, + }; + } + + /** + * Creates a new participant with the given initial data and assigns default slot properties. + * + * @param initialData - The initial data for the participant. + * @returns A new participant object with the provided initial data and default slot properties. + */ + private createParticipant(initialData: InitialParticipant): Participant { + return { + ...initialData, + activeComponents: [], + slot: { + index: null, + color: '#878291', + textColor: '#fff', + colorName: 'gray', + timestamp: Date.now(), + }, + }; + } + /** * Subscribes to room events such as participant joining, leaving, and updating. * @@ -157,7 +202,7 @@ export class Room { this.onLocalParticipantJoinedRoom(data); } - this.emit(ParticipantEvent.PARTICIPANT_JOINED, data); + this.emit(ParticipantEvent.PARTICIPANT_JOINED, this.transfromSocketMesssageToParticipant(data)); }; /** @@ -170,7 +215,10 @@ export class Room { private onLocalParticipantJoinedRoom = (data: PresenceEvent<{}>) => { this.room.presence.update(this.participant); - this.emit(ParticipantEvent.MY_PARTICIPANT_JOINED, data); + this.emit( + ParticipantEvent.MY_PARTICIPANT_JOINED, + this.transfromSocketMesssageToParticipant(data), + ); }; /** @@ -180,7 +228,7 @@ export class Room { * @fires ParticipantEvent.PARTICIPANT_LEFT - Emitted when a participant leaves the room. */ private onParticipantLeavesRoom = (data: PresenceEvent) => { - this.emit(ParticipantEvent.PARTICIPANT_LEFT, data); + this.emit(ParticipantEvent.PARTICIPANT_LEFT, this.transfromSocketMesssageToParticipant(data)); }; /** diff --git a/packages/room/src/core/types.ts b/packages/room/src/core/types.ts index 7d23cd76..a3767082 100644 --- a/packages/room/src/core/types.ts +++ b/packages/room/src/core/types.ts @@ -1,7 +1,7 @@ -import { Participant } from '../common/types/participant.types'; +import { InitialParticipant, Participant } from '../common/types/participant.types'; export interface RoomParams { - participant: Participant; + participant: InitialParticipant; } type RoomError = { diff --git a/packages/room/src/services/io/index.ts b/packages/room/src/services/io/index.ts index bbbc4697..e57a9b48 100644 --- a/packages/room/src/services/io/index.ts +++ b/packages/room/src/services/io/index.ts @@ -1,7 +1,7 @@ import * as Socket from '@superviz/socket-client'; import { Subject } from 'rxjs'; -import { Participant } from '../../common/types/participant.types'; +import { InitialParticipant, Participant } from '../../common/types/participant.types'; import config from '../config/index'; import { IOCState } from './types'; @@ -12,7 +12,7 @@ export class IOC { public stateSubject: Subject = new Subject(); - constructor(private participant: Participant) { + constructor(private participant: InitialParticipant) { this.createClient(); } From 8039b8cdd92b82e07e3551ec4cc9eb0ae6e6764d Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 3 Jan 2025 13:57:15 -0300 Subject: [PATCH 7/8] feat: remove all observers when leave the room --- packages/room/src/core/index.test.ts | 8 ++++++++ packages/room/src/core/index.ts | 16 +++++++++++++++- packages/room/src/index.ts | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/room/src/core/index.test.ts b/packages/room/src/core/index.test.ts index 6396d9a1..6a908a4b 100644 --- a/packages/room/src/core/index.test.ts +++ b/packages/room/src/core/index.test.ts @@ -50,6 +50,14 @@ describe('Room', () => { 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'; diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index d4dd40c2..bbec6f55 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -10,9 +10,12 @@ import { GeneralEvent, ParticipantEvent, RoomEventPayload, RoomParams, Callback, export class Room { private participant: Participant; + private io: IOC; - private logger: Logger; private room: SocketRoomType; + + private logger: Logger; + private subscriptions: Map, Subscription> = new Map(); private observers: Map> = new Map(); @@ -31,6 +34,17 @@ export class Room { public leave() { this.unsubscribeFromRoomEvents(); + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + + this.observers.forEach((observer) => { + observer.complete(); + }); + + this.subscriptions.clear(); + this.observers.clear(); + this.room.disconnect(); this.io.destroy(); } diff --git a/packages/room/src/index.ts b/packages/room/src/index.ts index fe59ac57..25a42a36 100644 --- a/packages/room/src/index.ts +++ b/packages/room/src/index.ts @@ -67,7 +67,7 @@ async function setUpEnvironment({ */ export async function createRoom(params: InitializeRoomParams): Promise { try { - const { developerToken, participant, roomId } = InitializeRoomSchema.parse(params); + const { participant } = InitializeRoomSchema.parse(params); await setUpEnvironment(params); From b84dba5543cfdd87deffb5365d13551a1fad405a Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 3 Jan 2025 14:18:33 -0300 Subject: [PATCH 8/8] feat: handle the connections erros and publish the room state for the users --- apps/playground/src/pages/superviz-room.tsx | 13 +++- packages/room/src/core/index.test.ts | 43 ++++++++++++-- packages/room/src/core/index.ts | 66 +++++++++++++++++++-- packages/room/src/core/types.ts | 9 ++- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/apps/playground/src/pages/superviz-room.tsx b/apps/playground/src/pages/superviz-room.tsx index b2775d6b..e998512c 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 } from '@superviz/room' +import { createRoom, type Room, ParticipantEvent, RoomEvent } from '@superviz/room' import { v4 as generateId } from "uuid"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -33,6 +33,7 @@ export function SuperVizRoom() { }); room.current = newRoom; + subscribeToEvents(); }, []); useEffect(() => { @@ -51,6 +52,12 @@ export function SuperVizRoom() { }) }); + Object.values(RoomEvent).forEach(event => { + room.current?.subscribe(event, (data) => { + console.log('New event from room, eventName:', event, 'data:', data); + }) + }); + setSubscribed(true); } @@ -61,6 +68,10 @@ export function SuperVizRoom() { room.current?.unsubscribe(event); }); + Object.values(RoomEvent).forEach(event => { + room.current?.unsubscribe(event) + }); + setSubscribed(false); } diff --git a/packages/room/src/core/index.test.ts b/packages/room/src/core/index.test.ts index 6a908a4b..c23df061 100644 --- a/packages/room/src/core/index.test.ts +++ b/packages/room/src/core/index.test.ts @@ -2,6 +2,7 @@ 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'; @@ -138,12 +139,46 @@ describe('Room', () => { expect(emitSpy).toHaveBeenCalledWith(ParticipantEvent.MY_PARTICIPANT_UPDATED, data.data); }); - it('should handle connection state change', () => { - const state = { connected: true } as any; - const logSpy = jest.spyOn(room['logger'], 'log'); + 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(logSpy).toHaveBeenCalledWith('connection state changed', state); + expect(emitSpy).toHaveBeenCalledWith('room.update', { status: state }); }); }); diff --git a/packages/room/src/core/index.ts b/packages/room/src/core/index.ts index bbec6f55..b5c8949e 100644 --- a/packages/room/src/core/index.ts +++ b/packages/room/src/core/index.ts @@ -6,7 +6,7 @@ import { Logger } from '../common/utils/logger'; import { IOC } from '../services/io'; import { IOCState } from '../services/io/types'; -import { GeneralEvent, ParticipantEvent, RoomEventPayload, RoomParams, Callback, EventOptions } from './types'; +import { GeneralEvent, ParticipantEvent, RoomEventPayload, RoomParams, Callback, EventOptions, RoomEvent } from './types'; export class Room { private participant: Participant; @@ -34,6 +34,12 @@ export class Room { public leave() { this.unsubscribeFromRoomEvents(); + this.emit(ParticipantEvent.PARTICIPANT_LEFT, this.participant); + this.emit(ParticipantEvent.MY_PARTICIPANT_LEFT, this.participant); + + this.room.disconnect(); + this.io.destroy(); + this.subscriptions.forEach((subscription) => { subscription.unsubscribe(); }); @@ -44,9 +50,6 @@ export class Room { this.subscriptions.clear(); this.observers.clear(); - - this.room.disconnect(); - this.io.destroy(); } /** @@ -270,6 +273,42 @@ export class Room { this.emit(ParticipantEvent.MY_PARTICIPANT_UPDATED, data.data); }; + /** + * Handles authentication errors during room initialization. + * + * This method logs an error message indicating that the website's domain is not whitelisted. + * It emits an `ERROR` event with the error code 'auth_error' and the error message. + * Finally, it calls the `leave` method to exit the room. + * + * @fires RoomEvent.ERROR - Emitted when an authentication error occurs. + */ + private onAuthError = () => { + const 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"; + + this.logger.log(message); + console.error(message); + + this.emit(RoomEvent.ERROR, { code: 'auth_error', message }); + this.leave(); + }; + + /** + * Handles the error when a user tries to connect to a room with the same account that is already + * connected. + * Logs the error message, emits an error event, and leaves the room. + * + * @fires RoomEvent.ERROR - Emitted when a user tries to connect to a room with the same account. + */ + private onSameAccountError = () => { + const 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.'; + + this.logger.log(message); + console.error(message); + + this.emit(RoomEvent.ERROR, { code: 'same_account_error', message }); + this.leave(); + }; + /** * @description Handles changes in the connection state. * @@ -277,5 +316,24 @@ export class Room { */ private onConnectionStateChange = (state: IOCState): void => { this.logger.log('connection state changed', state); + + const common = () => { + this.emit(RoomEvent.UPDATE, { status: state }); + }; + + const map = { + [IOCState.CONNECTING]: () => common(), + [IOCState.CONNECTION_ERROR]: () => common(), + [IOCState.CONNECTED]: () => common(), + [IOCState.DISCONNECTED]: () => common(), + [IOCState.RECONNECTING]: () => common(), + [IOCState.RECONNECT_ERROR]: () => common(), + + // error + [IOCState.AUTH_ERROR]: () => this.onAuthError(), + [IOCState.SAME_ACCOUNT_ERROR]: () => this.onSameAccountError(), + }; + + map[state](); }; } diff --git a/packages/room/src/core/types.ts b/packages/room/src/core/types.ts index a3767082..b1f0c8f9 100644 --- a/packages/room/src/core/types.ts +++ b/packages/room/src/core/types.ts @@ -1,4 +1,5 @@ import { InitialParticipant, Participant } from '../common/types/participant.types'; +import { IOCState } from '../services/io/types'; export interface RoomParams { participant: InitialParticipant; @@ -9,6 +10,10 @@ type RoomError = { message: string } +type RoomUpdate = { + status: IOCState, +} + export enum ParticipantEvent { MY_PARTICIPANT_JOINED = 'my-participant.joined', MY_PARTICIPANT_LEFT = 'my-participant.left', @@ -19,7 +24,8 @@ export enum ParticipantEvent { } export enum RoomEvent { - ERROR = 'room.error' + ERROR = 'room.error', + UPDATE = 'room.update', } export interface RoomEventPayloads { @@ -30,6 +36,7 @@ export interface RoomEventPayloads { [ParticipantEvent.PARTICIPANT_LEFT]: Participant; [ParticipantEvent.PARTICIPANT_UPDATED]: Participant; [RoomEvent.ERROR]: RoomError; + [RoomEvent.UPDATE]: RoomUpdate; } export type EventOptions = T | `${T}`