Source: js/service.js

'use strict';

(function(exports) {
  var DEBUG = false;
  /**
   * Shared some global property.
   * @type {Object}
   * @module Service
   */
  exports.Service = {
    /**
     * Stores the servers by the server name.
     * @type {Map}
     */
    _providers: new Map(),

    /**
     * Stores the services by the services name.
     * @type {Map}
     */
    _services: new Map(),

    /**
     * Stores the awaiting consumers by the service name.
     * @type {Map}
     */
    _requestsByService: new Map(),

    /**
     * Stores the awaiting consumers by the server name.
     * @type {Map}
     */
    _requestsByProvider: new Map(),

    /**
     * Request a service and get a promise.
     * The service name may include the name of server or not if it is unique.
     *
     * The request and query format are different,
     * request does not accept getter format.
     *
     * @example
     * Service.request('locked').then(function() {});
     * Service.request('addObserver', 'test.enabled', this).then(function() {});
     * Service.request('Statusbar:height').then(function() {});
     *
     * @param  {String} service Service name
     * @return {Promise}
     */
    request: function(service) {
      var requestItems = service.split(':');
      var args = Array.prototype.slice.call(arguments, 1);
      var self = this;
      this.debug(requestItems);
      if (requestItems.length > 1) {
        var serverName = requestItems[0];
        var serviceName = requestItems[1];
        if (this._providers.get(serverName)) {
          this.debug('service: ' + serviceName +
            ' is online, perform the request with ' + args.concat());
          return Promise.resolve(this._providers.get(serverName)[serviceName]
              .apply(self._providers.get(serverName), args));
        } else {
          return new Promise(function(resolve, reject) {
            self.debug('service: ' + service + ' is offline, queue the task.');
            if (!self._requestsByProvider.has(serverName)) {
              self._requestsByProvider.set(serverName, []);
            }
            self._requestsByProvider.get(serverName).push({
              service: serviceName,
              resolve: resolve,
              args: args
            });
          });
        }
        return;
      }
      if (this._services.has(service)) {
        var server = this._services.get(service);
        this.debug('service [' + service +
          '] provider [' + server.name + '] is online, perform the task.');
        return Promise.resolve(server[service].apply(server, args));
      } else {
        this.debug('service: ' + service + ' is offline, queue the task.');
        var promise = new Promise(function(resolve, reject) {
          self.debug('storing the requests...');
          if (!self._requestsByService.has(service)) {
            self._requestsByService.set(service, []);
          }
          self._requestsByService.get(service).push({
            service: service,
            resolve: resolve,
            args: args
          });
        });
        return promise;
      }
    },

    /**
     * Register an asynchronous service.
     * If there is any client awaiting this service, they will be executed after
     * registration.
     * @param  {String} service Service name
     * @param  {Object} server  The server object which has the service.
     */
    register: function(service, server) {
      var self = this;
      if (!this._providers.has(server.name)) {
        this._providers.set(server.name, server);
      }
      this.debug((server.name || '(Anonymous)') +
        ' is registering service: [' + service + ']');

      this.debug('checking awaiting requests by server..');
      if (this._requestsByProvider.has(server.name)) {
        this._requestsByProvider.get(server.name).forEach(function(request) {
          self.debug('resolving..', server,
            server.name, request.service, request.args);
          var returnValue = (typeof(server[request.service]) === 'function') ?
            server[request.service].apply(server, request.args) :
            server[request.service];
          request.resolve(returnValue);
        });
        this._requestsByProvider.delete(server.name);
      }

      if (!this._services.has(service)) {
        this._services.set(service, server);
      } else {
        this.debug('the service [' + service + '] has already been ' +
          'registered by other server:', this._services.get(service).name);
        return;
      }

      this.debug('checking awaiting requests by service..');
      if (this._requestsByService.has(service)) {
        this._requestsByService.get(service).forEach(function(request) {
          self.debug('resolving..', server, request.service);
          var returnValue = server[request.service].apply(server, request.args);
          request.resolve(returnValue);
        });
        this._requestsByService.delete(service);
      }
    },

    /**
     * Unregister an asynchronous service.
     * @param  {String} service The name of the service
     * @param  {Object} server  The server
     */
    unregister: function(service, server) {
      this._providers.delete(server.name);
      var se = this._services.get(service);
      if (se && server === se) {
        this._services.delete(service);
      }
    },

    _states: new Map(),
    _statesByState: new Map(),

    registerState: function(state, provider) {
      this._states.set(provider.name, provider);
      if (!provider.name) {
        console.warn(provider);
      }
      this._statesByState.set(state, provider);
    },

    unregisterState: function(state, provider) {
      this._states.delete(provider.name);
      var machine = this._statesByState.get(state);
      if (machine === provider) {
        this._statesByState.delete(state);
      }
    },

    /**
     * Synchonously query the state of specific state machine.
     * If the state machine is not started,
     * you will get undefined.
     *
     * @example
     * Service.query('FtuLauncher.isFtuRunning');
     * Service.query('isFtuRunning');
     *
     * @param  {String} state The machine name and the state name.
     * @return {String|Boolean|Number|Object}
     */
    query: function(stateString) {
      this.debug(stateString);
      var args = stateString.split('.');
      var state, provider;
      if (args.length > 1) {
        provider = this._states.get(args[0]);
        state = args[1];
      } else {
        state = args[0];
        provider = this._statesByState.get(state);
      }
      if (!provider) {
        this.debug('Provider not ready, return undefined state.');
        return undefined;
      }
      if (typeof(provider[state]) === 'function') {
        var functionArgs = Array.prototype.slice.call(arguments, 1);
        return provider[state].apply(provider, functionArgs);
      } else {
        return provider[state];
      }
    },

    /**
     * Record the start time of the system for later debugging usage.
     * @access private
     * @type {Number}
     * @memberOf module:Service
     */
    _start: new Date().getTime() / 1000,

    /**
     * Get current time offset from the start.
     * @return {Number} The time offset.
     * @memberOf module:Service
     */
    currentTime: function() {
      return (new Date().getTime() / 1000 - this._start).toFixed(3);
    },

    debug: function sys_debug() {
      if (DEBUG) {
        console.log('[System]' +
          '[' + window.Service.currentTime() + ']' +
          Array.slice(arguments).concat());
      }
    },

    get manifestURL() {
      return window.location.href.replace('index.html', 'manifest.webapp');
    }
  };
})(window);