Source: sfu-extended.js

/**
 * @namespace FlashphonerSFUExtended
 */


const {v4: uuidv4} = require("uuid");
const ws = require("./ws");
const roomApi = require("./room");
const promises = require("./promise");
const constants = require("./constants");
const logger = require("./logger");
const SFU_STATE = constants.SFU_STATE;
const SFU_EVENT = constants.SFU_EVENT;
const SFU_INTERNAL = constants.SFU_INTERNAL_API;
const sfu = {};
let rooms = {};
let connection = ws.createConnection();
let server;
const callbacks = {};

let state = SFU_STATE.NEW;
let connectionConfig;
let user;

function setupConnection(connection) {
    connection.onMessage = function(name, msg){
        switch (name) {
            case SFU_INTERNAL.DEFAULT_METHOD:
                //filter messages
                if (msg[0].type === SFU_INTERNAL.MESSAGE) {
                    notify(SFU_EVENT.MESSAGE, msg[0].message);
                } else if (msg[0].type === SFU_INTERNAL.MESSAGE_STATE) {
                    if (!promises.resolve(msg[0].internalMessageId, msg[0].status)) {
                        notify(SFU_EVENT.MESSAGE_STATE, msg[0].status);
                    }
                } else if (msg[0].type === SFU_INTERNAL.USER_LIST) {
                    promises.resolve(msg[0].internalMessageId, msg[0].list);
                    notify(SFU_EVENT.USER_LIST, msg[0].list);
                } else if (msg[0].type === SFU_INTERNAL.USER_CALENDAR) {
                    promises.resolve(msg[0].internalMessageId, msg[0].calendar);
                    notify(SFU_EVENT.USER_CALENDAR, msg[0].calendar);
                } else if (msg[0].type === SFU_INTERNAL.NEW_CHAT) {
                    if(!promises.resolve(msg[0].internalMessageId, msg[0].info)) {
                        notify(SFU_EVENT.NEW_CHAT, msg[0].info);
                    }
                } else if (msg[0].type === SFU_INTERNAL.CONTACT_INVITE) {
                    if(!promises.resolve(msg[0].internalMessageId, msg[0].invite)) {
                        notify(SFU_EVENT.CONTACT_INVITE, msg[0].invite);
                    }
                } else if (msg[0].type === SFU_INTERNAL.CONTACT_UPDATED) {
                    if(!promises.resolve(msg[0].internalMessageId, msg[0].contact)) {
                        notify(SFU_EVENT.CONTACT_UPDATE, msg[0].contact);
                    }
                } else if (msg[0].type === SFU_INTERNAL.CONTACT_REMOVED) {
                    if(!promises.resolve(msg[0].internalMessageId, msg[0].contact)) {
                        notify(SFU_EVENT.CONTACT_REMOVED, msg[0].contact);
                    }
                } else if (msg[0].type === SFU_INTERNAL.CHAT_DELETED) {
                    notify(SFU_EVENT.CHAT_DELETED, msg[0].info);
                } else if (msg[0].type === SFU_INTERNAL.CHAT_UPDATED) {
                    promises.resolve(msg[0].internalMessageId, msg[0].info);
                    notify(SFU_EVENT.CHAT_UPDATED, msg[0].info);
                } else if (msg[0].type === SFU_INTERNAL.USER_CHATS) {
                    promises.resolve(msg[0].internalMessageId, msg[0].chats);
                    notify(SFU_EVENT.USER_CHATS, msg[0].chats);
                } else if (msg[0].type === SFU_INTERNAL.PUBLIC_CHANNELS) {
                    promises.resolve(msg[0].internalMessageId, msg[0].channels);
                    notify(SFU_EVENT.PUBLIC_CHANNELS, msg[0].channels);
                } else if (msg[0].type === SFU_INTERNAL.CHAT_LOADED) {
                    promises.resolve(msg[0].internalMessageId, msg[0].chat);
                    notify(SFU_EVENT.CHAT_LOADED, msg[0].chat);
                } else if (msg[0].type === constants.SFU_ROOM_EVENT.OPERATION_FAILED && promises.promised(msg[0].internalMessageId)) {
                    promises.reject(msg[0].internalMessageId, msg[0]);
                } else if (msg[0].type === constants.SFU_EVENT.ACK && promises.promised(msg[0].internalMessageId)) {
                    promises.resolve(msg[0].internalMessageId);
                } else if (msg[0].roomName && msg[0].roomName.length > 0) {
                    //room event
                    const room = rooms[msg[0].roomName];
                    if (room) {
                        room.processEvent(msg[0]);
                    }
                } else {
                    notify(msg[0].type, msg[0]);
                }
                break;
            case "failed":
                notify(constants.SFU_EVENT.FAILED, msg[0]);
                break;
        }
    };

    connection.onClose = function(e) {
        state = SFU_STATE.DISCONNECTED;
        disconnect();
        notify(SFU_EVENT.DISCONNECTED, e);
    };

    connection.onError = function(e) {
        state = SFU_STATE.FAILED;
        notify(SFU_STATE.FAILED, e);
    };
}
/**
 * Connect to server.
 *
 * @param {Object} options SFU options
 * @param {String} options.url Server url
 * @param {String} options.username Username
 * @param {String} options.password Password
 * @param {String=} options.nickname Participant's nickname
 * @returns {Promise<void|Error>} Promise which resolves upon connect
 * @throws {TypeError} Error if no options provided
 * @memberof FlashphonerSFUExtended
 */
const connect = function(options) {
    if (!options) {
        throw new TypeError("No options provided");
    }
    logger.setPrefix(() => {
        return options.username;
    });
    setupConnection(connection);
    server = new URL(options.url).hostname;
    connectionConfig = {
        url: options.url,
        appName: SFU_INTERNAL.Z_APP,
        custom: {
            username: options.username,
            password: options.password,
            nickname: options.nickname
        }
    };
    return new Promise(function(resolve, reject) {
        if (state !== SFU_STATE.NEW && state !== SFU_STATE.DISCONNECTED && state !== SFU_STATE.FAILED) {
            reject(new Error("Can't connect with the state " + state));
            return;
        }
        connection.connect(connectionConfig).then(function (connectionConfig) {
            user = Object.freeze({
                username: connectionConfig[0].sipLogin,
                nickname: connectionConfig[0].sipVisibleName,
                pmi: connectionConfig[0].pmi
            });
            state = SFU_STATE.AUTHENTICATED;
            notify(SFU_EVENT.CONNECTED);
            resolve(user);
        }, function (e) {
            state = SFU_STATE.FAILED;
            notify(SFU_EVENT.FAILED, e);
            reject(e);
        });
    });
};

/**
 * @typedef MessageAttachment
 * @memberOf FlashphonerSFUExtended
 * @property attachment {Object} attachment info
 * @property attachment.type {String} "file|picture"
 * @property attachment.name {String} name of the attachment
 * @property attachment.size {Number} payload size
 * @property attachment.payload {String} base64 string that represents content of the attachment
 */

/**
 * Send message
 * @param {Object} msg Message
 * @param {String} msg.to Recipient's id (deprecated, use chatId instead)
 * @param {String} msg.chatId Indicates chat this message belongs to
 * @param {String} msg.body Message body
 * @param {Array<MessageAttachment>} msg.attachments
 * @returns {Promise} Promise will resolve upon message delivery and reject if delivery was unsuccessful
 * @throws {Error} error if api isn't connected
 * @memberOf FlashphonerSFUExtended
 */
const sendMessage = function(msg) {
    if (state !== SFU_STATE.AUTHENTICATED) {
        throw new Error("Can't send message while in " + state + " state");
    }
    return new Promise(function(resolve, reject) {
        //validate
        if (!msg) {
            reject(new Error("Can't send null message"));
        } else if ((!msg.body || msg.body === "") && (!msg.attachments || msg.attachments.length === 0)) {
            reject(new Error("Can't send message without content"));
        } else if (!msg.chatId || msg.chatId === "") {
            reject(new Error("Can't send message without a chatId"));
        } else {
            const id = uuidv4();
            promises.add(id, resolve, reject);
            connection.send(constants.SFU_INTERNAL_API.SEND_MESSAGE, {
                id: id,
                internalMessageId: id,
                to: msg.to,
                body: msg.body,
                chatId: msg.chatId,
                attachments: msg.attachments
            });
        }
    });
};

/**
<<<<<<< HEAD
 * Mark message
 * @param {Object} msg Message
 * @param {String} msg.id Message id
 * @param {String} msg.chatId Indicates chat this message belongs to
 * @returns {Promise} Promise resolves with a new state
 * @throws {Error} error if api isn't connected
=======
 * Fetch available user list from server
 * @returns {Promise<Array<FlashphonerSFUExtended.UserListEntry>>}
>>>>>>> zapp-26
 * @memberOf FlashphonerSFUExtended
 */
const markMessageRead = function(msg) {
    if (state !== SFU_STATE.AUTHENTICATED) {
        throw new Error("Can't mark message while in " + state + " state");
    }
    return new Promise(function(resolve, reject) {
        if (!msg) {
            reject(new Error("Can't mark null message"));
        } else if (!msg.chatId || msg.chatId === "") {
            reject(new Error("Can't mark message without a chatId"));
        } else if (!msg.id || msg.id === "") {
            reject(new Error("Can't mark message without massage id"));
        } else {
            const id = uuidv4();
            promises.add(id, resolve, reject);
            connection.send(constants.SFU_INTERNAL_API.MARK_MESSAGE_READ, {
                internalMessageId: id,
                id: msg.id,
                chatId: msg.chatId
            });
        }
    });
}

/**
 * Mark message
 * @param {Object} msg Message
 * @param {String} msg.id Message id
 * @param {String} msg.chatId Indicates chat this message belongs to
 * @returns {Promise} Promise resolves with a new state
 * @throws {Error} error if api isn't connected
 * @memberOf FlashphonerSFUExtended
 */
const markMessageUnread = function(msg) {
    if (state !== SFU_STATE.AUTHENTICATED) {
        throw new Error("Can't mark message while in " + state + " state");
    }
    return new Promise(function(resolve, reject) {
        if (!msg) {
            reject(new Error("Can't mark null message"));
        } else if (!msg.chatId || msg.chatId === "") {
            reject(new Error("Can't mark message without a chatId"));
        } else if (!msg.id || msg.id === "") {
            reject(new Error("Can't mark message without massage id"));
        } else {
            const id = uuidv4();
            promises.add(id, resolve, reject);
            connection.send(constants.SFU_INTERNAL_API.MARK_MESSAGE_UNREAD, {
                internalMessageId: id,
                id: msg.id,
                chatId: msg.chatId
            });
        }
    });
}

/**
 * Fetch available user list from server
 * @returns {Promise<Array<FlashphonerSFUExtended.UserListEntry>>}
 * @memberOf FlashphonerSFUExtended

 */
const getUserList = function() {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't get user list while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.GET_USER_LIST, {internalMessageId: id});
    });
};

/**
 * Fetch available calendar from server
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended

 */
const getUserCalendar = function() {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't get user calendar while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.GET_USER_CALENDAR, {internalMessageId: id});
    });
};

/**
 * Add event to the calendar
 * @param event {FlashphonerSFUExtended.UserCalendarEvent}
 * @returns {Promise<void|error>}
 * @memberOf FlashphonerSFUExtended

 */
const addCalendarEvent = function(event) {
    return new Promise(function(resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't add calendar event while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.ADD_CALENDAR_EVENT, {
            internalMessageId: id,
            event: event
        });
    });
};

/**
 * Remove event from the calendar
 * @param event {FlashphonerSFUExtended.UserCalendarEvent}
 * @returns {Promise<void|error>}
 * @memberOf FlashphonerSFUExtended

 */
const removeCalendarEvent = function(event) {
    return new Promise(function(resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't remove calendar event while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.REMOVE_CALENDAR_EVENT, {
            internalMessageId: id,
            event: event
        });
    });
};

/**
 * Fetch available chats from server
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const getUserChats = function() {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't get user chats while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.GET_USER_CHATS, {internalMessageId: id});
    });
};

const getPublicChannels = function() {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't get public channels while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.GET_PUBLIC_CHANNELS, {internalMessageId: id});
    });
};

/**
 * Fetch chat data from server
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const loadChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't load chats while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.LOAD_CHAT, {id: chat.id, internalMessageId: id});
    });
};

/**
 * Create chat
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const createChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't create chats while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.CREATE_CHAT, {
            id: chat.id,
            name: chat.name,
            members: chat.members,
            favourite: chat.favourite,
            internalMessageId: id,
            channel: chat.channel,
            channelType: chat.channelType,
            channelSendPolicy: chat.channelSendPolicy,
            sendPermissionList: chat.sendPermissionList
        });
    });
};

/**
 * Delete chat
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const deleteChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't delete chats while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.DELETE_CHAT, {id: chat.id, internalMessageId: id});
    });
};

/**
 * Rename chat
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const renameChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't rename chats while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.RENAME_CHAT, {id: chat.id, name: chat.name, internalMessageId: id});
    });
};

/**
 * Add member to chat
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const addMemberToChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't add member to chat while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.ADD_MEMBER_TO_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
    });
};

/**
 * Remove member from chat
 * @returns {Promise<Array<FlashphonerSFUExtended.UserCalendar>>}
 * @memberOf FlashphonerSFUExtended
 */
const removeMemberFromChat = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't remove member to chat while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.REMOVE_MEMBER_FROM_CHAT, {id: chat.id, member: chat.member, internalMessageId: id});
    });
};

/**
 *
 * @param invite
 * @returns {Promise<unknown>}
 */
const inviteContact = function(invite) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't invite contact while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.INVITE_CONTACT, {from: user.username, to: invite.to, internalMessageId: id});
    });
}

/**
 *
 * @param invite
 * @returns {Promise<unknown>}
 */
const confirmContact = function(invite) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't confirm contact while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.CONFIRM_CONTACT, {from: invite.from, to: invite.to, id: invite.id, internalMessageId: id});
    });
}

/**
 *
 * @param contact
 * @returns {Promise<unknown>}
 */
const removeContact = function(contact) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't remove contact while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.REMOVE_CONTACT, {id: contact.id, internalMessageId: id});
    });
}
/**
 *
 * @param channel
 * @returns {Promise<unknown>}
 */
const updateChannelSendPolicy = function(channel) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't change channel send policy while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.UPDATE_CHANNEL_SEND_POLICY, {
            id: channel.id,
            channelSendPolicy: channel.channelSendPolicy,
            internalMessageId: id
        });
    });
}

const addChannelSendPermissionListMember = function(channel) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't change channel send permission list while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.ADD_CHANNEL_SEND_PERMISSION_LIST_MEMBER, {
            id: channel.id,
            member: channel.member,
            internalMessageId: id
        });
    });
}

const removeChannelSendPermissionListMember = function(channel) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't change channel send permission list while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.REMOVE_CHANNEL_SEND_PERMISSION_LIST_MEMBER, {
            id: channel.id,
            member: channel.member,
            internalMessageId: id
        });
    });
}

const addChatToFavourites = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't add to favourites while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.ADD_CHAT_TO_FAVOURITES, {
            id: chat.id,
            internalMessageId: id
        });
    });
}

const removeChatFromFavourites = function(chat) {
    return new Promise(function (resolve, reject){
        if (state !== SFU_STATE.AUTHENTICATED) {
            reject(new Error("Can't remove from favourites while in " + state + " state"));
            return;
        }
        const id = uuidv4();
        promises.add(id, resolve, reject);
        connection.send(constants.SFU_INTERNAL_API.REMOVE_CHAT_FROM_FAVOURITES, {
            id: chat.id,
            internalMessageId: id
        });
    });
}

/**
 * Create room
 *
 * @param {Object} options Room options
 * @param {String} options.name Room name
 * @param {String} options.pin Room's pin
 * @param {Object} options.pc Peer connection
 * @returns {FlashphonerSFU.Room} Room
 * @throws {TypeError} Error if no options provided
 * @throws {Error} error if api isn't connected
 * @memberof FlashphonerSFUExtended
 */
const room = function(options) {
    if (!options) {
        throw new TypeError("No options provided");
    }
    if (state !== SFU_STATE.AUTHENTICATED) {
        throw new Error("Can't create room while in " + state + " state");
    }
    const opt = {
        connection: connection,
        name: options.name,
        pin: options.pin,
        pc: options.pc
    };
    const exports = roomApi.room(opt);
    rooms[options.name] = exports;
    const cleanup = function() {
        rooms[options.name].pc().close();
        rooms[options.name].pc().dispatchEvent(new Event("connectionstatechange"));
        delete rooms[options.name];
    };
    exports.on(constants.SFU_ROOM_EVENT.LEFT, function(participant){
        if (participant.name === user.nickname) {
            cleanup();
        }
    }).on(constants.SFU_ROOM_EVENT.EVICTED, function(participant) {
        if (participant.name === user.nickname) {
            cleanup();
        }
    }).on(constants.SFU_ROOM_EVENT.FAILED, cleanup
    ).on(constants.SFU_ROOM_EVENT.OPERATION_FAILED, function(e) {
        if (constants.SFU_OPERATIONS.ROOM_JOIN === e.operation) {
            cleanup();
        }
    });
    return exports;
};

/**
 * Get previously created room.
 * Note that this function will not return ended or failed room
 *
 * @param options {Object}
 * @param options.name {String} room name
 * @returns {FlashphonerSFU.Room} Room
 * @throws {TypeError} Error if no options provided
 * @throws {Error} error if api isn't connected
 * @memberof FlashphonerSFUExtended
 */
const getRoom = function(options) {
    if (!options) {
        throw new TypeError("No options provided");
    }
    if (state !== SFU_STATE.AUTHENTICATED) {
        throw new Error("Can't create room while in " + state + " state");
    }
    return rooms[options.name];
}

/**
 * FlashphonerSFUExtended event callback.
 *
 * @callback FlashphonerSFUExtended~eventCallback
 * @param {FlashphonerSFUExtended} sdk instance FlashphonerSFUExtended
 */

/**
 * Add session event callback.
 *
 * @param {String} event One of {@link FlashphonerSFU.SFU_EVENT} events
 * @param {FlashphonerSFUExtended~eventCallback} callback Callback function
 * @returns {FlashphonerSFUExtended} SDK instance callback was attached to
 * @throws {TypeError} Error if event is not specified
 * @throws {Error} Error if callback is not a valid function
 * @memberOf FlashphonerSFUExtended
 */
const on = function (event, callback) {
    if (!event) {
        throw new TypeError("Event can't be null");
    }
    if (!callback || typeof callback !== "function") {
        throw new Error("Callback needs to be a valid function");
    }
    if (!callbacks[event]) {
        callbacks[event] = [];
    }
    callbacks[event].push(callback);
    return sfu;
};

/**
 * Remove session event callback.
 *
 * @param {String} event One of {@link FlashphonerSFU.SFU_EVENT} events
 * @param {FlashphonerSFUExtended~eventCallback} callback Callback function
 * @returns {FlashphonerSFUExtended} SDK instance callback was removed from
 * @throws {TypeError} Error if event is not specified
 * @throws {Error} Error if callback is not a valid function
 * @memberOf FlashphonerSFUExtended
 */
const off = function (event, callback) {
    if (!event) {
        throw new TypeError("Event can't be null");
    }
    if (!callback || typeof callback !== "function") {
        throw new Error("Callback needs to be a valid function");
    }
    if (!callbacks[event]) {
        callbacks[event] = [];
    }
    let index = callbacks[event].findIndex(element => element === callback);
    if (index !== -1) {
        callbacks[event] = callbacks[event].slice(index, 1);
    }
    return sfu;
};

const notify = function(event, msg) {
    if (callbacks[event]) {
        for (const callback of callbacks[event]) {
            callback(msg);
        }
    }
};

/**
 * Disconnect sfu from the server
 * @memberOf FlashphonerSFUExtended
 */
const disconnect = function() {
    for (const [key, value] of Object.entries(rooms)) {
        value.leaveRoom();
    }
    user = undefined;
    connection.close();
    connection = ws.createConnection();
    state = SFU_STATE.DISCONNECTED;
    rooms = {};
};

sfu.on = on;
sfu.off = off;
sfu.connect = connect;
sfu.sendMessage = sendMessage;
sfu.markMessageRead = markMessageRead;
sfu.markMessageUnread = markMessageUnread;
sfu.getUserList = getUserList;
sfu.getUserCalendar = getUserCalendar;
sfu.addCalendarEvent = addCalendarEvent;
sfu.removeCalendarEvent = removeCalendarEvent;
sfu.getUserChats = getUserChats;
sfu.getPublicChannels = getPublicChannels;
sfu.loadChat = loadChat;
sfu.createChat = createChat;
sfu.deleteChat = deleteChat;
sfu.renameChat = renameChat;
sfu.addMemberToChat = addMemberToChat;
sfu.removeMemberFromChat = removeMemberFromChat;
sfu.inviteContact = inviteContact;
sfu.confirmContact = confirmContact;
sfu.removeContact = removeContact;
sfu.updateChannelSendPolicy = updateChannelSendPolicy;
sfu.addChannelSendPermissionListMember = addChannelSendPermissionListMember;
sfu.removeChannelSendPermissionListMember = removeChannelSendPermissionListMember;
sfu.addChatToFavourites = addChatToFavourites;
sfu.removeChatFromFavourites = removeChatFromFavourites;
sfu.room = room;
sfu.getRoom = getRoom;


/**
 * @typedef {Object} SFUUser
 * @property username {String} username
 * @property nickname {String} nickname
 * @memberOf FlashphonerSFUExtended
 */

/**
 * Get current user
 * @returns {FlashphonerSFUExtended.SFUUser} Returns current logged in user
 * @memberOf FlashphonerSFUExtended
 */
sfu.user = function(){
    return user;
};
/**
 * Get hostname of the server in use
 * @returns {String} Current server's hostname
 * @memberOf FlashphonerSFUExtended
 */
sfu.server = function() {
    return server;
};
sfu.disconnect = disconnect;

/**
 * Get sfu state
 * @returns {FlashphonerSFU.SFU_STATE} Current sfu state
 */
sfu.state = function() {
    return state;
};

sfu.constants = constants;


module.exports = sfu;