docs: 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;
      };
  }