Manual Reference Source Test

src/queueitem.js

/**
*  @file queueitem.js
*  @author Liqueur de Toile <contact@liqueurdetoile.com>
*  @licence AGPL-3.0 {@link https://github.com/liqueurdetoile/beloader/blob/master/LICENSE}
*/

import ObjectArray from 'dot-object-array';
import AbstractEventManager from 'core/AbstractEventManager';
import NoneLoader from 'loaders/NoneLoader';

/**
*  QueueItem handles all item behaviours in the loading queue.
*  Given its type and options, it will load the appropriate loader
*  and process request.
*
*  @version 1.0.0
*  @since 1.0.0
*  @author Liqueur de Toile <contact@liqueurdetoile.com>
*  @extends {AbstractEventManager}
*/
export default class QueueItem extends AbstractEventManager {
  /**
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  *
  *  @param {string} type Type of item. See {@link Beloader#fetch}
  *  @param {Beloader} parent Beloader calling instance
  *  @param {DotObjectArray} options Options for QueueItem and underlying loader
  */
  constructor(type, parent, options) {
    super(options.data.on);

    const _this = this;
    let loader;

    /**
    *  Requesting parent item
    *  @since 1.0.0
    *  @type {Beloader}
    */
    this.parent = parent;

    /**
    *  Map plugins
    *  @since 1.0.0
    *  @type {DotObjectArray}
    */
    this._plugins = parent._plugins;
    this._plugins.forEach((plugin, name) => {
      /** @ignore */
      _this[name] = plugin;
    });

    /**
    *  Stores item progress
    *  @since 1.0.0
    *  @type {DotObjectArray}
    */
    this.progress = new ObjectArray();

    /**
    *  Stores the state of the item
    *  @since 1.0.0
    *  @type {Object} state
    *  @property {boolean}  state.waiting `true` if item is waiting to be processed
    *  @property {boolean}  state.pending `true` if item's loading is in progress
    *  @property {boolean}  state.loaded `true` if item's loading is completed and successfull
    *  @property {boolean}  state.error `true` if item's loading is in error, aborted or timed out
    *  @property {boolean}  state.abort `true` if item's loading is aborted
    *  @property {boolean}  state.timeout `true` if item's loading is in timeout
    *  @property {boolean}  state.processed `true` if item's process is over (loading + initialization)
    *  @property {boolean}  state.resolved `true` if item's promise have been resolved
    *  @property {boolean}  state.ready `true` if item is ready to be used
    */
    this.state = {
      waiting: true,
      pending: false,
      loaded: false,
      error: false,
      abort: false,
      timeout: false,
      processed: false,
      resolved: false,
      ready: false
    };

    /**
    *  Id of the item
    *  @type {string}
    *  @see {Beloader#fetch}
    */
    this.id = options.data.id;

    /**
    *  Autoprocess trigger
    *  @type {boolean}
    *  @see {Beloader#fetch}
    */
    this.autoprocess = options.data.autoprocess;

    /**
    *  Async mode trigger
    *  @type {boolean}
    *  @see {Beloader#fetch}
    */
    this.async = options.data.async;

    /**
    *  Defer mode trigger
    *  @type {boolean}
    *  @see {Beloader#fetch}
    */
    this.defer = options.data.defer;

    /**
    *  Awaiting mode, dependencies listing
    *  @type {Array} awaiting
    *  @see {Beloader#fetch}
    */
    this.awaiting = [];
    if (typeof options.data.awaiting !== 'undefined') {
      if (options.data.awaiting instanceof Array) this.awaiting = options.data.awaiting;
      else this.awaiting = [options.data.awaiting];
    }

    /**
    *  Loader ready promise
    *  @since 1.0.0
    *  @type {Promise}
    */
    this.loaderReady = new Promise((resolve, reject) => {
      _this._loaderOK = resolve;
      _this._loaderKO = reject;
    });

    /**
    *  Item process promise
    *  @since 1.0.0
    *  @type {Promise}
    */
    this.promise = new Promise((resolve, reject) => {
      _this._resolve = resolve;
      _this._reject = reject;
    });

    // Import loader
    switch (type) {
      case 'webfont':
      case 'font':
        loader = 'FontLoader';
        break;
      case 'js':
      case 'script':
      case 'javascript':
      case 'ecmascript':
        loader = 'ScriptLoader';
        break;
      case 'style':
      case 'styles':
      case 'stylesheet':
      case 'css':
        loader = 'StylesheetLoader';
        break;
      case 'json':
        loader = 'JsonLoader';
        break;
      case 'image':
      case 'img':
        loader = 'ImageLoader';
        break;
      case 'plugin':
        loader = 'PluginLoader';
        break;
      case 'none':
        /**
        *  Loader instance
        *  @since 1.0.0
        *  @type {Loader}
        */
        this.loader = new NoneLoader(this, options);
        if (this.autoprocess) this.process();
        this._loaderOK(this);
        break;
      default:
        // Custom type loader
        if (options.data.loader) loader = 'CustomLoader';
        else throw new TypeError('BeLoader : No loader for assets with type ' + type);
    }

    /* istanbul ignore else */
    if (loader) {
      import(
        /* webpackChunkName: "[request]" */
        'loaders/' + loader
      ).then(function (Loader) {
        try {
          _this.loader = new Loader.default(_this, options); // eslint-disable-line
          if (_this.autoprocess) _this.process.call(_this);
          _this._loaderOK(_this);
        } catch (e) {
          _this._loaderKO(_this);
          _this._reject(e);
        }
      });
    }
  }

  /**
  *  Process the request for the QueueItem
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  *
  *  @emits {load}
  *  @emits {error}
  *  @emits {loadend}
  */
  process() {
    const _this = this;

    this.loaderReady.then(function () {
      try {
        _this.loader.promise.then(
          () => {
            _this.loader.fire('load', _this.loader);
          },
          (e) => {
            _this.loader.fire('error', _this.loader, e);
          }
        ).then(() => _this.fire('loadend', _this));
      } catch (e) {
        _this.loader.fire('error', _this.loader, e);
        _this.fire('loadend', _this);
      }
    });

    return this;
  }

  /**
  *  loadstart built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _loadstart() {
    let start = +new Date();

    // Update state
    this.state.waiting = false;
    this.state.pending = true;

    // Initialize loading statistics
    this.progress.push('start', start);
    this.progress.push('details', [{
      timestamp: start,
      duration: 0,
      chunked: 0,
      chunkrate: 0,
      elapsed: 0,
      loaded: 0,
      rate: 0,
      complete: 0
    }]);
    this.progress.push('loaded', 0);
  }

  /**
  *  load built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _load() {
    // Update state
    this.state.loaded = true;
    // Update data loading progress
    this.progress.data.complete = 100;
  }

  /**
  *  error built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _error(ev) {
    this.state.error = true;
    this.error = ev.data; // Store error string/object
  }

  /**
  *  abort built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _abort() {
    this.state.abort = true;
  }

  /**
  *  timeout built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _timeout() {
    this.state.timeout = true;
  }

  /**
  *  loadend built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _loadend() {
    // Update state
    this.state.pending = false;
    this.state.processed = true;

    this.progress.push('end', +new Date());
    this.progress.push('elapsed', this.progress.data.end - this.progress.data.start);
  }

  /**
  *  ready built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _ready() {
    if (this.id) {
      // Update awaitables
      this.parent._awaitables[this.id] = true;
      // Relaunch onloadend on beloader instance to trigger awaiting dependents
      this.parent._loadend();
    }
  }

  /**
  *  progress built-in callback
  *
  *  @version 1.0.0
  *  @since 1.0.0
  *  @author Liqueur de Toile <contact@liqueurdetoile.com>
  */
  _progress(event) {
    const ev = event.data;
    let t = +new Date();
    let pt = this.progress.data.details[this.progress.data.details.length - 1];
    let details;

    // Update total
    if (ev.lengthComputable) this.progress.push('total', ev.total);

    // Store step detail
    details = {
      timestamp: t,
      duration: t - pt.timestamp, // milliseconds
      chunked: ev.loaded - pt.loaded, // Bytes
      chunkrate: (ev.loaded - pt.loaded) / ((t - pt.timestamp) / 1000), // Bytes/s
      elapsed: t - this.progress.data.start, // milliseconds
      loaded: ev.loaded, // Bytes
      rate: ev.loaded / ((t - this.progress.data.start) / 1000), // Bytes/s
      complete: (this.progress.data.total ? (ev.loaded / this.progress.data.total) * 100 : 0) // Percent
    };

    this.progress.data.details.push(details);

    // Update globals instant state
    this.progress.push('elapsed', details.elapsed);
    this.progress.push('loaded', details.loaded);
    this.progress.push('rate', details.rate);
    this.progress.push('complete', details.complete);
  }
}