Source: addon/addonmanager.js

/*globals requireJS*/
/*eslint-env node*/
/**
 * 
 * @author pmeijer / https://github.com/pmeijer
 */
'use strict';

var Q = require('q'),
    superagent = require('superagent'),
    BranchMonitor = require('./branchmonitor'),
    EventDispatcher = requireJS('common/EventDispatcher'),
    STORAGE_CONSTANTS = requireJS('common/storage/constants'),
    Storage = requireJS('common/storage/nodestorage');

/**
 * Class for managing branch-monitors within a single project.
 * Branch-monitors are managing the add-ons within each branch.
 * @param {string} projectId
 * @param {object} mainLogger
 * @param {object} gmeConfig
 * @param {number} gmeConfig.addOn.monitorTimeout - Time to wait before stopping a monitor after only the monitor itself
 * is connected to the branch.
 * @param {object} [options]
 * @param {object} [options.webgmeUrl=http://127.0.0.1:gmeConfig.server.port]
 * @constructor
 * @ignore
 */
function AddOnManager(projectId, mainLogger, gmeConfig, options) {
    var self = this,
        logger = mainLogger.fork('AddOnManager:' + projectId),
        webgmeUrl,
        initDeferred,
        closeDeferred;

    options = options || {};
    webgmeUrl = options.webgmeUrl || 'http://127.0.0.1:' + gmeConfig.server.port;

    EventDispatcher.call(self);

    this.branchMonitors = {
        //branchName: {
        // instance: {BranchMonitor},
        // lastActivity: Date.now(),
        // stopTimeout: {
        //     id: {number},
        //     deferred: {Promise}
        // }
    };

    this.project = null;
    this.storage = null;
    this.initRequested = false;
    this.closeRequested = false;

    this.webgmeToken = null;
    this.renewingToken = false;

    this.inStoppedAndStarted = 0;

    function removeMonitor(branchName) {
        var remainingMonitors;

        delete self.branchMonitors[branchName];
        remainingMonitors = Object.keys(self.branchMonitors);

        logger.debug('Removing monitor [' + branchName + '] - remaining', remainingMonitors);
        if (remainingMonitors.length === 0 && self.inStoppedAndStarted === 0) {
            self.dispatchEvent('NO_MONITORS');
        }
    }

    /**
     * Opens up the storage based on the webgmeToken and opens and sets the project.
     *
     * @param {string} webgmeToken
     * @param {function} [callback]
     * @returns {Promise}
     */
    this.initialize = function (webgmeToken, callback) {
        if (self.initRequested === false) {
            initDeferred = Q.defer();
            self.initRequested = true;
            self.webgmeToken = webgmeToken;

            self.storage = Storage.createStorage(webgmeUrl, self.webgmeToken, logger, gmeConfig);
            self.storage.open(function (networkStatus) {
                if (networkStatus === STORAGE_CONSTANTS.CONNECTED) {
                    self.storage.openProject(projectId, function (err, project, branches, rights) {
                        if (err) {
                            self.close()
                                .finally(function () {
                                    initDeferred.reject(err);
                                });

                            return;
                        }

                        self.project = project;

                        if (rights.write === false) {
                            logger.warn('AddOnManager for project [' + projectId +
                                '] initialized without write access.');
                        }

                        self.initialized = true;
                        initDeferred.resolve();
                    });

                    self.storage.webSocket.addEventListener(STORAGE_CONSTANTS.NOTIFICATION,
                        function (emitter, eventData) {
                            logger.debug('received notification', eventData);
                            if (eventData.type === STORAGE_CONSTANTS.BRANCH_ROOM_SOCKETS) {
                                // If a new socket joined our branch -> emit to the branch room letting
                                // any newly connected users know that we are in this branch too.
                                self.storage.sendNotification({
                                    state: null, // No client state in the addOns
                                    type: STORAGE_CONSTANTS.CLIENT_STATE_NOTIFICATION,
                                    projectId: projectId,
                                    branchName: eventData.branchName
                                }, function (err) {
                                    if (err) {
                                        logger.error('Sending state notification failed', err);
                                    }
                                });
                            } else {
                                logger.debug('Unused notification', eventData.type);
                            }
                        }
                    );
                } else if (networkStatus === STORAGE_CONSTANTS.JWT_ABOUT_TO_EXPIRE) {
                    if (!self.renewingToken) {
                        self.renewingToken = true;
                        superagent.get(webgmeUrl + '/api/user/token')
                            .set('Authorization', 'Bearer ' + self.webgmeToken)
                            .end(function (err, res) {
                                self.renewingToken = false;
                                if (err) {
                                    logger.error(err);
                                } else {
                                    self.setToken(res.body.webgmeToken);
                                }
                            });
                    }
                } else if (networkStatus === STORAGE_CONSTANTS.DISCONNECTED) {
                    logger.warn('Lost connection to storage, awaiting reconnect...');
                } else if (networkStatus === STORAGE_CONSTANTS.RECONNECTED) {
                    logger.info('Storage reconnected!');
                } else {
                    logger.error('Connection problems' + networkStatus);
                    self.storage.close(function (err) {
                        if (err) {
                            logger.error(err);
                        }

                        initDeferred.reject(new Error('Problems connecting to the webgme server, network status: ' +
                            networkStatus));
                    });
                }
            });
        }

        return initDeferred.promise.nodeify(callback);
    };

    this.monitorBranch = function (branchName, callback) {
        var monitor = self.branchMonitors[branchName];

        function startNewTimer() {
            return setTimeout(function () {
                var timedDeferred = monitor.stopTimeout.deferred;
                monitor.stopTimeout = null;
                monitor.instance.stop()
                    .then(function () {
                        removeMonitor(branchName);
                        timedDeferred.resolve();
                    })
                    .catch(timedDeferred.reject);

            }, gmeConfig.addOn.monitorTimeout);
        }

        function startNewMonitor() {
            monitor = {
                lastActivity: Date.now(),
                stopTimeout: null,
                instance: new BranchMonitor(
                    self.webgmeToken,
                    self.storage,
                    self.project,
                    branchName,
                    logger,
                    gmeConfig,
                    options)
            };

            self.branchMonitors[branchName] = monitor;

            monitor.stopTimeout = {
                deferred: Q.defer(),
                id: startNewTimer(branchName)
            };

            logger.debug('monitorBranch [' + branchName + '] - starting new monitor');
            return monitor.instance.start();
        }

        if (monitor) {
            if (monitor.stopTimeout) {
                // The monitor has been created and the stop hasn't been triggered,
                // so clear the old timeout and set a new one.
                clearTimeout(monitor.stopTimeout.id);
                monitor.stopTimeout.id = startNewTimer(branchName);
                monitor.lastActivity = Date.now();

                return monitor.instance.start()
                    .nodeify(callback);
            } else {
                // The timeout has been triggered, and the monitor is in stopping stage.
                // We cannot simply remove the monitor before it has closed the branch,
                // therefore we need to register on the stop.promise and start a new monitor
                // (or use an earlier added one) once it has resolved.

                // The counter ensures that this manager isn't destroyed.
                self.inStoppedAndStarted += 1;
                return monitor.instance.stop()
                    .then(function () {
                        self.inStoppedAndStarted -= 1;
                        if (self.branchMonitors[branchName]) {
                            return self.branchMonitors[branchName].instance.start();
                        } else {
                            return startNewMonitor();
                        }
                    })
                    .nodeify(callback);
            }
        } else {
            return startNewMonitor()
                .nodeify(callback);
        }
    };

    this.unMonitorBranch = function (branchName, callback) {
        var deferred = Q.defer();
        deferred.resolve({});
        return deferred.promise.nodeify(callback);
    };

    this.queryAddOn = function (webgmeToken, branchName, addOnId, queryParams, callback) {
        var deferred = Q.defer();
        deferred.reject(new Error('Not Implemented!'));
        return deferred.promise.nodeify(callback);
    };

    this.close = function (callback) {

        function stopMonitor(branchName) {
            clearTimeout(self.branchMonitors[branchName].stopTimeout.id);
            return self.branchMonitors[branchName].instance.stop();
        }

        if (self.closeRequested === false) {
            closeDeferred = Q.defer();
            self.closeRequested = true;

            Q.allSettled(Object.keys(self.branchMonitors).map(stopMonitor))
                .then(function (/*results*/) {
                    //TODO: Check the results and at least log errors.
                    return Q.ninvoke(self.storage, 'close');
                })
                .then(function () {
                    closeDeferred.resolve();
                })
                .catch(closeDeferred.reject);
        }

        return closeDeferred.promise.nodeify(callback);
    };

    this.setToken = function (token) {
        self.webgmeToken = token;
        logger.debug('Setting new token..');
        if (self.storage) {
            self.storage.setToken(token);
        }
        Object.keys(self.branchMonitors).forEach(function (branchName) {
            self.branchMonitors[branchName].instance.setToken(token);
        });
    };

    this.getStatus = function (/*opts*/) {
        var status = {
            initRequested: self.initRequested,
            closeRequested: self.closeRequested,
            renewingToken: self.renewingToken,
            inStoppedAndStarted: self.inStoppedAndStarted,
            branchMonitors: {}
        };

        Object.keys(self.branchMonitors).forEach(function (branchName) {
            status.branchMonitors[branchName] = self.branchMonitors[branchName].instance.getStatus();
            status.branchMonitors[branchName].lastActivity = self.branchMonitors[branchName].lastActivity;
        });

        return status;
    };
}


// Inherit from the EventDispatcher
AddOnManager.prototype = Object.create(EventDispatcher.prototype);
AddOnManager.prototype.constructor = AddOnManager;

module.exports = AddOnManager;