<commit_before>const express = require('express');
const router = express.Router();
const bodyParser = require('body-parser').json();
const Portfolio = require('../models/portfolio');
const stockStore = require('../models/stockstore');
const yahooFinance = require('yahoo-finance');

function adjustPortfolio(portfolio, action, cost) {
    if (action === 'buy') {
        portfolio.cashValue -= cost;
        portfolio.stockValue += cost;
    } else {
        portfolio.cashValue += cost;
        portfolio.stockValue -= cost;
    };
    portfolio.netCalc();
    portfolio.markModified('stocks');
    return portfolio;
};

// for every single person having their own unique portfolio

router

    .get('/all', bodyParser, (req, res, next) => {
        Portfolio
            .find()
            .then(portfolios => {
                res.send(portfolios);
            })
            .catch(next);
    })

    .get('/leaderboard', bodyParser, (req, res, next) => {
        Portfolio
            .aggregate([
                { $sort: { netValue: -1 } },
                { $limit: 5 },
                { $lookup: {
                    from: 'users',
                    localField: 'userId',
                    foreignField: '_id',
                    as: 'user'
                }}
            ])
            .then(portfolios => {
                // console.log(portfolios);
                res.send(portfolios);
            })
            .catch(next);
    })

    .get('/', bodyParser, (req,res,next) => {
        const userId = req.user.id;
        let stockValueMap = {};
        let userPortfolio = {};
        let fieldValue = [ 'n', 's', 'a', 'g', 'h', 'y', 'd', 'r', 'b4', 'e', 'j4', 'r6', 's7' ];
        let stockTickers = [];
        Portfolio
            .findOne({userId})
            .then(portfolio => {
                stockTickers = Object.keys(portfolio.stocks);
                if (!stockTickers.length) throw {code: 403, error: 'No stocks in portfolio'};
                userPortfolio = portfolio;
                return yahooFinance.snapshot({
                    symbols: stockTickers,
                    fields: fieldValue
                });
            })
            .then(snapshotArr => {
                if (snapshotArr.ask) {
                    snapshotArr.forEach(snapshot => {
                        stockValueMap[snapshot.symbol] = snapshot.ask;
                    });
                    let stockValue = 0;
                    for(let keys in userPortfolio.stocks){ //calculates total stock value for each stock in each portfolio
                        stockValue += stockValueMap[keys] * userPortfolio.stocks[keys];
                    };
                    userPortfolio.stockValue = stockValue;
                    userPortfolio.netCalc();
                    return userPortfolio.save();
                } else {
                    const currDate = new Date();
                    const toDate = currDate.toISOString().match(/\d+\-\d+\-\d+/)[0];
                    const fromDate = new Date(currDate.setDate(currDate.getDate() - 1)).toISOString().match(/\d+\-\d+\-\d+/)[0];
                    return new Promise((resolve, reject) => {
                        stockTickers.forEach(function(ticker) {
                            yahooFinance.historical({
                                symbol: ticker,
                                from: fromDate,
                                to: toDate,
                                period: 'd'
                            }, (err, quotes) => {
                                if (err) reject(err);
                                stockValueMap[quotes[0].symbol] = quotes[0].close;
                                if (Object.keys(stockValueMap).length === stockTickers.length) {
                                    let stockValue = 0;
                                    for(let keys in userPortfolio.stocks){ //calculates total stock value for each stock in each portfolio
                                        stockValue += stockValueMap[keys] * userPortfolio.stocks[keys];
                                    };
                                    userPortfolio.stockValue = stockValue;
                                    userPortfolio.netCalc();
                                    userPortfolio.save((err, portfolio) => {
                                        if (err) reject(err);
                                        resolve(portfolio);
                                    });
                                };
                            });
                        });
                    });
                };
            })
            .then(userPortfolio => {
                res.send(userPortfolio);
            })
            .catch(next);
    })

    .put('/buy', bodyParser, (req, res, next) => {
        const {stock, shares, price} = req.body; //have object be {stock:ticker, shares:number, price:cost}?
        const userId = req.user.id;
        const cost = price * shares;

        if (!price || !stock || !shares) return next({code: 403, error: 'Invalid data submitted'});

        Promise.all([
            Portfolio
                .findOne({userId})
                .then(portfolio => {

                    if(cost > portfolio.cashValue) throw {code: 403, error: 'cost of stocks exceeds cashvalue in portfolio'};

                    if(stock in portfolio.stocks) portfolio.stocks[stock] += shares;
                    else portfolio.stocks[stock] = shares;

                    portfolio = adjustPortfolio(portfolio, 'buy', cost);
                    return portfolio.save();
                }),
            stockStore
                .findOne() //assumes there will be no changes!
                .then(store => {
                    if (!store) {
                        const newStore = new stockStore();
                        newStore.updateStocks(stock);
                        newStore.markModified('stocks');
                        return newStore.save();
                    } else {
                        store.updateStocks(stock);
                        store.markModified('stocks');
                        return store.save();
                    };
                })
        ])
        .then(([portfolio, store]) => {
            // console.log(store);
            res.send(portfolio);
        })
        .catch(next);
    })

    .put('/sell', bodyParser, (req, res, next) => {
        const {stock, shares, price} = req.body;
        const userId = req.user.id;
        const cost = price * shares;

        if (!price || !stock || !shares) return next({code: 403, error: 'Invalid data submitted'});

        Portfolio
            .findOne({userId})
            .then(portfolio => {
                if(!(stock in portfolio.stocks)) throw {code: 403, error: 'user currently does not own this stock'};
                if(shares > portfolio.stocks[stock]) throw {code: 403, error: 'user requesting to sell more shares then they possess'};

                portfolio.stocks[stock] -= shares;

                if(portfolio.stocks[stock] === 0) delete portfolio.stocks[stock];
                portfolio = adjustPortfolio(portfolio, 'sell', cost);
                portfolio.markModified('stocks');
                return portfolio.save();
            })
            .then(portfolio => {
                res.send(portfolio);
            })
            .catch(next);
    });

module.exports = router;<commit_msg>modified portfolios in case a stock in it goes to 0<commit_after>const express = require('express');
const router = express.Router();
const bodyParser = require('body-parser').json();
const Portfolio = require('../models/portfolio');
const stockStore = require('../models/stockstore');
const yahooFinance = require('yahoo-finance');

function adjustPortfolio(portfolio, action, cost) {
    if (action === 'buy') {
        portfolio.cashValue -= cost;
        portfolio.stockValue += cost;
    } else {
        portfolio.cashValue += cost;
        portfolio.stockValue -= cost;
    };
    portfolio.netCalc();
    portfolio.markModified('stocks');
    return portfolio;
};

// for every single person having their own unique portfolio

router

    .get('/all', bodyParser, (req, res, next) => {
        Portfolio
            .find()
            .then(portfolios => {
                res.send(portfolios);
            })
            .catch(next);
    })

    .get('/leaderboard', bodyParser, (req, res, next) => {
        Portfolio
            .aggregate([
                { $sort: { netValue: -1 } },
                { $limit: 5 },
                { $lookup: {
                    from: 'users',
                    localField: 'userId',
                    foreignField: '_id',
                    as: 'user'
                }}
            ])
            .then(portfolios => {
                // console.log(portfolios);
                res.send(portfolios);
            })
            .catch(next);
    })

    .get('/', bodyParser, (req,res,next) => {
        const userId = req.user.id;
        let stockValueMap = {};
        let userPortfolio = {};
        let fieldValue = [ 'n', 's', 'a', 'g', 'h', 'y', 'd', 'r', 'b4', 'e', 'j4', 'r6', 's7' ];
        let stockTickers = [];
        Portfolio
            .findOne({userId})
            .then(portfolio => {
                stockTickers = Object.keys(portfolio.stocks);
                if (!stockTickers.length) throw {code: 403, error: 'No stocks in portfolio'};
                userPortfolio = portfolio;
                return yahooFinance.snapshot({
                    symbols: stockTickers,
                    fields: fieldValue
                });
            })
            .then(snapshotArr => {
                if (snapshotArr.ask) {
                    snapshotArr.forEach(snapshot => {
                        stockValueMap[snapshot.symbol] = snapshot.ask;
                    });
                    let stockValue = 0;
                    for(let keys in userPortfolio.stocks){ //calculates total stock value for each stock in each portfolio
                        stockValue += stockValueMap[keys] * userPortfolio.stocks[keys];
                    };
                    userPortfolio.stockValue = stockValue;
                    userPortfolio.netCalc();
                    return userPortfolio.save();
                } else {
                    const currDate = new Date();
                    const toDate = currDate.toISOString().match(/\d+\-\d+\-\d+/)[0];
                    const fromDate = new Date(currDate.setDate(currDate.getDate() - 1)).toISOString().match(/\d+\-\d+\-\d+/)[0];
                    return new Promise((resolve, reject) => {
                        stockTickers.forEach(function(ticker) {
                            yahooFinance.historical({
                                symbol: ticker,
                                from: fromDate,
                                to: toDate,
                                period: 'd'
                            }, (err, quotes) => {
                                if (err) reject(err);
                                stockValueMap[quotes[0].symbol] = quotes[0].close;
                                if (Object.keys(stockValueMap).length === stockTickers.length) {
                                    let stockValue = 0;
                                    for(let keys in userPortfolio.stocks){ //calculates total stock value for each stock in each portfolio
                                        stockValue += stockValueMap[keys] * userPortfolio.stocks[keys];
                                    };
                                    userPortfolio.stockValue = stockValue;
                                    userPortfolio.netCalc();
                                    userPortfolio.save((err, portfolio) => {
                                        if (err) reject(err);
                                        resolve(portfolio);
                                    });
                                };
                            });
                        });
                    });
                };
            })
            .then(userPortfolio => {
                res.send(userPortfolio);
            })
            .catch(next);
    })

    .put('/buy', bodyParser, (req, res, next) => {
        const {stock, shares, price} = req.body; //have object be {stock:ticker, shares:number, price:cost}?
        const userId = req.user.id;
        const cost = price * shares;

        if (!price || !stock || !shares) return next({code: 403, error: 'Invalid data submitted'});

        Promise.all([
            Portfolio
                .findOne({userId})
                .then(portfolio => {

                    if(cost > portfolio.cashValue) throw {code: 403, error: 'cost of stocks exceeds cashvalue in portfolio'};

                    if(stock in portfolio.stocks) portfolio.stocks[stock] += shares;
                    else portfolio.stocks[stock] = shares;

                    portfolio = adjustPortfolio(portfolio, 'buy', cost);
                    return portfolio.save();
                }),
            stockStore
                .findOne() //assumes there will be no changes!
                .then(store => {
                    if (!store) {
                        const newStore = new stockStore();
                        newStore.updateStocks(stock);
                        newStore.markModified('stocks');
                        return newStore.save();
                    } else {
                        store.updateStocks(stock);
                        store.markModified('stocks');
                        return store.save();
                    };
                })
        ])
        .then(([portfolio, store]) => {
            // console.log(store);
            res.send(portfolio);
        })
        .catch(next);
    })

    .put('/sell', bodyParser, (req, res, next) => {
        const {stock, shares, price} = req.body;
        const userId = req.user.id;
        const cost = price * shares;

        if (!price || !stock || !shares) return next({code: 403, error: 'Invalid data submitted'});

        Portfolio
            .findOne({userId})
            .then(portfolio => {
                if(!(stock in portfolio.stocks)) throw {code: 403, error: 'user currently does not own this stock'};
                if(shares > portfolio.stocks[stock]) throw {code: 403, error: 'user requesting to sell more shares then they possess'};

                portfolio.stocks[stock] -= shares;

                if(portfolio.stocks[stock] === 0) {
                    const newPortfolio = Object.keys(portfolio.stocks).reduce((acc,curr) => {
                        if (curr !== stock) {
                            acc[curr] = portfolio.stocks[curr];
                        };
                        return acc;
                    }, Object.create(null));
                    portfolio.stocks = newPortfolio;
                };

                portfolio = adjustPortfolio(portfolio, 'sell', cost);
                portfolio.markModified('stocks');
                return portfolio.save();
            })
            .then(portfolio => {
                res.send(portfolio);
            })
            .catch(next);
    });

module.exports = router;<|endoftext|>


<commit_before>/*jshint multistr: true */

const querystring = require('querystring');
const ds = require('../services/documents');
const config = require('../config');
const helpers = require('../../shared/helpers');

const licenseMapping = require('../config').licenseMapping;

const SITEMAP_ASSET_LIMIT = 1000;

// One sitemap.xml file for crawlers, which points to all our sub sitemaps
exports.index = function(req, res, next) {
  var hostBase = req.headers['x-forwarded-host'] || req.get('host');

  ds.search({
    'size':0,
    'body': {
      'aggs': {
        'searchable': {
          'filter': config.search.baseQuery || {},
          'aggs': {
            'collection': {
              'terms': {'field': 'collection.keyword'},
              'aggs': {
                'maximalId': {
                  'max': {'field': 'id'}
                }
              }
            }
          }
        }
      }
    }
  }).then(function(response) {
    let sitemaps = [];
    let collections = response.aggregations.searchable.collection.buckets;
    collections.forEach(collection => {
      let maximalId = collection.maximalId.value;
      var sitemapCount = Math.ceil(maximalId / SITEMAP_ASSET_LIMIT);
      for (var i=0; i < maximalId; i+=SITEMAP_ASSET_LIMIT) {
        var baseUrl = 'http://' + hostBase + '/';
        let qs = querystring.stringify({
          fromId: i,
          toId: i + SITEMAP_ASSET_LIMIT
        });
        let path = collection.key + '/sitemap.xml?' + qs;
        sitemaps.push({
          location: baseUrl + path
        });
      }
    });
    res.header('Content-type', 'text/xml; charset=utf-8');
    res.render('sitemap', {
      sitemaps
    });
  }).then(null, next);
};

exports.catalog = function(req, res, next) {
  var hostBase = req.headers['x-forwarded-host'] || req.get('host');
  var collection = req.params.catalog;
  var fromId = parseInt(req.query.fromId, 10) || 0;
  var toId = parseInt(req.query.toId, 10) || (fromId + SITEMAP_ASSET_LIMIT);

  if (!collection) {
    throw new Error('No catalog specified');
  }

  var parameters = {
    'body': {
      'size': SITEMAP_ASSET_LIMIT,
      'query': {
        'bool': {
          'must': [
            {
              'range': {
                'id': {
                  'gte': fromId,
                  'lt': toId
                }
              }
            }, {
              'match': {
                'collection': collection
              }
            }
          ]
        }
      }
    }
  };

  if(config.search && config.search.baseQuery) {
    parameters.body.query.bool.must.push(config.search.baseQuery);
  }

  ds.search(parameters).then(function(result) {
    var urls = [];
    for (var i=0; i < result.hits.hits.length; ++i) {
      const metadata = result.hits.hits[i]._source;
      const relativeUrl = helpers.getDocumentURL(metadata);
      const location = helpers.getAbsoluteURL(req, relativeUrl);

      // YYYY-MM-DD
      var modificationTime = helpers.documentModified(metadata);
      var lastmod = new Date(modificationTime).toISOString();
      const elements = helpers.generateSitemapElements(req, metadata);

      urls.push({
        location,
        lastmod,
        elements
      });
    }

    res.header('Content-type', 'text/xml; charset=utf-8');
    res.render('urlset', {
      urls
    });
  }).then(null, next);
};
<commit_msg>Add comments, remove dead code and clean up how to get last modified.<commit_after>/*jshint multistr: true */

const querystring = require('querystring');
const ds = require('../services/documents');
const config = require('../config');
const helpers = require('../../shared/helpers');

const licenseMapping = require('../config').licenseMapping;

const SITEMAP_ASSET_LIMIT = 1000;

// One sitemap.xml file for crawlers, which points to all our sub sitemaps
exports.index = function(req, res, next) {
  var hostBase = req.headers['x-forwarded-host'] || req.get('host');

  // Count the number of items in each of our collections.
  ds.search({
    'size':0,
    'body': {
      'aggs': {
        'searchable': {
          'filter': config.search.baseQuery || {},
          'aggs': {
            'collection': {
              'terms': {'field': 'collection.keyword'},
              'aggs': {
                'maximalId': {
                  'max': {'field': 'id'}
                }
              }
            }
          }
        }
      }
    }
  }).then(function(response) {
    // Break each collection up into SITEMAP_ASSET_LIMIT sized chuncks and
    // produce an url for our /catalog endpoint that will produce the actual
    // sitemap.
    let sitemaps = [];
    let collections = response.aggregations.searchable.collection.buckets;
    collections.forEach(collection => {
      let maximalId = collection.maximalId.value;
      for (var i=0; i < maximalId; i+=SITEMAP_ASSET_LIMIT) {
        var baseUrl = 'http://' + hostBase + '/';
        let qs = querystring.stringify({
          fromId: i,
          toId: i + SITEMAP_ASSET_LIMIT
        });
        let path = collection.key + '/sitemap.xml?' + qs;
        sitemaps.push({
          location: baseUrl + path
        });
      }
    });
    res.header('Content-type', 'text/xml; charset=utf-8');
    res.render('sitemap', {
      sitemaps
    });
  }).then(null, next);
};

exports.catalog = function(req, res, next) {
  var collection = req.params.catalog;
  var fromId = parseInt(req.query.fromId, 10) || 0;
  var toId = parseInt(req.query.toId, 10) || (fromId + SITEMAP_ASSET_LIMIT);

  if (!collection) {
    throw new Error('No catalog specified');
  }

  // Get a list of assets in the range and render them out as sitemaps.
  var parameters = {
    'body': {
      'size': SITEMAP_ASSET_LIMIT,
      'query': {
        'bool': {
          'must': [
            {
              'range': {
                'id': {
                  'gte': fromId,
                  'lt': toId
                }
              }
            }, {
              'match': {
                'collection': collection
              }
            }
          ]
        }
      }
    }
  };

  if(config.search && config.search.baseQuery) {
    parameters.body.query.bool.must.push(config.search.baseQuery);
  }

  ds.search(parameters).then(function(result) {
    var urls = [];
    for (var i=0; i < result.hits.hits.length; ++i) {
      const metadata = result.hits.hits[i]._source;
      const relativeUrl = helpers.getDocumentURL(metadata);
      const location = helpers.getAbsoluteURL(req, relativeUrl);

      // YYYY-MM-DD
      var lastmod = new Date(metadata.item_modification_date.timestamp).toISOString();
      // Delegate the actual rendering to out collections-online customization.
      const elements = helpers.generateSitemapElements(req, metadata);

      urls.push({
        location,
        lastmod,
        elements
      });
    }

    res.header('Content-type', 'text/xml; charset=utf-8');
    res.render('urlset', {
      urls
    });
  }).then(null, next);
};
<|endoftext|>


<commit_before>/*
 * IDBWrapper - A cross-browser wrapper for IndexedDB
 * Copyright (c) 2011 Jens Arps
 * http://jensarps.de/
 *
 * Licensed under the MIT (X11) license
 */

(function (name, definition, global) {
  if (typeof define === 'function') {
    define(definition);
  } else {
    global[name] = definition();
  }
})('IDBStore', function () {

  var IDBStore;

  var defaults = {
    storeName: 'Store',
    dbVersion: 1,
    keyPath: 'id',
    autoIncrement: true,
    onStoreReady: function () {
    }
  };

  IDBStore = function (kwArgs, onStoreReady) {

    function fixupConstants (object, constants) {
      for (var prop in constants) {
        if (!(prop in object))
          object[prop] = constants[prop];
      }
    }

    mixin(this, defaults);
    mixin(this, kwArgs);
    this.dbName = 'IDBWrapper-' + this.storeName;

    onStoreReady && (this.onStoreReady = onStoreReady);

    this.idb = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
    this.consts = window.IDBTransaction || window.webkitIDBTransaction;
    fixupConstants(this.consts, {
      'READ_ONLY': 'readonly',
      'READ_WRITE': 'readwrite',
      'VERSION_CHANGE': 'versionchange'
    });
    this.cursor = window.IDBCursor || window.webkitIDBCursor;
    fixupConstants(this.cursor, {
      'NEXT': 'next',
      'NEXT_NO_DUPLICATE': 'nextunique',
      'PREV': 'prev',
      'PREV_NO_DUPLICATE': 'prevunique'
    });
    this.openDB();
  };

  IDBStore.prototype = {

    db: null,

    dbName: null,

    dbDescription: null,

    dbVersion: null,

    store: null,

    storeName: null,

    keyPath: null,

    autoIncrement: null,

    features: null,

    onStoreReady: null,

    openDB: function () {

      this.newVersionAPI = typeof this.idb.setVersion == 'undefined';

      if(!this.newVersionAPI){
        throw new Error('The IndexedDB implementation in this browser is outdated. Please upgrade your browser.');
      }

      var features = this.features = {};
      features.hasAutoIncrement = !window.mozIndexedDB; // TODO: Still, really?

      var openRequest;

      this.dbVersion = parseInt(this.dbVersion, 10);
      openRequest = this.idb.open(this.dbName, this.dbVersion);

      openRequest.onerror = hitch(this, function (error) {
        var gotVersionErr = false;
        if ('error' in error.target) {
          gotVersionErr = error.target.error.name == "VersionError";
        } else if ('errorCode' in error.target) {
          gotVersionErr = error.target.errorCode == 12; // TODO: Use const
        }

        if (gotVersionErr) {
          console.log('Version error');
          //setTimeout(hitch(this, 'openDB'));
        } else {
          console.error('Could not open database, error', error);
        }
      });

      openRequest.onsuccess = hitch(this, function (event) {
        this.db = event.target.result;

        if(this.db.objectStoreNames.contains(this.storeName)){
          console.log('object store found');
          this.openExistingObjectStore(this.onStoreReady);
        } else {
          console.log('object store NOT found', this.storeName);
          throw new Error('Cannot create a new store in this db for the current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.');
        }
      });

      openRequest.onupgradeneeded = hitch(this, function(/* IDBVersionChangeEvent */ event){
        this.db = event.target.result;

        if(this.db.objectStoreNames.contains(this.storeName)){
          console.log('object store found');
        } else {
          console.log('object store NOT found', this.storeName);

          this.db.createObjectStore(this.storeName, { keyPath: this.keyPath, autoIncrement: this.autoIncrement});
        }
      });
    },

    deleteDatabase: function () {
      if (this.idb.deleteDatabase) {
        this.idb.deleteDatabase(this.dbName);
      }
    },

    enterMutationState: function (onSuccess, onError) {
      if (this.newVersionAPI) {
        this.dbVersion++;
        var openRequest = this.idb.open(this.dbName, this.dbVersion, this.dbDescription);
        openRequest.onupgradeneeded = onSuccess;
        openRequest.onsuccess = onError;
        openRequest.onerror = onError;
        openRequest.onblocked = onError;
      } else {
        this.setVersion(onSuccess, onError);
      }
    },

    /*************************
     * object store handling *
     *************************/

    getObjectStore: function (onSuccess, onError) {
      if (this.hasObjectStore()) {
        this.openExistingObjectStore(onSuccess, onError);
      } else {
        this.createNewObjectStore(onSuccess, onError);
      }
    },

    hasObjectStore: function () {
      return this.db.objectStoreNames.contains(this.storeName);
    },

    createNewObjectStore: function (onSuccess, onError) {
      this.enterMutationState(hitch(this, function () {
        this.store = this.db.createObjectStore(this.storeName, { keyPath: this.keyPath, autoIncrement: this.autoIncrement});
        onSuccess && onSuccess(this.store);
      }), onError);
    },

    openExistingObjectStore: function (onSuccess, onError) {
      var emptyTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      this.store = emptyTransaction.objectStore(this.storeName);
      emptyTransaction.abort();
      onSuccess && onSuccess(this.store);
    },

    deleteObjectStore: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Failed to delete objectStore.', error);
      });
      this.enterMutationState(hitch(this, function (evt) {
        var db = evt.target.result;
        db.deleteObjectStore(this.storeName);
        var success = !this.hasObjectStore();
        onSuccess && success && onSuccess();
        onError && !success && onError();
      }), onError);
    },

    /*********************
     * data manipulation *
     *********************/

    put: function (dataObj, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not write data.', error);
      });
      onSuccess || (onSuccess = noop);
      if (typeof dataObj[this.keyPath] == 'undefined' && !this.features.hasAutoIncrement) {
        dataObj[this.keyPath] = this._getUID();
      }
      var putTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var putRequest = putTransaction.objectStore(this.storeName).put(dataObj);
      putRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      putRequest.onerror = onError;
    },

    get: function (key, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not read data.', error);
      });
      onSuccess || (onSuccess = noop);
      var getTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      var getRequest = getTransaction.objectStore(this.storeName).get(key);
      getRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      getRequest.onerror = onError;
    },

    remove: function (key, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not remove data.', error);
      });
      onSuccess || (onSuccess = noop);
      var removeTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var deleteRequest = removeTransaction.objectStore(this.storeName).delete(key);
      deleteRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      deleteRequest.onerror = onError;
    },

    getAll: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not read data.', error);
      });
      onSuccess || (onSuccess = noop);
      var getAllTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      var store = getAllTransaction.objectStore(this.storeName);
      if (store.getAll) {
        var getAllRequest = store.getAll();
        getAllRequest.onsuccess = function (event) {
          onSuccess(event.target.result);
        };
        getAllRequest.onerror = onError;
      } else {
        this._getAllCursor(getAllTransaction, onSuccess, onError);
      }
    },

    _getAllCursor: function (tr, onSuccess, onError) {
      var all = [];
      var store = tr.objectStore(this.storeName);
      var cursorRequest = store.openCursor();

      cursorRequest.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
          all.push(cursor.value);
          cursor['continue']();
        }
        else {
          onSuccess(all);
        }
      };
      cursorRequest.onError = onError;
    },

    clear: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not clear store.', error);
      });
      onSuccess || (onSuccess = noop);
      var clearTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var clearRequest = clearTransaction.objectStore(this.storeName).clear();
      clearRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      clearRequest.onerror = onError;
    },

    _getUID: function () {
      // FF bails at times on non-numeric ids. So we take an even
      // worse approach now, using current time as id. Sigh.
      return +new Date();
    },

    /************
     * indexing *
     ************/

    createIndex: function (indexName, propertyName, isUnique, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not create index.', error);
      });
      onSuccess || (onSuccess = noop);
      propertyName || (propertyName = indexName);

      var that = this;

      this.enterMutationState(hitch(this, function (evt) {
        var result = evt.target.result;
        var index;
        if (result.objectStore) { // transaction
          index = db.objectStore(this.storeName).createIndex(indexName, propertyName, { unique: !!isUnique });
        } else { // db
          var putTransaction = result.transaction([that.storeName] /* , this.consts.READ_WRITE */);
          var store = putTransaction.objectStore(that.storeName);
          index = store.createIndex(indexName, propertyName, { unique: !!isUnique });
        }
        onSuccess(index);
      }), onError);
    },

    getIndex: function (indexName) {
      return this.store.index(indexName);
    },

    getIndexList: function () {
      return this.store.indexNames;
    },

    hasIndex: function (indexName) {
      return this.store.indexNames.contains(indexName);
    },

    removeIndex: function (indexName, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not remove index.', error);
      });
      onSuccess || (onSuccess = noop);
      this.enterMutationState(hitch(this, function (evt) {
        evt.target.result.objectStore(this.storeName).deleteIndex(indexName);
        onSuccess();
      }), onError);
    },

    /**********
     * cursor *
     **********/

    iterate: function (callback, options) {
      options = mixin({
        index: null,
        order: 'ASC',
        filterDuplicates: false,
        keyRange: null,
        writeAccess: false,
        onEnd: null,
        onError: function (error) {
          console.error('Could not open cursor.', error);
        }
      }, options || {});

      var directionType = options.order.toLowerCase() == 'desc' ? 'PREV' : 'NEXT';
      if (options.filterDuplicates) {
        directionType += '_NO_DUPLICATE';
      }

      var cursorTransaction = this.db.transaction([this.storeName], this.consts[options.writeAccess ? 'READ_WRITE' : 'READ_ONLY']);
      var cursorTarget = cursorTransaction.objectStore(this.storeName);
      if (options.index) {
        cursorTarget = cursorTarget.index(options.index);
      }

      var cursorRequest = cursorTarget.openCursor(options.keyRange, this.cursor[directionType]);
      cursorRequest.onerror = options.onError;
      cursorRequest.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
          callback(cursor.value, cursor, cursorTransaction);
          cursor['continue']();
        } else {
          options.onEnd && options.onEnd() || callback(null, cursor, cursorTransaction)
        }
      };
    }

    /* key ranges */
    // TODO: implement

  };

  /** helpers **/

  var noop = function () {
  };
  var empty = {};
  var mixin = function (target, source) {
    var name, s;
    for (name in source) {
      s = source[name];
      if (s !== empty[name] && s !== target[name]) {
        target[name] = s;
      }
    }
    return target;
  };
  var hitch = function (scope, method) {
    if (!method) {
      method = scope;
      scope = null;
    }
    if (typeof method == "string") {
      scope = scope || window;
      if (!scope[method]) {
        throw(['method not found']);
      }
      return function () {
        return scope[method].apply(scope, arguments || []);
      };
    }
    return !scope ? method : function () {
      return method.apply(scope, arguments || []);
    };
  };

  return IDBStore;

}, this);
<commit_msg>Remove ability to manually enter mutation state<commit_after>/*
 * IDBWrapper - A cross-browser wrapper for IndexedDB
 * Copyright (c) 2011 Jens Arps
 * http://jensarps.de/
 *
 * Licensed under the MIT (X11) license
 */

(function (name, definition, global) {
  if (typeof define === 'function') {
    define(definition);
  } else {
    global[name] = definition();
  }
})('IDBStore', function () {

  var IDBStore;

  var defaults = {
    storeName: 'Store',
    dbVersion: 1,
    keyPath: 'id',
    autoIncrement: true,
    onStoreReady: function () {
    }
  };

  IDBStore = function (kwArgs, onStoreReady) {

    function fixupConstants (object, constants) {
      for (var prop in constants) {
        if (!(prop in object))
          object[prop] = constants[prop];
      }
    }

    mixin(this, defaults);
    mixin(this, kwArgs);
    this.dbName = 'IDBWrapper-' + this.storeName;

    onStoreReady && (this.onStoreReady = onStoreReady);

    this.idb = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
    this.consts = window.IDBTransaction || window.webkitIDBTransaction;
    fixupConstants(this.consts, {
      'READ_ONLY': 'readonly',
      'READ_WRITE': 'readwrite',
      'VERSION_CHANGE': 'versionchange'
    });
    this.cursor = window.IDBCursor || window.webkitIDBCursor;
    fixupConstants(this.cursor, {
      'NEXT': 'next',
      'NEXT_NO_DUPLICATE': 'nextunique',
      'PREV': 'prev',
      'PREV_NO_DUPLICATE': 'prevunique'
    });
    this.openDB();
  };

  IDBStore.prototype = {

    db: null,

    dbName: null,

    dbDescription: null,

    dbVersion: null,

    store: null,

    storeName: null,

    keyPath: null,

    autoIncrement: null,

    features: null,

    onStoreReady: null,

    openDB: function () {

      this.newVersionAPI = typeof this.idb.setVersion == 'undefined';

      if(!this.newVersionAPI){
        throw new Error('The IndexedDB implementation in this browser is outdated. Please upgrade your browser.');
      }

      var features = this.features = {};
      features.hasAutoIncrement = !window.mozIndexedDB; // TODO: Still, really?

      var openRequest;

      this.dbVersion = parseInt(this.dbVersion, 10);
      openRequest = this.idb.open(this.dbName, this.dbVersion);

      openRequest.onerror = hitch(this, function (error) {
        var gotVersionErr = false;
        if ('error' in error.target) {
          gotVersionErr = error.target.error.name == "VersionError";
        } else if ('errorCode' in error.target) {
          gotVersionErr = error.target.errorCode == 12; // TODO: Use const
        }

        if (gotVersionErr) {
          console.log('Version error');
          //setTimeout(hitch(this, 'openDB'));
        } else {
          console.error('Could not open database, error', error);
        }
      });

      openRequest.onsuccess = hitch(this, function (event) {
        this.db = event.target.result;

        if(this.db.objectStoreNames.contains(this.storeName)){
          console.log('object store found');
          this.openExistingObjectStore(this.onStoreReady);
        } else {
          console.log('object store NOT found', this.storeName);
          throw new Error('Cannot create a new store in this db for the current version. Please bump version number to ' + ( this.dbVersion + 1 ) + '.');
        }
      });

      openRequest.onupgradeneeded = hitch(this, function(/* IDBVersionChangeEvent */ event){
        this.db = event.target.result;

        if(this.db.objectStoreNames.contains(this.storeName)){
          console.log('object store found');
        } else {
          console.log('object store NOT found', this.storeName);

          this.db.createObjectStore(this.storeName, { keyPath: this.keyPath, autoIncrement: this.autoIncrement});
        }
      });
    },

    deleteDatabase: function () {
      if (this.idb.deleteDatabase) {
        this.idb.deleteDatabase(this.dbName);
      }
    },

    /*************************
     * object store handling *
     *************************/

    getObjectStore: function (onSuccess, onError) {
      if (this.hasObjectStore()) {
        this.openExistingObjectStore(onSuccess, onError);
      } else {
        this.createNewObjectStore(onSuccess, onError);
      }
    },

    hasObjectStore: function () {
      return this.db.objectStoreNames.contains(this.storeName);
    },

    createNewObjectStore: function (onSuccess, onError) {
      this.enterMutationState(hitch(this, function () {
        this.store = this.db.createObjectStore(this.storeName, { keyPath: this.keyPath, autoIncrement: this.autoIncrement});
        onSuccess && onSuccess(this.store);
      }), onError);
    },

    openExistingObjectStore: function (onSuccess, onError) {
      var emptyTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      this.store = emptyTransaction.objectStore(this.storeName);
      emptyTransaction.abort();
      onSuccess && onSuccess(this.store);
    },

    deleteObjectStore: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Failed to delete objectStore.', error);
      });
      this.enterMutationState(hitch(this, function (evt) {
        var db = evt.target.result;
        db.deleteObjectStore(this.storeName);
        var success = !this.hasObjectStore();
        onSuccess && success && onSuccess();
        onError && !success && onError();
      }), onError);
    },

    /*********************
     * data manipulation *
     *********************/

    put: function (dataObj, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not write data.', error);
      });
      onSuccess || (onSuccess = noop);
      if (typeof dataObj[this.keyPath] == 'undefined' && !this.features.hasAutoIncrement) {
        dataObj[this.keyPath] = this._getUID();
      }
      var putTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var putRequest = putTransaction.objectStore(this.storeName).put(dataObj);
      putRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      putRequest.onerror = onError;
    },

    get: function (key, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not read data.', error);
      });
      onSuccess || (onSuccess = noop);
      var getTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      var getRequest = getTransaction.objectStore(this.storeName).get(key);
      getRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      getRequest.onerror = onError;
    },

    remove: function (key, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not remove data.', error);
      });
      onSuccess || (onSuccess = noop);
      var removeTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var deleteRequest = removeTransaction.objectStore(this.storeName).delete(key);
      deleteRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      deleteRequest.onerror = onError;
    },

    getAll: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not read data.', error);
      });
      onSuccess || (onSuccess = noop);
      var getAllTransaction = this.db.transaction([this.storeName], this.consts.READ_ONLY);
      var store = getAllTransaction.objectStore(this.storeName);
      if (store.getAll) {
        var getAllRequest = store.getAll();
        getAllRequest.onsuccess = function (event) {
          onSuccess(event.target.result);
        };
        getAllRequest.onerror = onError;
      } else {
        this._getAllCursor(getAllTransaction, onSuccess, onError);
      }
    },

    _getAllCursor: function (tr, onSuccess, onError) {
      var all = [];
      var store = tr.objectStore(this.storeName);
      var cursorRequest = store.openCursor();

      cursorRequest.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
          all.push(cursor.value);
          cursor['continue']();
        }
        else {
          onSuccess(all);
        }
      };
      cursorRequest.onError = onError;
    },

    clear: function (onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not clear store.', error);
      });
      onSuccess || (onSuccess = noop);
      var clearTransaction = this.db.transaction([this.storeName], this.consts.READ_WRITE);
      var clearRequest = clearTransaction.objectStore(this.storeName).clear();
      clearRequest.onsuccess = function (event) {
        onSuccess(event.target.result);
      };
      clearRequest.onerror = onError;
    },

    _getUID: function () {
      // FF bails at times on non-numeric ids. So we take an even
      // worse approach now, using current time as id. Sigh.
      return +new Date();
    },

    /************
     * indexing *
     ************/

    createIndex: function (indexName, propertyName, isUnique, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not create index.', error);
      });
      onSuccess || (onSuccess = noop);
      propertyName || (propertyName = indexName);

      var that = this;

      this.enterMutationState(hitch(this, function (evt) {
        var result = evt.target.result;
        var index;
        if (result.objectStore) { // transaction
          index = db.objectStore(this.storeName).createIndex(indexName, propertyName, { unique: !!isUnique });
        } else { // db
          var putTransaction = result.transaction([that.storeName] /* , this.consts.READ_WRITE */);
          var store = putTransaction.objectStore(that.storeName);
          index = store.createIndex(indexName, propertyName, { unique: !!isUnique });
        }
        onSuccess(index);
      }), onError);
    },

    getIndex: function (indexName) {
      return this.store.index(indexName);
    },

    getIndexList: function () {
      return this.store.indexNames;
    },

    hasIndex: function (indexName) {
      return this.store.indexNames.contains(indexName);
    },

    removeIndex: function (indexName, onSuccess, onError) {
      onError || (onError = function (error) {
        console.error('Could not remove index.', error);
      });
      onSuccess || (onSuccess = noop);
      this.enterMutationState(hitch(this, function (evt) {
        evt.target.result.objectStore(this.storeName).deleteIndex(indexName);
        onSuccess();
      }), onError);
    },

    /**********
     * cursor *
     **********/

    iterate: function (callback, options) {
      options = mixin({
        index: null,
        order: 'ASC',
        filterDuplicates: false,
        keyRange: null,
        writeAccess: false,
        onEnd: null,
        onError: function (error) {
          console.error('Could not open cursor.', error);
        }
      }, options || {});

      var directionType = options.order.toLowerCase() == 'desc' ? 'PREV' : 'NEXT';
      if (options.filterDuplicates) {
        directionType += '_NO_DUPLICATE';
      }

      var cursorTransaction = this.db.transaction([this.storeName], this.consts[options.writeAccess ? 'READ_WRITE' : 'READ_ONLY']);
      var cursorTarget = cursorTransaction.objectStore(this.storeName);
      if (options.index) {
        cursorTarget = cursorTarget.index(options.index);
      }

      var cursorRequest = cursorTarget.openCursor(options.keyRange, this.cursor[directionType]);
      cursorRequest.onerror = options.onError;
      cursorRequest.onsuccess = function (event) {
        var cursor = event.target.result;
        if (cursor) {
          callback(cursor.value, cursor, cursorTransaction);
          cursor['continue']();
        } else {
          options.onEnd && options.onEnd() || callback(null, cursor, cursorTransaction)
        }
      };
    }

    /* key ranges */
    // TODO: implement

  };

  /** helpers **/

  var noop = function () {
  };
  var empty = {};
  var mixin = function (target, source) {
    var name, s;
    for (name in source) {
      s = source[name];
      if (s !== empty[name] && s !== target[name]) {
        target[name] = s;
      }
    }
    return target;
  };
  var hitch = function (scope, method) {
    if (!method) {
      method = scope;
      scope = null;
    }
    if (typeof method == "string") {
      scope = scope || window;
      if (!scope[method]) {
        throw(['method not found']);
      }
      return function () {
        return scope[method].apply(scope, arguments || []);
      };
    }
    return !scope ? method : function () {
      return method.apply(scope, arguments || []);
    };
  };

  return IDBStore;

}, this);
<|endoftext|>


<commit_before>/*eslint dot-notation: "off"*/
import {replace, when} from '../../../../test-helper';

describe.only('the products controller', () => {
  it('has no products', () => {
    let productsService;
    productsService = require('./service/products-service').productsService();
    replace(productsService, 'fetch');
    when(productsService.fetch()).thenResolve(['products']);

    let ProductsController = require('./products.controller')['ProductsController'];
    let productsController = new ProductsController(productsService);
    productsController.products.should.deepEqual([]);
  });

  it('fetches products from the server', () => {
    let productsService;
    let ProductsController;
    let productsController;

    productsService = require('./service/products-service').productsService();
    replace(productsService, 'fetch');
    when(productsService.fetch()).thenResolve(['products']);

    ProductsController = require('./products.controller')['ProductsController'];
    productsController = new ProductsController(productsService);

    return productsController.fetch().then(() => {
      productsController.products.should.deepEqual(['products']);
    });
  });
});

<commit_msg>factor out declarations<commit_after>/*eslint dot-notation: "off"*/
import {replace, when} from '../../../../test-helper';

describe.only('the products controller', () => {
  let productsService;
  let ProductsController;
  let productsController;

  it('has no products', () => {

    productsService = require('./service/products-service').productsService();
    replace(productsService, 'fetch');
    when(productsService.fetch()).thenResolve(['products']);

    ProductsController = require('./products.controller')['ProductsController'];
    productsController = new ProductsController(productsService);
    productsController.products.should.deepEqual([]);
  });

  it('fetches products from the server', () => {
    productsService = require('./service/products-service').productsService();
    replace(productsService, 'fetch');
    when(productsService.fetch()).thenResolve(['products']);

    ProductsController = require('./products.controller')['ProductsController'];
    productsController = new ProductsController(productsService);

    return productsController.fetch().then(() => {
      productsController.products.should.deepEqual(['products']);
    });
  });
});

<|endoftext|>


<commit_before>"use strict";

exports.exec = function(funs, done){
	iter(0);

	function iter(i){
		if( i >= funs.length ){
			done();
			return;
		}
		funs[i](function(err){
			if( err ){
				done(err);
				return;
			}
			iter(i+1);
		});
	}
};

exports.range = function(begin, end, fn){
	return function(done){
		iter(begin);

		function iter(i){
			if( i >= end ){
				done();
				return;
			}
			fn(i, function(err){
				if( err ){
					done(err);
					return;
				}
				iter(i+1);
			})
		}
	};
};

exports.forEach = function(arr, fn){
	return exports.range(0, arr.length, function(i, done){
		fn(arr[i], done);
	});
};

exports.forEachKey = function(obj, fn){
	return exports.forEach(Object.keys(obj), fn);
};

<commit_msg>cleanup<commit_after><|endoftext|>


<commit_before>'use strict';

var prettyBytes = require('pretty-bytes');
var chalk = require('chalk');
var utils = require('./lib/utils');
var csvparse = require('csv-parse');
var fs = require('fs');

function Output() {

	if (!(this instanceof Output)) {
		return new Output;
	}

	this.constants = {
		YOUR_IMAGE_WEIGHT     : '\nYour image weight: ',
		MEDIAN_MOBILE_WEIGHT  : 'Median mobile site image weight: ',
		MEDIAN_DESKTOP_WEIGHT : 'Median desktop site image weight: ',
		ON_MOBILE             : '\nOn Mobile:',
		ON_DESKTOP            : '\nOn Desktop:',
		MORE_BYTES_THAN       : 'You have more image bytes than',
		IMAGES_TO_OPTIMISE    : '\nImages to optimize:\n',
		PERCENTAGE_OF         : '% of sites'
	};

	/**
	 * PageSpeed Insights threshold
	 * @type {number}
	 */
	this.threshold = 70;

	/**
	 * What indices from BigQuery do we want to summarize scores
	 * for in the CLI output?
	 * @type {number[]}
	 */
	this.indicesOfInterest = [3, 6, 9]; // 25th, 50th, 75th percentile

	/**
	 * Include suggestions for URLs that could be improved score-wise
	 * This will not include a complete list of possible optimizations
	 * and relies on PageSpeed Insights API data
	 * @type {boolean}
	 */
	this.verbose = false;

	/**
	 * Is the weight higher than one of the BigQuery
	 * percentiles?
	 * @type {boolean}
	 */
	this.fasterThanAPercentile = false;
	this.bigQueryData = {
		desktop: [], mobile: [], titles: []
	};

	/**
	 * A subset of BigQuery data that we actually use for output
	 * This should represent the indices of interest from the
	 * bigQueryData.
	 * @type {{desktop: Array, mobile: Array, titles: Array}}
	 */
	this.outputData = {
		desktop: [], mobile: [], titles: []
	};

	/**
	 * Average image weights per site, based on BigQuery data
	 * @type {{desktop: number, mobile: number}}
	 */
	this.medians = {};

	/**
	 * Message shown when image weight is lower than one of the percentiles
	 * we care about.
	 */
	this.keepingWebFast = chalk.cyan('Thanks for keeping the web fast <3');

	var parser = csvparse({
		delimiter: ';'
	}, function (err, data) {

		// TODO: Refactor all of this to be less boilerplate-y.

		// Complete full set of available BigQuery Data
		this.bigQueryData.titles = data[0][0].split(',');
		this.bigQueryData.desktop = data[1][0].split(',');
		this.bigQueryData.mobile = data[2][0].split(',');

		// Sub-slice portions of data we're interested
		this.indicesOfInterest.forEach(function (item) {
			this.outputData.desktop.push(this.bigQueryData.desktop[item]);
			this.outputData.mobile.push(this.bigQueryData.mobile[item]);
			this.outputData.titles.push(this.bigQueryData.titles[item]);
		}.bind(this));

		this.medians.mobile = parseInt(this.bigQueryData.mobile[1], 10);
		this.medians.desktop = parseInt(this.bigQueryData.desktop[1], 10);

	}.bind(this));

	fs.createReadStream(__dirname + '/data/bigquery.csv').pipe(parser);
};

/**
 * Compare a supplied image weight with the weight in a supplied percentile.
 * @param siteImageWeight
 * @param percentileImageWeight
 * @param percentile
 * @returns {string} Summary message of comparisons to the percentile
 */
Output.prototype.compareWeightPercentile = function (siteImageWeight, percentileImageWeight, percentile) {
	if (siteImageWeight === undefined) {
		return;
	}
	var diff = (siteImageWeight - parseInt(percentileImageWeight, 10)) * 1000;
	if (diff > 0) {
		diff = chalk.red('+' + prettyBytes(diff));
	} else {
		diff = diff * -1;
		diff = chalk.green('-' + prettyBytes(diff));
		this.fasterThanAPercentile = true;
	}
	return diff + (' compared to sites in the ') + chalk.yellow(percentile.replace('p', '') + 'th') + ' percentile';
};

Output.prototype.compareWeights = function (siteImageWeight, sizes, percentiles) {
	if (siteImageWeight === undefined) {
		return;
	}
	var comparisons = '';
	siteImageWeight = parseInt(siteImageWeight, 10);
	for (var i = 0; i < percentiles.length; i++) {
		comparisons += this.compareWeightPercentile(siteImageWeight, sizes[i], percentiles[i]) + '\n';
	}
	return comparisons;
};

/**
 * Check if image weight is higher than one of the available percentile sizes
 * @param sizeImageWeight
 * @param sizes
 * @param percentiles
 * @returns {*}
 */
Output.prototype.getHighestPercentile = function (sizeImageWeight, sizes, percentiles) {
	if (sizeImageWeight === undefined) {
		return;
	}
	var highestPercentileMatch = -1;
	var result;

	// Begin with index 2 to avoid catching unnecessary labels
	// like `desktop` and `mobile` included in this row of data
	for (var i = 2; i < percentiles.length; i++) {
		sizes[i] = parseInt(sizes[i], 10);
		if (sizeImageWeight > sizes[i]) {
			highestPercentileMatch = i;
		}
	}

	if (highestPercentileMatch === -1){
		result = '0';
	} else {
		result = percentiles[highestPercentileMatch];
	}
	return result;
};

Output.prototype.process = function (parameters, response, done) {

	done = done || function () {};
	var logger = console.log;
	var error = null;
	this.threshold = parameters.threshold || this.threshold;
	this.verbose = parameters.verbose;
	var yourImageWeight = parseInt(response.pageStats.imageResponseBytes || 0, 10);
	var unoptimizedImages = response.formattedResults.ruleResults.OptimizeImages.urlBlocks;
	var desktopWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.desktop, this.outputData.titles);
	var mobileWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.mobile, this.outputData.titles);
	var highestPercentileDesktop = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.desktop, this.bigQueryData.titles);
	var highestPercentileMobile = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.mobile, this.bigQueryData.titles);
	var imagesToOptimize = '';

	if (this.verbose) {

		if (unoptimizedImages[1] !== undefined) {
			unoptimizedImages[1].urls.forEach(function (url) {
				url.result.args.forEach(function (x) {
					var result = '';
					switch (x.type) {
						case 'URL':
							result += chalk.green(x.value);
							break;
						case 'BYTES':
							result += 'Size: ' + chalk.red(x.value);
							break;
						case 'PERCENTAGE':
							result += 'Can be improved by ' + chalk.yellow(x.value) + '\n';
							break;
					}
					imagesToOptimize += result + '\n';
				});
			});
		}
	}

	logger([
		chalk.cyan(this.constants.YOUR_IMAGE_WEIGHT) + prettyBytes(yourImageWeight),
		chalk.gray(this.constants.MEDIAN_MOBILE_WEIGHT) + prettyBytes(this.medians.mobile * 1000),
		chalk.gray(this.constants.MEDIAN_DESKTOP_WEIGHT) + prettyBytes(this.medians.desktop * 1000),
		chalk.cyan(this.constants.ON_MOBILE),
		chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileDesktop.replace('p', '') + this.constants.PERCENTAGE_OF),
		mobileWeights,
		chalk.cyan(this.constants.ON_DESKTOP),
		chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileMobile.replace('p', '') + this.constants.PERCENTAGE_OF),
		desktopWeights,
		this.fasterThanAPercentile ? this.keepingWebFast: '',
		imagesToOptimize.length ? (chalk.underline(this.constants.IMAGES_TO_OPTIMISE) + imagesToOptimize + chalk.cyan('\nThis list does not include images which cannot be optimized further.\nYou may consider removing those images if possible.')) : '',
	].join('\n'));

	if (response.score < this.threshold) {
		error = new Error('Threshold of ' + this.threshold + ' not met with score of ' + response.score);
	}
	return done(error);
};

module.exports = Output;

<commit_msg>Formatting fixes for output module.<commit_after>'use strict';

var prettyBytes = require('pretty-bytes');
var chalk       = require('chalk');
var utils       = require('./lib/utils');
var csvparse    = require('csv-parse');
var fs          = require('fs');

function Output() {

	if (!(this instanceof Output)) {
		return new Output;
	}

	this.constants = {
		YOUR_IMAGE_WEIGHT     : '\nYour image weight: ',
		MEDIAN_MOBILE_WEIGHT  : 'Median mobile site image weight: ',
		MEDIAN_DESKTOP_WEIGHT : 'Median desktop site image weight: ',
		ON_MOBILE             : '\nOn Mobile:',
		ON_DESKTOP            : '\nOn Desktop:',
		MORE_BYTES_THAN       : 'You have more image bytes than ',
		IMAGES_TO_OPTIMISE    : '\nImages to optimize:\n',
		PERCENTAGE_OF         : '% of sites'
	};

	/**
	 * PageSpeed Insights threshold
	 * @type {number}
	 */
	this.threshold = 70;

	/**
	 * What indices from BigQuery do we want to summarize scores
	 * for in the CLI output?
	 * @type {number[]}
	 */
	this.indicesOfInterest = [3, 6, 9]; // 25th, 50th, 75th percentile

	/**
	 * Include suggestions for URLs that could be improved score-wise
	 * This will not include a complete list of possible optimizations
	 * and relies on PageSpeed Insights API data
	 * @type {boolean}
	 */
	this.verbose = false;

	/**
	 * Is the weight higher than one of the BigQuery
	 * percentiles?
	 * @type {boolean}
	 */
	this.fasterThanAPercentile = false;
	this.bigQueryData = {
		desktop: [], mobile: [], titles: []
	};

	/**
	 * A subset of BigQuery data that we actually use for output
	 * This should represent the indices of interest from the
	 * bigQueryData.
	 * @type {{desktop: Array, mobile: Array, titles: Array}}
	 */
	this.outputData = {
		desktop: [], mobile: [], titles: []
	};

	/**
	 * Average image weights per site, based on BigQuery data
	 * @type {{desktop: number, mobile: number}}
	 */
	this.medians = {};

	/**
	 * Message shown when image weight is lower than one of the percentiles
	 * we care about.
	 */
	this.keepingWebFast = chalk.cyan('Thanks for keeping the web fast <3');

	var parser = csvparse({
		delimiter: ';'
	}, function (err, data) {

		// TODO: Refactor all of this to be less boilerplate-y.

		// Complete full set of available BigQuery Data
		this.bigQueryData.titles = data[0][0].split(',');
		this.bigQueryData.desktop = data[1][0].split(',');
		this.bigQueryData.mobile = data[2][0].split(',');

		// Sub-slice portions of data we're interested
		this.indicesOfInterest.forEach(function (item) {
			this.outputData.desktop.push(this.bigQueryData.desktop[item]);
			this.outputData.mobile.push(this.bigQueryData.mobile[item]);
			this.outputData.titles.push(this.bigQueryData.titles[item]);
		}.bind(this));

		this.medians.mobile = parseInt(this.bigQueryData.mobile[1], 10);
		this.medians.desktop = parseInt(this.bigQueryData.desktop[1], 10);

	}.bind(this));

	fs.createReadStream(__dirname + '/data/bigquery.csv').pipe(parser);
};

/**
 * Compare a supplied image weight with the weight in a supplied percentile.
 * @param siteImageWeight
 * @param percentileImageWeight
 * @param percentile
 * @returns {string} Summary message of comparisons to the percentile
 */
Output.prototype.compareWeightPercentile = function (siteImageWeight, percentileImageWeight, percentile) {
	if (siteImageWeight === undefined) {
		return;
	}
	var diff = (siteImageWeight - parseInt(percentileImageWeight, 10)) * 1000;
	if (diff > 0) {
		diff = chalk.red('+' + prettyBytes(diff));
	} else {
		diff = diff * -1;
		diff = chalk.green('-' + prettyBytes(diff));
		this.fasterThanAPercentile = true;
	}
	return diff + (' compared to sites in the ') + chalk.yellow(percentile.replace('p', '') + 'th') + ' percentile';
};

Output.prototype.compareWeights = function (siteImageWeight, sizes, percentiles) {
	if (siteImageWeight === undefined) {
		return;
	}
	var comparisons = '';
	siteImageWeight = parseInt(siteImageWeight, 10);
	for (var i = 0; i < percentiles.length; i++) {
		comparisons += this.compareWeightPercentile(siteImageWeight, sizes[i], percentiles[i]) + '\n';
	}
	return comparisons;
};

/**
 * Check if image weight is higher than one of the available percentile sizes
 * @param sizeImageWeight
 * @param sizes
 * @param percentiles
 * @returns {*}
 */
Output.prototype.getHighestPercentile = function (sizeImageWeight, sizes, percentiles) {
	if (sizeImageWeight === undefined) {
		return;
	}
	var highestPercentileMatch = -1;
	var result;

	// Begin with index 2 to avoid catching unnecessary labels
	// like `desktop` and `mobile` included in this row of data
	for (var i = 2; i < percentiles.length; i++) {
		sizes[i] = parseInt(sizes[i], 10);
		if (sizeImageWeight > sizes[i]) {
			highestPercentileMatch = i;
		}
	}

	if (highestPercentileMatch === -1){
		result = '0';
	} else {
		result = percentiles[highestPercentileMatch];
	}
	return result;
};

Output.prototype.process = function (parameters, response, done) {

	done = done || function () {};
	var logger = console.log;
	var error = null;
	this.threshold = parameters.threshold || this.threshold;
	this.verbose = parameters.verbose;
	var yourImageWeight = parseInt(response.pageStats.imageResponseBytes || 0, 10);
	var unoptimizedImages = response.formattedResults.ruleResults.OptimizeImages.urlBlocks;
	var desktopWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.desktop, this.outputData.titles);
	var mobileWeights = this.compareWeights(yourImageWeight / 1000, this.outputData.mobile, this.outputData.titles);
	var highestPercentileDesktop = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.desktop, this.bigQueryData.titles);
	var highestPercentileMobile = this.getHighestPercentile(yourImageWeight / 1000, this.bigQueryData.mobile, this.bigQueryData.titles);
	var imagesToOptimize = '';

	if (this.verbose) {

		if (unoptimizedImages[1] !== undefined) {
			unoptimizedImages[1].urls.forEach(function (url) {
				url.result.args.forEach(function (x) {
					var result = '';
					switch (x.type) {
						case 'URL':
							result += chalk.green(x.value);
							break;
						case 'BYTES':
							result += 'Size: ' + chalk.red(x.value);
							break;
						case 'PERCENTAGE':
							result += 'Can be improved by ' + chalk.yellow(x.value) + '\n';
							break;
					}
					imagesToOptimize += result + '\n';
				});
			});
		}
	}

	logger([
		chalk.cyan(this.constants.YOUR_IMAGE_WEIGHT) + prettyBytes(yourImageWeight),
		chalk.gray(this.constants.MEDIAN_MOBILE_WEIGHT) + prettyBytes(this.medians.mobile * 1000),
		chalk.gray(this.constants.MEDIAN_DESKTOP_WEIGHT) + prettyBytes(this.medians.desktop * 1000),
		chalk.cyan(this.constants.ON_MOBILE),
		chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileDesktop.replace('p', '') + this.constants.PERCENTAGE_OF),
		mobileWeights,
		chalk.cyan(this.constants.ON_DESKTOP),
		chalk.magenta(this.constants.MORE_BYTES_THAN + highestPercentileMobile.replace('p', '') + this.constants.PERCENTAGE_OF),
		desktopWeights,
		this.fasterThanAPercentile ? this.keepingWebFast: '',
		imagesToOptimize.length ? (chalk.underline(this.constants.IMAGES_TO_OPTIMISE) + imagesToOptimize + chalk.cyan('\nThis list does not include images which cannot be optimized further.\nYou may consider removing those images if possible.')) : '',
	].join('\n'));

	if (response.score < this.threshold) {
		error = new Error('Threshold of ' + this.threshold + ' not met with score of ' + response.score);
	}
	return done(error);
};

module.exports = Output;

<|endoftext|>


<commit_before>JS.require('JS.Class');
JS.require('JS.Hash');

var __ = require('./import')('error');

var ResolverError = exports.ResolverError = new JS.Class('ResolverError', __.YAMLError, {});

var DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str',
    DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq',
    DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map';

exports.BaseResolver = new JS.Class('BaseResolver', {
  extend: {
    yamlImplicitResolvers: new JS.Hash(),

    addImplicitResolver: function (tag, regexp, first) {
      var cls = this;

      if (undefined === first) {
        first = [null];
      }

      first.forEach(function (ch) {
        cls.yamlImplicitResolvers.setDefault(ch, []).append(tag, regexp);
      });
    },
  },

  initialize: function () {
    this.resolverExactPaths = [];
    this.resolverPrefixPaths = [];
  },

  descendResolver: function (currentNode, currentIndex) {
    // not implemented due to experimental nature of method
    return;
  },

  ascendResolver: function () {
    // not implemented due to experimental nature of method
    return;
  },

  checkResolverPrefix: function (depth, path, kind, currentNode, currentIndex) {
    /* TBI
    node_check, index_check = path[depth-1]
    if isinstance(node_check, str):
        if current_node.tag != node_check:
            return
    elif node_check is not None:
        if not isinstance(current_node, node_check):
            return
    if index_check is True and current_index is not None:
        return
    if (index_check is False or index_check is None)    \
            and current_index is None:
        return
    if isinstance(index_check, str):
        if not (isinstance(current_index, ScalarNode)
                and index_check == current_index.value):
            return
    elif isinstance(index_check, int) and not isinstance(index_check, bool):
        if index_check != current_index:
            return
    return True
    */
  },

  resolve: function (kind, value, implicit) {
    /* TBI
    if kind is ScalarNode and implicit[0]:
        if value == '':
            resolvers = self.yaml_implicit_resolvers.get('', [])
        else:
            resolvers = self.yaml_implicit_resolvers.get(value[0], [])
        resolvers += self.yaml_implicit_resolvers.get(None, [])
        for tag, regexp in resolvers:
            if regexp.match(value):
                return tag
        implicit = implicit[1]
    if self.yaml_path_resolvers:
        exact_paths = self.resolver_exact_paths[-1]
        if kind in exact_paths:
            return exact_paths[kind]
        if None in exact_paths:
            return exact_paths[None]
    if kind is ScalarNode:
        return self.DEFAULT_SCALAR_TAG
    elif kind is SequenceNode:
        return self.DEFAULT_SEQUENCE_TAG
    elif kind is MappingNode:
        return self.DEFAULT_MAPPING_TAG
    */
  }
});

exports.Resolver = new JS.Class('Resolver', exports.BaseResolver, {
  extend: {
    yamlImplicitResolvers: exports.BaseResolver.yamlImplicitResolvers.dup()
  }
});

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:bool',
  new RegExp('^(?:yes|Yes|YES|no|No|NO' +
             '|true|True|TRUE|false|False|FALSE' +
             '|on|On|ON|off|Off|OFF)$'),
  ['y', 'Y', 'n', 'N', 't', 'T', 'f', 'F', 'o', 'O']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:float',
  new RegExp('^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?' +
             '|\.[0-9_]+(?:[eE][-+][0-9]+)?' +
             '|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*' +
             '|[-+]?\.(?:inf|Inf|INF)' +
             '|\.(?:nan|NaN|NAN))$'),
  ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:int',
  new RegExp('^(?:[-+]?0b[0-1_]+' +
             '|[-+]?0[0-7_]+' +
             '|[-+]?(?:0|[1-9][0-9_]*)' +
             '|[-+]?0x[0-9a-fA-F_]+' +
             '|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$'),
  ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:merge',
  new RegExp('^(?:<<)$'),
  ['<']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:null',
  new RegExp('^(?: ~' +
             '|null|Null|NULL' +
             '| )$'),
  ['~', 'n', 'N', '']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:timestamp',
  new RegExp('^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' +
             '|[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?' +
             '(?:[Tt]|[ \t]+)[0-9][0-9]?' +
             ':[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)?' +
             '(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$'),
  ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:value',
  new RegExp('^(?:=)$'),
  ['=']);

// The following resolver is only for documentation purposes. It cannot work
// because plain scalars cannot start with '!', '&', or '*'.
exports.Resolver.addImplicitResolver('tag:yaml.org,2002:yaml',
  new RegExp('^(?:!|&|\*)$'),
  ['!', '&', '*']);

////////////////////////////////////////////////////////////////////////////////
// vim:ts=2:sw=2
////////////////////////////////////////////////////////////////////////////////
<commit_msg>Finished Resolver<commit_after>JS.require('JS.Class');
JS.require('JS.Hash');

var __ = require('./import')('nodes');

var DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str',
    DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq',
    DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map';

exports.BaseResolver = new JS.Class('BaseResolver', {
  extend: {
    yamlImplicitResolvers: new JS.Hash(),
    addImplicitResolver: function (tag, regexp, first) {
      var cls = this;

      if (undefined === first) {
        first = [null];
      }

      first.forEach(function (ch) {
        cls.yamlImplicitResolvers.setDefault(ch, [[tag, regexp]]);
      });
    },
  },

  initialize: function () {
    this.resolverExactPaths = [];
    this.resolverPrefixPaths = [];
  },

  descendResolver: function (currentNode, currentIndex) {
    // not implemented due to experimental nature of method
    return;
  },

  ascendResolver: function () {
    // not implemented due to experimental nature of method
    return;
  },

  resolve: function (kind, value, implicit) {
    var resolvers, i, tag, regexp;

    if (kind.isA(__.ScalarNode) && implicit[0]) {
      if (value == '') {
        resolvers = this.klass.yamlImplicitResolvers.get('', []);
      } else {
        resolvers = this.klass.yamlImplicitResolvers.get(value[0], []);
      }

      resolvers.push(this.klass.yamlImplicitResolvers.get(null, []));

      for (i in resolvers) {
        tag = resolvers[i][0];
        regexp = resolvers[i][1];
        if (regexp.test(value)) {
          return tag;
        }
      }

      implicit = implicit[1];
    }

    if (kind.isA(__.ScalarNode)) {
      return DEFAULT_SCALAR_TAG;
    } else if (kind.isA(__.SequenceNode)) {
      return DEFAULT_SEQUENCE_TAG;
    } else if (kind.isA(__.MappingNode)) {
      return DEFAULT_MAPPING_TAG;
    }
  }
});

exports.Resolver = new JS.Class('Resolver', exports.BaseResolver, {
  extend: {
    yamlImplicitResolvers: exports.BaseResolver.yamlImplicitResolvers.dup()
  }
});

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:bool',
  new RegExp('^(?:yes|Yes|YES|no|No|NO' +
             '|true|True|TRUE|false|False|FALSE' +
             '|on|On|ON|off|Off|OFF)$'),
  ['y', 'Y', 'n', 'N', 't', 'T', 'f', 'F', 'o', 'O']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:float',
  new RegExp('^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?' +
             '|\.[0-9_]+(?:[eE][-+][0-9]+)?' +
             '|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*' +
             '|[-+]?\.(?:inf|Inf|INF)' +
             '|\.(?:nan|NaN|NAN))$'),
  ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:int',
  new RegExp('^(?:[-+]?0b[0-1_]+' +
             '|[-+]?0[0-7_]+' +
             '|[-+]?(?:0|[1-9][0-9_]*)' +
             '|[-+]?0x[0-9a-fA-F_]+' +
             '|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$'),
  ['-', '+', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:merge',
  new RegExp('^(?:<<)$'),
  ['<']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:null',
  new RegExp('^(?: ~' +
             '|null|Null|NULL' +
             '| )$'),
  ['~', 'n', 'N', '']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:timestamp',
  new RegExp('^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' +
             '|[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]?' +
             '(?:[Tt]|[ \t]+)[0-9][0-9]?' +
             ':[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)?' +
             '(?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$'),
  ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);

exports.Resolver.addImplicitResolver('tag:yaml.org,2002:value',
  new RegExp('^(?:=)$'),
  ['=']);

// The following resolver is only for documentation purposes. It cannot work
// because plain scalars cannot start with '!', '&', or '*'.
exports.Resolver.addImplicitResolver('tag:yaml.org,2002:yaml',
  new RegExp('^(?:!|&|\*)$'),
  ['!', '&', '*']);

////////////////////////////////////////////////////////////////////////////////
// vim:ts=2:sw=2
////////////////////////////////////////////////////////////////////////////////
<|endoftext|>


<commit_before>const assert = require('assert');
const desktopCapturer = require('electron').desktopCapturer;

describe('desktopCapturer', function() {
  it('should return a non-empty array of sources', function(done) {
    desktopCapturer.getSources({
      types: ['window', 'screen']
    }, function(error, sources) {
      assert.equal(error, null);
      assert.notEqual(sources.length, 0);
      done();
    });
  });

  it('does not throw an error when called more than once (regression)', function(done) {
    var callCount = 0;
    var callback = function (error, sources) {
      callCount++;
      assert.equal(error, null);
      assert.notEqual(sources.length, 0);
      if (callCount === 2) done();
    }

    desktopCapturer.getSources({types: ['window', 'screen']}, callback);
    desktopCapturer.getSources({types: ['window', 'screen']}, callback);
  })
});
<commit_msg>Add missing semicolons<commit_after>const assert = require('assert');
const desktopCapturer = require('electron').desktopCapturer;

describe('desktopCapturer', function() {
  it('should return a non-empty array of sources', function(done) {
    desktopCapturer.getSources({
      types: ['window', 'screen']
    }, function(error, sources) {
      assert.equal(error, null);
      assert.notEqual(sources.length, 0);
      done();
    });
  });

  it('does not throw an error when called more than once (regression)', function(done) {
    var callCount = 0;
    var callback = function (error, sources) {
      callCount++;
      assert.equal(error, null);
      assert.notEqual(sources.length, 0);
      if (callCount === 2) done();
    };

    desktopCapturer.getSources({types: ['window', 'screen']}, callback);
    desktopCapturer.getSources({types: ['window', 'screen']}, callback);
  });
});
<|endoftext|>


<commit_before>var geom = require('pex-geom');
var Vec3 = geom.Vec3;
var Geometry = geom.Geometry;

function LineBuilder() {
  Geometry.call(this, { vertices: true, colors: true })
}

LineBuilder.prototype = Object.create(Geometry.prototype);

LineBuilder.prototype.addLine = function(a, b, colorA, colorB) {
  colorA = colorA || { r: 1, g: 1, b: 1, a: 1 };
  colorB = colorB || colorA;
  this.vertices.push(Vec3.create().copy(a));
  this.vertices.push(Vec3.create().copy(b));
  this.colors.push(colorA);
  this.colors.push(colorB);
  this.vertices.dirty = true;
  this.colors.dirty = true;
};

LineBuilder.prototype.addPath = function(path, color, numSamples, showPoints) {
  numSamples = numSamples || path.points.length;
  color = color || { r: 1, g: 1, b: 1, a: 1 };

  var prevPoint = path.getPointAt(0);
  if (showPoints) this.addCross(prevPoint, 0.1, color);
  for(var i=1; i<numSamples; i++) {
    var point;
    if (path.points.length == numSamples) {
      point = path.getPoint(i/(numSamples-1));
    }
    else {
      point = path.getPointAt(i/(numSamples-1));
    }
    this.addLine(prevPoint, point, color);
    prevPoint = point;
    if (showPoints) this.addCross(prevPoint, 0.1, color);
  }
  this.vertices.dirty = true;
  this.colors.dirty = true;
}

LineBuilder.prototype.addCross = function(pos, size, color) {
  size = size || 0.1;
  var halfSize = size / 2;
  color = color || { r: 1, g: 1, b: 1, a: 1 };
  this.vertices.push(Vec3.create().set(pos.x - halfSize, pos.y, pos.z));
  this.vertices.push(Vec3.create().set(pos.x + halfSize, pos.y, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y - halfSize, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y + halfSize, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y, pos.z - halfSize));
  this.vertices.push(Vec3.create().set(pos.x, pos.y, pos.z + halfSize));
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
};

LineBuilder.prototype.reset = function() {
  this.vertices.length = 0;
  this.colors.length = 0;
  this.vertices.dirty = true;
  this.colors.dirty = true;
};

module.exports = LineBuilder;
<commit_msg>Returning this for LineBuilder method chaining<commit_after>var geom = require('pex-geom');
var Vec3 = geom.Vec3;
var Geometry = geom.Geometry;

function LineBuilder() {
  Geometry.call(this, { vertices: true, colors: true })
}

LineBuilder.prototype = Object.create(Geometry.prototype);

LineBuilder.prototype.addLine = function(a, b, colorA, colorB) {
  colorA = colorA || { r: 1, g: 1, b: 1, a: 1 };
  colorB = colorB || colorA;
  this.vertices.push(Vec3.create().copy(a));
  this.vertices.push(Vec3.create().copy(b));
  this.colors.push(colorA);
  this.colors.push(colorB);
  this.vertices.dirty = true;
  this.colors.dirty = true;
  return this;
};

LineBuilder.prototype.addPath = function(path, color, numSamples, showPoints) {
  numSamples = numSamples || path.points.length;
  color = color || { r: 1, g: 1, b: 1, a: 1 };

  var prevPoint = path.getPointAt(0);
  if (showPoints) this.addCross(prevPoint, 0.1, color);
  for(var i=1; i<numSamples; i++) {
    var point;
    if (path.points.length == numSamples) {
      point = path.getPoint(i/(numSamples-1));
    }
    else {
      point = path.getPointAt(i/(numSamples-1));
    }
    this.addLine(prevPoint, point, color);
    prevPoint = point;
    if (showPoints) this.addCross(prevPoint, 0.1, color);
  }
  this.vertices.dirty = true;
  this.colors.dirty = true;
  return this;
}

LineBuilder.prototype.addCross = function(pos, size, color) {
  size = size || 0.1;
  var halfSize = size / 2;
  color = color || { r: 1, g: 1, b: 1, a: 1 };
  this.vertices.push(Vec3.create().set(pos.x - halfSize, pos.y, pos.z));
  this.vertices.push(Vec3.create().set(pos.x + halfSize, pos.y, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y - halfSize, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y + halfSize, pos.z));
  this.vertices.push(Vec3.create().set(pos.x, pos.y, pos.z - halfSize));
  this.vertices.push(Vec3.create().set(pos.x, pos.y, pos.z + halfSize));
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  this.colors.push(color);
  return this;
};

LineBuilder.prototype.reset = function() {
  this.vertices.length = 0;
  this.colors.length = 0;
  this.vertices.dirty = true;
  this.colors.dirty = true;
  return this;
};

module.exports = LineBuilder;
<|endoftext|>