From e0f09d1b4577c5c3885b004c0d3ab7e317b22b9e Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 23 Jun 2024 20:54:05 -0300 Subject: [PATCH] feat: observer and handle ioc connection states --- __mocks__/io.mock.ts | 8 ++++ src/core/launcher/index.ts | 38 ++++++++-------- src/services/io/index.test.ts | 45 ++++++++++++++++-- src/services/io/index.ts | 86 ++++++++++++++++++++++++++--------- src/services/io/types.ts | 9 ++++ 5 files changed, 140 insertions(+), 46 deletions(-) diff --git a/__mocks__/io.mock.ts b/__mocks__/io.mock.ts index 2f90530b..e38ffb8a 100644 --- a/__mocks__/io.mock.ts +++ b/__mocks__/io.mock.ts @@ -2,6 +2,14 @@ import { jest } from '@jest/globals'; import { Room } from '@superviz/socket-client'; export const MOCK_IO = { + ClientState: { + CONNECTED: 'CONNECTED', + CONNECTING: 'CONNECTING', + DISCONNECTED: 'DISCONNECTED', + CONNECTION_ERROR: 'CONNECTION_ERROR', + RECONNECTING: 'RECONNECTING', + RECONNECT_ERROR: 'RECONNECT_ERROR', + }, PresenceEvents: { JOINED_ROOM: 'presence.joined-room', LEAVE: 'presence.leave', diff --git a/src/core/launcher/index.ts b/src/core/launcher/index.ts index 3700f181..76dc1820 100644 --- a/src/core/launcher/index.ts +++ b/src/core/launcher/index.ts @@ -13,6 +13,7 @@ import ApiService from '../../services/api'; import config from '../../services/config'; import { EventBus } from '../../services/event-bus'; import { IOC } from '../../services/io'; +import { IOCState } from '../../services/io/types'; import LimitsService from '../../services/limits'; import { Presence3DManager } from '../../services/presence-3d-manager'; import { SlotService } from '../../services/slot'; @@ -29,7 +30,7 @@ export class Launcher extends Observable implements DefaultLauncher { private activeComponentsInstances: Partial[] = []; private ioc: IOC; - private LauncherRealtimeRoom: Socket.Room; + private room: Socket.Room; private eventBus: EventBus = new EventBus(); private timestamp: number = 0; @@ -49,7 +50,7 @@ export class Launcher extends Observable implements DefaultLauncher { group.publish(participantGroup); this.ioc = new IOC(localParticipant.value); - this.LauncherRealtimeRoom = this.ioc.createRoom('launcher'); + this.room = this.ioc.createRoom('launcher'); // internal events without realtime this.eventBus = new EventBus(); @@ -169,9 +170,9 @@ export class Launcher extends Observable implements DefaultLauncher { this.eventBus.destroy(); this.eventBus = undefined; - this.LauncherRealtimeRoom?.presence.off(Socket.PresenceEvents.JOINED_ROOM); - this.LauncherRealtimeRoom?.presence.off(Socket.PresenceEvents.LEAVE); - this.LauncherRealtimeRoom?.presence.off(Socket.PresenceEvents.UPDATE); + this.room?.presence.off(Socket.PresenceEvents.JOINED_ROOM); + this.room?.presence.off(Socket.PresenceEvents.LEAVE); + this.room?.presence.off(Socket.PresenceEvents.UPDATE); this.ioc?.destroy(); this.isDestroyed = true; @@ -224,8 +225,6 @@ export class Launcher extends Observable implements DefaultLauncher { return true; }; - /** Ably Listeners */ - private onAuthentication = (isAuthenticated: boolean): void => { if (isAuthenticated) return; @@ -305,7 +304,12 @@ export class Launcher extends Observable implements DefaultLauncher { const { participants, localParticipant } = useStore(StoreType.GLOBAL); // retrieve the current participants in the room - this.LauncherRealtimeRoom.presence.get((presences) => { + this.ioc.stateSubject.subscribe((state) => { + if (state === IOCState.AUTH_ERROR) { + this.onAuthentication(false); + } + }); + this.room.presence.get((presences) => { const participantsMap: Record = {}; presences.forEach((presence) => { @@ -325,20 +329,14 @@ export class Launcher extends Observable implements DefaultLauncher { participants.publish(participantsMap); }); - this.LauncherRealtimeRoom.presence.on( + this.room.presence.on( Socket.PresenceEvents.JOINED_ROOM, this.onParticipantJoinedIOC, ); - this.LauncherRealtimeRoom.presence.on( - Socket.PresenceEvents.LEAVE, - this.onParticipantLeaveIOC, - ); + this.room.presence.on(Socket.PresenceEvents.LEAVE, this.onParticipantLeaveIOC); - this.LauncherRealtimeRoom.presence.on( - Socket.PresenceEvents.UPDATE, - this.onParticipantUpdatedIOC, - ); + this.room.presence.on(Socket.PresenceEvents.UPDATE, this.onParticipantUpdatedIOC); const { hasJoinedRoom } = useStore(StoreType.GLOBAL); hasJoinedRoom.publish(true); @@ -357,12 +355,12 @@ export class Launcher extends Observable implements DefaultLauncher { if (presence.id !== localParticipant.value.id) return; // Assign a slot to the participant - const slot = new SlotService(this.LauncherRealtimeRoom); + const slot = new SlotService(this.room); await slot.assignSlot(); this.timestamp = presence.timestamp; - this.LauncherRealtimeRoom.presence.update(localParticipant.value); + this.room.presence.update(localParticipant.value); this.logger.log('launcher service @ onParticipantJoined - local participant joined'); this.onParticipantJoined(presence); @@ -418,7 +416,7 @@ export class Launcher extends Observable implements DefaultLauncher { } as Participant); this.timestamp = presence.timestamp; - this.LauncherRealtimeRoom.presence.update(localParticipant.value); + this.room.presence.update(localParticipant.value); this.publish(ParticipantEvent.LOCAL_UPDATED, presence.data); diff --git a/src/services/io/index.test.ts b/src/services/io/index.test.ts index 167b1b72..c22240ae 100644 --- a/src/services/io/index.test.ts +++ b/src/services/io/index.test.ts @@ -1,6 +1,9 @@ -import { MOCK_IO } from '../../../__mocks__/io.mock'; +import * as Socket from '@superviz/socket-client'; + import { MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; +import { IOCState } from './types'; + import { IOC } from '.'; describe('io', () => { @@ -28,8 +31,42 @@ describe('io', () => { expect(room).toHaveProperty('emit'); }); - test('should publish state changes', () => { - const next = jest.fn(); - instance?.onStateChange(next); + test('should force reconnect', () => { + const spy = jest.spyOn(instance as any, 'forceReconnect'); + const callback = jest.fn(); + + instance?.stateSubject.subscribe(callback); + instance?.['handleConnectionState']({ + state: Socket.ClientState.DISCONNECTED, + reason: '', + }); + + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledTimes(1); + expect(callback).toHaveBeenCalledWith(IOCState.DISCONNECTED); + }); + + test('should not force reconnect if reason is Unauthorized connection', () => { + const spy = jest.spyOn(instance as any, 'forceReconnect'); + const callback = jest.fn(); + + instance?.stateSubject.subscribe(callback); + instance?.['handleConnectionState']({ + state: Socket.ClientState.DISCONNECTED, + reason: 'Unauthorized connection', + }); + + expect(callback).toHaveBeenCalledWith(IOCState.AUTH_ERROR); + expect(spy).not.toHaveBeenCalled(); + }); + + test('should not force reconnect if state is not DISCONNECTED or RECONNECT_ERROR', () => { + const spy = jest.spyOn(instance as any, 'forceReconnect'); + + instance?.['handleConnectionState']({ + state: Socket.ClientState.CONNECTED, + }); + + expect(spy).not.toHaveBeenCalled(); }); }); diff --git a/src/services/io/index.ts b/src/services/io/index.ts index e0914a41..9bf7f7be 100644 --- a/src/services/io/index.ts +++ b/src/services/io/index.ts @@ -4,22 +4,16 @@ import { Subject } from 'rxjs'; import { Participant } from '../../common/types/participant.types'; import config from '../config/index'; +import { IOCState } from './types'; + export class IOC { public state: Socket.ConnectionState; public client: Socket.Realtime; - private stateSubject: Subject = new Subject(); + public stateSubject: Subject = new Subject(); - constructor(participant: Participant) { - let environment = config.get('environment') as 'dev' | 'prod'; - environment = ['dev', 'prod'].includes(environment) ? environment : 'dev'; - - this.client = new Socket.Realtime(config.get('apiKey'), environment, { - id: participant.id, - name: participant.name, - }); - - this.subscribeToDefaultEvents(); + constructor(private participant: Participant) { + this.createClient(); } /** @@ -33,25 +27,73 @@ export class IOC { } /** - * @function onStateChange - * @description Subscribe to the socket connection state changes - * @param next {Function} + * @function subscribeToDefaultEvents + * @description subscribe to the default socket events + * @returns {void} + */ + private subscribeToDefaultEvents(): void { + this.client.connection.on(this.handleConnectionState); + } + + private handleConnectionState(state: Socket.ConnectionState): void { + const needsToReconnectStates = [ + Socket.ClientState.DISCONNECTED, + Socket.ClientState.RECONNECT_ERROR, + ]; + + if ( + needsToReconnectStates.includes(state.state) && + state.reason !== 'Unauthorized connection' + ) { + this.forceReconnect(); + } + + if (state.reason === 'Unauthorized connection') { + console.error( + '[Superviz] Unauthorized connection. Please check your API key and if your domain is white listed.', + ); + + this.state = { + state: Socket.ClientState.DISCONNECTED, + reason: 'Unauthorized connection', + }; + + this.stateSubject.next(IOCState.AUTH_ERROR); + + return; + } + + this.state = state; + this.stateSubject.next(state.state as unknown as IOCState); + } + + /** + * @function forceReconnect + * @description force the socket to reconnect * @returns {void} */ - public onStateChange(next: (state: Socket.ConnectionState) => void): void { - this.stateSubject.subscribe(next); + private forceReconnect(): void { + this.client?.destroy(); + this.client = null; + + this.createClient(); } /** - * @function subscribeToDefaultEvents - * @description subscribe to the default socket events + * @function createClient + * @description create a new socket client * @returns {void} */ - private subscribeToDefaultEvents(): void { - this.client.connection.on((state) => { - this.state = state; - this.stateSubject.next(state); + public createClient(): void { + let environment = config.get('environment') as 'dev' | 'prod'; + environment = ['dev', 'prod'].includes(environment) ? environment : 'dev'; + + this.client = new Socket.Realtime(config.get('apiKey'), environment, { + id: this.participant.id, + name: this.participant.name, }); + + this.subscribeToDefaultEvents(); } /** diff --git a/src/services/io/types.ts b/src/services/io/types.ts index e69de29b..61514ccf 100644 --- a/src/services/io/types.ts +++ b/src/services/io/types.ts @@ -0,0 +1,9 @@ +export enum IOCState { + CONNECTED = 'CONNECTED', + CONNECTING = 'CONNECTING', + DISCONNECTED = 'DISCONNECTED', + CONNECTION_ERROR = 'CONNECTION_ERROR', + RECONNECTING = 'RECONNECTING', + RECONNECT_ERROR = 'RECONNECT_ERROR', + AUTH_ERROR = 'AUTH_ERROR', +}