@viewar/call
Version:
ViewAR Call
395 lines (345 loc) • 9.59 kB
JavaScript
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)}`;
}
};