<commit_before>import JsTurbo from 'jsturbo'

/**
 * @typedef {ColumnInfo} Description of the columns of tabular data.
 * @property {number} dateIndex - Date index.
 * @property {number} payeeIndex - Payee index.
 * @property {number} inflowIndex - Inflow index.
 */

/**
 * Map a [header type] to an [array of possible header labels].
 */
let POSSIBLE_HEADER_LABELS = {
  dateIndex: ['datum', 'date', 'data'],
  payeeIndex: ['mottagare', 'transaktion', 'payee', 'descrição', 'histórico', 'lançamento'],
  inflowIndex: ['belopp', 'belopp sek', 'inflow', 'value', 'valor']
}

/**
 * Find information about the columns of tabular data.
 * @param  {TabularData} tabularData - Tabular data.
 * @return {ColumnInfo} Column information.
 */
function getColumnInfo(tabularData) {
  var columnInfo;
  //1: try using the table header
  if (tabularData.header) {
    columnInfo = findIndexesFromHeader(tabularData.header)
    if (isValid(columnInfo)) {
      return columnInfo
    }
  }

  // 2: try using the table values
  if (tabularData.data) {
    columnInfo = findIndexesFromValues(tabularData.data)
    if (isValid(columnInfo)) {
      return columnInfo
    }
  }
  throw Error('Could not find column information.')
}

/**
 * Find the index of each header type based on the header labels.
 * @param {string[]} tableHeaderLabels - Labels on the table headers.
 * @return {ColumnInfo} Column information.
 */
function findIndexesFromHeader(tableHeaderLabels) {
  var columnInfo = {}
  for(var headerType in POSSIBLE_HEADER_LABELS) {
    var possibleHeaderLabels = POSSIBLE_HEADER_LABELS[headerType]
    var index = findHeaderIndexByLabel(tableHeaderLabels, possibleHeaderLabels)
    if(index >= 0) {
      columnInfo[headerType] = index
    }
  }
  return columnInfo
}

/**
 * Find the index of each header type based on the table values.
 * @param {string[]} tableValues - Table values.
 * @return {ColumnInfo} Column information.
 */
function findIndexesFromValues(tableValues) {
  var row = tableValues[0]
  var columnInfo = {}

  for (var i = 0; i < row.length; i++) {
    if (JsTurbo.date.isDate(row[i]) || isAlternativeDateFormat(row[i])) {
      columnInfo.dateIndex = i
      break
    }
  }

  for (i = 0; i < row.length; i++) {
    if (i === columnInfo.dateIndex) {
      continue
    }

    if (JsTurbo.str.isNumber(row[i]) || isAlternativenumberFormat(row[i])) {
      columnInfo.inflowIndex = i
      break
    }
  }

  for (i = 0; i < row.length; i++) {
    if (i === columnInfo.dateIndex ||
      i === columnInfo.inflowIndex) {
      continue
    }

    // Payee: at least 3 letters
    if (JsTurbo.str.containsAlpha(row[i], 3)) {
      columnInfo.payeeIndex = i
      break
    }
  }
  return columnInfo
}

/**
 * Given all the table headers, find the index of the header with the label more similar to the provided labels.
 * @param {string[]} tableHeaderLabels - Labels on the table headers.
 * @param {string[]} possibleLabelsForHeader - Possible labels for the searched header (in decrescent order of priority).
 * @return {number|undefined} Index of the header or undefined, if not found.
 *
 * Example: find the 'inflow' header index
 * - tableHeaderLabels = ['date', 'Amount', 'Balance $']
 * - possibleLabelsForHeader = ['total', 'amount', 'value', '$']
 * Returns 1, since there were 2 matches ('Amount' ~ 'amount' and 'Balance $' ~ '$'), but the first had greater priority.
 */
function findHeaderIndexByLabel(tableHeaderLabels, possibleLabelsForHeader) {
  var bestMatch = undefined

  for (var headerIdx = 0; headerIdx < tableHeaderLabels.length; headerIdx++) {
    var headerLabel = tableHeaderLabels[headerIdx]

    // Index of possibleLabelsForHeader is usead as priority (smaller index = higher priority)
    for (var prio = 0; prio < possibleLabelsForHeader.length; prio++) {
      var possibleHeader = possibleLabelsForHeader[prio]

      if(containsIgnoringCase(headerLabel, possibleHeader)) {
        //match!
        if (bestMatch === undefined || prio < bestMatch.prio) {
          bestMatch = {
            prio: prio,
            index: headerIdx
          }
        }
      }
    }
  }
  if (bestMatch === undefined) {
    return undefined
  }
  return bestMatch.index
}

/**
 * Check if the column info is valid.
 * @param  {ColumnInfo} Column information.
 * @return {Boolean}
 */
function isValid (columnInfo) {
  // Required indexes
  if (!(columnInfo.dateIndex >= 0) ||
    !(columnInfo.inflowIndex >= 0)) {
    return false
  }

  // Indexes should be unique
  if(columnInfo.inflowIndex === columnInfo.dateIndex ||
    columnInfo.inflowIndex === columnInfo.payeeIndex ||
    columnInfo.dateIndex === columnInfo.payeeIndex) {
    return false
  }
  return true
}

/**
 * Check if a string contains another string ignoring case.
 * @param  {string} str - String where the search should be executed.
 * @param  {string} searched - String that should be contained.
 * @return {Boolean}
 */
function containsIgnoringCase(str, searched) {
  return str.toLowerCase().indexOf(searched.toLowerCase()) >= 0
}

// TODO move to jsturbo
function isAlternativeDateFormat(text) {
  return /^\s*\d{1,2}\s*\w{3}\s*$/i.test(text)
}

// TODO move to jsturbo
function isAlternativenumberFormat(text) {
  //TODO...
  return false
}

module.exports = {
  getColumnInfo: getColumnInfo,
  //private methods exposed for testing only
  _containsIgnoringCase: containsIgnoringCase,
  _isValid: isValid,
  _findHeaderIndexByLabel: findHeaderIndexByLabel,
  _findIndexesFromValues: findIndexesFromValues,
  _findIndexesFromHeader: findIndexesFromHeader
}
<commit_msg>Test alternative formats on column finder<commit_after>import JsTurbo from 'jsturbo'

/**
 * @typedef {ColumnInfo} Description of the columns of tabular data.
 * @property {number} dateIndex - Date index.
 * @property {number} payeeIndex - Payee index.
 * @property {number} inflowIndex - Inflow index.
 */

/**
 * Map a [header type] to an [array of possible header labels].
 */
let POSSIBLE_HEADER_LABELS = {
  dateIndex: ['datum', 'date', 'data'],
  payeeIndex: ['mottagare', 'transaktion', 'payee', 'descrição', 'histórico', 'lançamento'],
  inflowIndex: ['belopp', 'belopp sek', 'inflow', 'value', 'valor']
}

/**
 * Find information about the columns of tabular data.
 * @param  {TabularData} tabularData - Tabular data.
 * @return {ColumnInfo} Column information.
 */
function getColumnInfo(tabularData) {
  var columnInfo;
  //1: try using the table header
  if (tabularData.header) {
    columnInfo = findIndexesFromHeader(tabularData.header)
    if (isValid(columnInfo)) {
      return columnInfo
    }
  }

  // 2: try using the table values
  if (tabularData.data) {
    columnInfo = findIndexesFromValues(tabularData.data)
    if (isValid(columnInfo)) {
      return columnInfo
    }
  }
  throw Error('Could not find column information.')
}

/**
 * Find the index of each header type based on the header labels.
 * @param {string[]} tableHeaderLabels - Labels on the table headers.
 * @return {ColumnInfo} Column information.
 */
function findIndexesFromHeader(tableHeaderLabels) {
  var columnInfo = {}
  for(var headerType in POSSIBLE_HEADER_LABELS) {
    var possibleHeaderLabels = POSSIBLE_HEADER_LABELS[headerType]
    var index = findHeaderIndexByLabel(tableHeaderLabels, possibleHeaderLabels)
    if(index >= 0) {
      columnInfo[headerType] = index
    }
  }
  return columnInfo
}

/**
 * Find the index of each header type based on the table values.
 * @param {string[]} tableValues - Table values.
 * @return {ColumnInfo} Column information.
 */
function findIndexesFromValues(tableValues) {
  var row = tableValues[0]
  var columnInfo = {}

  for (var i = 0; i < row.length; i++) {
    if (JsTurbo.date.isDate(row[i]) || isAlternativeDateFormat(row[i])) {
      columnInfo.dateIndex = i
      break
    }
  }

  for (i = 0; i < row.length; i++) {
    if (i === columnInfo.dateIndex) {
      continue
    }

    if (JsTurbo.str.isNumber(row[i]) || isCurrencyNumber(row[i])) {
      columnInfo.inflowIndex = i
      break
    }
  }

  for (i = 0; i < row.length; i++) {
    if (i === columnInfo.dateIndex ||
      i === columnInfo.inflowIndex) {
      continue
    }

    // Payee: at least 3 letters
    if (JsTurbo.str.containsAlpha(row[i], 3)) {
      columnInfo.payeeIndex = i
      break
    }
  }
  return columnInfo
}

/**
 * Given all the table headers, find the index of the header with the label more similar to the provided labels.
 * @param {string[]} tableHeaderLabels - Labels on the table headers.
 * @param {string[]} possibleLabelsForHeader - Possible labels for the searched header (in decrescent order of priority).
 * @return {number|undefined} Index of the header or undefined, if not found.
 *
 * Example: find the 'inflow' header index
 * - tableHeaderLabels = ['date', 'Amount', 'Balance $']
 * - possibleLabelsForHeader = ['total', 'amount', 'value', '$']
 * Returns 1, since there were 2 matches ('Amount' ~ 'amount' and 'Balance $' ~ '$'), but the first had greater priority.
 */
function findHeaderIndexByLabel(tableHeaderLabels, possibleLabelsForHeader) {
  var bestMatch = undefined

  for (var headerIdx = 0; headerIdx < tableHeaderLabels.length; headerIdx++) {
    var headerLabel = tableHeaderLabels[headerIdx]

    // Index of possibleLabelsForHeader is usead as priority (smaller index = higher priority)
    for (var prio = 0; prio < possibleLabelsForHeader.length; prio++) {
      var possibleHeader = possibleLabelsForHeader[prio]

      if(containsIgnoringCase(headerLabel, possibleHeader)) {
        //match!
        if (bestMatch === undefined || prio < bestMatch.prio) {
          bestMatch = {
            prio: prio,
            index: headerIdx
          }
        }
      }
    }
  }
  if (bestMatch === undefined) {
    return undefined
  }
  return bestMatch.index
}

/**
 * Check if the column info is valid.
 * @param  {ColumnInfo} Column information.
 * @return {Boolean}
 */
function isValid (columnInfo) {
  // Required indexes
  if (!(columnInfo.dateIndex >= 0) ||
    !(columnInfo.inflowIndex >= 0)) {
    return false
  }

  // Indexes should be unique
  if(columnInfo.inflowIndex === columnInfo.dateIndex ||
    columnInfo.inflowIndex === columnInfo.payeeIndex ||
    columnInfo.dateIndex === columnInfo.payeeIndex) {
    return false
  }
  return true
}

/**
 * Check if a string contains another string ignoring case.
 * @param  {string} str - String where the search should be executed.
 * @param  {string} searched - String that should be contained.
 * @return {Boolean}
 */
function containsIgnoringCase(str, searched) {
  return str.toLowerCase().indexOf(searched.toLowerCase()) >= 0
}

// TODO move to jsturbo
function isAlternativeDateFormat(text) {
  // [space][DD][space][mmm][space]
  return /^\s*\d{1,2}\s*\w{3}\s*$/i.test(text)
}

// TODO move to jsturbo
function isCurrencyNumber(text) {
  // [space] [curency] [space] [digits] ([space_or_separator] [digits]) [space] [curency] [space]
  return /^\s?\w{0,3}\s?\d+([\s\.,]?\d*)*\s?\w{0,3}\s?$/gi.test(text)
}

module.exports = {
  getColumnInfo: getColumnInfo,
  //private methods exposed for testing only
  _containsIgnoringCase: containsIgnoringCase,
  _isValid: isValid,
  _findHeaderIndexByLabel: findHeaderIndexByLabel,
  _findIndexesFromValues: findIndexesFromValues,
  _findIndexesFromHeader: findIndexesFromHeader
}
<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter;
var _ = require('underscore');
var path = require('path');
var fs = require('fs');
var Promise = require('bluebird');
var machine = require('./DockerMachine');
var virtualBox = require('./VirtualBox');
var setupUtil = require('./SetupUtil');
var util = require('./Util');
var assign = require('object-assign');
var metrics = require('./Metrics');

var _currentStep = null;
var _error = null;
var _cancelled = false;
var _retryPromise = null;
var _requiredSteps = [];

var _steps = [{
  name: 'download',
  title: 'Downloading VirtualBox',
  message: 'VirtualBox is being downloaded. Kitematic requires VirtualBox to run containers.',
  totalPercent: 35,
  percent: 0,
  run: function (progressCallback) {
    var packagejson = util.packagejson();
    return setupUtil.download(setupUtil.virtualBoxUrl(), path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
      progressCallback(percent);
    });
  }
}, {
  name: 'install',
  title: 'Installing VirtualBox & Docker',
  message: 'VirtualBox & Docker are being installed or upgraded in the background. We may need you to type in your password to continue.',
  totalPercent: 5,
  percent: 0,
  seconds: 5,
  run: Promise.coroutine(function* (progressCallback) {
    var cmd = setupUtil.copyBinariesCmd() + ' && ' + setupUtil.fixBinariesCmd();
    if (!virtualBox.installed()) {
      yield virtualBox.killall();
      cmd += ' && ' + setupUtil.installVirtualBoxCmd();
    } else {
      if (!setupUtil.needsBinaryFix()) {
        return;
      }
    }
    try {
      progressCallback(50); // TODO: detect when the installation has started so we can simulate progress
      yield util.exec(setupUtil.macSudoCmd(cmd));
    } catch (err) {
      throw null;
    }
  })
}, {
  name: 'init',
  title: 'Starting Docker VM',
  message: 'To run Docker containers on your computer, Kitematic is starting a Linux virutal machine. This may take a minute...',
  totalPercent: 60,
  percent: 0,
  seconds: 52,
  run: Promise.coroutine(function* (progressCallback) {
    setupUtil.simulateProgress(this.seconds, progressCallback);
    yield virtualBox.vmdestroy('kitematic-vm');
    var exists = yield machine.exists();
    if (!exists) {
      yield machine.create();
      return;
    } else if ((yield machine.state()) === 'Error') {
      try {
        yield machine.rm();
      } catch (err) {}
      yield machine.create();
      return;
    }

    var isoversion = machine.isoversion();
    var packagejson = util.packagejson();
    if (!isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) {
      console.log('upgrading');
      yield machine.stop();
      yield machine.upgrade();
    }
    yield machine.start();
  })
}];

var SetupStore = assign(Object.create(EventEmitter.prototype), {
  PROGRESS_EVENT: 'setup_progress',
  STEP_EVENT: 'setup_step',
  ERROR_EVENT: 'setup_error',
  step: function () {
    return _currentStep;
  },
  steps: function () {
    return _.indexBy(_steps, 'name');
  },
  stepCount: function () {
    return _requiredSteps.length;
  },
  number: function () {
    return _.indexOf(_requiredSteps, _currentStep) + 1;
  },
  percent: function () {
    var sofar = 0;
    var totalPercent = _requiredSteps.reduce((prev, step) => prev + step.totalPercent, 0);
    _.each(_requiredSteps, step => {
      sofar += step.totalPercent * step.percent / 100;
    });
    return Math.min(Math.round(100 * sofar / totalPercent), 99);
  },
  error: function () {
    return _error;
  },
  cancelled: function () {
    return _cancelled;
  },
  retry: function () {
    _error = null;
    _cancelled = false;
    _retryPromise.resolve();
  },
  wait: function () {
    _retryPromise = Promise.defer();
    return _retryPromise.promise;
  },
  requiredSteps: Promise.coroutine(function* () {
    if (_requiredSteps.length) {
      return Promise.resolve(_requiredSteps);
    }
    var packagejson = util.packagejson();
    var isoversion = machine.isoversion();
    var required = {};
    var vboxfile = path.join(util.supportDir(), packagejson['virtualbox-filename']);
    required.download = !virtualBox.installed() && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== packagejson['virtualbox-checksum']);
    required.install = !virtualBox.installed() || setupUtil.needsBinaryFix();
    required.init = !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0;

    var exists = yield machine.exists();
    if (isoversion && setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) {
      this.steps().init.seconds = 33;
    } else if (exists && (yield machine.state()) !== 'Error') {
      this.steps().init.seconds = 13;
    }

    _requiredSteps = _steps.filter(function (step) {
      return required[step.name];
    });
    return Promise.resolve(_requiredSteps);
  }),
  updateBinaries: function () {
    if (setupUtil.needsBinaryFix()) {
      return Promise.resolve();
    }
    if (setupUtil.shouldUpdateBinaries()) {
      return util.exec(setupUtil.copyBinariesCmd());
    }
    return Promise.resolve();
  },
  run: Promise.coroutine(function* () {
    metrics.track('Started Setup');
    yield this.updateBinaries();
    var steps = yield this.requiredSteps();
    for (let step of steps) {
      console.log(step.name);
      _currentStep = step;
      step.percent = 0;
      while (true) {
        try {
          this.emit(this.STEP_EVENT);
          yield step.run(percent => {
            if (_currentStep) {
              step.percent = percent;
              this.emit(this.PROGRESS_EVENT);
            }
          });
          metrics.track('Completed Step', {
            name: step.name
          });
          step.percent = 100;
          break;
        } catch (err) {
          metrics.track('Setup Failed', {
            step: step.name
          });
          if (err) {
            console.log('Setup encountered an error.');
            console.log(err);
            console.log(err.stack);
            _error = err;
            this.emit(this.ERROR_EVENT);
          } else {
            _cancelled = true;
            this.emit(this.STEP_EVENT);
          }
          yield this.wait();
        }
      }
    }
    metrics.track('Finished Setup');
    _currentStep = null;
  })
});

module.exports = SetupStore;
<commit_msg>Fix setup failed event<commit_after>var EventEmitter = require('events').EventEmitter;
var _ = require('underscore');
var path = require('path');
var fs = require('fs');
var Promise = require('bluebird');
var machine = require('./DockerMachine');
var virtualBox = require('./VirtualBox');
var setupUtil = require('./SetupUtil');
var util = require('./Util');
var assign = require('object-assign');
var metrics = require('./Metrics');
var bugsnag = require('bugsnag-js');

var _currentStep = null;
var _error = null;
var _cancelled = false;
var _retryPromise = null;
var _requiredSteps = [];

var _steps = [{
  name: 'download',
  title: 'Downloading VirtualBox',
  message: 'VirtualBox is being downloaded. Kitematic requires VirtualBox to run containers.',
  totalPercent: 35,
  percent: 0,
  run: function (progressCallback) {
    var packagejson = util.packagejson();
    return setupUtil.download(setupUtil.virtualBoxUrl(), path.join(util.supportDir(), packagejson['virtualbox-filename']), packagejson['virtualbox-checksum'], percent => {
      progressCallback(percent);
    });
  }
}, {
  name: 'install',
  title: 'Installing VirtualBox & Docker',
  message: 'VirtualBox & Docker are being installed or upgraded in the background. We may need you to type in your password to continue.',
  totalPercent: 5,
  percent: 0,
  seconds: 5,
  run: Promise.coroutine(function* (progressCallback) {
    var cmd = setupUtil.copyBinariesCmd() + ' && ' + setupUtil.fixBinariesCmd();
    if (!virtualBox.installed()) {
      yield virtualBox.killall();
      cmd += ' && ' + setupUtil.installVirtualBoxCmd();
    } else {
      if (!setupUtil.needsBinaryFix()) {
        return;
      }
    }
    try {
      progressCallback(50); // TODO: detect when the installation has started so we can simulate progress
      yield util.exec(setupUtil.macSudoCmd(cmd));
    } catch (err) {
      throw null;
    }
  })
}, {
  name: 'init',
  title: 'Starting Docker VM',
  message: 'To run Docker containers on your computer, Kitematic is starting a Linux virutal machine. This may take a minute...',
  totalPercent: 60,
  percent: 0,
  seconds: 52,
  run: Promise.coroutine(function* (progressCallback) {
    setupUtil.simulateProgress(this.seconds, progressCallback);
    yield virtualBox.vmdestroy('kitematic-vm');
    var exists = yield machine.exists();
    if (!exists) {
      yield machine.create();
      return;
    } else if ((yield machine.state()) === 'Error') {
      try {
        yield machine.rm();
      } catch (err) {}
      yield machine.create();
      return;
    }

    var isoversion = machine.isoversion();
    var packagejson = util.packagejson();
    if (!isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) {
      console.log('upgrading');
      yield machine.stop();
      yield machine.upgrade();
    }
    yield machine.start();
  })
}];

var SetupStore = assign(Object.create(EventEmitter.prototype), {
  PROGRESS_EVENT: 'setup_progress',
  STEP_EVENT: 'setup_step',
  ERROR_EVENT: 'setup_error',
  step: function () {
    return _currentStep;
  },
  steps: function () {
    return _.indexBy(_steps, 'name');
  },
  stepCount: function () {
    return _requiredSteps.length;
  },
  number: function () {
    return _.indexOf(_requiredSteps, _currentStep) + 1;
  },
  percent: function () {
    var sofar = 0;
    var totalPercent = _requiredSteps.reduce((prev, step) => prev + step.totalPercent, 0);
    _.each(_requiredSteps, step => {
      sofar += step.totalPercent * step.percent / 100;
    });
    return Math.min(Math.round(100 * sofar / totalPercent), 99);
  },
  error: function () {
    return _error;
  },
  cancelled: function () {
    return _cancelled;
  },
  retry: function () {
    _error = null;
    _cancelled = false;
    _retryPromise.resolve();
  },
  wait: function () {
    _retryPromise = Promise.defer();
    return _retryPromise.promise;
  },
  requiredSteps: Promise.coroutine(function* () {
    if (_requiredSteps.length) {
      return Promise.resolve(_requiredSteps);
    }
    var packagejson = util.packagejson();
    var isoversion = machine.isoversion();
    var required = {};
    var vboxfile = path.join(util.supportDir(), packagejson['virtualbox-filename']);
    required.download = !virtualBox.installed() && (!fs.existsSync(vboxfile) || setupUtil.checksum(vboxfile) !== packagejson['virtualbox-checksum']);
    required.install = !virtualBox.installed() || setupUtil.needsBinaryFix();
    required.init = !(yield machine.exists()) || (yield machine.state()) !== 'Running' || !isoversion || setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0;

    var exists = yield machine.exists();
    if (isoversion && setupUtil.compareVersions(isoversion, packagejson['docker-version']) < 0) {
      this.steps().init.seconds = 33;
    } else if (exists && (yield machine.state()) !== 'Error') {
      this.steps().init.seconds = 13;
    }

    _requiredSteps = _steps.filter(function (step) {
      return required[step.name];
    });
    return Promise.resolve(_requiredSteps);
  }),
  updateBinaries: function () {
    if (setupUtil.needsBinaryFix()) {
      return Promise.resolve();
    }
    if (setupUtil.shouldUpdateBinaries()) {
      return util.exec(setupUtil.copyBinariesCmd());
    }
    return Promise.resolve();
  },
  run: Promise.coroutine(function* () {
    metrics.track('Started Setup');
    yield this.updateBinaries();
    var steps = yield this.requiredSteps();
    for (let step of steps) {
      console.log(step.name);
      _currentStep = step;
      step.percent = 0;
      while (true) {
        try {
          this.emit(this.STEP_EVENT);
          yield step.run(percent => {
            if (_currentStep) {
              step.percent = percent;
              this.emit(this.PROGRESS_EVENT);
            }
          });
          metrics.track('Completed Step', {
            name: step.name
          });
          step.percent = 100;
          break;
        } catch (err) {
          if (err) {
            console.log('Setup encountered an error.');
            console.log(err);
            console.log(err.stack);
            metrics.track('Setup Failed', {
              step: step.name
            });
            bugsnag.notify(err);
            _error = err;
            this.emit(this.ERROR_EVENT);
          } else {
            _cancelled = true;
            this.emit(this.STEP_EVENT);
          }
          yield this.wait();
        }
      }
    }
    metrics.track('Finished Setup');
    _currentStep = null;
  })
});

module.exports = SetupStore;
<|endoftext|>


<commit_before>Blend.defineClass('Blend.layout.container.Layout', {
    extend: 'Blend.layout.Layout',
    cssPrefix: null,
    containerEl: null,
    init: function () {
        var me = this;
        me.callParent.apply(me, arguments);
        me._itemCSS = Blend.cssPrefix(me.cssPrefix + '-layout-item');
    },
    setContainerElement: function (el) {
        var me = this;
        me.containerEl = el;
        Blend.CSS.set(me.containerEl, Blend.cssPrefix(me.cssPrefix + '-layout'));
    },
    getElements: function () {
        var me = this, view,
                elements = [], vitems = [], defaults = me.view.defaults, el;
        if (!me._rendered) {
            Blend.foreach(me.view.items, function (itemCfg, idx) {
                view = me.createItemView(itemCfg, defaults);
                me.createItemLayoutContext(view);
                vitems.push(view);
                el = view.getElement({
                    cls: me.getItemCSS()
                });
                me.containerEl.appendChild(el);
                elements.push(el);
            });
            ;
            me.view.items = vitems;
            me._elements = elements;
            me._rendered = true;
        }
        return me._elements;
    },
    getItemCSS: function (view) {
        var me = this;
        return [Blend.cssPrefix('conatiner-item'), me._itemCSS];
    },
    createItemView: function (viewCfg, defaults) {
        var me = this;
        return Blend.ui.Component.createView.apply(me.view, [viewCfg, me.view, defaults]);
    },
    createItemLayoutContext: function (view) {
        view.setLayoutContext({});
    }
});<commit_msg>Added queueing and de dequeueing utility functions<commit_after>Blend.defineClass('Blend.layout.container.Layout', {
    extend: 'Blend.layout.Layout',
    cssPrefix: null,
    containerEl: null,
    init: function () {
        var me = this;
        me.callParent.apply(me, arguments);
        me._itemCSS = Blend.cssPrefix(me.cssPrefix + '-layout-item');
    },
    setContainerElement: function (el) {
        var me = this;
        me.containerEl = el;
        Blend.CSS.set(me.containerEl, Blend.cssPrefix(me.cssPrefix + '-layout'));
    },
    /**
     * @internal
     */
    queueElements: function () {
        var me = this;
        if (!me._queue) {
            me._queue = Blend.createDocumentFragment();
            Blend.foreach(me.view.items, function (el) {
                me._queue.appendChild(el);
            });
        }
    },
    /**
     * @internal
     */
    deQueueElements: function (parent) {
        var me = this;
        if (me._queue) {
            Blend.foreach(me._queue, function (el) {
                parent.appendChild(el);
            });
            me._queue = null;
        }
    },
    getElements: function () {
        var me = this, view,
                elements = [], vitems = [], defaults = me.view.defaults, el;
        if (!me._rendered) {
            Blend.foreach(me.view.items, function (itemCfg, idx) {
                view = me.createItemView(itemCfg, defaults);
                me.createItemLayoutContext(view);
                vitems.push(view);
                el = view.getElement({
                    cls: me.getItemCSS()
                });
                me.containerEl.appendChild(el);
                elements.push(el);
            });
            ;
            me.view.items = vitems;
            me._elements = elements;
            me._rendered = true;
        }
        return me._elements;
    },
    getItemCSS: function (view) {
        var me = this;
        return [Blend.cssPrefix('conatiner-item'), me._itemCSS];
    },
    createItemView: function (viewCfg, defaults) {
        var me = this;
        return Blend.ui.Component.createView.apply(me.view, [viewCfg, me.view, defaults]);
    },
    createItemLayoutContext: function (view) {
        view.setLayoutContext({});
    }
});<|endoftext|>


<commit_before>(function () {
	var _options = JSON.parse(document.querySelector('body').dataset.options);
	var _oidToken, _uploadUrl, _contentsCss;

	window.addEventListener('load', replaceEditor);
	document.addEventListener('voe.openLightBox', replaceEditor);
	document.addEventListener('voe.refresh', replaceEditor);

	function replaceEditor() {
		if (!window.tinyMCE || !tinyMCE.editors.length) return;

		_oidToken = tinyMCE.settings.oidToken;
		_uploadUrl = tinyMCE.settings.postUrl;
		_contentsCss = tinyMCE.settings.content_css;

		if (window.CKEDITOR) {
			replace();
			return;
		}

		window.CKEDITOR_BASEPATH = _options.baseUrl + 'ckeditor4/';
		injectScript(_options.baseUrl + 'ckeditor4/ckeditor.js', function() {
			CKEDITOR.disableAutoInline = true;

			var interval = setInterval(function() {
				if (CKEDITOR.status === 'loaded') {
					clearInterval(interval);
					replace();
				}
			}, 100);
		});
	}

	function replace() {
		for (var i = 0, l = tinyMCE.editors.length; i < l; i++) {
			var textarea = tinyMCE.editors[0].getElement();
			var textareaContainer = textarea.parentNode;

			tinyMCE.editors[0].destroy();

			if (textareaContainer.classList.contains('inline-edit-content')) {
				textarea.setAttribute('contenteditable', 'false');
				textarea.addEventListener('focus', function() {
					textarea.setAttribute('contenteditable', 'true');

					initCKEditor(textarea, {
						blur: function (e) {
							var id = this.element.$.id;

							$('.main-panel-scroller').off('scroll');

							textarea.setAttribute('contenteditable', 'false');
							e.editor.destroy();

							V1.Topics.Publish('RichText/Blur', {
								target: {id: id}
							});
						},
						key: function (event) {
							V1.Topics.Publish('RichText/Keyup', event.data.domEvent.$);
						},
						instanceReady: function (e) {
							var $editor = $(e.editor.element.$);
							var $panel = $(document.getElementById('cke_' + $editor.attr('id')));

							$panel.css('maxWidth', $editor.outerWidth() + 'px');

							var $scroller = $('.main-panel-scroller');
							var panelHeight = $panel.outerHeight();
							var editorHeight = $editor.outerHeight();
							var scrollerOffset = $scroller.offset().top;

							$panel.offset({
								top: $editor.offset().top - $panel.outerHeight(),
								left: $editor.offset().left
							})

							e.editor.on('focus', function () {
								setPanelPosition();
								$scroller.on('scroll', function () {
									// setPanelPosition();
									debouncedSetPanelPosition();
								});
							});

							function debounce(f, ms) {

								var timer = null;

								return function (...args) {
									const onComplete = () => {
										f.apply(this, args);
										timer = null;
									}

									if (timer) {
										clearTimeout(timer);
									}

									timer = setTimeout(onComplete, ms);
								};
							}

							var debouncedSetPanelPosition = debounce(setPanelPosition, 50);

							function setPanelPosition() {
								var editorOffset = $editor.offset().top;
								var editorOffsetBottom = editorOffset + editorHeight;
								var panelOffset = editorOffset - panelHeight;

								if (panelOffset <= scrollerOffset && editorOffsetBottom >= scrollerOffset) {
									$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 1}, 150);
								} else if (editorOffsetBottom < scrollerOffset) {
									$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 0}, 150);
								} else {
									$panel.animate({top: panelOffset, left: $editor.offset().left, opacity: 1}, 150);
								}
							}

							e.editor.on('simpleuploads.startUpload', function (ev) {
								ev.data.extraFields = {
									oidToken: oidToken
								};
							});

							e.editor.on('simpleuploads.serverResponse', function (ev) {
								ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
							});
						}
					});
				});
			} else {
				initCKEditor(textarea, {
					change: function () {
						this.updateElement();
						V1.Html.Event.Fire(this.element.$, "change");
					},
					instanceReady: function (e) {
						e.editor.on('simpleuploads.startUpload', function (ev) {
							ev.data.extraFields = {
								oidToken: _oidToken
							};
						});

						window.editor = e.editor;

						e.editor.on('simpleuploads.serverResponse', function (ev) {
							ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
						});

						e.editor.on('simpleuploads.finishedUpload', function () {
							this.updateElement();
							V1.Html.Event.Fire(this.element.$, "change");
						});
					}
				});
			}
		}
	}

	function initCKEditor(element, on, isInline) {
		var config = {
			height: _options.minHeightTinyMCE,
			autoGrow_minHeight: _options.minHeightTinyMCE,
			autoGrow_maxHeight: _options.maxHeightTinyMCE,
			filebrowserUploadUrl: _uploadUrl,
			filebrowserImageUploadUrl: _uploadUrl,
			coreStyles_strike: {
				element: 'span',
				attributes: {'style': 'text-decoration: line-through;'},
				overrides: 'strike'
			},
			stylesSet: [
				{name: 'Red text', element: 'span', styles: {'color': 'red'}},
				{name: 'Blue text', element: 'span', styles: {'color': 'blue'}},
				{name: 'Green text', element: 'span', styles: {'color': 'green'}},
				{name: 'Yellow highlight', element: 'span', styles: {'background-color': 'yellow'}},
				{name: 'Light green highlight', element: 'span', styles: {'background-color': 'lightgreen'}},
				{name: 'Light blue highlight', element: 'span', styles: {'background-color': 'lightblue'}}
			],
			extraPlugins: 'simpleuploads,justify,autogrow,keystrokes,notification',
			simpleuploads_inputname: 'image',
			toolbarGroups: [
				{name: 'clipboard', groups: ['clipboard', 'undo']},
				{name: 'basicstyles', groups: ['basicstyles', 'cleanup']},
				{name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'bidi', 'paragraph']},
				{name: 'editing', groups: ['find', 'selection', 'spellchecker', 'editing']},
				{name: 'links', groups: ['links']},
				{name: 'insert', groups: ['insert', 'addImage']},
				{name: 'forms', groups: ['forms']},
				{name: 'document', groups: ['mode', 'document', 'doctools']},
				{name: 'others', groups: ['others']},
				{name: 'styles', groups: ['styles']},
				{name: 'colors', groups: ['colors']},
				{name: 'about', groups: ['about']},
				{name: 'tools', groups: ['tools']}
			],
			removeButtons: 'Subscript,Superscript,Image,Source,Blockquote,About,addFile',
			on: on || {}
		};

		if (isInline) {
			CKEDITOR.inline(element, config);
		} else {
			config.contentsCss = [_options.baseUrl + 'proximaNova.css', _options.baseUrl + 'typography.css'];
			CKEDITOR.replace(element, config);
		}
	}
})();
<commit_msg>small fix<commit_after>(function () {
	var _options = JSON.parse(document.querySelector('body').dataset.options);
	var _oidToken, _uploadUrl, _contentsCss;

	window.addEventListener('load', replaceEditor);
	document.addEventListener('voe.openLightBox', replaceEditor);
	document.addEventListener('voe.refresh', replaceEditor);

	function replaceEditor() {
		if (!window.tinyMCE || !tinyMCE.editors.length) return;

		_oidToken = tinyMCE.settings.oidToken;
		_uploadUrl = tinyMCE.settings.postUrl;
		_contentsCss = tinyMCE.settings.content_css;

		if (window.CKEDITOR) {
			replace();
			return;
		}

		window.CKEDITOR_BASEPATH = _options.baseUrl + 'ckeditor4/';
		injectScript(_options.baseUrl + 'ckeditor4/ckeditor.js', function() {
			CKEDITOR.disableAutoInline = true;

			var interval = setInterval(function() {
				if (CKEDITOR.status === 'loaded') {
					clearInterval(interval);
					replace();
				}
			}, 100);
		});
	}

	function replace() {
		for (var i = 0, l = tinyMCE.editors.length; i < l; i++) {
			var textarea = tinyMCE.editors[0].getElement();
			var textareaContainer = textarea.parentNode;

			tinyMCE.editors[0].destroy();

			if (textareaContainer.classList.contains('inline-edit-content')) {
				textarea.setAttribute('contenteditable', 'false');
				textarea.addEventListener('focus', function() {
					textarea.setAttribute('contenteditable', 'true');

					initCKEditor(textarea, {
						blur: function (e) {
							var id = this.element.$.id;

							$('.main-panel-scroller').off('scroll');

							textarea.setAttribute('contenteditable', 'false');
							e.editor.destroy();

							V1.Topics.Publish('RichText/Blur', {
								target: {id: id}
							});
						},
						key: function (event) {
							V1.Topics.Publish('RichText/Keyup', event.data.domEvent.$);
						},
						instanceReady: function (e) {
							var $editor = $(e.editor.element.$);
							var $panel = $(document.getElementById('cke_' + $editor.attr('id')));

							$panel.css('maxWidth', $editor.outerWidth() + 'px');

							var $scroller = $('.main-panel-scroller');
							var panelHeight = $panel.outerHeight();
							var editorHeight = $editor.outerHeight();
							var scrollerOffset = $scroller.offset().top;

							$panel.offset({
								top: $editor.offset().top - $panel.outerHeight(),
								left: $editor.offset().left
							});

							e.editor.on('focus', function () {
								setPanelPosition();
								$scroller.on('scroll', function () {
									// setPanelPosition();
									debouncedSetPanelPosition();
								});
							});

							function debounce(f, ms) {

								var timer = null;

								return function (...args) {
									const onComplete = () => {
										f.apply(this, args);
										timer = null;
									}

									if (timer) {
										clearTimeout(timer);
									}

									timer = setTimeout(onComplete, ms);
								};
							}

							var debouncedSetPanelPosition = debounce(setPanelPosition, 50);

							function setPanelPosition() {
								var editorOffset = $editor.offset().top;
								var editorOffsetBottom = editorOffset + editorHeight;
								var panelOffset = editorOffset - panelHeight;

								if (panelOffset <= scrollerOffset && editorOffsetBottom >= scrollerOffset) {
									$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 1}, 150);
								} else if (editorOffsetBottom < scrollerOffset) {
									$panel.animate({top: scrollerOffset, left: $editor.offset().left, opacity: 0}, 150);
								} else {
									$panel.animate({top: panelOffset, left: $editor.offset().left, opacity: 1}, 150);
								}
							}

							e.editor.on('simpleuploads.startUpload', function (ev) {
								ev.data.extraFields = {
									oidToken: oidToken
								};
							});

							e.editor.on('simpleuploads.serverResponse', function (ev) {
								ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
							});
						}
					}, true);
				});
			} else {
				initCKEditor(textarea, {
					change: function () {
						this.updateElement();
						V1.Html.Event.Fire(this.element.$, "change");
					},
					instanceReady: function (e) {
						e.editor.on('simpleuploads.startUpload', function (ev) {
							ev.data.extraFields = {
								oidToken: _oidToken
							};
						});

						window.editor = e.editor;

						e.editor.on('simpleuploads.serverResponse', function (ev) {
							ev.data.url = ev.data.xhr.responseText.match(/"Url":"([^"]*)/i)[1];
						});

						e.editor.on('simpleuploads.finishedUpload', function () {
							this.updateElement();
							V1.Html.Event.Fire(this.element.$, "change");
						});
					}
				});
			}
		}
	}

	function initCKEditor(element, on, isInline) {
		var config = {
			height: _options.minHeightTinyMCE,
			autoGrow_minHeight: _options.minHeightTinyMCE,
			autoGrow_maxHeight: _options.maxHeightTinyMCE,
			filebrowserUploadUrl: _uploadUrl,
			filebrowserImageUploadUrl: _uploadUrl,
			coreStyles_strike: {
				element: 'span',
				attributes: {'style': 'text-decoration: line-through;'},
				overrides: 'strike'
			},
			stylesSet: [
				{name: 'Red text', element: 'span', styles: {'color': 'red'}},
				{name: 'Blue text', element: 'span', styles: {'color': 'blue'}},
				{name: 'Green text', element: 'span', styles: {'color': 'green'}},
				{name: 'Yellow highlight', element: 'span', styles: {'background-color': 'yellow'}},
				{name: 'Light green highlight', element: 'span', styles: {'background-color': 'lightgreen'}},
				{name: 'Light blue highlight', element: 'span', styles: {'background-color': 'lightblue'}}
			],
			extraPlugins: 'simpleuploads,justify,autogrow,keystrokes,notification',
			simpleuploads_inputname: 'image',
			toolbarGroups: [
				{name: 'clipboard', groups: ['clipboard', 'undo']},
				{name: 'basicstyles', groups: ['basicstyles', 'cleanup']},
				{name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'bidi', 'paragraph']},
				{name: 'editing', groups: ['find', 'selection', 'spellchecker', 'editing']},
				{name: 'links', groups: ['links']},
				{name: 'insert', groups: ['insert', 'addImage']},
				{name: 'forms', groups: ['forms']},
				{name: 'document', groups: ['mode', 'document', 'doctools']},
				{name: 'others', groups: ['others']},
				{name: 'styles', groups: ['styles']},
				{name: 'colors', groups: ['colors']},
				{name: 'about', groups: ['about']},
				{name: 'tools', groups: ['tools']}
			],
			removeButtons: 'Subscript,Superscript,Image,Source,Blockquote,About,addFile',
			on: on || {}
		};

		if (isInline) {
			CKEDITOR.inline(element, config);
		} else {
			config.contentsCss = [_options.baseUrl + 'proximaNova.css', _options.baseUrl + 'typography.css'];
			CKEDITOR.replace(element, config);
		}
	}
})();
<|endoftext|>


<commit_before>/* jshint ignore:start */
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
var Templates = ComponentRegistry.Templates;
// This file was automatically generated from Slider.soy.
// Please don't edit this file by hand.

/**
 * @fileoverview Templates in namespace Templates.Slider.
 * @hassoydeltemplate {Slider}
 * @hassoydeltemplate {Slider.input}
 * @hassoydeltemplate {Slider.label}
 * @hassoydeltemplate {Slider.rail}
 * @hassoydelcall {Slider}
 * @hassoydelcall {Slider.input}
 * @hassoydelcall {Slider.label}
 * @hassoydelcall {Slider.rail}
 */

if (typeof Templates.Slider == 'undefined') { Templates.Slider = {}; }

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.content = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
' + Templates.Slider.input(opt_data, null, opt_ijData) + Templates.Slider.label(opt_data, null, opt_ijData) + Templates.Slider.rail(opt_data, null, opt_ijData) + '
');
};
if (goog.DEBUG) {
  Templates.Slider.content.soyTemplateName = 'Templates.Slider.content';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.input = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
<input name="' + soy.$$escapeHtmlAttribute(opt_data.inputName ? opt_data.inputName : opt_data.id) + '" type="hidden" value="' + soy.$$escapeHtmlAttribute(opt_data.value) + '">
');
};
if (goog.DEBUG) {
  Templates.Slider.input.soyTemplateName = 'Templates.Slider.input';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.label = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
' + soy.$$escapeHtml(opt_data.value) + '
');
};
if (goog.DEBUG) {
  Templates.Slider.label.soyTemplateName = 'Templates.Slider.label';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.rail = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('

');
};
if (goog.DEBUG) {
  Templates.Slider.rail.soyTemplateName = 'Templates.Slider.rail';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s30_9ba724a7 = function(opt_data, opt_ignored, opt_ijData) {
  opt_data = opt_data || {};
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(soy.$$getDelegateFn(soy.$$getDelTemplateId('Slider'), 'element', true)({elementClasses: opt_data.elementClasses, elementContent: soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks('' + Templates.Slider.content(opt_data, null, opt_ijData)), id: opt_data.id}, null, opt_ijData));
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s30_9ba724a7.soyTemplateName = 'Templates.Slider.__deltemplate_s30_9ba724a7';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider'), '', 0, Templates.Slider.__deltemplate_s30_9ba724a7);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s36_d5c3b9a6 = function(opt_data, opt_ignored, opt_ijData) {
  opt_data = opt_data || {};
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
' + soy.$$escapeHtml(opt_data.elementContent) + '
');
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s36_d5c3b9a6.soyTemplateName = 'Templates.Slider.__deltemplate_s36_d5c3b9a6';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider'), 'element', 0, Templates.Slider.__deltemplate_s36_d5c3b9a6);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s44_7f0f645b = function(opt_data, opt_ignored, opt_ijData) {
  var output = '';
  var elementId__soy45 = (opt_data.id ? opt_data.id : '') + '-' + (opt_data.surfaceId != null ? opt_data.surfaceId : 'input');
  output += '
' + soy.$$escapeHtml(opt_data.elementContent) + '
';
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(output);
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s44_7f0f645b.soyTemplateName = 'Templates.Slider.__deltemplate_s44_7f0f645b';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.input'), 'element', 0, Templates.Slider.__deltemplate_s44_7f0f645b);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s51_cdcb45ca = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(soy.$$getDelegateFn(soy.$$getDelTemplateId('Slider.input'), 'element', true)({elementContent: soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks('' + Templates.Slider.input(opt_data, null, opt_ijData)), id: opt_data.id, surfaceId: opt_data.surfaceId}, null, opt_ijData));
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s51_cdcb45ca.soyTemplateName = 'Templates.Slider.__deltemplate_s51_cdcb45ca';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.input'), '', 0, Templates.Slider.__deltemplate_s51_cdcb45ca);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s57_103f6c5b = function(opt_data, opt_ignored, opt_ijData) {
  var output = '';
  var elementId__soy58 = (opt_data.id ? opt_data.id : '') + '-' + (opt_data.surfaceId != null ? opt_data.surfaceId : 'label');
  output += '
' + soy.$$escapeHtml(opt_data.elementContent) + '
';
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(output);
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s57_103f6c5b.soyTemplateName = 'Templates.Slider.__deltemplate_s57_103f6c5b';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.label'), 'element', 0, Templates.Slider.__deltemplate_s57_103f6c5b);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s64_ec914241 = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(soy.$$getDelegateFn(soy.$$getDelTemplateId('Slider.label'), 'element', true)({elementContent: soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks('' + Templates.Slider.label(opt_data, null, opt_ijData)), id: opt_data.id, surfaceId: opt_data.surfaceId}, null, opt_ijData));
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s64_ec914241.soyTemplateName = 'Templates.Slider.__deltemplate_s64_ec914241';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.label'), '', 0, Templates.Slider.__deltemplate_s64_ec914241);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s70_b374ec5e = function(opt_data, opt_ignored, opt_ijData) {
  var output = '';
  var elementId__soy71 = (opt_data.id ? opt_data.id : '') + '-' + (opt_data.surfaceId != null ? opt_data.surfaceId : 'rail');
  output += '
' + soy.$$escapeHtml(opt_data.elementContent) + '
';
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(output);
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s70_b374ec5e.soyTemplateName = 'Templates.Slider.__deltemplate_s70_b374ec5e';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.rail'), 'element', 0, Templates.Slider.__deltemplate_s70_b374ec5e);

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.__deltemplate_s77_970c9793 = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml(soy.$$getDelegateFn(soy.$$getDelTemplateId('Slider.rail'), 'element', true)({elementContent: soydata.VERY_UNSAFE.$$ordainSanitizedHtmlForInternalBlocks('' + Templates.Slider.rail(opt_data, null, opt_ijData)), id: opt_data.id, surfaceId: opt_data.surfaceId}, null, opt_ijData));
};
if (goog.DEBUG) {
  Templates.Slider.__deltemplate_s77_970c9793.soyTemplateName = 'Templates.Slider.__deltemplate_s77_970c9793';
}
soy.$$registerDelegateFn(soy.$$getDelTemplateId('Slider.rail'), '', 0, Templates.Slider.__deltemplate_s77_970c9793);

Templates.Slider.content.params = ["id"];
Templates.Slider.input.params = ["id","inputName","value"];
Templates.Slider.label.params = ["id","value"];
Templates.Slider.rail.params = ["id"];
export default Templates.Slider;
/* jshint ignore:end */
<commit_msg>Updates compiled soy file<commit_after>/* jshint ignore:start */
import ComponentRegistry from 'bower:metal/src/component/ComponentRegistry';
var Templates = ComponentRegistry.Templates;
// This file was automatically generated from Slider.soy.
// Please don't edit this file by hand.

/**
 * @fileoverview Templates in namespace Templates.Slider.
 */

if (typeof Templates.Slider == 'undefined') { Templates.Slider = {}; }

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.content = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
' + Templates.Slider.input(opt_data, null, opt_ijData) + Templates.Slider.label(opt_data, null, opt_ijData) + Templates.Slider.rail(opt_data, null, opt_ijData) + '
');
};
if (goog.DEBUG) {
  Templates.Slider.content.soyTemplateName = 'Templates.Slider.content';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.input = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
<input name="' + soy.$$escapeHtmlAttribute(opt_data.inputName ? opt_data.inputName : opt_data.id) + '" type="hidden" value="' + soy.$$escapeHtmlAttribute(opt_data.value) + '">
');
};
if (goog.DEBUG) {
  Templates.Slider.input.soyTemplateName = 'Templates.Slider.input';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.label = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('
' + soy.$$escapeHtml(opt_data.value) + '
');
};
if (goog.DEBUG) {
  Templates.Slider.label.soyTemplateName = 'Templates.Slider.label';
}

/**
 * @param {Object.<string, *>=} opt_data
 * @param {(null|undefined)=} opt_ignored
 * @param {Object.<string, *>=} opt_ijData
 * @return {!soydata.SanitizedHtml}
 * @suppress {checkTypes}
 */
Templates.Slider.rail = function(opt_data, opt_ignored, opt_ijData) {
  return soydata.VERY_UNSAFE.ordainSanitizedHtml('

');
};
if (goog.DEBUG) {
  Templates.Slider.rail.soyTemplateName = 'Templates.Slider.rail';
}

Templates.Slider.content.params = ["id"];
Templates.Slider.input.params = ["id","inputName","value"];
Templates.Slider.label.params = ["id","value"];
Templates.Slider.rail.params = ["id"];
export default Templates.Slider;
/* jshint ignore:end */
<|endoftext|>


<commit_before>'use strict';

var BaseModel = require('model-toolkit').BaseModel;
var BookingItem = require('./booking-order-item');

module.exports = class BookingOrder extends BaseModel {
    constructor(source) {
        super('booking-order', '1.0.0');

        // Define properties.
        this.code='';
        this.bookingDate = new Date();
        this.deliveryDate = new Date();

        this.garmentBuyerId={};
        this.garmentBuyerName='';
        this.garmentBuyerCode='';

        this.garmentSectionId={};
        this.garmentSectionName='';
        this.garmentSectionCode='';

        this.orderQuantity=0;
        this.remark = '';
        this.isMasterPlan=false;
        this.isCanceled=false;

        this.expiredBookingOrder=0;
        this.canceledDate= null;

        this.items=[];
        this.canceledItems=[];

        this.copy(source);

        this.items = (this.items || []).map(item => new BookingItem(item));
        this.canceledItems = (this.canceledItems || []).map(item => new BookingItem(item));
    }
};
<commit_msg>add expiredDeletedDate<commit_after>'use strict';

var BaseModel = require('model-toolkit').BaseModel;
var BookingItem = require('./booking-order-item');

module.exports = class BookingOrder extends BaseModel {
    constructor(source) {
        super('booking-order', '1.0.0');

        // Define properties.
        this.code='';
        this.bookingDate = new Date();
        this.deliveryDate = new Date();

        this.garmentBuyerId={};
        this.garmentBuyerName='';
        this.garmentBuyerCode='';

        this.garmentSectionId={};
        this.garmentSectionName='';
        this.garmentSectionCode='';

        this.orderQuantity=0;
        this.remark = '';
        this.isMasterPlan=false;
        this.isCanceled=false;

        this.expiredBookingOrder=0;
        this.expiredDeletedDate=null;
        this.canceledDate= null;

        this.items=[];
        this.canceledItems=[];

        this.copy(source);

        this.items = (this.items || []).map(item => new BookingItem(item));
        this.canceledItems = (this.canceledItems || []).map(item => new BookingItem(item));
    }
};
<|endoftext|>


<commit_before>/**
 * Some video types and codecs leads to smaller file sizes
 * for comparable video qualities.
 *
 * If the client supports those, we want to prioritize those sources
 * over the others available in the MPD manifest.
 */
const DEFAULT_VIDEO_PRIORITIES = [
  '[mimeType="video/webm"][codecs^="vp09"]',
  '[mimeType="video/webm"]',
  '[mimeType="video/mp4"]',
];

/**
 * Same for audio, but right now we have no real preference here.
 */
const DEFAULT_AUDIO_PRIORITIES = [
  '[mimeType="audio/mp4"]',
];

/**
 * A stream can be composed from multiple media, e.g. video, audio, subtitles etc.
 *
 * These are all the types the Streamer has support for.
 */
const ALL_STREAM_TYPES = ['audio', 'video'];

/**
 * Streams raw media data to the <video> element.
 */
export default class {
  constructor(videoEl, parser, opts = {}) {
    this.videoEl = videoEl;
    this.parser = parser;
    this.opts = opts;

    this.stream = {
      media: {
        baseURL: '/',
        streamTypes: [],
        representations: this.initRepresentations(),
        lastRepresentationsIds: {},
        duration: this.parser.duration,
      },
      buffer: {
        throttled: false,
        throttleTimeout: null,
        healthyDuration: this.parser.minBufferTime,
        mediaSource: null,
        sourceBuffers: {
          audio: null,
          video: null,
        },
      },
      measuredDownlink: null,
    };

    // Iterate which stream types we have separate representations for.
    this.stream.media.streamTypes = Object.keys(this.stream.media.representations);

    /**
     * Define a base URL for the files referenced in the manifest file.
     */
    const manifestSrc = this.getOpt('manifestSrc');
    if (manifestSrc) {
      const manifestURL = new URL(manifestSrc);
      const manifestPathDir = manifestURL.pathname.replace(/[^/]+$/, ''); // Strip trailing filename.

      this.stream.media.baseURL = `${manifestURL.origin}${manifestPathDir}`;
    }

    this.initializeStream();
  }

  /**
   * Initializes all the differents parts of the stream.
   */
  async initializeStream() {
    /**
     * Create and attach a `MediaSource` element.
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
     */
    await this.initializeMediaSource();

    /**
     * Create, extend and attach the `SourceBuffer` objects.
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer
     */
    this.initializeBuffers();

    /**
     * Respond to video playback and seek operations to buffer enough data
     * ahead at any point.
     */
    this.observeVideoUpdates();
  }

  /**
   * Initializes the Media Source and appends it to a video element.
   *
   * @returns {Promise} Promise that resolves with opened MediaSource instance.
   */
  initializeMediaSource() {
    const mediaSource = new MediaSource();
    this.videoEl.src = URL.createObjectURL(mediaSource);

    return new Promise((resolve) => {
      mediaSource.addEventListener('sourceopen', (e) => {
        this.stream.buffer.mediaSource = e.target;
        this.stream.buffer.mediaSource.duration = this.stream.media.duration;
        resolve();
      });
    });
  }

  /**
   * Initialize a `SourceBuffer` for each component of the stream,
   * e.g. one for video, one for audio.
   */
  initializeBuffers() {
    const { streamTypes, representations } = this.stream.media;

    streamTypes.forEach((streamType) => {
      const mimeString = this.getRepresentationMimeString(representations[streamType][0]);
      this.stream.buffer.sourceBuffers[streamType] = this.initializeSourceBuffer(mimeString);
    });

    this.bufferAhead();
  }

  /**
   * Observes the video player for `currentTime` updates and add more data
   * to the buffer if necessary.
   */
  observeVideoUpdates() {
    this.videoEl.addEventListener('timeupdate', () => {
      if (!this.stream.buffer.throttled) this.bufferAhead();
    });
    this.videoEl.addEventListener('seeking', () => {
      this.unthrottleBuffer();
      this.bufferAhead();
    });
  }

  /**
   * Returns a reported or measured downlink capacity in MBits per second.
   *
   * @returns {number} Reported or measured downlink capacity in MBits per second.
   */
  getDownlink() {
    let navigatorDownlink = 10;
    if (navigator.connection) {
      navigatorDownlink = navigator.connection.downlink;
    }
    return this.stream.measuredDownlink
      || navigatorDownlink
      || 10; // Assume broadband if we don't know.
  }

  /**
   * Returns the MIME type for a representation.
   *
   * @param {object} representation Information about the representation.
   *
   * @returns {string} MIME type, optionally with codecs string, too.
   */
  getRepresentationMimeString(representation) {
    return representation.mimeType && representation.codecs
      ? `${representation.mimeType}; codecs="${representation.codecs}"`
      : `${representation.mimeType}`;
  }

  /**
   * Get a combination of video and audio media representations
   * that will fit currently available bandwidth.
   *
   * @todo Select appropriate audio, too.
   *
   * @returns {object} Selected `video` and `audio` representations.
   */
  getRepresentationsByBandwidth() {
    const representations = {
      /*
       * Right now, if we have any `audio` representations, we just pick the first one,
       * because audio isn't really too much data.
       */
      audio: this.stream.media.representations.audio[0] || null,
      video: null,
    };
    const downlinkInBits = this.getDownlink() * 1000000;
    let usedBandwidth = 0;

    if (representations.audio) {
      usedBandwidth += parseInt(representations.audio.bandwidth || 200000, 10);
    }

    /**
     * From all video representations, pick the best quality one that
     * still fits the available bandwidth.
     */
    representations.video = this.stream.media.representations.video.reduce(
      (previous, current) => {
        const currentBandwidth = parseInt(current.bandwidth, 10);
        const previousBandwidth = parseInt(previous.bandwidth, 10);

        const currentIsHigherQuality = currentBandwidth > previousBandwidth;
        const currentFitsDownlink = (currentBandwidth + usedBandwidth) <= downlinkInBits;
        const previousDoesntFitDownlink = (previousBandwidth + usedBandwidth) > downlinkInBits;
        const currentIsLessData = currentBandwidth < previousBandwidth;

        if (currentIsHigherQuality && currentFitsDownlink) return current;
        if (previousDoesntFitDownlink && currentIsLessData) return current;
        return previous;
      },
      this.stream.media.representations.video[0],
    );
    return representations;
  }

  /**
   * Fetch all representations present in the MPD file and filter
   * those that the current client can't play out.
   *
   * @returns {object[]} All representations that the current client is able to play.
   */
  initRepresentations() {
    /**
     * Returns whether the provided representation is playable by the current client.
     *
     * @param {object} representation Representation object returned by parser.queryRepresentations.
     *
     * @returns {boolean} Is this representation playable by the current client.
     */
    const canPlayFilter = (representation) => {
      const testMime = this.getRepresentationMimeString(representation);
      return this.videoEl.canPlayType(testMime) === 'probably';
    };

    const representations = {};
    const priorities = {
      video: this.getOpt('videoPriorities') || DEFAULT_VIDEO_PRIORITIES,
      audio: this.getOpt('audioPriorities') || DEFAULT_AUDIO_PRIORITIES,
    };

    /**
     * Select sets of video and audio representations that are playable
     * in the client with respect to indicated priorities.
     */
    ALL_STREAM_TYPES.forEach(
      (contentType) => {
        let query;
        do {
          query = priorities[contentType].shift() || '';

          /**
           * @todo Support languages other than the hardcoded English.
           */
          representations[contentType] = this.parser.queryRepresentations(query, contentType, 'eng');
          representations[contentType] = representations[contentType].filter(canPlayFilter);
        } while (query);
      },
    );

    if (representations.video.length === 0) {
      throw new Error('[Streamer] No playable video representation found.');
    }

    return representations;
  }

  /**
   * Get a value from options passed to the Streamer constructor.
   *
   * @param {string} name         Name of the option to be retrived.
   * @param {*}      defaultValue Value returned if the option doesn't exist.
   *
   * @returns {*} Option value.
   */
  getOpt(name, defaultValue = null) {
    return this.opts[name] || defaultValue;
  }

  /**
   * Sets an option value.
   *
   * @param {string} name   Name of the option to be used for storage.
   * @param {*}      value  Value to be stored.
   */
  setOpt(name, value) {
    this.opts[name] = value;
  }

  /**
   * "Locks" buffering for 0.5 second.
   */
  throttleBuffer() {
    this.stream.buffer.throttled = true;
    this.stream.buffer.throttleTimeout = setTimeout(() => {
      this.stream.buffer.throttled = false;
    }, 500);
  }

  /**
   * "Unlocks" buffering.
   */
  unthrottleBuffer() {
    this.stream.buffer.throttled = false;
    if (this.stream.buffer.throttleTimeout) {
      clearTimeout(this.stream.buffer.throttleTimeout);
      this.stream.buffer.throttleTimeout = null;
    }
  }

  /**
   * Initializes and returns a SourceBuffer.
   *
   * Also extends it with `dataQueue` and `fileQueue` objects that
   * simplify pumping chunks of data into them.
   *
   * @param {string} mimeString Mime string for the SourceBuffer.
   *
   * @returns {SourceBuffer} Initialized `SourceBuffer`.
   */
  initializeSourceBuffer(mimeString) {
    const sourceBuffer = this.stream.buffer.mediaSource.addSourceBuffer(mimeString);

    /**
     * The `dataQueue` object simplifies pumping binary data into the buffer. It makes
     * sure the buffer is always done writing previous chunk of data before another
     * append request takes place.
     */
    sourceBuffer.dataQueue = {
      internal: {
        entries: [],
        updating: false,
      },

      /**
       * Appends data to the buffer.
       *
       * @param {Uint8Array} data Data.
       */
      append(data) {
        this.internal.entries.push(data);
        if (!this.internal.updating) {
          this.step();
        }
      },

      /**
       * Buffer is ready to accept more data. Check the internal queue to see
       * if we have more data lined up.
       */
      step() {
        const data = this.internal.entries.shift();
        if (data) {
          this.internal.updating = true;
          sourceBuffer.appendBuffer(data);
        }
      },
    };

    /**
     * The `fileQueue` object simplifies loading chunk files into the `SourceBuffer`.
     *
     * It internally tracks all files already added to the buffer and refuses adding
     * them twice unless that operation is forced.
     *
     * It also implements a way to access the file chunks and pump data out of them
     * through to the `dataQueue` in the order of addition.
     */
    sourceBuffer.fileQueue = {
      internal: {
        files: [],
        bufferedFiles: new Set(),
        reading: false,
      },

      /**
       * Adds a file to be appended to the buffer.
       *
       * @param {object}  fileObj        File object.
       * @param {boolean} fileObj.force  Force the file to the buffer even if we appended it before.
       * @param {string}  fileObj.url    URL of the chunk to be appended to the buffer.
       * @param {string}  fileObj.id     ID of the file to be appended.
       */
      add(fileObj) {
        if (!fileObj.force && this.internal.bufferedFiles.has(fileObj.id)) return;

        this.internal.files.push(fileObj.url);
        this.internal.bufferedFiles.add(fileObj.id);

        if (!this.internal.reading) this.step();
      },

      /**
       * Check if the previous read and write is finished and if yes,
       * get the next file in queue and process that.
       */
      step() {
        const filename = this.internal.files.shift();
        if (!filename) return;

        this.internal.reading = true;
        fetch(filename).then((response) => response.body).then(
          async (body) => {
            /**
             * @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader
             */
            const reader = body.getReader();
            const readChunk = async () => {
              const { value, done } = await reader.read();
              return [value, done];
            };

            let data;
            let done;

            /* eslint-disable no-await-in-loop */
            do {
              [data, done] = await readChunk(); // await here is fine, the process is sequential.
              if (!done) sourceBuffer.enqueueAppendBuffer(data);
            } while (!done);
            /* eslint-enable no-await-in-loop */

            this.internal.reading = false;
            this.step();
          },
        );
      },
    };

    sourceBuffer.addEventListener('updateend', (e) => {
      e.target.dataQueue.internal.updating = false;
      e.target.dataQueue.step();
    });

    /**
     * Expose public methods to avoid using `dataQueue` and `fileQueue` directly.
     */
    sourceBuffer.enqueueAppendBuffer = sourceBuffer.dataQueue.append.bind(sourceBuffer.dataQueue);
    sourceBuffer.enqueueFile = sourceBuffer.fileQueue.add.bind(sourceBuffer.fileQueue);

    return sourceBuffer;
  }

  /**
   * @typedef {object} healthResult
   * @property {boolean} isHealthy      Whether the `heathyDuration` of the buffer is loaded ahead.
   * @property {number}  bufferEndTime  The end time of the current buffer range ahead.
   */

  /**
   * Returns health information about the provided buffer.
   *
   * @param {SourceBuffer} buffer SourceBuffer to determine health for.
   *
   * @returns {healthResult} Health result of the provided source buffer.
   */
  getBufferHealth(buffer) {
    const currentPlaybackTime = this.videoEl.currentTime;
    const { healthyDuration } = this.stream.buffer;
    const bufferedTimeRanges = buffer.buffered;

    let bufferLeft = 0;
    let bufferEndTime = currentPlaybackTime;

    /**
     * Find the buffered time range the playback is currently in if there is one.
     */
    for (let i = 0; i < bufferedTimeRanges.length; i += 1) {
      const start = bufferedTimeRanges.start(i);
      const end = bufferedTimeRanges.end(i);

      if (currentPlaybackTime >= start && currentPlaybackTime <= end) {
        bufferLeft = end - currentPlaybackTime;
        bufferEndTime = end;
      }
    }

    return {
      isHealthy: bufferLeft >= healthyDuration,
      bufferEndTime,
    };
  }

  /**
   * Inspects buffer health and loads more upcoming data to the buffer
   * if the amount of data in the buffer is considered unhealthy low.
   */
  bufferAhead() {
    this.throttleBuffer();

    const currentRepresentations = this.getRepresentationsByBandwidth();
    const { baseURL, streamTypes, lastRepresentationsIds } = this.stream.media;

    streamTypes.forEach((streamType) => {
      const buffer = this.stream.buffer.sourceBuffers[streamType];
      const bufferHealth = this.getBufferHealth(buffer);

      if (bufferHealth.isHealthy) {
        return;
      }

      const representation = currentRepresentations[streamType];
      const chunkIndex = representation.getSegmentIndexByTime(bufferHealth.bufferEndTime);

      if (!chunkIndex) {
        return;
      }

      const lastRepresentationId = lastRepresentationsIds[streamType];
      const filesToBuffer = [];

      /**
       * If we switch between representations mid-stream, we need to inject a so-called
       * initialization chunk again to indicate the parameters of the data ahead changed.
       */
      if (representation.id !== lastRepresentationId) {
        filesToBuffer.push({
          id: `initial-${representation.id}`,
          url: `${baseURL}${representation.getInitialSegment()}`,
          force: true,
        });
      }
      this.stream.media.lastRepresentationsIds[streamType] = representation.id;

      const chunkIndicesToLoad = [chunkIndex];
      if (chunkIndex < representation.maxChunkIndex) {
        chunkIndicesToLoad.push(chunkIndex + 1);
      }

      chunkIndicesToLoad.forEach(
        (index) => {
          filesToBuffer.push({
            id: index,
            url: `${baseURL}${representation.getSegmentByIndex(index)}`,
          });
        },
      );

      filesToBuffer.map(
        (fileObj) => buffer.enqueueFile(fileObj),
      );
    });
  }
}
<commit_msg>Honor indicated priorities when selecting the set of representations.<commit_after>/**
 * Some video types and codecs leads to smaller file sizes
 * for comparable video qualities.
 *
 * If the client supports those, we want to prioritize those sources
 * over the others available in the MPD manifest.
 */
const DEFAULT_VIDEO_PRIORITIES = [
  '[mimeType="video/webm"][codecs^="vp09"]',
  '[mimeType="video/webm"]',
  '[mimeType="video/mp4"]',
];

/**
 * Same for audio, but right now we have no real preference here.
 */
const DEFAULT_AUDIO_PRIORITIES = [
  '[mimeType="audio/mp4"]',
];

/**
 * A stream can be composed from multiple media, e.g. video, audio, subtitles etc.
 *
 * These are all the types the Streamer has support for.
 */
const ALL_STREAM_TYPES = ['audio', 'video'];

/**
 * Streams raw media data to the <video> element.
 */
export default class {
  constructor(videoEl, parser, opts = {}) {
    this.videoEl = videoEl;
    this.parser = parser;
    this.opts = opts;

    this.stream = {
      media: {
        baseURL: '/',
        streamTypes: [],
        representations: this.initRepresentations(),
        lastRepresentationsIds: {},
        duration: this.parser.duration,
      },
      buffer: {
        throttled: false,
        throttleTimeout: null,
        healthyDuration: this.parser.minBufferTime,
        mediaSource: null,
        sourceBuffers: {
          audio: null,
          video: null,
        },
      },
      measuredDownlink: null,
    };

    // Iterate which stream types we have separate representations for.
    this.stream.media.streamTypes = Object.keys(this.stream.media.representations);

    /**
     * Define a base URL for the files referenced in the manifest file.
     */
    const manifestSrc = this.getOpt('manifestSrc');
    if (manifestSrc) {
      const manifestURL = new URL(manifestSrc);
      const manifestPathDir = manifestURL.pathname.replace(/[^/]+$/, ''); // Strip trailing filename.

      this.stream.media.baseURL = `${manifestURL.origin}${manifestPathDir}`;
    }

    this.initializeStream();
  }

  /**
   * Initializes all the differents parts of the stream.
   */
  async initializeStream() {
    /**
     * Create and attach a `MediaSource` element.
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaSource
     */
    await this.initializeMediaSource();

    /**
     * Create, extend and attach the `SourceBuffer` objects.
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer
     */
    this.initializeBuffers();

    /**
     * Respond to video playback and seek operations to buffer enough data
     * ahead at any point.
     */
    this.observeVideoUpdates();
  }

  /**
   * Initializes the Media Source and appends it to a video element.
   *
   * @returns {Promise} Promise that resolves with opened MediaSource instance.
   */
  initializeMediaSource() {
    const mediaSource = new MediaSource();
    this.videoEl.src = URL.createObjectURL(mediaSource);

    return new Promise((resolve) => {
      mediaSource.addEventListener('sourceopen', (e) => {
        this.stream.buffer.mediaSource = e.target;
        this.stream.buffer.mediaSource.duration = this.stream.media.duration;
        resolve();
      });
    });
  }

  /**
   * Initialize a `SourceBuffer` for each component of the stream,
   * e.g. one for video, one for audio.
   */
  initializeBuffers() {
    const { streamTypes, representations } = this.stream.media;

    streamTypes.forEach((streamType) => {
      const mimeString = this.getRepresentationMimeString(representations[streamType][0]);
      this.stream.buffer.sourceBuffers[streamType] = this.initializeSourceBuffer(mimeString);
    });

    this.bufferAhead();
  }

  /**
   * Observes the video player for `currentTime` updates and add more data
   * to the buffer if necessary.
   */
  observeVideoUpdates() {
    this.videoEl.addEventListener('timeupdate', () => {
      if (!this.stream.buffer.throttled) this.bufferAhead();
    });
    this.videoEl.addEventListener('seeking', () => {
      this.unthrottleBuffer();
      this.bufferAhead();
    });
  }

  /**
   * Returns a reported or measured downlink capacity in MBits per second.
   *
   * @returns {number} Reported or measured downlink capacity in MBits per second.
   */
  getDownlink() {
    let navigatorDownlink = 10;
    if (navigator.connection) {
      navigatorDownlink = navigator.connection.downlink;
    }
    return this.stream.measuredDownlink
      || navigatorDownlink
      || 10; // Assume broadband if we don't know.
  }

  /**
   * Returns the MIME type for a representation.
   *
   * @param {object} representation Information about the representation.
   *
   * @returns {string} MIME type, optionally with codecs string, too.
   */
  getRepresentationMimeString(representation) {
    return representation.mimeType && representation.codecs
      ? `${representation.mimeType}; codecs="${representation.codecs}"`
      : `${representation.mimeType}`;
  }

  /**
   * Get a combination of video and audio media representations
   * that will fit currently available bandwidth.
   *
   * @todo Select appropriate audio, too.
   *
   * @returns {object} Selected `video` and `audio` representations.
   */
  getRepresentationsByBandwidth() {
    const representations = {
      /*
       * Right now, if we have any `audio` representations, we just pick the first one,
       * because audio isn't really too much data.
       */
      audio: this.stream.media.representations.audio[0] || null,
      video: null,
    };
    const downlinkInBits = this.getDownlink() * 1000000;
    let usedBandwidth = 0;

    if (representations.audio) {
      usedBandwidth += parseInt(representations.audio.bandwidth || 200000, 10);
    }

    /**
     * From all video representations, pick the best quality one that
     * still fits the available bandwidth.
     */
    representations.video = this.stream.media.representations.video.reduce(
      (previous, current) => {
        const currentBandwidth = parseInt(current.bandwidth, 10);
        const previousBandwidth = parseInt(previous.bandwidth, 10);

        const currentIsHigherQuality = currentBandwidth > previousBandwidth;
        const currentFitsDownlink = (currentBandwidth + usedBandwidth) <= downlinkInBits;
        const previousDoesntFitDownlink = (previousBandwidth + usedBandwidth) > downlinkInBits;
        const currentIsLessData = currentBandwidth < previousBandwidth;

        if (currentIsHigherQuality && currentFitsDownlink) return current;
        if (previousDoesntFitDownlink && currentIsLessData) return current;
        return previous;
      },
      this.stream.media.representations.video[0],
    );
    return representations;
  }

  /**
   * Fetch all representations present in the MPD file and filter
   * those that the current client can't play out.
   *
   * @returns {object[]} All representations that the current client is able to play.
   */
  initRepresentations() {
    /**
     * Returns whether the provided representation is playable by the current client.
     *
     * @param {object} representation Representation object returned by parser.queryRepresentations.
     *
     * @returns {boolean} Is this representation playable by the current client.
     */
    const canPlayFilter = (representation) => {
      const testMime = this.getRepresentationMimeString(representation);
      return this.videoEl.canPlayType(testMime) === 'probably';
    };

    const representations = {};
    const priorities = {
      video: this.getOpt('videoPriorities') || DEFAULT_VIDEO_PRIORITIES,
      audio: this.getOpt('audioPriorities') || DEFAULT_AUDIO_PRIORITIES,
    };

    /**
     * Select sets of video and audio representations that are playable
     * in the client with respect to indicated priorities.
     */
    ALL_STREAM_TYPES.forEach(
      (contentType) => {
        let query;
        do {
          query = priorities[contentType].shift() || '';

          /**
           * @todo Support languages other than the hardcoded English.
           */
          representations[contentType] = this.parser.queryRepresentations(query, contentType, 'eng');
          representations[contentType] = representations[contentType].filter(canPlayFilter);
        } while (!representations[contentType]);
      },
    );

    if (representations.video.length === 0) {
      throw new Error('[Streamer] No playable video representation found.');
    }
    console.log(representations);

    return representations;
  }

  /**
   * Get a value from options passed to the Streamer constructor.
   *
   * @param {string} name         Name of the option to be retrived.
   * @param {*}      defaultValue Value returned if the option doesn't exist.
   *
   * @returns {*} Option value.
   */
  getOpt(name, defaultValue = null) {
    return this.opts[name] || defaultValue;
  }

  /**
   * Sets an option value.
   *
   * @param {string} name   Name of the option to be used for storage.
   * @param {*}      value  Value to be stored.
   */
  setOpt(name, value) {
    this.opts[name] = value;
  }

  /**
   * "Locks" buffering for 0.5 second.
   */
  throttleBuffer() {
    this.stream.buffer.throttled = true;
    this.stream.buffer.throttleTimeout = setTimeout(() => {
      this.stream.buffer.throttled = false;
    }, 500);
  }

  /**
   * "Unlocks" buffering.
   */
  unthrottleBuffer() {
    this.stream.buffer.throttled = false;
    if (this.stream.buffer.throttleTimeout) {
      clearTimeout(this.stream.buffer.throttleTimeout);
      this.stream.buffer.throttleTimeout = null;
    }
  }

  /**
   * Initializes and returns a SourceBuffer.
   *
   * Also extends it with `dataQueue` and `fileQueue` objects that
   * simplify pumping chunks of data into them.
   *
   * @param {string} mimeString Mime string for the SourceBuffer.
   *
   * @returns {SourceBuffer} Initialized `SourceBuffer`.
   */
  initializeSourceBuffer(mimeString) {
    const sourceBuffer = this.stream.buffer.mediaSource.addSourceBuffer(mimeString);

    /**
     * The `dataQueue` object simplifies pumping binary data into the buffer. It makes
     * sure the buffer is always done writing previous chunk of data before another
     * append request takes place.
     */
    sourceBuffer.dataQueue = {
      internal: {
        entries: [],
        updating: false,
      },

      /**
       * Appends data to the buffer.
       *
       * @param {Uint8Array} data Data.
       */
      append(data) {
        this.internal.entries.push(data);
        if (!this.internal.updating) {
          this.step();
        }
      },

      /**
       * Buffer is ready to accept more data. Check the internal queue to see
       * if we have more data lined up.
       */
      step() {
        const data = this.internal.entries.shift();
        if (data) {
          this.internal.updating = true;
          sourceBuffer.appendBuffer(data);
        }
      },
    };

    /**
     * The `fileQueue` object simplifies loading chunk files into the `SourceBuffer`.
     *
     * It internally tracks all files already added to the buffer and refuses adding
     * them twice unless that operation is forced.
     *
     * It also implements a way to access the file chunks and pump data out of them
     * through to the `dataQueue` in the order of addition.
     */
    sourceBuffer.fileQueue = {
      internal: {
        files: [],
        bufferedFiles: new Set(),
        reading: false,
      },

      /**
       * Adds a file to be appended to the buffer.
       *
       * @param {object}  fileObj        File object.
       * @param {boolean} fileObj.force  Force the file to the buffer even if we appended it before.
       * @param {string}  fileObj.url    URL of the chunk to be appended to the buffer.
       * @param {string}  fileObj.id     ID of the file to be appended.
       */
      add(fileObj) {
        if (!fileObj.force && this.internal.bufferedFiles.has(fileObj.id)) return;

        this.internal.files.push(fileObj.url);
        this.internal.bufferedFiles.add(fileObj.id);

        if (!this.internal.reading) this.step();
      },

      /**
       * Check if the previous read and write is finished and if yes,
       * get the next file in queue and process that.
       */
      step() {
        const filename = this.internal.files.shift();
        if (!filename) return;

        this.internal.reading = true;
        fetch(filename).then((response) => response.body).then(
          async (body) => {
            /**
             * @see https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader
             */
            const reader = body.getReader();
            const readChunk = async () => {
              const { value, done } = await reader.read();
              return [value, done];
            };

            let data;
            let done;

            /* eslint-disable no-await-in-loop */
            do {
              [data, done] = await readChunk(); // await here is fine, the process is sequential.
              if (!done) sourceBuffer.enqueueAppendBuffer(data);
            } while (!done);
            /* eslint-enable no-await-in-loop */

            this.internal.reading = false;
            this.step();
          },
        );
      },
    };

    sourceBuffer.addEventListener('updateend', (e) => {
      e.target.dataQueue.internal.updating = false;
      e.target.dataQueue.step();
    });

    /**
     * Expose public methods to avoid using `dataQueue` and `fileQueue` directly.
     */
    sourceBuffer.enqueueAppendBuffer = sourceBuffer.dataQueue.append.bind(sourceBuffer.dataQueue);
    sourceBuffer.enqueueFile = sourceBuffer.fileQueue.add.bind(sourceBuffer.fileQueue);

    return sourceBuffer;
  }

  /**
   * @typedef {object} healthResult
   * @property {boolean} isHealthy      Whether the `heathyDuration` of the buffer is loaded ahead.
   * @property {number}  bufferEndTime  The end time of the current buffer range ahead.
   */

  /**
   * Returns health information about the provided buffer.
   *
   * @param {SourceBuffer} buffer SourceBuffer to determine health for.
   *
   * @returns {healthResult} Health result of the provided source buffer.
   */
  getBufferHealth(buffer) {
    const currentPlaybackTime = this.videoEl.currentTime;
    const { healthyDuration } = this.stream.buffer;
    const bufferedTimeRanges = buffer.buffered;

    let bufferLeft = 0;
    let bufferEndTime = currentPlaybackTime;

    /**
     * Find the buffered time range the playback is currently in if there is one.
     */
    for (let i = 0; i < bufferedTimeRanges.length; i += 1) {
      const start = bufferedTimeRanges.start(i);
      const end = bufferedTimeRanges.end(i);

      if (currentPlaybackTime >= start && currentPlaybackTime <= end) {
        bufferLeft = end - currentPlaybackTime;
        bufferEndTime = end;
      }
    }

    return {
      isHealthy: bufferLeft >= healthyDuration,
      bufferEndTime,
    };
  }

  /**
   * Inspects buffer health and loads more upcoming data to the buffer
   * if the amount of data in the buffer is considered unhealthy low.
   */
  bufferAhead() {
    this.throttleBuffer();

    const currentRepresentations = this.getRepresentationsByBandwidth();
    const { baseURL, streamTypes, lastRepresentationsIds } = this.stream.media;

    streamTypes.forEach((streamType) => {
      const buffer = this.stream.buffer.sourceBuffers[streamType];
      const bufferHealth = this.getBufferHealth(buffer);

      if (bufferHealth.isHealthy) {
        return;
      }

      const representation = currentRepresentations[streamType];
      const chunkIndex = representation.getSegmentIndexByTime(bufferHealth.bufferEndTime);

      if (!chunkIndex) {
        return;
      }

      const lastRepresentationId = lastRepresentationsIds[streamType];
      const filesToBuffer = [];

      /**
       * If we switch between representations mid-stream, we need to inject a so-called
       * initialization chunk again to indicate the parameters of the data ahead changed.
       */
      if (representation.id !== lastRepresentationId) {
        filesToBuffer.push({
          id: `initial-${representation.id}`,
          url: `${baseURL}${representation.getInitialSegment()}`,
          force: true,
        });
      }
      this.stream.media.lastRepresentationsIds[streamType] = representation.id;

      const chunkIndicesToLoad = [chunkIndex];
      if (chunkIndex < representation.maxChunkIndex) {
        chunkIndicesToLoad.push(chunkIndex + 1);
      }

      chunkIndicesToLoad.forEach(
        (index) => {
          filesToBuffer.push({
            id: index,
            url: `${baseURL}${representation.getSegmentByIndex(index)}`,
          });
        },
      );

      filesToBuffer.map(
        (fileObj) => buffer.enqueueFile(fileObj),
      );
    });
  }
}
<|endoftext|>