API Docs for:
Show:

File: /home/salva/workspace/offliner/src/offliner-client.js

(function (exports) {
  'use strict';

  var nextPromiseId = 1;

  var originalOff = exports.off;

  var root = (function () {
    var root = new URL(
      document.currentScript.dataset.root || '',
      window.location.origin
    ).href;
    return root.endsWith('/') ? root : root + '/';
  }());

  var workerURL =
    root + (document.currentScript.dataset.worker || 'offliner-worker.js');

  /**
   * The exported global `off` object contains methods for communicating with
   * the offliner worker in charge.
   *
   * @class OfflinerClient
   */
  exports.off = {

    /**
     * Callbacks for the events.
     *
     * @property _eventListeners
     * @type Object
     * @private
     */
    _eventListeners: {},

    /**
     * Implementation callbacks for cross promises by its unique id.
     *
     * @property _xpromises
     * @type Object
     * @private
     */
    _xpromises: {},

    /**
     * Call `restore()` when you want the `off` name in the global scope for
     * other purposes. The method will restore the previous contents to the
     * global variable and return the `OfflinerClient`.
     *
     * @method restore
     * @return {OfflinerClient} The current offliner client.
     */
    restore: function () {
      exports.off = originalOff;
      return this;
    },

    /**
     * Register the offliner worker. The worker will be installed with
     * root `/` [scope](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Syntax)
     * unless you add the `data-root` attribute to the script tag.
     *
     * In the same way, the client will look for a script in the specified root
     * called `offliner-worker.js`. If you want to change this behaviour, use
     * the `data-worker` attribute.
     *
     * For instance, suppose your web application is running under:
     * https://lodr.github.com/offliner
     *
     * And you have your worker at:
     * https://lodr.github.com/offliner/worker.js
     *
     * Then the script tag should looks like:
     * ```html
     * <script src="js/offliner-client.js" data-root="offliner" data-worker="worker.js"></script>
     * ```
     *
     * @method install
     * @return {Promise} A promise resolving if the installation success.
     */
    install: function () {
      this._installMessageHandlers();
      return navigator.serviceWorker.register(workerURL, {
        scope: root
      });
    },

    /**
     * If you are using offliner as a serviceworkerware middleware, instead
     * of calling {{#crossLink OfflinerClient/install:method}}{{/crossLink}},
     * call `connect()` to avoid registering the worker.
     *
     * @method connect
     */
    connect: function () {
      this._installMessageHandlers();
    },

    /**
     * Attaches a listener for a type of event.
     *
     * @method on
     * @param type {String} The type of the event.
     * @param handler {Callback} The callback receiving the event.
     * @param willBeThis {Object} The context object `this` for the `handler`.
     */
    on: function (type, handler, willBeThis) {
      if (!this._has(type, handler, willBeThis)) {
        this._eventListeners[type] = this._eventListeners[type] || [];
        this._eventListeners[type].push([handler, willBeThis]);
      }
    },

    /**
     * Request an update to offliner.
     *
     * @method update
     * @return {Promise} If the update process is successful, the promise will
     * resolve to a new version and an
     * {{#crossLink OfflinerClient/activationPending:event}}{{/crossLink}}
     * will be triggered. If the update is not needed, the promise will be
     * rejected with `no-update-needed` reason.
     */
    update: function () {
      return this._xpromise('update');
    },

    /**
     * Performs the activation of the pending update. I.e. replaces the current
     * cache with that updated in the update process. Normally, you want to
     * reload the application when the activation ends successfuly.
     *
     * @method activate
     * @return {Promise} A promise resolving into the activated version or
     * rejected with `no-activation-pending` if there was not an activation.
     */
    activate: function () {
      return this._xpromise('activate');
    },

    /**
     * Run the listeners for some type of event.
     *
     * @method _runListeners
     * @param type {String} The type of the events selecting the listeners to
     * be run.
     * @param evt {Object} The event contents.
     */
    _runListeners: function (type, evt) {
      var listeners = this._eventListeners[type] || [];
      listeners.forEach(function (listenerAndThis) {
        var listener = listenerAndThis[0];
        var willBeThis = listenerAndThis[1];
        listener.call(willBeThis, evt);
      });
    },

    /**
     * Registers the listeners for enabling communication between the worker
     * and the client code.
     *
     * @method _installMessageHandlers
     */
    _installMessageHandlers: function installMessageHandlers() {
      var that = this;
      if (!installMessageHandlers.done) {
        if (typeof BroadcastChannel === 'function') {
          var bc = new BroadcastChannel('offliner-channel');
          bc.onmessage = onmessage;
        }
        else {
          window.addEventListener('message', onmessage);
        }
        installMessageHandlers.done = true;
      }

      function onmessage(e) {
        var msg = e.data;
        var type = msg ? msg.type : '';
        var typeAndSubType = type.split(':');
        if (typeAndSubType[0] === 'offliner') {
          that._handleMessage(typeAndSubType[1], msg);
        }
      }
    },

    /**
     * Discriminates between {{#crossLink OfflinerClient/xpromise:event}}{{/crossLink}}
     * events which are treated in a special way and the rest of the events that
     * simply trigger the default dispatching algorithm.
     *
     * @method _handleMessage
     * @param offlinerType {String} The type of the message without the
     * `offliner:` prefix.
     * @param msg {Any} The event.
     */
    _handleMessage: function (offlinerType, msg) {
      var sw = navigator.serviceWorker;
      if (offlinerType === 'xpromise') {
        this._resolveCrossPromise(msg);
      }
      else {
        this._runListeners(offlinerType, msg);
      }
    },

    /**
     * @method _has
     * @param type {String} The type for the listener registration.
     * @param handler {Function} The listener.
     * @param willBeThis {Object} The context object `this` which the function
     * will be called with.
     * @return `true` if the listener registration already exists.
     */
    _has: function (type, handler, willBeThis) {
      var listeners = this._eventListeners[type] || [];
      for (var i = 0, listenerAndThis; (listenerAndThis = listeners[i]); i++) {
        if (listenerAndThis[0] === handler &&
            listenerAndThis[1] === willBeThis) {
          return true;
        }
      }
      return false;
    },

    /**
     * Creates a cross promise registration. A _cross promise_ or xpromise
     * is a special kind of promise that is generated in the client but whose
     * implementation is in a worker.
     *
     * @method _xpromise
     * @param order {String} The string for the implementation part to select
     * the implementation to run.
     * @return {Promise} A promise delegating its implementation in some code
     * running in a worker.
     */
    _xpromise: function (order) {
      return new Promise(function (accept, reject) {
        var uniqueId = nextPromiseId++;
        var msg = {
          type: 'xpromise',
          id: uniqueId,
          order: order
        };
        this._xpromises[uniqueId] = [accept, reject];
        this._send(msg);
      }.bind(this));
    },

    /**
     * Sends a message to the worker.
     * @method _send
     * @param msg {Any} The message to be sent.
     */
    _send: function (msg) {
      navigator.serviceWorker.controller.postMessage(msg);
    },

    /**
     * Resolves a cross promise based on information received by the
     * implementation in the worker.
     *
     * @method _resolveCrossPromise
     * @param msg {Object} An object with the proper data to resolve a xpromise.
     */
    _resolveCrossPromise: function (msg) {
      var implementation = this._xpromises[msg.id];
      if (implementation) {
        implementation[msg.status === 'rejected' ? 1 : 0](msg.value);
      }
      else {
        console.warn('Trying to resolve unexistent promise:', msg.id);
      }
    }
  };

}(this));