UNPKG

@viewar/call

Version:

ViewAR Call

395 lines (345 loc) 9.59 kB
import { Observable, Subject } from 'rxjs'; import io from 'socket.io-client'; import { logDebug, logError } from '../utils'; import { CLIENT_CHANGE_JOIN, CLIENT_CHANGE_LEFT, CLIENT_CHANGE_UPDATED, } from './constants/client-change-types'; import { DEFAULT_HOST, DEFAULT_MAXATTEMPTS, DEFAULT_TIMEOUT, } from './constants/defaults'; import { MESSAGE_TYPE_MESSAGE } from './constants/message-types'; /** * Create a new socket connection with methods to join/create/leave a room. User is allowed to be in one room only at one time. */ export const createSocketConnection = (args = {}) => { const { host = DEFAULT_HOST, timeout = DEFAULT_TIMEOUT, maxAttempts = DEFAULT_MAXATTEMPTS, debug = false, } = args; const errorObserver = new Subject(); const clientChangedObserver = new Subject(); const disconnectObserver = new Subject(); let messageObserver = {}; let socket; let connected = false; let isConnecting = false; let room = ''; let clients = []; let clientData = {}; return { connect, // Room handling joinRoom, leaveRoom, getRooms, setClientData, // Data handling send, getData, // State get connected() { return connected; }, get isConnecting() { return isConnecting; }, get clients() { return clients.filter((client) => client.id !== socket.id); }, get room() { return room; }, get id() { return socket.id; }, get clientChanged() { return clientChangedObserver; }, get error() { return errorObserver; }, get disconnect() { return disconnectObserver; }, get clientData() { return clientData; }, }; // --------------------------------------------------------------------------------------------------------------------- // PUBLIC // --------------------------------------------------------------------------------------------------------------------- /** * Join a room with a specific room id. A socket connection can only be in one room. * * @param roomId The room id to join. * @param username The username if connecting as admin. * @param password The password if connecting as admin. * @param appId The app ID. * @param appVersion The app version. * @param clientData The user details as json object. * @returns {boolean} Returns false if the operation failed (e.g. socket is not connected. */ async function joinRoom( roomId, username, password, appId, appVersion, newClientData ) { if (!connected) { logError('Socket not connected.'); return false; } if (room) { await leaveRoom(); } clientData = Object.assign({}, newClientData); const callbackId = generateCallbackId(); return await new Promise((resolve) => { // Join room. socket.emit( 'joinRoom', { room: roomId, clientData, username, password, appId, appVersion }, callbackId ); // Wait for server answer. socket.once(callbackId, ({ err, res }) => { if (err) { logError('Could not join room.', err); resolve(false); } else { // Listen for client changes. socket.on('clientJoined', onClientJoined); socket.on('clientLeft', onClientLeft); socket.on('clientChangedData', onClientChangedData); clients = res.clients || []; room = res.name; resolve(true); } }); }); } /** * Leave the current room. */ async function leaveRoom() { if (!connected) { logError('Socket not connected.'); return false; } if (!room) { logError('Not in a room. Join a room first.'); return false; } socket.emit('leaveRoom', { room: room }); socket.off('clientJoined', onClientJoined); socket.off('clientLeft', onClientLeft); socket.off('clientChangedData', onClientChangedData); room = null; clients = []; return true; } /** * Updates own client's data on server. * * @param data The new client data (as json object). */ function setClientData(data) { clientData = Object.assign({}, clientData, data); socket.emit('setClientData', { data }); } /** * Gets a list with rooms from the server. * @returns {*} A list of room ids. */ async function getRooms() { if (!connected) { logError('Socket not connected.'); return false; } const callbackId = generateCallbackId(); return await new Promise((resolve) => { socket.emit('getRooms', callbackId); socket.once(callbackId, (rooms) => { resolve(rooms); }); }); } /** * Send a message to the current room. * * @param data The content of the message (any serializable type). * @param messageType The type of the message (optional). * @param receiver The room to send the message to (optional). * @returns {boolean} Message sent successfully. */ async function send(data, messageType = MESSAGE_TYPE_MESSAGE, receiver) { if (!connected) { logError('Socket not connected.'); return false; } if (!room) { logError('Not in a room. Join a room first.'); return false; } socket.emit('send', { room, data, messageType, id: receiver }); } /** * Get observer for a specific message type. * * @param messageType The message type to listen to. * @returns {*} An observable. */ function getData(messageType = MESSAGE_TYPE_MESSAGE) { if (!messageObserver[messageType]) { messageObserver[messageType] = new Observable((observer) => { if (socket) { socket.on(messageType, (data) => observer.next(data)); } }); } return messageObserver[messageType]; } // --------------------------------------------------------------------------------------------------------------------- // PRIVATE // --------------------------------------------------------------------------------------------------------------------- /** * Connects to the given host. */ async function connect() { if (!isConnecting) { isConnecting = true; connected = false; room = null; clients = []; messageObserver = {}; return await new Promise((resolve) => { socket = io(host, { upgrade: false, transports: ['websocket'], path: '/call', timeout, reconnectionAttempts: maxAttempts, }); socket.on('error', onError); // Log all events to console: const events = [ 'connect', 'connect_error', 'connect_timeout', 'disconnect', 'error', 'ping', 'pong', 'reconnect', 'reconnect_attempt', 'reconnect_error', 'reconnect_failed', 'reconnecting', ]; events.forEach((event) => socket.on(event, (...args) => { logDebug(debug, `${event} ${JSON.stringify(...args)}`); }) ); const detach = () => { socket.off('connect', handleConnect); socket.off('reconnect_failed', handleReconnectFailed); socket.off('disconnect', handleDisconnect); }; const handleConnect = () => { connected = true; detach(); resolve(true); }; const handleReconnectFailed = () => { connected = false; detach(); resolve(false); }; const handleDisconnect = () => { onDisconnect(); detach(); resolve(false); }; socket.on('connect', handleConnect); socket.on('reconnect_failed', handleReconnectFailed); socket.on('disconnect', handleDisconnect); }); } return false; } /** * On disconnect handler. */ function onDisconnect() { connected = false; isConnecting = false; room = null; clients = []; disconnectObserver.next(); } /** * Handle a new client in the current room. * @param client The new client. */ function onClientJoined(client) { clients.push(client); clientChangedObserver.next({ clients, id: client.id, type: CLIENT_CHANGE_JOIN, }); } /** * Handle a client who left the current room. * @param clientId The client id. */ function onClientLeft(clientId) { const index = clients.findIndex((client) => client.id === clientId); if (index !== -1) { clients.splice(index, 1); clientChangedObserver.next({ clients, id: clientId, type: CLIENT_CHANGE_LEFT, }); } } /** * Handle changed data of a client. * @param changedClient The new client data. */ function onClientChangedData(changedClient) { const index = clients.findIndex((client) => client.id === changedClient.id); clients[index] = changedClient; clientChangedObserver.next({ clients, id: changedClient.id, type: CLIENT_CHANGE_UPDATED, }); } /** * Handle socket io errors. * @param {*} message Error message. */ function onError(message) { errorObserver.next({ message, }); } /** * Creates a random unique id for callbacks. * @returns {string} The generated callback id. */ function generateCallbackId() { return `temp_${Date.now()}_${Math.random().toString(36).substring(7)}`; } };