chat-engine/src/components/session.js

const Emitter = require('../modules/emitter');

/**
Automatically created by supplying ```enableSync: true``` in ```ChatEngineCore.create```.
Access via {@link Me#session}.

Used to synchronize ChatEngine sessions between desktop and mobile, duplicate windows, etc.
Sessions are the same when identical instance of {@link ChatEngine} and {@link Me} connects to ChatEngine.
Should not be created directly.
@class Session
@extends Emitter
@extends RootEmitter
*/
class Session extends Emitter {

    constructor(chatEngine) {

        super(chatEngine);

        this.name = 'Session';

        this.chatEngine = chatEngine;

        /**
         * Stores a map of {@link Chat} objects that this {@link Me} has joined across all clients.
         * @type {Object}
         */
        this.chats = {};

        /**
         * The {@link Chat} that syncs session between instances.
         * @type {this}
         */
        this.sync = null;

    }

    /**
     * Forwards sync events from other instances into callback functions
     * @private
     */
    subscribe() {

        this.sync = new this.chatEngine.Chat([this.chatEngine.global.channel, 'user', this.chatEngine.me.uuid, 'me.', 'sync'].join('#'), false, this.chatEngine.ceConfig.enableSync, {}, 'system');

        // subscribe to the events on our sync chat and forward them
        this.sync.on('$.session.notify.chat.join', (payload) => {
            this.onJoin(payload.data.subject);
        });

        this.sync.on('$.session.notify.chat.leave', (payload) => {
            this.onleave(payload.data.subject);
        });

    }

    /**
     * Uses PubNub channel groups to restore a session for this uuid
     * @private
     */
    restore() {

        // these are custom groups that separate custom chats from system chats
        // for better fitlering
        let groups = ['custom', 'system'];

        // loop through the groups
        groups.forEach((group) => {

            // generate the channel group string for PubNub using the current uuid
            let channelGroup = [this.chatEngine.ceConfig.globalChannel, this.chatEngine.me.uuid, group].join('#');

            // ask pubnub for a list of channels for this group
            this.chatEngine.pubnub.channelGroups.listChannels({
                channelGroup
            }, (status, response) => {

                if (status.error) {

                    /**
                     * There was a problem restoring the session
                     * @event Chat#$"."error"."sync
                     * @property {String} ceError The specific error thrown by ChatEngine
                     */
                    this.chatEngine.throwError(this.chatEngine, '_emit', 'sync', new Error('There was a problem restoring your session from PubNub servers.'), { status });

                } else {

                    // loop through the returned channels
                    response.channels.forEach((channel) => {

                        // call the same callback as if we were notified about them
                        this.onJoin({
                            channel,
                            private: this.chatEngine.parseChannel(channel).private,
                            group
                        });

                        /**
                        * Fired when session has been restored at boot. Fired once per
                        * session group.
                        * @event Session#$"."group"."restored
                        * @type {object}
                        * @property {string} group The {@link Chat#group} that has been restored.
                        */
                        this.trigger('$.group.restored', { group });

                    });

                }

            });

        });

    }

    /**
     * Callback fired when another instance has joined a chat
     * @private
     */
    join(chat) {

        // don't rebroadcast chats in session we've already heard about
        if (!this.chats[chat.group] || !this.chats[chat.group][chat.channel]) {
            this.sync.emit('$.session.notify.chat.join', { subject: chat.objectify() });
        }

    }

    /**
     * Callback fired when another instance has left a chat
     * @private
     */
    leave(chat) {
        this.sync.emit('$.session.notify.chat.leave', { subject: chat.objectify() });
    }

    /**
    Stores {@link Chat} within ```ChatEngine.session``` keyed based on the ```chat.group``` property.
    @param {Object} chat JSON object representing {@link Chat}. Originally supplied via {@link Chat#objectify}.
    @private
    */
    onJoin(chat) {

        // create the chat group if it doesn't exist
        this.chats[chat.group] = this.chats[chat.group] || {};

        // check the chat exists within the global list but is not grouped
        let existingChat = this.chatEngine.chats[chat.channel];

        // if it exists
        if (existingChat) {

            // assign it to the group
            this.chats[chat.group][chat.channel] = existingChat;
        } else {

            // otherwise, try to recreate it with the server information
            this.chats[chat.group][chat.channel] = new this.chatEngine.Chat(chat.channel, chat.private, false, chat.meta, chat.group);

            /**
            * Fired when another identical instance of {@link ChatEngine} and {@link Me} joins a {@link Chat} that this instance of {@link ChatEngine} is unaware of.
            * Used to synchronize ChatEngine sessions between desktop and mobile, duplicate windows, etc.
            * ChatEngine stores sessions on the server side identified by {@link User#uuid}.
            * @event Session#$"."chat"."join
            * @type {object}
            * @property {Chat} chat The chat added to the user session.
            @example
            *
            * // Logged in as "Ian" in first window
            * ChatEngine.me.session.on('$.chat.join', (data) => {
            *     console.log('I joined a new chat in a second window!', data.chat);
            * });
            *
            * // Logged in as "Ian" in second window
            * new ChatEngine.Chat('another-chat');
            */
            this.trigger('$.chat.join', { chat: this.chats[chat.group][chat.channel] });

        }

    }

    /**
    Removes {@link Chat} within this.chats
    @private
    */
    onleave(chat) {

        if (this.chats[chat.group] && this.chats[chat.group][chat.channel]) {

            chat = this.chats[chat.group][chat.channel] || chat;

            /**
            * Fired when another identical instance of {@link ChatEngine} with an identical {@link Me} leaves a {@link Chat} via {@link Chat#leave}.
            * @event Session#$"."chat"."leave
            * @type {object}
            * @property {Chat} chat The chat removed from the user session.
            */
            delete this.chatEngine.chats[chat.channel];
            delete this.chats[chat.group][chat.channel];

        }

        this.trigger('$.chat.leave', { chat });

    }

}

module.exports = Session;