carriage.js

import each from 'async/each';

/**
 * Universal basic carriage for funicular.
 * @memberof ancient-funicular
 */
class Carriage {
  
  /**
   * @param {Funicular} funicular - Link to instance of {@link Funicular}, used as namespace of carriages for this carriage.
   * @param {String} name - Unique name of this carriage.
   * @param data - Any outer system data needed for mounting one carriage.
   */
  constructor(funicular, name, data) {
    
    /**
     * Link to instance of {@link Funicular}, used as namespace of carriages for this carriage.
     * @type {Funicular}
     * @memberof Carriage
     * @protected
     */
    this.funicular = funicular;
    
    /**
     * Unique id of carriages in funicular instance.
     * @type {Number}
     * @memberof Carriage
     * @protected
     */
    this.id = funicular.lastCarriageId++;
    
    /**
     * Unique name of this carriage.
     * @type {String}
     * @memberof Carriage
     * @protected
     */
    this.name = name;
    
    /**
     * Any outer system data needed for mounting one carriage.
     * @memberof Carriage
     * @protected
     */
    this.data = data;
    
    /**
     * String stage of carriage. Used for place carriage in execution space.
     * @type {String}
     * @memberof Carriage
     * @protected
     */
    this.stage = 'constructed';
    
    /**
     * Object with childs carriages by names.
     * @type {CarriagesByNames}
     * @memberof Carriage
     * @protected
     */
    this.childs = {};
    
    /**
     * Object with parents carriages by names.
     * @type {CarriagesByIds}
     * @memberof Carriage
     * @protected
     */
    this.parents = {};
    
    /**
     * Callbacks after mount.
     * @type {Carriage~mountCallback[]}
     * @memberof Carriage
     * @private
     */
    this.mountedCallbacks = [];
    
    /**
     * @callback Carriage~mountedCallback
     * @param [error]
     * @param {Carriage} carriage
     */
    
    /**
     * Callbacks after remount.
     * @type {Carriage~remountedCallbacks[]}
     * @memberof Carriage
     * @private
     */
    this.remountedCallbacks = [];
    
    /**
     * @callback Carriage~remountedCallbacks
     * @param [unmountError]
     * @param {Carriage} oldCarriage
     * @param [mountError]
     * @param {Carriage} [newCarriage]
     */
    
    /**
     * Callbacks after unmount.
     * @type {Carriage~unmountCallback[]}
     * @memberof Carriage
     * @private
     */
    this.unmountedCallbacks = [];
    
    /**
     * @callback Carriage~unmountedCallback
     * @param [error]
     * @param {Carriage} carriage
     */
    
    /**
     * Last error. It is recorded if an error was received while mounting or unmounting.
     * Can be recorded anywhere, it is worth considering that it can be and check it before important actions.
     * @memberof Carriage
     * @protected
     */
    this.error;
    
  }
  
  /**
   * Get unique state. If sended argument `state` returns boolean.
   * @param {Boolean} [state] - if sended, set this carriage as unique primary of this name.
   * @returns {Boolean}
   */
  unique(state) {
    if (this.name) {
      if (typeof(state) == 'boolean') {
        if (state) {
          this.funicular.namedCarriages[this.name][0] = this;
        } else {
          delete this.funicular.namedCarriages[this.name][0];
        }
      }
      return this.funicular.namedCarriages[this.name][0] == this;
    } else return false;
  }
  
  /**
   * Tie one child with this carriage parent.
   * @param {Carriage} child
   */
  tieChild(child) {
    this.childs[child.name] = child;
    child.parents[this.id] = this;
  }
  
  /**
   * Untie one child with this carriage parent.
   * @param {Carriage} child
   */
  untieChild(child) {
    delete this.childs[child.name];
    delete child.parents[this.id];
  }
  
  /**
   * Based on data, mount this carriage.
   * Use {@link Carriage.enable} for useful functionality.
   * Calls all functions from {@link Carriage.mountedCallbacks}, if this carriage did unmount.
   */
  mount() {
    this.enable((error) => {
      this.error = error;
      this.stage = error?'error':'mounted';
      if (!error) {
        if (typeof(this.name) == 'string') {
          this.funicular.namedCarriages[this.name] = this.funicular.namedCarriages[this.name] || {};
          this.funicular.namedCarriages[this.name][this.id] = this;
          this.unique(true);
        } else {
          this.funicular.unnamedCarriages[this.id] = this;
        }
      }
      for (var c in this.mountedCallbacks) {
        this.mountedCallbacks[c].call(this, error, this);
      }
    });
  }
  
  /**
   * Mount carriage as child for this carriage.
   * @param {String} name
   * @param data
   * @param {Carriage~mountCallback} [callback]
   */
  mountChild(name, data, callback) {
    this.funicular.mount(
      name, data,
      (error, child) => {
        if (this.stage != 'unmounted') {
          this.tieChild(child);
          callback();
        }
      },
      (unmountError, oldChild, mountError, newChild) => {
        this.untieChild(oldChild);
        this.tieChild(newChild);
      },
      (error, child) => {
        this.untieChild(child);
      }
    );
  }
  
  /**
   * You must override it with useful functionality.
   * Can gets and mount childs carriages into {@link Carriage.childs}.
   * It method must added, enable, run and write all that was forgeted, disabled, stoped and erased at {@link Carriage.disable}.
   * It must call callback! Otherwise, the demounting chain may break.
   * @param {Carriage~enableCallback} callback
   */
  enable(callback) {
    callback();
  }
  
  /**
   * @callback Carriage~enableCallback
   * @param error
   */
  
  /**
   * Remount this carriage.
   * @todo
   * If {@link Carriage.invalid} is `true`, mount new carriage based on this name and data and unmount this carriage.
   */
  remount() {
    var tempMountedCallback = (mountError, carriage) => {
      carriage.mountedCallbacks.splice(carriage.mountedCallbacks.indexOf(tempMountedCallback), 1);
      carriage.mountedCallbacks = this.mountedCallbacks;
      this.mountedCallbacks = [];
      carriage.remountedCallbacks = this.remountedCallbacks;
      carriage.unmountedCallbacks = this.unmountedCallbacks;
      this.unmountedCallbacks = [];
      this.unmount((unmountError) => {
        for (var c in this.remountedCallbacks) {
          this.remountedCallbacks[c].call(this, unmountError, this, mountError, carriage);
        }
        this.remountedCallbacks = [];
        for (var parent in this.parents) {
          this.parents[parent].childDidRemount(unmountError, this, mountError, carriage);
        }
      });
    };
    this.funicular.mount(
      this.name, this.data,
      tempMountedCallback, undefined, undefined,
      true
    );
  }
  
  /**
   * Sort unmount this carriage with childs.
   * The carriages necessary for some other carriages will not be unmounted.
   * Calls all functions from {@link Carriage.unmountedCallbacks}, if this carriage did unmount.
   * Calls argument callback in any case, even if the unmount did not happen.
   * @param {Carriage~unmountCallback} [callback]
   */
  unmount(callback) {
    for (var p in this.parents) {
      if (callback) callback(undefined, this);
      return; // If parents exists, umount cannot be done.
    }
    if (this.stage == 'mounted') {
      this.unmountChilds();
      this.disable((error) => {
        this.error = error;
        this.stage = this.error?'error':'unmounted';
        if (typeof(this.name) == 'string') {
          delete this.funicular.namedCarriages[this.name][this.id];
          if (this.funicular.namedCarriages[this.name][0] == this) {
            delete this.funicular.namedCarriages[this.name][0];
          };
        } else {
          delete this.funicular.unnamedCarriages[this.id];
        }
        for (var c in this.unmountedCallbacks) {
          this.unmountedCallbacks[c].call(this, error, this);
        }
        if (callback) callback(error, this);
      });
    } else {
      if (callback) callback(undefined, this);
    }
  }
  
  /**
   * Cicle for sort unmount childs of this carriage.
   * Not asynchronously and with callback, because there is no sense in waiting for the end of the unmounting childs.
   * If you do not agree please describe the reason in the issues.
   */
  unmountChilds() {
    for (var c in this.childs) {
      var child = this.childs[c];
      this.untieChild(child);
      child.unmount();
    }
  }
  
  /**
   * You must override it with useful functionality.
   * It method must forget, disable, stop and erase all that was added, enabled, runned and writed at {@link Carriage.enable}.
   * It must call callback! Otherwise, the demounting chain may break.
   * @param {Carriage~disableCallback} callback
   */
  disable(callback) {
    callback();
  }
  
  /**
   * @callback Carriage~disableCallback
   * @param error
   */
  
  /**
   * You can override it with useful functionality.
   * Synchronously answers the question - is it necessary to remount this parent while remounting the child?
   * @param [unmountError]
   * @param {Carriage} oldCarriage
   * @param [mountError]
   * @param {Carriage} [newCarriage]
   */
  shouldRemount(unmountError, olda, mountError, newa) {
    return false;
  }
}

export default Carriage;