From 0ad7c7e98019cdfdd5dcaf5f575fff0ad6bf1dfa Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 18 Jul 2024 12:18:41 -0300 Subject: [PATCH 1/2] fix: remove the socket client dependency and implement it directly in the SDK --- __mocks__/io.mock.ts | 2 +- jest.config.js | 1 - jest.setup.js | 2 +- package.json | 5 +- src/components/base/index.ts | 2 +- src/components/form-elements/index.ts | 2 +- src/components/form-elements/types.ts | 2 +- src/components/presence-mouse/canvas/index.ts | 2 +- src/components/presence-mouse/html/index.ts | 2 +- src/components/realtime/channel.ts | 2 +- src/components/video/index.test.ts | 2 +- src/components/video/index.ts | 2 +- src/components/who-is-online/index.ts | 2 +- src/core/launcher/index.test.ts | 2 - src/core/launcher/index.ts | 2 +- .../socket/common/types/callbacks.types.ts | 1 + src/lib/socket/common/types/event.types.ts | 44 ++++ src/lib/socket/common/types/presence.types.ts | 8 + src/lib/socket/connection/index.ts | 128 +++++++++++ src/lib/socket/connection/types.ts | 41 ++++ src/lib/socket/index.ts | 23 ++ src/lib/socket/presence/index.ts | 196 +++++++++++++++++ src/lib/socket/presence/types.ts | 14 ++ src/lib/socket/realtime/index.ts | 62 ++++++ src/lib/socket/room/index.ts | 208 ++++++++++++++++++ src/lib/socket/room/types.ts | 39 ++++ src/services/io/index.test.ts | 2 +- src/services/io/index.ts | 2 +- .../presence-3d-manager/index.test.ts | 2 +- src/services/presence-3d-manager/index.ts | 2 +- src/services/roomState/index.test.ts | 2 +- src/services/roomState/index.ts | 8 +- src/services/roomState/type.ts | 3 +- src/services/slot/index.ts | 2 +- yarn.lock | 57 ++--- 35 files changed, 803 insertions(+), 73 deletions(-) create mode 100644 src/lib/socket/common/types/callbacks.types.ts create mode 100644 src/lib/socket/common/types/event.types.ts create mode 100644 src/lib/socket/common/types/presence.types.ts create mode 100644 src/lib/socket/connection/index.ts create mode 100644 src/lib/socket/connection/types.ts create mode 100644 src/lib/socket/index.ts create mode 100644 src/lib/socket/presence/index.ts create mode 100644 src/lib/socket/presence/types.ts create mode 100644 src/lib/socket/realtime/index.ts create mode 100644 src/lib/socket/room/index.ts create mode 100644 src/lib/socket/room/types.ts diff --git a/__mocks__/io.mock.ts b/__mocks__/io.mock.ts index e38ffb8a..78795ee2 100644 --- a/__mocks__/io.mock.ts +++ b/__mocks__/io.mock.ts @@ -1,5 +1,5 @@ import { jest } from '@jest/globals'; -import { Room } from '@superviz/socket-client'; +import { Room } from '../src/lib/socket'; export const MOCK_IO = { ClientState: { diff --git a/jest.config.js b/jest.config.js index 13f1e82b..5559ec80 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,6 @@ module.exports = { '/e2e/', '/src/web-components', ], - transformIgnorePatterns: ['node_modules/(?!@superviz/socket-client)'], transform: { '^.+\\.ts$': 'ts-jest', '^.+\\.js$': 'ts-jest', diff --git a/jest.setup.js b/jest.setup.js index ff4045e5..e631e1b8 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -36,4 +36,4 @@ global.DOMPoint = class { } }; -jest.mock('@superviz/socket-client', () => MOCK_IO); +jest.mock('./src/lib/socket', () => MOCK_IO); diff --git a/package.json b/package.json index c7657163..e8ceb3bd 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "yargs": "^17.7.2" }, "dependencies": { - "@superviz/socket-client": "1.8.2", "bowser": "^2.11.0", "bowser-jr": "^1.0.6", "debug": "^4.3.4", @@ -92,7 +91,9 @@ "lodash.isequal": "^4.5.0", "luxon": "^3.4.4", "rxjs": "^7.8.1", - "semantic-release-version-file": "^1.0.2" + "semantic-release-version-file": "^1.0.2", + "socket.io-client": "^4.7.5", + "zod": "^3.23.8" }, "config": { "commitizen": { diff --git a/src/components/base/index.ts b/src/components/base/index.ts index e4a88efc..43d0f0bd 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; import { Group } from '../../common/types/participant.types'; diff --git a/src/components/form-elements/index.ts b/src/components/form-elements/index.ts index 01452c66..8047d04f 100644 --- a/src/components/form-elements/index.ts +++ b/src/components/form-elements/index.ts @@ -1,4 +1,4 @@ -import { SocketEvent } from '@superviz/socket-client'; +import { SocketEvent } from '../../lib/socket'; import { Participant } from '../../common/types/participant.types'; import { StoreType } from '../../common/types/stores.types'; diff --git a/src/components/form-elements/types.ts b/src/components/form-elements/types.ts index 5d07277f..eacd4527 100644 --- a/src/components/form-elements/types.ts +++ b/src/components/form-elements/types.ts @@ -1,4 +1,4 @@ -import { SocketEvent } from '@superviz/socket-client'; +import { SocketEvent } from '../../lib/socket'; export type FormElementsProps = { fields?: string[] | string; diff --git a/src/components/presence-mouse/canvas/index.ts b/src/components/presence-mouse/canvas/index.ts index 7a5250b5..c90fbc20 100644 --- a/src/components/presence-mouse/canvas/index.ts +++ b/src/components/presence-mouse/canvas/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../../lib/socket'; import { throttle } from 'lodash'; import { RealtimeEvent } from '../../../common/types/events.types'; diff --git a/src/components/presence-mouse/html/index.ts b/src/components/presence-mouse/html/index.ts index e0929f7e..e0e7d0d9 100644 --- a/src/components/presence-mouse/html/index.ts +++ b/src/components/presence-mouse/html/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../../lib/socket'; import { isEqual } from 'lodash'; import { Subscription, fromEvent, throttleTime } from 'rxjs'; diff --git a/src/components/realtime/channel.ts b/src/components/realtime/channel.ts index 07e05baf..5b5dab8d 100644 --- a/src/components/realtime/channel.ts +++ b/src/components/realtime/channel.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import throttle from 'lodash/throttle'; import { ComponentLifeCycleEvent } from '../../common/types/events.types'; diff --git a/src/components/video/index.test.ts b/src/components/video/index.test.ts index cb174d09..4f83371f 100644 --- a/src/components/video/index.test.ts +++ b/src/components/video/index.test.ts @@ -1,6 +1,6 @@ import { TextEncoder, TextDecoder } from 'util'; -import { PresenceEvent } from '@superviz/socket-client'; +import { PresenceEvent } from '../../lib/socket'; import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; diff --git a/src/components/video/index.ts b/src/components/video/index.ts index 1f736211..253de2aa 100644 --- a/src/components/video/index.ts +++ b/src/components/video/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents, Room } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room } from '../../lib/socket'; import { ColorsVariables } from '../../common/types/colors.types'; import { diff --git a/src/components/who-is-online/index.ts b/src/components/who-is-online/index.ts index f3c03689..691fce6a 100644 --- a/src/components/who-is-online/index.ts +++ b/src/components/who-is-online/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents } from '../../lib/socket'; import { RealtimeEvent, WhoIsOnlineEvent } from '../../common/types/events.types'; import { Participant, Avatar } from '../../common/types/participant.types'; diff --git a/src/core/launcher/index.test.ts b/src/core/launcher/index.test.ts index afcf3463..2fe101c3 100644 --- a/src/core/launcher/index.test.ts +++ b/src/core/launcher/index.test.ts @@ -1,5 +1,3 @@ -import { PresenceEvent } from '@superviz/socket-client'; - import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index e2f05128..0571e5c6 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { isEqual } from 'lodash'; import { ParticipantEvent } from '../../common/types/events.types'; diff --git a/src/lib/socket/common/types/callbacks.types.ts b/src/lib/socket/common/types/callbacks.types.ts new file mode 100644 index 00000000..a97e7e2d --- /dev/null +++ b/src/lib/socket/common/types/callbacks.types.ts @@ -0,0 +1 @@ +export type ErrorCallback = (error: Error) => void; diff --git a/src/lib/socket/common/types/event.types.ts b/src/lib/socket/common/types/event.types.ts new file mode 100644 index 00000000..96dbc206 --- /dev/null +++ b/src/lib/socket/common/types/event.types.ts @@ -0,0 +1,44 @@ +/** + * @enum RoomEvents + * @description events that the server listens to in the room module + * @property JOIN_ROOM - event to join a room + * @property LEAVE_ROOM - event to leave a room + * @property UPDATE - event to update a room + * @property JOINED_ROOM - event to indicate a user has joined a room + * @property ERROR - event to indicate an error in the room module + */ +export enum RoomEvents { + JOIN_ROOM = 'room.join', + JOINED_ROOM = 'room.joined', + LEAVE_ROOM = 'room.leave', + UPDATE = 'room.update', + ERROR = 'room.error', +} + +export enum InternalRoomEvents { + GET = 'room.get', +} + +/** + * @enum PresenceEvents + * @description events that the server listens to in the presence module + * @property JOINED_ROOM - event to indicate a user has joined a room + * @property LEAVE - event to indicate a user has left a room + * @property UPDATE - event to indicate a user has updated their presence + * @property ERROR - event to indicate an error in the presence module + */ +export enum PresenceEvents { + JOINED_ROOM = 'presence.joined-room', + LEAVE = 'presence.leave', + ERROR = 'presence.error', + UPDATE = 'presence.update', +} + +/** + * @enum InternalPresenceEvents + * @description events that the server listens to in the presence module + * @property GET - event to get the presence list + */ +export enum InternalPresenceEvents { + GET = 'presence.get', +} diff --git a/src/lib/socket/common/types/presence.types.ts b/src/lib/socket/common/types/presence.types.ts new file mode 100644 index 00000000..2b600164 --- /dev/null +++ b/src/lib/socket/common/types/presence.types.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const PresenceSchema = z.object({ + id: z.string(), + name: z.string(), +}); + +export type Presence = z.infer; diff --git a/src/lib/socket/connection/index.ts b/src/lib/socket/connection/index.ts new file mode 100644 index 00000000..08087084 --- /dev/null +++ b/src/lib/socket/connection/index.ts @@ -0,0 +1,128 @@ +import { Subject } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; +import { RoomEvents } from '../common/types/event.types'; + +import { ClientState, ConnectionState, SocketErrorEvent, SocketEvent } from './types'; +import { Logger } from '../../../common/utils'; + +export class ClientConnection { + private logger: Logger; + private stateObserver: Subject; + + public state: ClientState; + + constructor(private socket: Socket) { + this.logger = new Logger('@superviz/sdk/socket-client/connection'); + this.subscribeToManagerEvents(); + this.stateObserver = new Subject(); + } + + public on(next: (state: ConnectionState) => void, error?: ErrorCallback) { + if (this.stateObserver.closed) { + this.stateObserver = new Subject(); + } + + this.stateObserver.subscribe({ + next, + error, + }); + } + + public off() { + if (this.stateObserver.closed) return; + + this.stateObserver.unsubscribe(); + } + + /** + * @function subscribeToManagerEvents + * @description Subscribe to the manager events + * @returns {void} + */ + private subscribeToManagerEvents(): void { + this.socket.on('connect', this.onConnect); + this.socket.on('disconnect', this.onDisconnect); + this.socket.on('connect_error', this.onConnectError); + this.socket.io.on('error', this.onConnectionError); + this.socket.io.on('reconnect', this.onReconnect); + this.socket.io.on('reconnect_attempt', this.onReconnecAttempt); + this.socket.io.on('reconnect_error', this.onReconnectError); + this.socket.io.on('reconnect_failed', this.onReconnectFailed); + + // custom validations listener + this.socket.on(SocketEvent.ERROR, this.onCustomError); + } + + /** + * @function changeState + * @description Change the state of the connection + * @returns {void} + */ + private changeState(state: ClientState, reason?: string): void { + this.state = state; + + if (this.stateObserver.closed) return; + + this.stateObserver.next({ + state, + reason, + }); + } + + /** Manager events handlers */ + + private onConnect = () => { + this.logger.log('connection @ on connect', 'Connected to the socket'); + this.changeState(ClientState.CONNECTED); + }; + + private onDisconnect = (reason: Socket.DisconnectReason) => { + this.logger.log('connection @ on disconnect', 'Disconnected from the socket'); + this.changeState(ClientState.DISCONNECTED, reason); + }; + + private onConnectError = (error: Error) => { + this.logger.log('connection @ on connect error', 'Connection error', error); + this.changeState(ClientState.CONNECTION_ERROR, error.message); + }; + + private onConnectionError = (error: Error) => { + this.logger.log('connection @ on connection error', 'Connection error', error); + this.changeState(ClientState.CONNECTION_ERROR, error.message); + }; + + private onReconnect = () => { + this.logger.log('connection @ on reconnect', 'Reconnected to the socket'); + this.changeState(ClientState.CONNECTED); + }; + + private onReconnectError = (error: Error) => { + this.logger.log('connection @ on reconnect error', 'Reconnect error', error); + this.changeState(ClientState.RECONNECT_ERROR, error.message); + }; + + private onReconnectFailed = () => { + this.logger.log('connection @ on reconnect failed', 'Failed to reconnect to the socket'); + this.changeState(ClientState.RECONNECT_ERROR); + }; + + private onReconnecAttempt = (attempt: number) => { + this.logger.log('connection @ on reconnect attempt', `Reconnect attempt #${attempt}`); + this.changeState(ClientState.RECONNECTING, `Reconnect attempt #${attempt}`); + }; + + private onCustomError = (error: SocketErrorEvent) => { + if (error.needsToDisconnect) { + this.socket.disconnect(); + } + + if (error.level === 'error') { + console.error('[SuperViz - Error]', 'Type: ', error.errorType, 'Message :', error.message); + return; + } + + console.warn('[SuperViz - Warning]', 'Type: ', error.errorType, 'Message :', error.message); + }; +} diff --git a/src/lib/socket/connection/types.ts b/src/lib/socket/connection/types.ts new file mode 100644 index 00000000..dde82aab --- /dev/null +++ b/src/lib/socket/connection/types.ts @@ -0,0 +1,41 @@ +/** + * @enum ClientState + * @description the state of the client + * @property CONNECTED - the client is connected + * @property CONNECTING - the client is connecting + * @property DISCONNECTED - the client is disconnected + * @property CONNECTION_ERROR - the client has a connection error + * @property RECONNECTING - the client is reconnecting + * @property RECONNECT_ERROR - the client has a reconnect error + */ +export enum ClientState { + CONNECTED = 'CONNECTED', + CONNECTING = 'CONNECTING', + DISCONNECTED = 'DISCONNECTED', + CONNECTION_ERROR = 'CONNECTION_ERROR', + RECONNECTING = 'RECONNECTING', + RECONNECT_ERROR = 'RECONNECT_ERROR', +} + +/** + * @interface ConnectionState + * @description the state of the connection + * @property state - the state of the connection + * @property reason - the reason for the state change + */ +export interface ConnectionState { + state: ClientState; + reason?: string; +} + +export type SocketErrorEvent = { + errorType: 'message-size-limit' | 'rate-limit' | 'room-connections-limit'; + message: string; + connectionId: string; + needsToDisconnect: boolean; + level: 'error' | 'warn'; +}; + +export enum SocketEvent { + ERROR = 'socket-event.error', +} diff --git a/src/lib/socket/index.ts b/src/lib/socket/index.ts new file mode 100644 index 00000000..6a37879e --- /dev/null +++ b/src/lib/socket/index.ts @@ -0,0 +1,23 @@ +import { PresenceEvents, RoomEvents } from './common/types/event.types'; +import { ClientState, ConnectionState } from './connection/types'; +import { PresenceRoom } from './presence'; +import { PresenceCallback, PresenceEvent } from './presence/types'; +import { Realtime } from './realtime'; +import { Room } from './room'; +import { Callback, SocketEvent, JoinRoomPayload, RoomHistory } from './room/types'; + +export { + Realtime, + PresenceEvents, + RoomEvents, + ClientState, + ConnectionState, + Callback, + SocketEvent, + JoinRoomPayload, + RoomHistory, + PresenceCallback, + PresenceEvent, + Room, + PresenceRoom, +}; diff --git a/src/lib/socket/presence/index.ts b/src/lib/socket/presence/index.ts new file mode 100644 index 00000000..d633afcc --- /dev/null +++ b/src/lib/socket/presence/index.ts @@ -0,0 +1,196 @@ +import { Subject } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; +import { InternalPresenceEvents, PresenceEvents } from '../common/types/event.types'; +import type { Presence } from '../common/types/presence.types'; + +import { PresenceCallback, PresenceEvent, PresenceEventFromServer } from './types'; +import { Logger } from '../../../common/utils'; + +export class PresenceRoom { + private logger: Logger; + private presences: Set = new Set(); + private observers: Map> = new Map(); + + constructor(private io: Socket, private presence: Presence, private roomId: string) { + this.logger = new Logger('@superviz/sdk/socket-client/presence'); + + this.registerSubsjects(); + this.subscribeToPresenceEvents(); + } + + public static register(io: Socket, presence: Presence, roomId: string) { + return new PresenceRoom(io, presence, roomId); + } + + /** + * @function get + * @description Get the presences in the room + * @returns {void} + */ + public get(next: (data: PresenceEvent[]) => void, error?: ErrorCallback): void { + const subject = new Subject(); + + subject.subscribe({ + next, + error, + }); + + const callback = (event: { + presences: PresenceEventFromServer[]; + connectionId: string; + timestamp: number; + roomId: string; + }) => { + const presences = event.presences.map((presence) => ({ + connectionId: presence.connectionId, + data: presence.data, + id: presence.id, + name: presence.name, + timestamp: presence.timestamp, + })); + + this.logger.log('presence room @ get', event); + this.io.off(InternalPresenceEvents.GET, callback); + subject.next(presences); + subject.complete(); + }; + + this.io.on(InternalPresenceEvents.GET, callback); + this.io.emit(InternalPresenceEvents.GET, this.roomId); + } + + /** + * @function update + * @description update the presence data in the room + * @param payload - The data to update + * @returns {void} + */ + public update(payload: T): void { + const body: PresenceEvent = { + connectionId: this.io.id, + data: payload, + id: this.presence.id, + name: this.presence.name, + timestamp: Date.now(), + }; + + this.io.emit(PresenceEvents.UPDATE, this.roomId, body); + this.logger.log('presence room @ update', this.roomId, body); + } + + public destroy(): void { + this.io.off(PresenceEvents.JOINED_ROOM, this.onPresenceJoin); + this.io.off(PresenceEvents.LEAVE, this.onPresenceLeave); + this.io.off(PresenceEvents.UPDATE, this.onPresenceUpdate); + + this.observers.forEach((observer) => observer.unsubscribe()); + this.observers.clear(); + } + + /** + * @function registerSubsjects + * @description Register the subjects for the presence events + * @returns {void} + */ + private registerSubsjects(): void { + this.observers.set(PresenceEvents.JOINED_ROOM, new Subject()); + this.observers.set(PresenceEvents.LEAVE, new Subject()); + this.observers.set(PresenceEvents.UPDATE, new Subject()); + } + + /** + * @function on + * @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 on( + event: PresenceEvents, + callback: PresenceCallback, + error?: ErrorCallback, + ): void { + this.observers.get(event).subscribe({ + error, + next: callback, + }); + } + + /** + * @function off + * @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 off(event: PresenceEvents): void { + this.observers.get(event).unsubscribe(); + } + + /** + * @function subscribeToPresenceEvents + * @description Subscribe to the presence events + * @returns {void} + */ + private subscribeToPresenceEvents(): void { + this.io.on(PresenceEvents.JOINED_ROOM, this.onPresenceJoin); + this.io.on(PresenceEvents.LEAVE, this.onPresenceLeave); + this.io.on(PresenceEvents.UPDATE, this.onPresenceUpdate); + } + + /** + * @function onPresenceJoin + * @description Handle the presence join event + * @param event - The presence event + * @returns {void} + */ + private onPresenceJoin = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence join', event); + this.presences.add(event); + this.observers.get(PresenceEvents.JOINED_ROOM).next({ + connectionId: event.connectionId, + data: event.data, + id: event.id, + name: event.name, + timestamp: event.timestamp, + }); + }; + + /** + * @function onPresenceLeave + * @description Handle the presence leave event + * @param event - The presence event + * @returns {void} + */ + private onPresenceLeave = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence leave', event); + this.presences.delete(event); + this.observers.get(PresenceEvents.LEAVE).next(event); + }; + + /** + * @function onPresenceUpdate + * @description Handle the presence update event + * @param event - The presence event + * @returns {void} + */ + private onPresenceUpdate = (event: PresenceEventFromServer): void => { + if (event?.roomId !== this.roomId) return; + + this.logger.log('presence room @ presence update', event); + this.observers.get(PresenceEvents.UPDATE).next({ + connectionId: event.connectionId, + data: event.data, + id: event.id, + name: event.name, + roomId: event?.roomId, + timestamp: event.timestamp, + } as PresenceEvent); + }; +} diff --git a/src/lib/socket/presence/types.ts b/src/lib/socket/presence/types.ts new file mode 100644 index 00000000..89b986e3 --- /dev/null +++ b/src/lib/socket/presence/types.ts @@ -0,0 +1,14 @@ +export type PresenceEvent = { + id: string; + name: string; + connectionId: string; + data: T; + timestamp: number; +}; + +export interface PresenceEventFromServer extends PresenceEvent { + roomKey: string; + roomId: string; +} + +export type PresenceCallback = (event: PresenceEvent) => void; diff --git a/src/lib/socket/realtime/index.ts b/src/lib/socket/realtime/index.ts new file mode 100644 index 00000000..f45d54de --- /dev/null +++ b/src/lib/socket/realtime/index.ts @@ -0,0 +1,62 @@ +import { type Socket, Manager } from 'socket.io-client'; + +import { Presence } from '../common/types/presence.types'; +import { ClientConnection } from '../connection'; +import { ClientState } from '../connection/types'; +import { Room } from '../room'; + +export class Realtime { + private socket: Socket; + private manager: Manager; + public connection: ClientConnection; + + constructor( + private apiKey: string, + private environment: 'dev' | 'prod', + private presence: Presence, + ) { + this.manager = new Manager('https://io.superviz.com', { + addTrailingSlash: false, + secure: true, + withCredentials: true, + reconnection: true, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + reconnectionAttempts: 5, + extraHeaders: { + 'sv-api-key': this.apiKey, + }, + }); + + // use the default namespace to handle the connections + const { origin } = window.location; + this.socket = this.manager.socket(`/${environment}`, { + auth: { + apiKey: this.apiKey, + origin, + envirioment: this.environment, + }, + }); + + this.connection = new ClientConnection(this.socket); + } + + public get state(): ClientState { + return this.connection.state; + } + + /** + * @function connect + * @param room - The room name + * @param maxConnections - The maximum number of connections allowed in the room + * @returns {Room} - The room instance + */ + public connect(room: string, maxConnections?: number | 'unlimited'): Room { + return Room.register(this.socket, this.presence, room, this.apiKey, maxConnections); + } + + public destroy() { + this.socket.disconnect(); + this.connection.off(); + } +} diff --git a/src/lib/socket/room/index.ts b/src/lib/socket/room/index.ts new file mode 100644 index 00000000..7c205a02 --- /dev/null +++ b/src/lib/socket/room/index.ts @@ -0,0 +1,208 @@ +import { Subject, Subscription } from 'rxjs'; +import type { Socket } from 'socket.io-client'; + +import { ErrorCallback } from '../common/types/callbacks.types'; +import { InternalRoomEvents, RoomEvents } from '../common/types/event.types'; +import { Presence } from '../common/types/presence.types'; +import { PresenceRoom } from '../presence'; + +import { Callback, SocketEvent, JoinRoomPayload, RoomHistory } from './types'; +import { Logger } from '../../../common/utils'; + +export class Room { + private logger: Logger; + private isJoined: boolean = false; + private subscriptions: Map, Subscription> = new Map(); + private observers: Map> = new Map(); + + public presence: PresenceRoom; + + constructor( + private io: Socket, + private user: Presence, + private roomId: string, + private apiKey: string, + private maxConnections: number | 'unlimited' = 100, + ) { + this.logger = new Logger('@superviz/sdk/socket-client/room'); + + const payload: JoinRoomPayload = { + name: roomId, + user, + maxConnections, + }; + + this.presence = PresenceRoom.register(io, user, roomId); + this.io.emit(RoomEvents.JOIN_ROOM, payload); + this.subscribeToRoomEvents(); + } + + public static register( + io: Socket, + presence: Presence, + roomId: string, + apiKey: string, + maxConnections: number | 'unlimited', + ): Room { + return new Room(io, presence, roomId, apiKey, maxConnections); + } + + /** + * @function on + * @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 on(event: string, callback: Callback): void { + this.logger.log('room @ on', event); + + let subject = this.observers.get(event); + + if (!subject) { + subject = new Subject(); + this.observers.set(event, subject); + + this.io.on(event, (data: SocketEvent) => { + this.publishEventToClient(event, data); + }); + } + + this.subscriptions.set(callback, subject.subscribe(callback)); + } + + /** + * @function off + * @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 off(event: string, callback?: Callback): void { + this.logger.log('room @ off', event); + + if (!callback) { + this.observers.delete(event); + this.io.off(event); + return; + } + + this.subscriptions.get(callback)?.unsubscribe(); + } + + /** + * @function emit + * @description Emit an event + * @param event - The event to emit + * @param payload - The payload to send with the event + * @returns {void} + */ + public emit(event: string, payload: T): void { + if (!this.isJoined) { + this.logger.log('Cannot emit event. Not joined to room'); + return; + } + + const body: SocketEvent = { + name: event, + roomId: this.roomId, + presence: this.user, + connectionId: this.io.id, + data: payload, + timestamp: Date.now(), + }; + + this.io.emit(RoomEvents.UPDATE, this.roomId, body); + this.logger.log('room @ emit', event, payload); + } + + /** + * @function get + * @description Get the presences in the room + * @returns {void} + */ + public history(next: (data: RoomHistory) => void, error?: ErrorCallback): void { + const subject = new Subject(); + + subject.subscribe({ + next, + error, + }); + + const callback = (event: RoomHistory) => { + this.logger.log('room @ history', event); + this.io.off(InternalRoomEvents.GET, callback); + subject.next(event); + subject.complete(); + subject.unsubscribe(); + }; + + this.io.on(InternalRoomEvents.GET, callback); + this.io.emit(InternalRoomEvents.GET, this.roomId); + } + + /** + * @function disconnect + * @description Disconnect from the room + * @returns {void} + */ + public disconnect(): void { + this.logger.log('room @ disconnect', 'Leaving room...', this.roomId); + this.io.emit(RoomEvents.LEAVE_ROOM, this.roomId); + + // unsubscribe from all events + this.subscriptions.forEach((subscription) => subscription.unsubscribe()); + this.subscriptions.clear(); + this.observers.forEach((subject) => subject.unsubscribe()); + this.observers.clear(); + + this.presence.destroy(); + } + + /** + * @function publishEventToClient + * @description Publish an event to the client + * @param event - The event to publish + * @param data - The data to publish + * @returns {void} + */ + private publishEventToClient(event: string, data: SocketEvent): void { + const subject = this.observers.get(event); + + if (!subject || data.roomId !== this.roomId) return; + + subject.next(data); + } + + /* + * @function subscribeToRoomEvents + * @description Subscribe to room events + * @returns {void} + */ + private subscribeToRoomEvents(): void { + this.io.on(RoomEvents.JOINED_ROOM, this.onJoinedRoom); + this.io.on(RoomEvents.ERROR, (event: SocketEvent) => { + this.logger.log('Error:', event.data); + }); + + this.io.on(`http:${this.roomId}:${this.apiKey}`, this.onHttpEvent); + } + + /** + * @function onJoinedRoom + * @description handles the event when a user joins a room. + * @param event The socket event containing presence data. + * @returns {void} + */ + private onJoinedRoom = (event: SocketEvent<{ name: string }>): void => { + if (this.roomId !== event?.data?.name) return; + + this.isJoined = true; + this.io.emit(RoomEvents.JOINED_ROOM, this.roomId, event.data); + this.logger.log('room @ joined', event); + }; + + private onHttpEvent = (event: SocketEvent) => { + this.publishEventToClient(event.name, event); + }; +} diff --git a/src/lib/socket/room/types.ts b/src/lib/socket/room/types.ts new file mode 100644 index 00000000..e40f3c0b --- /dev/null +++ b/src/lib/socket/room/types.ts @@ -0,0 +1,39 @@ +import z from 'zod'; + +import { PresenceSchema } from '../common/types/presence.types'; + +export type SocketEvent = { + name: string; + roomId: string; + connectionId: string; + presence?: z.infer; + data: T; + timestamp: number; +}; + +export type RoomHistory = { + roomId: string; + room: { + id: string; + name: string; + userId: string; + apiKey: string; + createdAt: Date; + }; + events: SocketEvent[]; + connectionId: string; + timestamp: Date; +}; + +export type Callback = (event: SocketEvent) => void; + +export const JoinRoomSchema = z.object({ + name: z.string(), + user: z.object({ + id: z.string(), + name: z.string(), + }), + maxConnections: z.union([z.number(), z.literal('unlimited'), z.undefined()]), +}); + +export type JoinRoomPayload = z.infer; diff --git a/src/services/io/index.test.ts b/src/services/io/index.test.ts index c22240ae..db411355 100644 --- a/src/services/io/index.test.ts +++ b/src/services/io/index.test.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; diff --git a/src/services/io/index.ts b/src/services/io/index.ts index 97d52aec..658e4272 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { Subject } from 'rxjs'; import { Participant } from '../../common/types/participant.types'; diff --git a/src/services/presence-3d-manager/index.test.ts b/src/services/presence-3d-manager/index.test.ts index 5baa63dd..01fb912a 100644 --- a/src/services/presence-3d-manager/index.test.ts +++ b/src/services/presence-3d-manager/index.test.ts @@ -1,4 +1,4 @@ -import { PresenceEvents } from '@superviz/socket-client'; +import { PresenceEvents } from '../../lib/socket'; import { MOCK_IO } from '../../../__mocks__/io.mock'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; diff --git a/src/services/presence-3d-manager/index.ts b/src/services/presence-3d-manager/index.ts index 269e1d4b..bc687123 100644 --- a/src/services/presence-3d-manager/index.ts +++ b/src/services/presence-3d-manager/index.ts @@ -1,4 +1,4 @@ -import { PresenceEvent, PresenceEvents, Room, SocketEvent } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room, SocketEvent } from '../../lib/socket'; import { throttle } from 'lodash'; import { Participant } from '../../common/types/participant.types'; diff --git a/src/services/roomState/index.test.ts b/src/services/roomState/index.test.ts index 627aacd5..a3c064a6 100644 --- a/src/services/roomState/index.test.ts +++ b/src/services/roomState/index.test.ts @@ -1,6 +1,6 @@ import { TextEncoder, TextDecoder } from 'util'; -import { PresenceEvent, PresenceEvents, RoomEvents } from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents } from '../../lib/socket'; import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { TranscriptState } from '../../common/types/events.types'; diff --git a/src/services/roomState/index.ts b/src/services/roomState/index.ts index 33866d89..b4aa61b2 100644 --- a/src/services/roomState/index.ts +++ b/src/services/roomState/index.ts @@ -1,10 +1,4 @@ -import { - PresenceEvent, - PresenceEvents, - Room, - RoomEvents, - SocketEvent, -} from '@superviz/socket-client'; +import { PresenceEvent, PresenceEvents, Room, SocketEvent } from '../../lib/socket'; import { TranscriptState } from '../../common/types/events.types'; import { Participant, ParticipantType } from '../../common/types/participant.types'; diff --git a/src/services/roomState/type.ts b/src/services/roomState/type.ts index d35c18bc..71d70aed 100644 --- a/src/services/roomState/type.ts +++ b/src/services/roomState/type.ts @@ -1,8 +1,7 @@ -import { PresenceEvent } from '@superviz/socket-client'; +import { PresenceEvent } from '../../lib/socket'; import { TranscriptState } from '../../common/types/events.types'; import { Participant } from '../../common/types/participant.types'; -import { DrawingData } from '../video-conference-manager/types'; export interface VideoRoomProperties { hostClientId?: string; diff --git a/src/services/slot/index.ts b/src/services/slot/index.ts index b034452a..03f681fd 100644 --- a/src/services/slot/index.ts +++ b/src/services/slot/index.ts @@ -1,4 +1,4 @@ -import * as Socket from '@superviz/socket-client'; +import * as Socket from '../../lib/socket'; import { INDEX_IS_WHITE_TEXT, diff --git a/yarn.lock b/yarn.lock index e5abc401..bcdb9811 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2472,13 +2472,6 @@ unbzip2-stream "1.4.3" yargs "17.7.1" -"@reactivex/rxjs@^6.6.7": - version "6.6.7" - resolved "https://registry.yarnpkg.com/@reactivex/rxjs/-/rxjs-6.6.7.tgz#52ab48f989aba9cda2b995acc904a43e6e1b3b40" - integrity sha512-xZIV2JgHhWoVPm3uVcFbZDRVJfx2hgqmuTX7J4MuKaZ+j5jN29agniCPBwrlCmpA15/zLKcPi7/bogt0ZwOFyA== - dependencies: - tslib "^1.9.0" - "@rollup/plugin-node-resolve@^15.0.1": version "15.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.1.0.tgz#9ffcd8e8c457080dba89bb9fcb583a6778dc757e" @@ -2662,22 +2655,9 @@ "@sinonjs/commons" "^3.0.0" "@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== - -"@superviz/socket-client@1.8.2": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@superviz/socket-client/-/socket-client-1.8.2.tgz#054a19df95e144ae99f459ce75f38795feffbcb9" - integrity sha512-pB4Pq9GYL7iXFN5ppri9D5sG2ff5Yg/muBoT6pgW2scj91OL2741/ULuxcvTZiUtCW6H7ndHuplVMyHCczkIAA== - dependencies: - "@reactivex/rxjs" "^6.6.7" - debug "^4.3.5" - lodash "^4.17.21" - rxjs "^7.8.1" - semantic-release-version-file "^1.0.2" - socket.io-client "^4.7.5" - zod "^3.23.8" + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== "@tootallnate/once@2": version "2.0.0" @@ -4755,7 +4735,7 @@ debounce@^1.2.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: +debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -4776,7 +4756,7 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.1, debug@^4.3.5: +debug@^4.3.1, debug@~4.3.1, debug@~4.3.2: version "4.3.5" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== @@ -5035,20 +5015,20 @@ end-of-stream@^1.1.0: once "^1.4.0" engine.io-client@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.3.tgz#4cf6fa24845029b238f83c628916d9149c399bc5" - integrity sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q== + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.4.tgz#b8bc71ed3f25d0d51d587729262486b4b33bd0d0" + integrity sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" engine.io-parser "~5.2.1" - ws "~8.11.0" + ws "~8.17.1" xmlhttprequest-ssl "~2.0.0" engine.io-parser@~5.2.1: - version "5.2.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.2.tgz#37b48e2d23116919a3453738c5720455e64e1c49" - integrity sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw== + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== entities@^4.4.0: version "4.5.0" @@ -10598,11 +10578,6 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - tslib@^2.0.1, tslib@^2.4.0: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" @@ -11148,10 +11123,10 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xml-name-validator@^4.0.0: version "4.0.0" From 6aeb7f02016a715c5bcac9b701f86620cb5351af Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 19 Jul 2024 13:31:47 -0300 Subject: [PATCH 2/2] feat: load limits from server --- src/core/index.ts | 19 +++---------------- src/services/api/index.test.ts | 20 +++++++++++--------- src/services/api/index.ts | 18 +++++++----------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index 100def89..a5a55b92 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -107,9 +107,10 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { throw new Error('Failed to load configuration from server'); }); @@ -129,21 +130,7 @@ const init = async (apiKey: string, options: SuperVizSdkOptions): Promise { return { @@ -209,7 +209,7 @@ describe('ApiService', () => { const baseUrl = 'https://dev.nodeapi.superviz.com'; const response = await ApiService.fetchLimits(baseUrl, VALID_API_KEY); - expect(response).toEqual(CHECK_LIMITS_MOCK.usage); + expect(response).toEqual(CHECK_LIMITS_MOCK.limits); }); }); @@ -217,7 +217,9 @@ describe('ApiService', () => { test('should return the participants', async () => { const response = await ApiService.fetchParticipantsByGroup('any_group_id'); - expect(response).toEqual([{"avatar": null, "id": "any_user_id", "name": "any_name", "email": "any_email"}]); + expect(response).toEqual([ + { avatar: null, id: 'any_user_id', name: 'any_name', email: 'any_email' }, + ]); }); }); @@ -225,10 +227,10 @@ describe('ApiService', () => { test('should create a mention', async () => { const response = await ApiService.createMentions({ commentsId: 'any_comment_id', - participants: [] + participants: [], }); expect(response).toEqual({}); }); - }) + }); }); diff --git a/src/services/api/index.ts b/src/services/api/index.ts index f549bf3e..83a84f55 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -2,14 +2,14 @@ import { SuperVizSdkOptions } from '../../common/types/sdk-options.types'; import { doRequest } from '../../common/utils'; import { Annotation } from '../../components/comments/types'; import config from '../config'; - +import { ComponentLimits } from '../limits/types'; import { AnnotationParams, CommentParams, CreateOrUpdateParticipantParams, FetchAnnotationsParams, - MentionParams + MentionParams, } from './types'; export default class ApiService { @@ -32,11 +32,11 @@ export default class ApiService { return doRequest(url, 'POST', { apiKey }); } - static async fetchLimits(baseUrl: string, apikey: string) { - const path: string = '/user/check_limits'; + static async fetchLimits(baseUrl: string, apikey: string): Promise { + const path: string = '/user/check_limits_v2'; const url: string = this.createUrl(baseUrl, path); const result = await doRequest(url, 'GET', '', { apikey }); - return result.usage; + return result.limits; } static async fetchWaterMark(baseUrl: string, apiKey: string) { @@ -130,18 +130,14 @@ export default class ApiService { return doRequest(url, 'POST', body, { apikey }); } - static async fetchParticipantsByGroup( - groupId: string, - ) { + static async fetchParticipantsByGroup(groupId: string) { const path = `/groups/participants/${groupId}`; const baseUrl = config.get('apiUrl'); const url = this.createUrl(baseUrl, path, { take: 10000 }); return doRequest(url, 'GET', undefined, { apikey: config.get('apiKey') }); } - static async createMentions( - mentionParams: MentionParams - ) { + static async createMentions(mentionParams: MentionParams) { const path = '/mentions'; const baseUrl = config.get('apiUrl'); const url = this.createUrl(baseUrl, path);