<commit_before>import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import Alert from '../components/Alert'
import { IdentityActions } from '../store/identities'

import { getNamePrices, isNameAvailable,
         hasNameBeenPreordered, isABlockstackName } from '../utils/name-utils'
import { authorizationHeaderValue } from '../utils'

const WALLET_URL = '/wallet/deposit'

function mapStateToProps(state) {
  return {
    username: '',
    localIdentities: state.identities.localIdentities,
    lookupUrl: state.settings.api.nameLookupUrl,
    registerUrl: state.settings.api.registerUrl,
    priceUrl: state.settings.api.priceUrl,
    blockstackApiAppId: state.settings.api.blockstackApiAppId,
    blockstackApiAppSecret: state.settings.api.blockstackApiAppSecret,
    analyticsId: state.account.analyticsId,
    identityAddresses: state.account.identityAccount.addresses,
    api: state.settings.api,
    identityKeypairs: state.account.identityAccount.keypairs,
    registration: state.identities.registration,
    addressBalanceUrl: state.settings.api.addressBalanceUrl,
    coreWalletBalance: state.account.coreWalletBalance,
    coreWalletAddress: state.account.coreWalletAddress
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(Object.assign({}, IdentityActions, AccountActions), dispatch)
}

class RegisterPage extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    localIdentities: PropTypes.object.isRequired,
    lookupUrl: PropTypes.string.isRequired,
    registerUrl: PropTypes.string.isRequired,
    blockstackApiAppId: PropTypes.string.isRequired,
    blockstackApiAppSecret: PropTypes.string.isRequired,
    analyticsId: PropTypes.string.isRequired,
    identityAddresses: PropTypes.array.isRequired,
    registerName: PropTypes.func.isRequired,
    identityKeypairs: PropTypes.array.isRequired,
    registration: PropTypes.object.isRequired,
    addressBalanceUrl: PropTypes.string.isRequired,
    refreshCoreWalletBalance: PropTypes.func.isRequired,
    coreWalletBalance: PropTypes.number.isRequired,
    coreWalletAddress: PropTypes.string
  }

  static contextTypes = {
    router: PropTypes.object.isRequired
  }

  constructor(props) {
    super(props)

    this.state = {
      registrationLock: false,
      username: props.username,
      nameCost: 0,
      alerts: [],
      type: 'person',
      tlds: {
        person: 'id',
        organization: 'corp'
      },
      nameLabels: {
        person: 'Username',
        organization: 'Domain'
      },
      zeroBalance: props.coreWalletBalance <= 0
    }

    this.onChange = this.onChange.bind(this)
    this.registerIdentity = this.registerIdentity.bind(this)
    this.updateAlert = this.updateAlert.bind(this)
    this.displayPricingAndAvailabilityAlerts = this.displayPricingAndAvailabilityAlerts.bind(this)
    this.displayRegistrationAlerts = this.displayRegistrationAlerts.bind(this)
    this.displayZeroBalanceAlert = this.displayZeroBalanceAlert.bind(this)
  }

  componentWillReceiveProps(nextProps) {

    // Clear alerts
    this.setState({
      alerts:[]
    })

    if(this.props.coreWalletAddress != nextProps.coreWalletAddress) {
      this.props.refreshCoreWalletBalance(nextProps.addressBalanceUrl, nextProps.coreWalletAddress)
    }

    const registration = nextProps.registration
    const zeroBalance = this.props.coreWalletBalance <= 0

    this.setState({
      zeroBalance: zeroBalance
    })

    if (zeroBalance) {
      this.displayZeroBalanceAlert()
    } else if (registration.registrationSubmitting ||
      registration.registrationSubmitted ||
      registration.profileUploading ||
      registration.error)
      this.displayRegistrationAlerts(registration)
    else {
      this.displayPricingAndAvailabilityAlerts(registration)
    }

  }

  componentDidMount() {
    if(this.props.coreWalletAddress != null) {
      this.props.refreshCoreWalletBalance(this.props.addressBalanceUrl, this.props.coreWalletAddress)
    }
    if (this.state.zeroBalance) {
      this.displayZeroBalanceAlert()
    }
  }

  displayRegistrationAlerts(registration) {
    if(registration.error) {
      this.updateAlert('danger', 'There was a problem submitting your registration.')
    } else {
      if(registration.profileUploading)
        this.updateAlert('info', 'Signing & uploading your profile...')
      else if(registration.registrationSubmitting)
        this.updateAlert('info', 'Submitting your registration to your Blockstack Core node...')
      else if(registration.registrationSubmitted)
        this.updateAlert('success', 'Congrats! Your name is preordered! Registration will automatically complete over the next few hours.')
    }
  }

  displayPricingAndAvailabilityAlerts(registration) {
    let tld = this.state.tlds[this.state.type]
    const domainName = `${this.state.username}.${tld}`

    if(domainName === registration.lastNameEntered) {
      if(registration.names[domainName].error) {
        const error = registration.names[domainName].error
        console.error(error)
        this.updateAlert('danger', `There was a problem checking on price & availability of ${domainName}`)
      } else {
        if(registration.names[domainName].checkingAvailability)
          this.updateAlert('info', `Checking if ${domainName} available...`)
        else if(registration.names[domainName].available) {
          if(registration.names[domainName].checkingPrice) {
            this.updateAlert('info', `${domainName} is available! Checking price...`)
          } else {
            const price = registration.names[domainName].price
            if(price < this.props.coreWalletBalance) {
              this.updateAlert('info', `${domainName} costs ~${price} btc to register.`)
            } else {
              const shortfall = price - this.props.coreWalletBalance
              this.updateAlert('danger', `Your wallet doesn't have enough money to buy ${domainName}. Please send at least ${shortfall} more bitcoin to your wallet.`, WALLET_URL)
            }
          }
        } else {
          this.updateAlert('danger', `${domainName} has already been registered.`)
        }
      }
    }
  }

  displayZeroBalanceAlert() {
    this.updateAlert('danger', 'You need to deposit bitcoin before you can register a username. Click here to go to your wallet.', WALLET_URL)
  }

  onChange(event) {
    if (event.target.name === 'username') {
      const username = event.target.value.toLowerCase().replace(/\W+/g, ''),
      tld = this.state.tlds[this.state.type],
      domainName = `${username}.${tld}`

      this.setState({
        username: username
      })

      if(username === '') {
        this.setState({
          alerts:[]
        })
        return
      }

      if(this.timer) {
        clearInterval(this.timer)
      }

      event.persist()
      const _this = this

      this.timer = setTimeout( () => {
        if(!isABlockstackName(domainName)) {
          _this.updateAlert('danger', `${domainName} Not valid Blockstack name`)
          return
        }

        this.props.checkNameAvailabilityAndPrice(this.props.api, domainName)

      },
      500) // wait 500ms after user stops typing to check availability
    }
  }

  updateAlert(alertStatus, alertMessage, url=null) {
    this.setState({
      alerts: [{
        status: alertStatus,
        message: alertMessage,
        url: url
      }]
    })
  }

  registerIdentity(event) {
    if (this.state.registrationLock) {
      return
    }

    this.setState({ registrationLock: true })

    const username = this.state.username

    if (username.length === 0) {
      this.updateAlert('danger', 'Name must have at least one character')
      return
    }

    const tld = this.state.tlds[this.state.type],
    domainName = username + '.' + tld

    const nameHasBeenPreordered = hasNameBeenPreordered(domainName, this.props.localIdentities)

    if (nameHasBeenPreordered) {
      this.updateAlert('danger', 'Name has already been preordered')
      this.setState({ registrationLock: false })
    } else {
      const address = this.props.identityAddresses[0]
      const keypair = this.props.identityKeypairs[0]

      this.props.registerName(this.props.api, domainName, address, keypair)
      this.updateAlert('success', 'Name preordered! Waiting for registration confirmation.')
      this.setState({ registrationLock: false })
    }

    const analyticsId = this.props.analyticsId
    mixpanel.track('Register identity', { distinct_id: analyticsId })
    mixpanel.track('Perform action', { distinct_id: analyticsId })
  }

  render() {
    let tld = this.state.tlds[this.state.type],
        nameLabel = this.state.nameLabels[this.state.type]
    return (

            { this.state.alerts.map(function(alert, index) {
              return (
                <Alert key={index} message={alert.message} status={alert.status} url={alert.url} />
              )
            })}
            <fieldset className="form-group">
              {nameLabel}

                <input
                  name="username"
                  className="form-control"
                  placeholder={nameLabel}
                  value={this.state.username}
                  onChange={this.onChange}
                  disabled={this.state.zeroBalance}/>
                .{tld}

            </fieldset>

              <button className="btn btn-blue" onClick={this.registerIdentity}
              disabled={this.props.registration.preventRegistration || this.state.zeroBalance}>
                Register
              </button>

    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(RegisterPage)

/*

            Registration Cost

                  {this.state.nameCost} mBTC

<fieldset className="form-group">
  <select name="type" className="c-select"
    defaultValue={this.state.type} onChange={this.onChange}>
    <option value="person">Person</option>
    <option value="organization">Organization</option>
  </select>
</fieldset>
*/
<commit_msg>display min amount & address in zero balance alert #343<commit_after>import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'

import Alert from '../components/Alert'
import { AccountActions } from '../store/account'
import { IdentityActions } from '../store/identities'

import { getNamePrices, isNameAvailable,
         hasNameBeenPreordered, isABlockstackName } from '../utils/name-utils'
import { authorizationHeaderValue } from '../utils'

const WALLET_URL = '/wallet/deposit'

function mapStateToProps(state) {
  return {
    username: '',
    localIdentities: state.identities.localIdentities,
    lookupUrl: state.settings.api.nameLookupUrl,
    registerUrl: state.settings.api.registerUrl,
    priceUrl: state.settings.api.priceUrl,
    blockstackApiAppId: state.settings.api.blockstackApiAppId,
    blockstackApiAppSecret: state.settings.api.blockstackApiAppSecret,
    analyticsId: state.account.analyticsId,
    identityAddresses: state.account.identityAccount.addresses,
    api: state.settings.api,
    identityKeypairs: state.account.identityAccount.keypairs,
    registration: state.identities.registration,
    addressBalanceUrl: state.settings.api.addressBalanceUrl,
    coreWalletBalance: state.account.coreWalletBalance,
    coreWalletAddress: state.account.coreWalletAddress
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(Object.assign({}, IdentityActions, AccountActions), dispatch)
}

class RegisterPage extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    localIdentities: PropTypes.object.isRequired,
    lookupUrl: PropTypes.string.isRequired,
    registerUrl: PropTypes.string.isRequired,
    blockstackApiAppId: PropTypes.string.isRequired,
    blockstackApiAppSecret: PropTypes.string.isRequired,
    analyticsId: PropTypes.string.isRequired,
    identityAddresses: PropTypes.array.isRequired,
    registerName: PropTypes.func.isRequired,
    identityKeypairs: PropTypes.array.isRequired,
    registration: PropTypes.object.isRequired,
    addressBalanceUrl: PropTypes.string.isRequired,
    refreshCoreWalletBalance: PropTypes.func.isRequired,
    coreWalletBalance: PropTypes.number.isRequired,
    coreWalletAddress: PropTypes.string
  }

  static contextTypes = {
    router: PropTypes.object.isRequired
  }

  constructor(props) {
    super(props)

    this.state = {
      registrationLock: false,
      username: props.username,
      nameCost: 0,
      alerts: [],
      type: 'person',
      tlds: {
        person: 'id',
        organization: 'corp'
      },
      nameLabels: {
        person: 'Username',
        organization: 'Domain'
      },
      zeroBalance: props.coreWalletBalance <= 0
    }

    this.onChange = this.onChange.bind(this)
    this.registerIdentity = this.registerIdentity.bind(this)
    this.updateAlert = this.updateAlert.bind(this)
    this.displayPricingAndAvailabilityAlerts = this.displayPricingAndAvailabilityAlerts.bind(this)
    this.displayRegistrationAlerts = this.displayRegistrationAlerts.bind(this)
    this.displayZeroBalanceAlert = this.displayZeroBalanceAlert.bind(this)
  }

  componentWillReceiveProps(nextProps) {

    // Clear alerts
    this.setState({
      alerts:[]
    })

    if(this.props.coreWalletAddress != nextProps.coreWalletAddress) {
      this.props.refreshCoreWalletBalance(nextProps.addressBalanceUrl, nextProps.coreWalletAddress)
    }

    const registration = nextProps.registration
    const zeroBalance = this.props.coreWalletBalance <= 0

    this.setState({
      zeroBalance: zeroBalance
    })

    if (zeroBalance) {
      this.displayZeroBalanceAlert()
    } else if (registration.registrationSubmitting ||
      registration.registrationSubmitted ||
      registration.profileUploading ||
      registration.error)
      this.displayRegistrationAlerts(registration)
    else {
      this.displayPricingAndAvailabilityAlerts(registration)
    }

  }

  componentDidMount() {
    if(this.props.coreWalletAddress != null) {
      this.props.refreshCoreWalletBalance(this.props.addressBalanceUrl, this.props.coreWalletAddress)
    }
    if (this.state.zeroBalance) {
      this.displayZeroBalanceAlert()
    }
  }

  displayRegistrationAlerts(registration) {
    if(registration.error) {
      this.updateAlert('danger', 'There was a problem submitting your registration.')
    } else {
      if(registration.profileUploading)
        this.updateAlert('info', 'Signing & uploading your profile...')
      else if(registration.registrationSubmitting)
        this.updateAlert('info', 'Submitting your registration to your Blockstack Core node...')
      else if(registration.registrationSubmitted)
        this.updateAlert('success', 'Congrats! Your name is preordered! Registration will automatically complete over the next few hours.')
    }
  }

  displayPricingAndAvailabilityAlerts(registration) {
    let tld = this.state.tlds[this.state.type]
    const domainName = `${this.state.username}.${tld}`

    if(domainName === registration.lastNameEntered) {
      if(registration.names[domainName].error) {
        const error = registration.names[domainName].error
        console.error(error)
        this.updateAlert('danger', `There was a problem checking on price & availability of ${domainName}`)
      } else {
        if(registration.names[domainName].checkingAvailability)
          this.updateAlert('info', `Checking if ${domainName} available...`)
        else if(registration.names[domainName].available) {
          if(registration.names[domainName].checkingPrice) {
            this.updateAlert('info', `${domainName} is available! Checking price...`)
          } else {
            const price = registration.names[domainName].price
            if(price < this.props.coreWalletBalance) {
              this.updateAlert('info', `${domainName} costs ~${price} btc to register.`)
            } else {
              const shortfall = price - this.props.coreWalletBalance
              this.updateAlert('danger', `Your wallet doesn't have enough money to buy ${domainName}. Please send at least ${shortfall} more bitcoin to your wallet.`, WALLET_URL)
            }
          }
        } else {
          this.updateAlert('danger', `${domainName} has already been registered.`)
        }
      }
    }
  }

  displayZeroBalanceAlert() {
    this.updateAlert('danger', `You need to deposit at least 0.01 bitcoins before you can register a username.<br> Click here to go to your wallet or send bitcoins directly to ${this.props.coreWalletAddress}`, WALLET_URL)
  }

  onChange(event) {
    if (event.target.name === 'username') {
      const username = event.target.value.toLowerCase().replace(/\W+/g, ''),
      tld = this.state.tlds[this.state.type],
      domainName = `${username}.${tld}`

      this.setState({
        username: username
      })

      if(username === '') {
        this.setState({
          alerts:[]
        })
        return
      }

      if(this.timer) {
        clearInterval(this.timer)
      }

      event.persist()
      const _this = this

      this.timer = setTimeout( () => {
        if(!isABlockstackName(domainName)) {
          _this.updateAlert('danger', `${domainName} Not valid Blockstack name`)
          return
        }

        this.props.checkNameAvailabilityAndPrice(this.props.api, domainName)

      },
      500) // wait 500ms after user stops typing to check availability
    }
  }

  updateAlert(alertStatus, alertMessage, url=null) {
    this.setState({
      alerts: [{
        status: alertStatus,
        message: alertMessage,
        url: url
      }]
    })
  }

  registerIdentity(event) {
    if (this.state.registrationLock) {
      return
    }

    this.setState({ registrationLock: true })

    const username = this.state.username

    if (username.length === 0) {
      this.updateAlert('danger', 'Name must have at least one character')
      return
    }

    const tld = this.state.tlds[this.state.type],
    domainName = username + '.' + tld

    const nameHasBeenPreordered = hasNameBeenPreordered(domainName, this.props.localIdentities)

    if (nameHasBeenPreordered) {
      this.updateAlert('danger', 'Name has already been preordered')
      this.setState({ registrationLock: false })
    } else {
      const address = this.props.identityAddresses[0]
      const keypair = this.props.identityKeypairs[0]

      this.props.registerName(this.props.api, domainName, address, keypair)
      this.updateAlert('success', 'Name preordered! Waiting for registration confirmation.')
      this.setState({ registrationLock: false })
    }

    const analyticsId = this.props.analyticsId
    mixpanel.track('Register identity', { distinct_id: analyticsId })
    mixpanel.track('Perform action', { distinct_id: analyticsId })
  }

  render() {
    let tld = this.state.tlds[this.state.type],
        nameLabel = this.state.nameLabels[this.state.type]
    return (

            { this.state.alerts.map(function(alert, index) {
              return (
                <Alert key={index} message={alert.message} status={alert.status} url={alert.url} />
              )
            })}
            <fieldset className="form-group">
              {nameLabel}

                <input
                  name="username"
                  className="form-control"
                  placeholder={nameLabel}
                  value={this.state.username}
                  onChange={this.onChange}
                  disabled={this.state.zeroBalance}/>
                .{tld}

            </fieldset>

              <button className="btn btn-blue" onClick={this.registerIdentity}
              disabled={this.props.registration.preventRegistration || this.state.zeroBalance}>
                Register
              </button>

    )
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(RegisterPage)

/*

            Registration Cost

                  {this.state.nameCost} mBTC

<fieldset className="form-group">
  <select name="type" className="c-select"
    defaultValue={this.state.type} onChange={this.onChange}>
    <option value="person">Person</option>
    <option value="organization">Organization</option>
  </select>
</fieldset>
*/
<|endoftext|>


<commit_before>var path = require('path');
var fs = require('fs');
var gulp = require('gulp');
var less = require('gulp-less');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
var gutil = require('gulp-util');
var prettify = require('gulp-prettify');
var jshint = require('gulp-jshint');
var watch = require('gulp-watch');
var plumber = require('gulp-plumber');
var nunjucksRender = require('gulp-nunjucks-render');
var print = require('gulp-print');
var lazypipe = require('lazypipe');
var yaml = require('js-yaml');
var createSchema = require('./yaml-schema');
var gs = require('glob-stream');
var templateCache = require('gulp-angular-templatecache');
var _ = require('lodash');
var connect = require('gulp-connect');
var argv = require('yargs').argv;
var through = require('through2');
var batch = require('gulp-batch');
var spawn = require('child_process').spawn;

var fix_slashes = function(filepath){
  return filepath.replace(/\\/g, '/')
};

var formatPath = function (filePath) {
  return fix_slashes(path.resolve(filePath));
};

var glop = function (pattern, cb) {
  var result = [];
  gs.create(pattern)
    .on('data', function(file){
      file.relative = fix_slashes(path.relative(config.server.root, file.path));
      result.push(file);
    }).on('end', function(){
      cb(null, result);
    });
};

var config, root, client, vendor, build, whichConfig, karmaServer;
if(argv.config) {
  whichConfig = formatPath(argv.config);
}
else{
  whichConfig = formatPath('../config.yaml');
}

root = fix_slashes(path.dirname(whichConfig)) + '/';

var schema = createSchema(root);

var makeConfig = function(){
  var configFile = fs.readFileSync(whichConfig, encoding = 'utf-8');
  config = yaml.load(configFile, { schema: schema});
  client = config.paths.client;
  vendor = config.paths.vendor;
  build = config.paths.build;
  var env = nunjucksRender.nunjucks.configure(client);
  env.addFilter('glop', glop, true);
};

var nunjucksContext = {
  _: _
};

var makeContext = function (context) {
  return _.extend({}, nunjucksContext, context);
};

var copy_to_out = function(out_path) {
  return lazypipe()
    .pipe(rename, path.basename(out_path))
    .pipe(gulp.dest, path.dirname(out_path));
};

var lint = lazypipe()
  .pipe(print, function(filepath){
    return "Linting " + formatPath(filepath);
  })
  .pipe(jshint)
  .pipe(jshint.reporter, 'jshint-stylish');

var build_html = function (html_config) {
  return lazypipe()
    .pipe(nunjucksRender, makeContext(html_config))
    .pipe(prettify, {indent_size: 4})
    .pipe(copy_to_out(html_config.dest));
};

var build_less = function (less_config) {
  return lazypipe()
    .pipe(nunjucksRender, makeContext(less_config))
    .pipe(less, less_config)
    .pipe(copy_to_out(less_config.dest));
};

var build_templates = function (tpl_config) {
  return lazypipe()
    .pipe(rename, function(path){
      path.dirname = "";
    })
    .pipe(templateCache, tpl_config)
    .pipe(copy_to_out(tpl_config.dest));
};

var reload = lazypipe()
  .pipe(print, function (filepath) {
    return "Reloading " + formatPath(filepath);
  }).pipe(connect.reload);

var errorPlumber = function(){
  return plumber({
    errorHandler: function (error) {
      gutil.beep();
      console.log(error);
      this.emit('end');
    }});
};

makeConfig();

gulp.task('templates', function(){
  if(config.templates === undefined) return;
  return gulp.src(config.templates.src)
    .pipe(build_templates(config.templates)())
});

gulp.task('html', ['templates', 'less'], function(){
  if(config.html === undefined) return;
  return gulp.src(config.html.src)
    .pipe(build_html(config.html)());
});

gulp.task('less', function(){
  if(config.less === undefined) return;
  return gulp.src(config.less.src)
    .pipe(build_less(config.less)());
});

gulp.task('lint', function(){
  if(config.lint === undefined) return;
  return gulp.src(config.lint.src)
    .pipe(lint());
});

gulp.task('karma', function(){
  if(config.karma === undefined) return;
  config.karma.singleRun = true;
  return spawn('node', [path.join(__dirname, 'karmaBackground.js'), JSON.stringify(config.karma)],
    {stdio: 'inherit'});
});

gulp.task('copy', function(){
  if(config.copy === undefined) return;
  _([config.copy.src, config.copy.dest])
    .zip()
    .map(function(obj){
      console.log(obj[0]);
      console.log(obj[1]);
      gulp.src(obj[0])
        .pipe(gulp.dest(obj[1]))
    });
});

gulp.task('build', ['templates', 'less', 'html', 'lint', 'copy', 'karma']);

gulp.task('watch', ['templates', 'less', 'html', 'copy', 'lint'], function () {
  if(config.server !== undefined) {
    connect.server({
      root: config.server.root,
      livereload: config.server.livereload,
      port: config.server.port,
      middleware: function(connect, opt){
        return [
          function(req, res, next){
            res.setHeader("Access-Control-Allow-Origin", "*");
            next();
          }
        ]
      }
    });
  }

  var watches = [];
  var addWatch = function (src) {
    watches.push(watch(src));
    return watches[watches.length - 1];
  };

  var addBatchWatch = function(src, cb) {
    watches.push(watch(src, batch(cb)));
    return watches[watches.length -1];
  };

  gulp.src(whichConfig)
    .pipe(watch(whichConfig, function (file) {
      if(file.isNull()) return;
      watches.forEach(function (watch) {
        watch.close();
      });
      if(karmaServer !== undefined) karmaServer.kill('SIGTERM');

      watches = [];
      try {
        console.log('Making config, reloading watches.');
        makeConfig();
      } catch (err) {
        console.log("Error while generating gulp configuration.");
        console.log(err);
        this.emit('end');
      }

      if (config.lint !== undefined && config.lint.watch !== undefined) {
        addWatch(config.lint.watch)
          .pipe(errorPlumber())
          .pipe(lint());
      }

      if (config.html !== undefined && config.html.watch !== undefined) {
        addBatchWatch(config.html.watch, function (files) {
          return gulp.src(config.html.src)
            .pipe(errorPlumber())
            .pipe(build_html(config.html)())
            .pipe(reload());
        });
      }

      if (config.less !== undefined && config.less.watch !== undefined) {
        addBatchWatch(config.less.watch, function (files) {
          return gulp.src(config.less.src)
            .pipe(errorPlumber())
            .pipe(build_less(config.less)())
            .pipe(reload());
        });
      }

      if (config.templates !== undefined && config.templates.watch !== undefined) {
        addBatchWatch(config.templates.watch, function (files) {
          return gulp.src(config.templates.src)
            .pipe(errorPlumber())
            .pipe(build_templates(config.templates)())
            .pipe(reload());
        });
      }

      if (config.copy !== undefined){
        _([config.copy.src, config.copy.dest])
          .zip()
          .map(function(obj){
            addWatch(obj[0])
              .pipe(errorPlumber())
              .pipe(gulp.dest(obj[1]))
              .pipe(reload());
          });
      }

      if (config.karma !== undefined){
        karmaServer = spawn('node',
          [path.join(__dirname, 'karmaBackground.js'), JSON.stringify(config.karma)],
          {stdio: 'inherit'}
        );
      }
    }));
});<commit_msg>Updated gulpfile to handle changing config properly.<commit_after>var path = require('path');
var fs = require('fs');
var gulp = require('gulp');
var less = require('gulp-less');
var rename = require('gulp-rename');
var uglify = require('gulp-uglify');
var gutil = require('gulp-util');
var prettify = require('gulp-prettify');
var jshint = require('gulp-jshint');
var watch = require('gulp-watch');
var plumber = require('gulp-plumber');
var nunjucksRender = require('gulp-nunjucks-render');
var print = require('gulp-print');
var lazypipe = require('lazypipe');
var yaml = require('js-yaml');
var createSchema = require('./yaml-schema');
var gs = require('glob-stream');
var templateCache = require('gulp-angular-templatecache');
var _ = require('lodash');
var connect = require('gulp-connect');
var argv = require('yargs').argv;
var through = require('through2');
var batch = require('gulp-batch');
var spawn = require('child_process').spawn;

var fix_slashes = function(filepath){
  return filepath.replace(/\\/g, '/')
};

var formatPath = function (filePath) {
  return fix_slashes(path.resolve(filePath));
};

var glop = function (pattern, cb) {
  var result = [];
  gs.create(pattern)
    .on('data', function(file){
      file.relative = fix_slashes(path.relative(config.server.root, file.path));
      result.push(file);
    }).on('end', function(){
      cb(null, result);
    });
};

var config, root, client, vendor, build, whichConfig, karmaServer;
if(argv.config) {
  whichConfig = formatPath(argv.config);
}
else{
  whichConfig = formatPath('../config.yaml');
}

root = fix_slashes(path.dirname(whichConfig)) + '/';

var schema = createSchema(root);

var makeConfig = function(){
  var configFile = fs.readFileSync(whichConfig, encoding = 'utf-8');
  config = yaml.load(configFile, { schema: schema});
  client = config.paths.client;
  vendor = config.paths.vendor;
  build = config.paths.build;
  var env = nunjucksRender.nunjucks.configure(client);
  env.addFilter('glop', glop, true);
};

var nunjucksContext = {
  _: _
};

var makeContext = function (context) {
  return _.extend({}, nunjucksContext, context);
};

var copy_to_out = function(out_path) {
  return lazypipe()
    .pipe(rename, path.basename(out_path))
    .pipe(gulp.dest, path.dirname(out_path));
};

var lint = lazypipe()
  .pipe(print, function(filepath){
    return "Linting " + formatPath(filepath);
  })
  .pipe(jshint)
  .pipe(jshint.reporter, 'jshint-stylish');

var build_html = function (html_config) {
  return lazypipe()
    .pipe(nunjucksRender, makeContext(html_config))
    .pipe(prettify, {indent_size: 4})
    .pipe(copy_to_out(html_config.dest));
};

var build_less = function (less_config) {
  return lazypipe()
    .pipe(nunjucksRender, makeContext(less_config))
    .pipe(less, less_config)
    .pipe(copy_to_out(less_config.dest));
};

var build_templates = function (tpl_config) {
  return lazypipe()
    .pipe(rename, function(path){
      path.dirname = "";
    })
    .pipe(templateCache, tpl_config)
    .pipe(copy_to_out(tpl_config.dest));
};

var reload = lazypipe()
  .pipe(print, function (filepath) {
    return "Reloading " + formatPath(filepath);
  }).pipe(connect.reload);

var errorPlumber = function(){
  return plumber({
    errorHandler: function (error) {
      gutil.beep();
      console.log(error);
      this.emit('end');
    }});
};

makeConfig();

gulp.task('config', function(){
  try{
    makeConfig();
  } catch(err) {
    console.log("Error while generating gulp configuration.");
    console.log(err);
    config = {};
  }
});

gulp.task('templates', ['config'], function(){
  if(config.templates === undefined) return;
  return gulp.src(config.templates.src)
    .pipe(build_templates(config.templates)())
});

gulp.task('html', ['config', 'templates', 'less'], function(){
  if(config.html === undefined) return;
  return gulp.src(config.html.src)
    .pipe(build_html(config.html)());
});

gulp.task('less', ['config'], function(){
  if(config.less === undefined) return;
  return gulp.src(config.less.src)
    .pipe(build_less(config.less)());
});

gulp.task('lint', ['config'], function(){
  if(config.lint === undefined) return;
  return gulp.src(config.lint.src)
    .pipe(lint());
});

gulp.task('karma', ['config'], function(){
  if(config.karma === undefined) return;
  config.karma.singleRun = true;
  return spawn('node', [path.join(__dirname, 'karmaBackground.js'), JSON.stringify(config.karma)],
    {stdio: 'inherit'});
});

gulp.task('copy', ['config'], function(){
  if(config.copy === undefined) return;
  _([config.copy.src, config.copy.dest])
    .zip()
    .map(function(obj){
      console.log(obj[0]);
      console.log(obj[1]);
      gulp.src(obj[0])
        .pipe(gulp.dest(obj[1]))
    });
});

gulp.task('build', ['templates', 'less', 'html', 'lint', 'copy', 'karma']);

gulp.task('prewatch', ['templates', 'less', 'html', 'copy', 'lint']);

gulp.task('watch', ['prewatch'], function () {
  if(config.server !== undefined) {
    connect.server({
      root: config.server.root,
      livereload: config.server.livereload,
      port: config.server.port,
      middleware: function(connect, opt){
        return [
          function(req, res, next){
            res.setHeader("Access-Control-Allow-Origin", "*");
            next();
          }
        ]
      }
    });
  }

  var watches = [];
  var addWatch = function (src) {
    watches.push(watch(src));
    return watches[watches.length - 1];
  };

  var addBatchWatch = function(src, cb) {
    watches.push(watch(src, batch(cb)));
    return watches[watches.length -1];
  };

  gulp.watch(whichConfig, ['prewatch']);

  gulp.src(whichConfig)
    .pipe(watch(whichConfig, function (file) {
      if(file.isNull()) return;
      watches.forEach(function (watch) {
        watch.close();
      });
      if(karmaServer !== undefined) karmaServer.kill('SIGTERM');

      watches = [];
      try {
        console.log('Making config, reloading watches.');
        makeConfig();
      } catch (err) {
        console.log('Configuration error.');
        config = {};
      }

      if (config.lint !== undefined && config.lint.watch !== undefined) {
        addWatch(config.lint.watch)
          .pipe(errorPlumber())
          .pipe(lint());
      }

      if (config.html !== undefined && config.html.watch !== undefined) {
        addBatchWatch(config.html.watch, function (files) {
          return gulp.src(config.html.src)
            .pipe(errorPlumber())
            .pipe(build_html(config.html)())
            .pipe(reload());
        });
      }

      if (config.less !== undefined && config.less.watch !== undefined) {
        addBatchWatch(config.less.watch, function (files) {
          return gulp.src(config.less.src)
            .pipe(errorPlumber())
            .pipe(build_less(config.less)())
            .pipe(reload());
        });
      }

      if (config.templates !== undefined && config.templates.watch !== undefined) {
        addBatchWatch(config.templates.watch, function (files) {
          return gulp.src(config.templates.src)
            .pipe(errorPlumber())
            .pipe(build_templates(config.templates)())
            .pipe(reload());
        });
      }

      if (config.copy !== undefined){
        _([config.copy.src, config.copy.dest])
          .zip()
          .map(function(obj){
            addWatch(obj[0])
              .pipe(errorPlumber())
              .pipe(gulp.dest(obj[1]))
              .pipe(reload());
          });
      }

      if (config.karma !== undefined){
        karmaServer = spawn('node',
          [path.join(__dirname, 'karmaBackground.js'), JSON.stringify(config.karma)],
          {stdio: 'inherit'}
        );
      }
    }));
});<|endoftext|>


<commit_before>/* jslint node: true */
'use strict';

var React = require('react');
var ReactDom = require('react-dom');
var MapboxSocket = require('lightstream-socket').MapboxSocket;
var VelomaggCollection = require('./VelomaggCollection');
var MapQuadtree = require('../lib/MapQuadtree');
var BoundingBox = require('../lib/BoundingBox');
var L = require('mapbox.js');

var VeloMap = require('./VeloMap.jsx');

function MapFlow(config, i18n) {
  this.config = config;
  this.i18n = i18n;
}

MapFlow.prototype.setUp = function setUp() {
  this.velomagg = new VelomaggCollection();

  this._initializeMap();
  this._initializeQuadtree();
  this._createLayer();
  this._openSocket();
};

MapFlow.prototype._initializeMap = function _initializeMap() {
  var leaflet_bounds = L.latLngBounds(
    L.latLng(this.config.bounding_box.south_west[1],this.config.bounding_box.south_west[0]),
    L.latLng(this.config.bounding_box.north_east[1],this.config.bounding_box.north_east[0])
  );
  this.map = L.mapbox.map('map')
    .fitBounds(leaflet_bounds);
};

MapFlow.prototype._initializeQuadtree = function _initializeQuadtree() {
  var bounds = this._fixedQuadtreeBounds();
  var quadtree = this.quadtree = new MapQuadtree(bounds, 1);

  this.velomagg.on('add', function (model) {
    quadtree.addItem(model);
  });
};

MapFlow.prototype._fixedQuadtreeBounds = function _fixedQuadtreeBounds() {
  return new BoundingBox(
    this.config.bounding_box.south_west[0],
    this.config.bounding_box.south_west[1],
    this.config.bounding_box.north_east[0],
    this.config.bounding_box.north_east[1]);
};

MapFlow.prototype._createLayer = function _createLayer() {
  var mapElement = React.createElement(VeloMap, {
    quadtree: this.quadtree,
    map: this.map,
    config: this.config,
    i18n: this.i18n
  });
  ReactDom.render(mapElement, document.getElementById('map-component'));
};

MapFlow.prototype._openSocket = function _openSocket() {
  var options = {
    retry_interval: 5000
  };
  var socket = new MapboxSocket('ws://' + this.config.hostname + '/socket/', 'station', options);

  socket.on('opened', function () {
    if (!this.map_attached) {
      socket.attachMap(this.map);
      this.map_attached = true;
    }
  }.bind(this));

  socket.on('new_items', function (station) {
    this.velomagg.set([station], { remove:false });
  }.bind(this));

  socket.on('error', function (error) {
    console.log('error in socket', error);
  });

  socket.on('closed', function () {
    console.log('socket has been closed');
  });

  socket.connect();
};

module.exports = MapFlow;
<commit_msg>task: avoid checking if map has already been attached, due to socket lib enhancement<commit_after>/* jslint node: true */
'use strict';

var React = require('react');
var ReactDom = require('react-dom');
var MapboxSocket = require('lightstream-socket').MapboxSocket;
var VelomaggCollection = require('./VelomaggCollection');
var MapQuadtree = require('../lib/MapQuadtree');
var BoundingBox = require('../lib/BoundingBox');
var L = require('mapbox.js');

var VeloMap = require('./VeloMap.jsx');

function MapFlow(config, i18n) {
  this.config = config;
  this.i18n = i18n;
}

MapFlow.prototype.setUp = function setUp() {
  this.velomagg = new VelomaggCollection();

  this._initializeMap();
  this._initializeQuadtree();
  this._createLayer();
  this._openSocket();
};

MapFlow.prototype._initializeMap = function _initializeMap() {
  var leaflet_bounds = L.latLngBounds(
    L.latLng(this.config.bounding_box.south_west[1],this.config.bounding_box.south_west[0]),
    L.latLng(this.config.bounding_box.north_east[1],this.config.bounding_box.north_east[0])
  );
  this.map = L.mapbox.map('map')
    .fitBounds(leaflet_bounds);
};

MapFlow.prototype._initializeQuadtree = function _initializeQuadtree() {
  var bounds = this._fixedQuadtreeBounds();
  var quadtree = this.quadtree = new MapQuadtree(bounds, 1);

  this.velomagg.on('add', function (model) {
    quadtree.addItem(model);
  });
};

MapFlow.prototype._fixedQuadtreeBounds = function _fixedQuadtreeBounds() {
  return new BoundingBox(
    this.config.bounding_box.south_west[0],
    this.config.bounding_box.south_west[1],
    this.config.bounding_box.north_east[0],
    this.config.bounding_box.north_east[1]);
};

MapFlow.prototype._createLayer = function _createLayer() {
  var mapElement = React.createElement(VeloMap, {
    quadtree: this.quadtree,
    map: this.map,
    config: this.config,
    i18n: this.i18n
  });
  ReactDom.render(mapElement, document.getElementById('map-component'));
};

MapFlow.prototype._openSocket = function _openSocket() {
  var options = {
    retry_interval: 5000
  };
  var socket = new MapboxSocket('ws://' + this.config.hostname + '/socket/', 'station', options);

  socket.on('opened', function () {
    socket.attachMap(this.map);
  }.bind(this));

  socket.on('new_items', function (station) {
    this.velomagg.set([station], { remove:false });
  }.bind(this));

  socket.on('error', function (error) {
    console.log('error in socket', error);
  });

  socket.on('closed', function () {
    console.log('socket has been closed');
  });

  socket.connect();
};

module.exports = MapFlow;
<|endoftext|>


<commit_before>ngDefine('cockpit.directives', [ 'angular' ], function(module, angular) {
  'use strict';

  module.directive('viewPills', [ '$location', 'Views', function($location, Views) {

    var ViewPillsController = [ '$scope', 'Views', '$location', function($scope, Views, $location) {
      var providers = Views.getProviders({ component: $scope.id });
      $scope.providers = providers;

      $scope.isActive = function(provider) {
        return $location.path().indexOf('/' + provider.id) != -1;
      };

      $scope.getUrl = function(provider) {
        return '#' + $location.path().replace(/[^\/]*$/, provider.id);
      };
    }];

    return {
      restrict: 'EAC',
      scope: {
        id: '@'
      },
      template:
'
' +
'
{{ provider.label }}
' +
'
',
      replace: true,
      controller: ViewPillsController
    };
  }]);
});<commit_msg>improve(cockpit/views): add a css class to the view pills<commit_after>/* global ngDefine: false */
ngDefine('cockpit.directives', [], function(module) {
  'use strict';

  module.directive('viewPills', [
  function() {
    var ViewPillsController = [
      '$scope',
      'Views',
      '$location',
    function($scope, Views, $location) {
      var providers = Views.getProviders({ component: $scope.id });
      $scope.providers = providers;

      $scope.isActive = function(provider) {
        return $location.path().indexOf('/' + provider.id) != -1;
      };

      $scope.getUrl = function(provider) {
        return '#' + $location.path().replace(/[^\/]*$/, provider.id);
      };
    }];

    return {
      restrict: 'EAC',
      scope: {
        id: '@'
      },
      template:
'
' +
'
' +
'    {{ provider.label }}' +
'
' +
'
',
      replace: true,
      controller: ViewPillsController
    };
  }]);
});
<|endoftext|>


<commit_before>//Matthew Shotton, R&D User Experience,© BBC 2015
import GraphNode from "../graphnode";
import { compileShader, createShaderProgram, createElementTexutre, updateTexture } from "../utils.js";
import { RenderException } from "../exceptions.js";

class ProcessingNode extends GraphNode{
    /**
    * Initialise an instance of a ProcessingNode.
    *
    * This class is not used directly, but is extended to create CompositingNodes, TransitionNodes, and EffectNodes.
    */
    constructor(gl, renderGraph, definition, inputNames, limitConnections){
        super(gl, renderGraph, inputNames, limitConnections);
        this._vertexShader = compileShader(gl, definition.vertexShader, gl.VERTEX_SHADER);
        this._fragmentShader = compileShader(gl, definition.fragmentShader, gl.FRAGMENT_SHADER);
        this._definition = definition;
        this._properties = {};//definition.properties;
        //copy definition properties
        for(let propertyName in definition.properties){
            let propertyValue = definition.properties[propertyName].value;
            //if an array then shallow copy it
            if(Object.prototype.toString.call(propertyValue) === "[object Array]"){
                propertyValue = definition.properties[propertyName].value.slice();
            }
            let propertyType = definition.properties[propertyName].type;
            this._properties[propertyName] = {type:propertyType, value:propertyValue};
        }

        this._inputTextureUnitMapping =[];
        this._maxTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
        this._boundTextureUnits = 0;
        this._parameterTextureCount = 0;
        this._inputTextureCount = 0;
        this._texture = createElementTexutre(gl);
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        //compile the shader
        this._program = createShaderProgram(gl, this._vertexShader, this._fragmentShader);

        //create and setup the framebuffer
        this._framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._texture,0);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        //create properties on this object for the passed properties
        for (let propertyName in this._properties){
            Object.defineProperty(this, propertyName, {
                get:function(){return this._properties[propertyName].value;},
                set:function(passedValue){this._properties[propertyName].value = passedValue;}
            });
        }

        //create texutres for any texture properties
        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image){
                this._properties[propertyName].texture = createElementTexutre(gl);
                this._properties[propertyName].texutreUnit = gl.TEXTURE0 + this._boundTextureUnits;
                this._boundTextureUnits += 1;
                this._parameterTextureCount +=1;
                if (this._boundTextureUnits > this._maxTextureUnits){
                    throw new RenderException("Trying to bind more than available textures units to shader");
                }
            }
        }

        //calculate texutre units for input textures
        for(let inputName of definition.inputs){
            this._inputTextureUnitMapping.push({name:inputName, textureUnit:gl.TEXTURE0 + this._boundTextureUnits});
            this._boundTextureUnits += 1;
            this._inputTextureCount += 1;
            if (this._boundTextureUnits > this._maxTextureUnits){
                throw new RenderException("Trying to bind more than available textures units to shader");
            }
        }

        //find the locations of the properties in the compiled shader
        for (let propertyName in this._properties){
            if (this._properties[propertyName].type === "uniform"){
                this._properties[propertyName].location = this._gl.getUniformLocation(this._program, propertyName);
            }
        }
        this._currentTimeLocation = this._gl.getUniformLocation(this._program, "currentTime");
        this._currentTime = 0;

        //Other setup
        let positionLocation = gl.getAttribLocation(this._program, "a_position");
        let buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([
                1.0, 1.0,
                0.0, 1.0,
                1.0, 0.0,
                1.0, 0.0,
                0.0, 1.0,
                0.0, 0.0]),
            gl.STATIC_DRAW);
        let texCoordLocation = gl.getAttribLocation(this._program, "a_texCoord");
        gl.enableVertexAttribArray(texCoordLocation);
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
        this._displayName = "ProcessingNode";

    }

    /**
    * Sets the passed processing node property to the passed value.
    * @param {string} name - The name of the processing node parameter to modify.
    * @param {Object} value - The value to set it to.
    *
    * @example
    * var ctx = new VideoContext();
    * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
    * monoNode.setProperty("inputMix", [1.0,0.0,0.0]); //Just use red channel
    */
    setProperty(name, value){
        this._properties[name].value = value;
    }

    /**
    * Sets the passed processing node property to the passed value.
    * @param {string} name - The name of the processing node parameter to get.
    *
    * @example
    * var ctx = new VideoContext();
    * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
    * console.log(monoNode.getProperty("inputMix")); //Will output [0.4,0.6,0.2], the default value from the effect definition.
    *
    */
    getProperty(name){
        return this._properties[name].value;
    }

    /**
    * Destroy and clean-up the node.
    */
    destroy(){
        super.destroy();
        //destrpy texutres for any texture properties
        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image){
                this._gl.deleteTexture(this._properties[propertyName].texture);
                this._texture = undefined;
            }
        }
        //Detach shaders
        this._gl.detachShader(this._program, this._vertexShader);
        this._gl.detachShader(this._program, this._fragmentShader);
        //Delete shaders
        this._gl.deleteShader(this._vertexShader);
        this._gl.deleteShader(this._fragmentShader);
        //Delete program
        this._gl.deleteProgram(this._program);
        //Delete Framebuffer
        this._gl.deleteFramebuffer(this._framebuffer);
    }

    _update(currentTime){
        this._currentTime = currentTime;
    }

    _seek(currentTime){
        this._currentTime = currentTime;
    }

    _render(){
        this._rendered = true;
        let gl = this._gl;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

        gl.useProgram(this._program);

        //upload the default uniforms
        gl.uniform1f(this._currentTimeLocation, parseFloat(this._currentTime));

        //upload/update the custom uniforms
        let textureOffset = 0;

        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            let propertyType = this._properties[propertyName].type;
            let propertyLocation = this._properties[propertyName].location;
            if (propertyType !== "uniform") continue;

            if (typeof propertyValue === "number"){
                gl.uniform1f(propertyLocation, propertyValue);
            }
            else if( Object.prototype.toString.call(propertyValue) === "[object Array]"){
                if(propertyValue.length === 1){
                    gl.uniform1fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 2){
                    gl.uniform2fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 3){
                    gl.uniform3fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 4){
                    gl.uniform4fv(propertyLocation, propertyValue);
                } else{
                    console.debug("Shader parameter", propertyName, "is too long an array:", propertyValue);
                }
            } else if(propertyValue instanceof Image){
                let texture =  this._properties[propertyName].texture;
                let textureUnit = this._properties[propertyName].texutreUnit;
                updateTexture(gl, texture, propertyValue);

                gl.activeTexture(textureUnit);
                gl.uniform1i(propertyLocation, textureOffset);
                textureOffset += 1;
                gl.bindTexture(gl.TEXTURE_2D, texture);
            }
            else{
                //TODO - add tests for textures
                /*gl.activeTexture(gl.TEXTURE0 + textureOffset);
                gl.uniform1i(parameterLoctation, textureOffset);
                gl.bindTexture(gl.TEXTURE_2D, textures[textureOffset-1]);*/
            }
        }

    }
}

export default ProcessingNode;
<commit_msg>fixed missing texture delete in processing node destroy method<commit_after>//Matthew Shotton, R&D User Experience,© BBC 2015
import GraphNode from "../graphnode";
import { compileShader, createShaderProgram, createElementTexutre, updateTexture } from "../utils.js";
import { RenderException } from "../exceptions.js";

class ProcessingNode extends GraphNode{
    /**
    * Initialise an instance of a ProcessingNode.
    *
    * This class is not used directly, but is extended to create CompositingNodes, TransitionNodes, and EffectNodes.
    */
    constructor(gl, renderGraph, definition, inputNames, limitConnections){
        super(gl, renderGraph, inputNames, limitConnections);
        this._vertexShader = compileShader(gl, definition.vertexShader, gl.VERTEX_SHADER);
        this._fragmentShader = compileShader(gl, definition.fragmentShader, gl.FRAGMENT_SHADER);
        this._definition = definition;
        this._properties = {};//definition.properties;
        //copy definition properties
        for(let propertyName in definition.properties){
            let propertyValue = definition.properties[propertyName].value;
            //if an array then shallow copy it
            if(Object.prototype.toString.call(propertyValue) === "[object Array]"){
                propertyValue = definition.properties[propertyName].value.slice();
            }
            let propertyType = definition.properties[propertyName].type;
            this._properties[propertyName] = {type:propertyType, value:propertyValue};
        }

        this._inputTextureUnitMapping =[];
        this._maxTextureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS);
        this._boundTextureUnits = 0;
        this._parameterTextureCount = 0;
        this._inputTextureCount = 0;
        this._texture = createElementTexutre(gl);
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.canvas.width, gl.canvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        //compile the shader
        this._program = createShaderProgram(gl, this._vertexShader, this._fragmentShader);

        //create and setup the framebuffer
        this._framebuffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, this._framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this._texture,0);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        //create properties on this object for the passed properties
        for (let propertyName in this._properties){
            Object.defineProperty(this, propertyName, {
                get:function(){return this._properties[propertyName].value;},
                set:function(passedValue){this._properties[propertyName].value = passedValue;}
            });
        }

        //create texutres for any texture properties
        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image){
                this._properties[propertyName].texture = createElementTexutre(gl);
                this._properties[propertyName].texutreUnit = gl.TEXTURE0 + this._boundTextureUnits;
                this._boundTextureUnits += 1;
                this._parameterTextureCount +=1;
                if (this._boundTextureUnits > this._maxTextureUnits){
                    throw new RenderException("Trying to bind more than available textures units to shader");
                }
            }
        }

        //calculate texutre units for input textures
        for(let inputName of definition.inputs){
            this._inputTextureUnitMapping.push({name:inputName, textureUnit:gl.TEXTURE0 + this._boundTextureUnits});
            this._boundTextureUnits += 1;
            this._inputTextureCount += 1;
            if (this._boundTextureUnits > this._maxTextureUnits){
                throw new RenderException("Trying to bind more than available textures units to shader");
            }
        }

        //find the locations of the properties in the compiled shader
        for (let propertyName in this._properties){
            if (this._properties[propertyName].type === "uniform"){
                this._properties[propertyName].location = this._gl.getUniformLocation(this._program, propertyName);
            }
        }
        this._currentTimeLocation = this._gl.getUniformLocation(this._program, "currentTime");
        this._currentTime = 0;

        //Other setup
        let positionLocation = gl.getAttribLocation(this._program, "a_position");
        let buffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.enableVertexAttribArray(positionLocation);
        gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bufferData(
            gl.ARRAY_BUFFER,
            new Float32Array([
                1.0, 1.0,
                0.0, 1.0,
                1.0, 0.0,
                1.0, 0.0,
                0.0, 1.0,
                0.0, 0.0]),
            gl.STATIC_DRAW);
        let texCoordLocation = gl.getAttribLocation(this._program, "a_texCoord");
        gl.enableVertexAttribArray(texCoordLocation);
        gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
        this._displayName = "ProcessingNode";

    }

    /**
    * Sets the passed processing node property to the passed value.
    * @param {string} name - The name of the processing node parameter to modify.
    * @param {Object} value - The value to set it to.
    *
    * @example
    * var ctx = new VideoContext();
    * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
    * monoNode.setProperty("inputMix", [1.0,0.0,0.0]); //Just use red channel
    */
    setProperty(name, value){
        this._properties[name].value = value;
    }

    /**
    * Sets the passed processing node property to the passed value.
    * @param {string} name - The name of the processing node parameter to get.
    *
    * @example
    * var ctx = new VideoContext();
    * var monoNode = ctx.effect(VideoContext.DEFINITIONS.MONOCHROME);
    * console.log(monoNode.getProperty("inputMix")); //Will output [0.4,0.6,0.2], the default value from the effect definition.
    *
    */
    getProperty(name){
        return this._properties[name].value;
    }

    /**
    * Destroy and clean-up the node.
    */
    destroy(){
        super.destroy();
        //destrpy texutres for any texture properties
        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            if (propertyValue instanceof Image){
                this._gl.deleteTexture(this._properties[propertyName].texture);
                this._texture = undefined;
            }
        }
        //Destroy main
        this._gl.deleteTexture(this._texture);
        this._texture = undefined;
        //Detach shaders
        this._gl.detachShader(this._program, this._vertexShader);
        this._gl.detachShader(this._program, this._fragmentShader);
        //Delete shaders
        this._gl.deleteShader(this._vertexShader);
        this._gl.deleteShader(this._fragmentShader);
        //Delete program
        this._gl.deleteProgram(this._program);
        //Delete Framebuffer
        this._gl.deleteFramebuffer(this._framebuffer);
    }

    _update(currentTime){
        this._currentTime = currentTime;
    }

    _seek(currentTime){
        this._currentTime = currentTime;
    }

    _render(){
        this._rendered = true;
        let gl = this._gl;
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

        gl.useProgram(this._program);

        //upload the default uniforms
        gl.uniform1f(this._currentTimeLocation, parseFloat(this._currentTime));

        //upload/update the custom uniforms
        let textureOffset = 0;

        for (let propertyName in this._properties){
            let propertyValue = this._properties[propertyName].value;
            let propertyType = this._properties[propertyName].type;
            let propertyLocation = this._properties[propertyName].location;
            if (propertyType !== "uniform") continue;

            if (typeof propertyValue === "number"){
                gl.uniform1f(propertyLocation, propertyValue);
            }
            else if( Object.prototype.toString.call(propertyValue) === "[object Array]"){
                if(propertyValue.length === 1){
                    gl.uniform1fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 2){
                    gl.uniform2fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 3){
                    gl.uniform3fv(propertyLocation, propertyValue);
                } else if(propertyValue.length === 4){
                    gl.uniform4fv(propertyLocation, propertyValue);
                } else{
                    console.debug("Shader parameter", propertyName, "is too long an array:", propertyValue);
                }
            } else if(propertyValue instanceof Image){
                let texture =  this._properties[propertyName].texture;
                let textureUnit = this._properties[propertyName].texutreUnit;
                updateTexture(gl, texture, propertyValue);

                gl.activeTexture(textureUnit);
                gl.uniform1i(propertyLocation, textureOffset);
                textureOffset += 1;
                gl.bindTexture(gl.TEXTURE_2D, texture);
            }
            else{
                //TODO - add tests for textures
                /*gl.activeTexture(gl.TEXTURE0 + textureOffset);
                gl.uniform1i(parameterLoctation, textureOffset);
                gl.bindTexture(gl.TEXTURE_2D, textures[textureOffset-1]);*/
            }
        }

    }
}

export default ProcessingNode;
<|endoftext|>


<commit_before>'use strict';

const flatten = require('lodash.flatten');
const upperFirst = require('lodash.upperfirst');
const assert = require('assert');
const ERROR = 'error';
const ON = 'on';
const HIERARCHY_SEPARATOR = '.';
const DEFAULT_LIFECYCLE = ['before', ON, 'after'];
const DEFAULT_TIMEOUT = 300;

/**
 * Listeners by event.
 *
 * [message]: {
 *   before: ...Function,
 *   on: ...Function,
 *   after: ...Function,
 * }
 *
 * @var Object
 */
let listeners = {};

/**
 * Procedures referenced by the name.
 *
 * [name]: Function
 *
 * @var Object
 */
let procedures = {};

/**
 * Namespaced HotPress instances.
 *
 * @var Object<String: HotPress>
 */
let namespaces = {};

/**
 * Returns all the listeners for a given message. The returned value will be an
 * object whose keys are 'before', 'on' and 'after' of which each will contain
 * an array of functions/listeners.
 *
 * @param String message
 * @return Object<before: [], on: [], after: []>
 */
function getListenersFor(message) {
  if (!listeners[message]) {
    listeners[message] = this.lifecycle.reduce((acc, method) => (
      Object.assign(acc, {[method]: []})
    ), {});
  }
  return listeners[message];
}

/**
 * Returns an immutable version of all listeners on a particular event
 * name/message.
 *
 * @param String message
 * @return []
 */
function getAllListenersFor(message) {
  return Object
    .keys(listeners[message] || {})
    .reduce((acc, part) => acc.concat(listeners[message][part]), []);
}

/**
 * Returns a list of messages that should be called in order for a specific
 * message
 *
 * @param String message
 * @return String[]
 */
function getHierarchy(message) {
  let parts = message.split(HIERARCHY_SEPARATOR);
  let hierarchy = [parts[0], '*'];
  parts.reduce((message, part) => {
    let prefix = message + HIERARCHY_SEPARATOR;
    let newMessage = prefix + part;
    let wildcard = prefix + '*';
    hierarchy.unshift(newMessage, wildcard);
    return newMessage;
  });
  return hierarchy;
}

/**
 * Prepends an event name, if the string exists.
 *
 * @param String name
 * @param String prefix
 * @return String
 */
function prependEventName(name, prefix) {
  return prefix ? `${prefix}.${name}` : name;
}

/**
 * Adds a listener to a specific part of an event's lifecycle.
 *
 * @param String part
 * @param String message
 * @param Function fn
 */
function onPart(part, message, fn) {
  fn._hpRemoved = false;
  getListenersFor.call(this, prependEventName(message, this.prefix))[part].push(fn);
}

/**
 * Adds a listener to a specific part of the event lifecycle and removes it
 * as soon as it's been called.
 *
 * @param Function subscribe
 * @param String message
 * @param Function fn
 */
function oncePart(subscribe, message, fn) {
  let subscriber = (...args) => {
    this.off(message, subscriber);
    return fn(...args);
  };
  subscribe(message, subscriber);
}

/**
 * Create a promise and reject it in a given amount of milliseconds.
 *
 * @param Number timeout
 * @return Promise
 */
function errorAfterMS(timeout) {
  return new Promise((_, reject) => {
    if (typeof timeout === 'number') setTimeout(
      () => reject(new HotPressTimeoutError(timeout)),
      timeout
    );
  });
}

/**
 * Subscribes emittion of an array of events to another event. All data will be
 * passed to the emittions.
 *
 * @param Function subscribe
 * @param String message
 * @param String[] triggers
 */
function triggersPart(subscribe, message, triggers) {
  subscribe(message, (_, ...data) => Promise.all(
    triggers.map(message => this.emit(message, ...data))
  ));
}

/**
 * Creates an emitter based on the event name/message and data. The emitter
 * can then be used to emit each part of the lifecycle.
 *
 * @param String message
 * @param Any[] data
 * @return Function
 */
function createEmitter(message, data) {
  const hierarchy = getHierarchy(message);

  const call = fn => Promise
    .resolve()
    .then(() => !fn._hpRemoved && fn(message, ...data));

  const promise = fn => Promise
    .race([errorAfterMS(this.timeout), call(fn)])
    .catch(error => emit.call(this, prependEventName(message, ERROR), [error]));

  return part => Promise.all(flatten(
    hierarchy.map(message => getListenersFor.call(this, message)[part].map(promise))
  ));
}

/**
 * Begin the event lifecycle for a given event and data.
 *
 * @param String message
 * @param Any[] data
 * @param Number timeout  Optional amount of milliseconds to try each listener
 * @return Promise
 */
function emit(message, data) {
  let emit = createEmitter.call(this, message, data);
  return this.lifecycle.reduce((promise, method) => (
    promise.then(() => emit(method))
  ), Promise.resolve());
}

/**
 * Finds and returns duplicates in an array.
 *
 * @param Any[]
 * @return Any[]
 */
function findDuplicates(arr) {
  let uniques = arr
    .map(it => ({count: 1, item: it}))
    .reduce(
      (acc, it) => Object.assign(acc, {[it.item]: (acc[it.item] || 0) + it.count}),
      {}
    );
  return Object
    .keys(uniques)
    .filter(it => uniques[it] > 1);
}

/**
 * Removes a listener from a given event.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
function removeListener(message, fn) {
  let listeners = getListenersFor.call(this, message);
  let removed = 0;
  for (let key in listeners) {
    let set = listeners[key];
    let index = set.indexOf(fn);
    if (index !== -1) {
      set[index]._hpRemoved = true;
      set.splice(index, 1);
      removed++;
      break;
    }
  }
  return removed;
}

/**
 * Removes all listeners from a given event.
 *
 * @param String message
 * @return Number
 */
function removeAllListeners(message) {
  let all = getAllListenersFor(message);
  let amount = all ? all.length : 0;
  all.forEach(listener => listener._hpRemoved = true);
  delete listeners[message];
  return amount;
}

/**
 * Returns a method name we may use to attach a single listener.
 *
 * @param String method
 * @return String
 */
function singularMethodName(method) {
  return method === ON ? 'once' : `once${upperFirst(method)}`;
}

/**
 * Returns a method name we may use as the trigger listener.
 *
 * @param String method
 * @param String
 */
function triggerMethodName(method) {
  return method === ON ? 'triggers' : `triggers${upperFirst(method)}`;
}

/**
 * The exposable methods for each HotPress namespace.
 *
 * @class HotPress
 * @param String prefix
 * @param String[] lifecycle
 * @param Number timeout
 */
class HotPress {

  constructor(
    prefix = '',
    lifecycle = DEFAULT_LIFECYCLE,
    timeout = DEFAULT_TIMEOUT
  ) {
    /**
     * The prefix to add to all messages
     *
     * @prop String prefix
     */
    this.prefix = prefix;

    /**
     * The lifecycle list
     * @prop String[] lifecycle
     */
    this._lifecycle = [];
    this.lifecycle = lifecycle;

    /**
     * Timeout to stop long processes within the event lifecycle.
     *
     * @prop Number timeout
     */
    this.timeout = timeout;
  }

  get lifecycle() {
    return this._lifecycle;
  }

  set lifecycle(lifecycle) {
    let {_lifecycle} = this;
    let duplicates = findDuplicates(lifecycle);
    this._lifecycle = lifecycle;

    assert(
      !duplicates.length,
      `Lifecycle contains duplicates (${duplicates})`
    );

    assert(
      ~lifecycle.indexOf(ON),
      `Lifecycle must contain an "on" method (${lifecycle})`
    );

    _lifecycle.forEach(method => { delete this[method]; });

    lifecycle.forEach(method => {
      let singularName = singularMethodName(method);
      let triggersName = triggerMethodName(method);
      let singularTriggersName = triggerMethodName(singularName);

      this[method] = onPart.bind(this, method);
      this[singularName] = oncePart.bind(this, this[method]);
      this[triggersName] = triggersPart.bind(this, this[method]);
      this[singularTriggersName] = triggersPart.bind(this, this[singularName]);
    });
  }

  /**
   * Will call the listener once each of the specified events have been emitted.
   * The events are given as an object, where each key will specify the part of
   * the event lifecycle and the value is an array of event names/messages.
   *
   * ```
   * all({
   *   before: ['foo', 'bar'],
   *   on: ['another'],
   *   after: ['event']
   * }, ({foo, bar, another, event}) => {
   *   // You'll receive data for each event that fired
   * });
   * ```
   *
   * @param Object<before: String[], on: String[], after: String[]> messages
   * @param Function fn
   */
  all(messages, fn) {
    let toDo;
    let dataCollection;
    let size = Object
      .keys(messages)
      .reduce((size, part) => size + messages[part].length, 0);

    const subscriber = (message, ...data) => {
      dataCollection[message] = data;
      toDo--;
      if (!toDo) {
        fn(dataCollection);
        init();
      }
    };

    const registerSubscribers = (prop, method) => {
      if (messages[prop]) messages[prop].forEach(message => {
        method(message, subscriber);
      });
    };

    const init = () => {
      toDo = size;
      dataCollection = {};
      this.lifecycle.forEach(method => {
        registerSubscribers(method, this[singularMethodName(method)]);
      });
    };

    init();
  }

  /**
   * Removes the specified listener from a given event. If no listener is
   * specified, all will be removed.
   *
   * @param String message
   * @param Function fn
   * @return Number
   */
  off(message, fn) {
    message = prependEventName(message, this.prefix);
    return fn
      ? removeListener.call(this, message, fn)
      : removeAllListeners.call(this, message);
  }

  /**
   * Adds a listener to the end of the event lifecycle for just one emittion.
   *
   * @param String message
   * @param ...Any data
   * @return Promise
   */
  emit(message, ...data) {
    message = prependEventName(message, this.prefix);
    return emit.call(this, message, data);
  }

  /**
   * Creates another version of hot press where all events are prefixed with
   * the given string.
   *
   * @param String namespace
   * @return HotPress
   */
  ns(name) {
    let fullName = prependEventName(name, this.prefix);
    if (namespaces[fullName]) return namespaces[fullName];
    return name.split(HIERARCHY_SEPARATOR).reduce((parent, name) => {
      name = prependEventName(name, parent.prefix);
      return namespaces[name] = namespaces[name] || new HotPress(
        name, parent.lifecycle, parent.timeout
      );
    }, this);
  }

  /**
   * Register a process to a name.
   *
   * @param String name
   * @param Function proc
   * @throws HotPressExistingProcedureError
   */
  reg(name, proc) {
    name = prependEventName(name, this.prefix);
    proc._hpRemove = false;
    if (procedures[name]) throw new HotPressExistingProcedureError(name);
    procedures[name] = proc;
  }

  /**
   * Deregisters a process.
   *
   * @param String name
   * @return Number
   */
  dereg(name) {
    name = prependEventName(name, this.prefix);
    if (procedures[name]) {
      procedures[name]._hpRemoved = true;
      delete procedures[name];
      return 1;
    }
    return 0;
  }

  /**
   * Calls a procedure and begins an event lifecycle too.
   *
   * @param String name
   * @param ...Any
   * @return Promise
   */
  call(name, ...data) {
    name = prependEventName(name, this.prefix);
    let emit = createEmitter.call(this, name, data);
    let proc = procedures[name] || (() => {});
    return this.lifecycle.reduce((promise, method) => promise.then(() => (
      method === ON
        ? Promise.all([
          !proc._hpRemoved && proc(...data),
          emit(method)
        ])
        : emit(method)
    )), Promise.resolve());
  }

}

class HotPressTimeoutError extends Error {
  constructor(ms) {
    super(`Exceeded ${ms}ms`);
  }
}

class HotPressExistingProcedureError extends Error {
  constructor(name) {
    super(`The procedure "${name}" is already registered`);
  }
}

module.exports = Object.assign(
  new HotPress(), {HotPressTimeoutError, HotPressExistingProcedureError}
);
<commit_msg>:art: Refactor<commit_after>'use strict';

const flatten = require('lodash.flatten');
const upperFirst = require('lodash.upperfirst');
const assert = require('assert');
const ERROR = 'error';
const ON = 'on';
const HIERARCHY_SEPARATOR = '.';
const DEFAULT_LIFECYCLE = ['before', ON, 'after'];
const DEFAULT_TIMEOUT = 300;

/**
 * Listeners by event.
 *
 * [message]: {
 *   before: ...Function,
 *   on: ...Function,
 *   after: ...Function,
 * }
 *
 * @var Object
 */
let listeners = {};

/**
 * Procedures referenced by the name.
 *
 * [name]: Function
 *
 * @var Object
 */
let procedures = {};

/**
 * Namespaced HotPress instances.
 *
 * @var Object<String: HotPress>
 */
let namespaces = {};

/**
 * Returns all the listeners for a given message. The returned value will be an
 * object whose keys are 'before', 'on' and 'after' of which each will contain
 * an array of functions/listeners.
 *
 * @param String message
 * @return Object<before: [], on: [], after: []>
 */
function getListenersFor(message) {
  if (!listeners[message]) {
    listeners[message] = this.lifecycle.reduce((acc, method) => (
      Object.assign(acc, {[method]: []})
    ), {});
  }
  return listeners[message];
}

/**
 * Returns an immutable version of all listeners on a particular event
 * name/message.
 *
 * @param String message
 * @return []
 */
function getAllListenersFor(message) {
  return Object
    .keys(listeners[message] || {})
    .reduce((acc, part) => acc.concat(listeners[message][part]), []);
}

/**
 * Returns a list of messages that should be called in order for a specific
 * message
 *
 * @param String message
 * @return String[]
 */
function getHierarchy(message) {
  let parts = message.split(HIERARCHY_SEPARATOR);
  let hierarchy = [parts[0], '*'];
  parts.reduce((message, part) => {
    let prefix = message + HIERARCHY_SEPARATOR;
    let newMessage = prefix + part;
    let wildcard = prefix + '*';
    hierarchy.unshift(newMessage, wildcard);
    return newMessage;
  });
  return hierarchy;
}

/**
 * Prepends an event name, if the string exists.
 *
 * @param String name
 * @param String prefix
 * @return String
 */
function prependHierarchy(name, prefix) {
  return prefix ? `${prefix}.${name}` : name;
}

/**
 * Adds a listener to a specific part of an event's lifecycle.
 *
 * @param String part
 * @param String message
 * @param Function fn
 */
function onPart(part, message, fn) {
  fn._hpRemoved = false;
  getListenersFor.call(this, prependHierarchy(message, this.prefix))[part].push(fn);
}

/**
 * Adds a listener to a specific part of the event lifecycle and removes it
 * as soon as it's been called.
 *
 * @param Function subscribe
 * @param String message
 * @param Function fn
 */
function oncePart(subscribe, message, fn) {
  let subscriber = (...args) => {
    this.off(message, subscriber);
    return fn(...args);
  };
  subscribe(message, subscriber);
}

/**
 * Create a promise and reject it in a given amount of milliseconds.
 *
 * @param Number timeout
 * @return Promise
 */
function errorAfterMS(timeout) {
  return new Promise((_, reject) => {
    if (typeof timeout === 'number') setTimeout(
      () => reject(new HotPressTimeoutError(timeout)),
      timeout
    );
  });
}

/**
 * Subscribes emittion of an array of events to another event. All data will be
 * passed to the emittions.
 *
 * @param Function subscribe
 * @param String message
 * @param String[] triggers
 */
function triggersPart(subscribe, message, triggers) {
  subscribe(message, (_, ...data) => Promise.all(
    triggers.map(message => this.emit(message, ...data))
  ));
}

/**
 * Creates an emitter based on the event name/message and data. The emitter
 * can then be used to emit each part of the lifecycle.
 *
 * @param String message
 * @param Any[] data
 * @return Function
 */
function createEmitter(message, data) {
  const hierarchy = getHierarchy(message);

  const call = fn => Promise
    .resolve()
    .then(() => !fn._hpRemoved && fn(message, ...data));

  const promise = fn => Promise
    .race([errorAfterMS(this.timeout), call(fn)])
    .catch(error => emit.call(this, prependHierarchy(message, ERROR), [error]));

  return part => Promise.all(flatten(
    hierarchy.map(message => getListenersFor.call(this, message)[part].map(promise))
  ));
}

/**
 * Begin the event lifecycle for a given event and data.
 *
 * @param String message
 * @param Any[] data
 * @param Number timeout  Optional amount of milliseconds to try each listener
 * @return Promise
 */
function emit(message, data) {
  let emit = createEmitter.call(this, message, data);
  return this.lifecycle.reduce((promise, method) => (
    promise.then(() => emit(method))
  ), Promise.resolve());
}

/**
 * Finds and returns duplicates in an array.
 *
 * @param Any[]
 * @return Any[]
 */
function findDuplicates(arr) {
  let uniques = arr
    .map(it => ({count: 1, item: it}))
    .reduce(
      (acc, it) => Object.assign(acc, {[it.item]: (acc[it.item] || 0) + it.count}),
      {}
    );
  return Object
    .keys(uniques)
    .filter(it => uniques[it] > 1);
}

/**
 * Removes a listener from a given event.
 *
 * @param String message
 * @param Function fn
 * @return Number
 */
function removeListener(message, fn) {
  let listeners = getListenersFor.call(this, message);
  let removed = 0;
  for (let key in listeners) {
    let set = listeners[key];
    let index = set.indexOf(fn);
    if (index !== -1) {
      set[index]._hpRemoved = true;
      set.splice(index, 1);
      removed++;
      break;
    }
  }
  return removed;
}

/**
 * Removes all listeners from a given event.
 *
 * @param String message
 * @return Number
 */
function removeAllListeners(message) {
  let all = getAllListenersFor(message);
  let amount = all ? all.length : 0;
  all.forEach(listener => listener._hpRemoved = true);
  delete listeners[message];
  return amount;
}

/**
 * Returns a method name we may use to attach a single listener.
 *
 * @param String method
 * @return String
 */
function singularMethodName(method) {
  return method === ON ? 'once' : `once${upperFirst(method)}`;
}

/**
 * Returns a method name we may use as the trigger listener.
 *
 * @param String method
 * @param String
 */
function triggerMethodName(method) {
  return method === ON ? 'triggers' : `triggers${upperFirst(method)}`;
}

/**
 * The exposable methods for each HotPress namespace.
 *
 * @class HotPress
 * @param String prefix
 * @param String[] lifecycle
 * @param Number timeout
 */
class HotPress {

  constructor(
    prefix = '',
    lifecycle = DEFAULT_LIFECYCLE,
    timeout = DEFAULT_TIMEOUT
  ) {
    /**
     * The prefix to add to all messages
     *
     * @prop String prefix
     */
    this.prefix = prefix;

    /**
     * The lifecycle list
     * @prop String[] lifecycle
     */
    this._lifecycle = [];
    this.lifecycle = lifecycle;

    /**
     * Timeout to stop long processes within the event lifecycle.
     *
     * @prop Number timeout
     */
    this.timeout = timeout;
  }

  get lifecycle() {
    return this._lifecycle;
  }

  set lifecycle(lifecycle) {
    let {_lifecycle} = this;
    let duplicates = findDuplicates(lifecycle);
    this._lifecycle = lifecycle;

    assert(
      !duplicates.length,
      `Lifecycle contains duplicates (${duplicates})`
    );

    assert(
      ~lifecycle.indexOf(ON),
      `Lifecycle must contain an "on" method (${lifecycle})`
    );

    _lifecycle.forEach(method => { delete this[method]; });

    lifecycle.forEach(method => {
      let singularName = singularMethodName(method);
      let triggersName = triggerMethodName(method);
      let singularTriggersName = triggerMethodName(singularName);

      this[method] = onPart.bind(this, method);
      this[singularName] = oncePart.bind(this, this[method]);
      this[triggersName] = triggersPart.bind(this, this[method]);
      this[singularTriggersName] = triggersPart.bind(this, this[singularName]);
    });
  }

  /**
   * Will call the listener once each of the specified events have been emitted.
   * The events are given as an object, where each key will specify the part of
   * the event lifecycle and the value is an array of event names/messages.
   *
   * ```
   * all({
   *   before: ['foo', 'bar'],
   *   on: ['another'],
   *   after: ['event']
   * }, ({foo, bar, another, event}) => {
   *   // You'll receive data for each event that fired
   * });
   * ```
   *
   * @param Object<before: String[], on: String[], after: String[]> messages
   * @param Function fn
   */
  all(messages, fn) {
    let toDo;
    let dataCollection;
    let size = Object
      .keys(messages)
      .reduce((size, part) => size + messages[part].length, 0);

    const subscriber = (message, ...data) => {
      dataCollection[message] = data;
      toDo--;
      if (!toDo) {
        fn(dataCollection);
        init();
      }
    };

    const registerSubscribers = (prop, method) => {
      if (messages[prop]) messages[prop].forEach(message => {
        method(message, subscriber);
      });
    };

    const init = () => {
      toDo = size;
      dataCollection = {};
      this.lifecycle.forEach(method => {
        registerSubscribers(method, this[singularMethodName(method)]);
      });
    };

    init();
  }

  /**
   * Removes the specified listener from a given event. If no listener is
   * specified, all will be removed.
   *
   * @param String message
   * @param Function fn
   * @return Number
   */
  off(message, fn) {
    message = prependHierarchy(message, this.prefix);
    return fn
      ? removeListener.call(this, message, fn)
      : removeAllListeners.call(this, message);
  }

  /**
   * Adds a listener to the end of the event lifecycle for just one emittion.
   *
   * @param String message
   * @param ...Any data
   * @return Promise
   */
  emit(message, ...data) {
    message = prependHierarchy(message, this.prefix);
    return emit.call(this, message, data);
  }

  /**
   * Creates another version of hot press where all events are prefixed with
   * the given string.
   *
   * @param String namespace
   * @return HotPress
   */
  ns(name) {
    let fullName = prependHierarchy(name, this.prefix);
    if (namespaces[fullName]) return namespaces[fullName];
    return name.split(HIERARCHY_SEPARATOR).reduce((parent, name) => {
      name = prependHierarchy(name, parent.prefix);
      return namespaces[name] = namespaces[name] || new HotPress(
        name, parent.lifecycle, parent.timeout
      );
    }, this);
  }

  /**
   * Register a process to a name.
   *
   * @param String name
   * @param Function proc
   * @throws HotPressExistingProcedureError
   */
  reg(name, proc) {
    name = prependHierarchy(name, this.prefix);
    proc._hpRemove = false;
    if (procedures[name]) throw new HotPressExistingProcedureError(name);
    procedures[name] = proc;
  }

  /**
   * Deregisters a process.
   *
   * @param String name
   * @return Number
   */
  dereg(name) {
    name = prependHierarchy(name, this.prefix);
    if (procedures[name]) {
      procedures[name]._hpRemoved = true;
      delete procedures[name];
      return 1;
    }
    return 0;
  }

  /**
   * Calls a procedure and begins an event lifecycle too.
   *
   * @param String name
   * @param ...Any
   * @return Promise
   */
  call(name, ...data) {
    name = prependHierarchy(name, this.prefix);
    let emit = createEmitter.call(this, name, data);
    let proc = procedures[name] || (() => {});
    return this.lifecycle.reduce((promise, method) => promise.then(() => (
      method === ON
        ? Promise.all([
          !proc._hpRemoved && proc(...data),
          emit(method)
        ])
        : emit(method)
    )), Promise.resolve());
  }

}

class HotPressTimeoutError extends Error {
  constructor(ms) {
    super(`Exceeded ${ms}ms`);
  }
}

class HotPressExistingProcedureError extends Error {
  constructor(name) {
    super(`The procedure "${name}" is already registered`);
  }
}

module.exports = Object.assign(
  new HotPress(), {HotPressTimeoutError, HotPressExistingProcedureError}
);
<|endoftext|>