<commit_before>/**
 * @class Ext.ux.form.field.DateTime
 * @extends Ext.form.FieldContainer
 * @version 0.2 (July 20th, 2011)
 * @author atian25 (http://www.sencha.com/forum/member.php?51682-atian25)
 * @author ontho (http://www.sencha.com/forum/member.php?285806-ontho)
 * @author jakob.ketterl (http://www.sencha.com/forum/member.php?25102-jakob.ketterl)
 * @link http://www.sencha.com/forum/showthread.php?134345-Ext.ux.form.field.DateTime
 * from http://www.sencha.com/forum/showthread.php?134345-Ext.ux.form.field.DateTime&p=863449&viewfull=1#post863449
 */
Ext.define('Ext.ux.form.field.DateTime', {
    extend:'Ext.form.FieldContainer',
    mixins:{
        field:'Ext.form.field.Field'
    },
    alias: 'widget.xdatetime',

    //configurables

    combineErrors: true,
    msgTarget: 'under',
    layout: 'hbox',
    readOnly: false,

    /**
     * @cfg {String} dateFormat
     * Convenience config for specifying the format of the date portion.
     * This value is overridden if format is specified in the dateConfig.
     * The default is 'Y-m-d'
     */
    dateFormat: 'Y-m-d',
    /**
     * @cfg {String} timeFormat
     * Convenience config for specifying the format of the time portion.
     * This value is overridden if format is specified in the timeConfig.
     * The default is 'H:i:s'
     */
    timeFormat: 'H:i:s',
//    /**
//     * @cfg {String} dateTimeFormat
//     * The format used when submitting the combined value.
//     * Defaults to 'Y-m-d H:i:s'
//     */
//    dateTimeFormat: 'Y-m-d H:i:s',
    /**
     * @cfg {Object} dateConfig
     * Additional config options for the date field.
     */
    dateConfig:{},
    /**
     * @cfg {Object} timeConfig
     * Additional config options for the time field.
     */
    timeConfig:{},

    // properties

    dateValue: null, // Holds the actual date
    /**
     * @property dateField
     * @type Ext.form.field.Date
     */
    dateField: null,
    /**
     * @property timeField
     * @type Ext.form.field.Time
     */
    timeField: null,

    initComponent: function(){
        var me = this
            ,i = 0
            ,key
            ,tab;

        me.items = me.items || [];

        me.dateField = Ext.create('Ext.form.field.Date', Ext.apply({
            format:me.dateFormat,
            flex:1,
            isFormField:false, //exclude from field query's
            submitValue:false
        }, me.dateConfig));
        me.items.push(me.dateField);

        me.timeField = Ext.create('Ext.form.field.Time', Ext.apply({
            format:me.timeFormat,
            flex:1,
            isFormField:false, //exclude from field query's
            submitValue:false
        }, me.timeConfig));
        me.items.push(me.timeField);

        for (; i < me.items.length; i++) {
            me.items[i].on('focus', Ext.bind(me.onItemFocus, me));
            me.items[i].on('blur', Ext.bind(me.onItemBlur, me));
            me.items[i].on('specialkey', function(field, event){
                key = event.getKey();
                tab = key == event.TAB;

                if (tab && me.focussedItem == me.dateField) {
                    event.stopEvent();
                    me.timeField.focus();
                    return;
                }

                me.fireEvent('specialkey', field, event);
            });
        }

        me.callParent();

        // this dummy is necessary because Ext.Editor will not check whether an inputEl is present or not
        this.inputEl = {
            dom: document.createElement('div'),
            swallowEvent:function(){}
        };

        me.initField();
    },

    focus:function(){
        this.callParent(arguments);
        this.dateField.focus();
    },

    onItemFocus:function(item){
        if (this.blurTask){
            this.blurTask.cancel();
        }
        this.focussedItem = item;
    },

    onItemBlur:function(item, e){
        var me = this;
        if (item != me.focussedItem){ return; }
        // 100ms to focus a new item that belongs to us, otherwise we will assume the user left the field
        me.blurTask = new Ext.util.DelayedTask(function(){
            me.fireEvent('blur', me, e);
        });
        me.blurTask.delay(100);
    },

    getValue: function(){
        var value = null
            ,date = this.dateField.getSubmitValue()
            ,time = this.timeField.getSubmitValue()
            ,format;

        if (date){
            if (time){
                format = this.getFormat();
                value = Ext.Date.parse(date + ' ' + time, format);
            } else {
                value = this.dateField.getValue();
            }
        }
        return value;
    },

    getSubmitValue: function(){
//        var value = this.getValue();
//        return value ? Ext.Date.format(value, this.dateTimeFormat) : null;

        var me = this
            ,format = me.getFormat()
            ,value = me.getValue();

        return value ? Ext.Date.format(value, format) : null;
    },

    setValue: function(value){
        if (Ext.isString(value)){
            value = Ext.Date.parse(value, this.getFormat()); //this.dateTimeFormat
        }
        this.dateField.setValue(value);
        this.timeField.setValue(value);
    },

    getFormat: function(){
        return (this.dateField.submitFormat || this.dateField.format) + " " + (this.timeField.submitFormat || this.timeField.format);
    },

    // Bug? A field-mixin submits the data from getValue, not getSubmitValue
    getSubmitData: function(){
        var me = this
            ,data = null;

        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
            data = {};
            data[me.getName()] = '' + me.getSubmitValue();
        }
        return data;
    }
});
<commit_msg>Make xdatetime respect read_only (issue #86)<commit_after>/**
 * @class Ext.ux.form.field.DateTime
 * @extends Ext.form.FieldContainer
 * @version 0.2 (July 20th, 2011)
 * @author atian25 (http://www.sencha.com/forum/member.php?51682-atian25)
 * @author ontho (http://www.sencha.com/forum/member.php?285806-ontho)
 * @author jakob.ketterl (http://www.sencha.com/forum/member.php?25102-jakob.ketterl)
 * @link http://www.sencha.com/forum/showthread.php?134345-Ext.ux.form.field.DateTime
 * from http://www.sencha.com/forum/showthread.php?134345-Ext.ux.form.field.DateTime&p=863449&viewfull=1#post863449
 */
Ext.define('Ext.ux.form.field.DateTime', {
    extend:'Ext.form.FieldContainer',
    mixins:{
        field:'Ext.form.field.Field'
    },
    alias: 'widget.xdatetime',

    //configurables

    combineErrors: true,
    msgTarget: 'under',
    layout: 'hbox',
    readOnly: false,

    /**
     * @cfg {String} dateFormat
     * Convenience config for specifying the format of the date portion.
     * This value is overridden if format is specified in the dateConfig.
     * The default is 'Y-m-d'
     */
    dateFormat: 'Y-m-d',
    /**
     * @cfg {String} timeFormat
     * Convenience config for specifying the format of the time portion.
     * This value is overridden if format is specified in the timeConfig.
     * The default is 'H:i:s'
     */
    timeFormat: 'H:i:s',
//    /**
//     * @cfg {String} dateTimeFormat
//     * The format used when submitting the combined value.
//     * Defaults to 'Y-m-d H:i:s'
//     */
//    dateTimeFormat: 'Y-m-d H:i:s',
    /**
     * @cfg {Object} dateConfig
     * Additional config options for the date field.
     */
    dateConfig:{},
    /**
     * @cfg {Object} timeConfig
     * Additional config options for the time field.
     */
    timeConfig:{},

    // properties

    dateValue: null, // Holds the actual date
    /**
     * @property dateField
     * @type Ext.form.field.Date
     */
    dateField: null,
    /**
     * @property timeField
     * @type Ext.form.field.Time
     */
    timeField: null,

    initComponent: function(){
        var me = this
            ,i = 0
            ,key
            ,tab;

        me.items = me.items || [];

        me.dateField = Ext.create('Ext.form.field.Date', Ext.apply({
            format:me.dateFormat,
            flex:1,
            isFormField:false, //exclude from field query's
            submitValue:false,
            readOnly: me.readOnly
        }, me.dateConfig));
        me.items.push(me.dateField);

        me.timeField = Ext.create('Ext.form.field.Time', Ext.apply({
            format:me.timeFormat,
            flex:1,
            isFormField:false, //exclude from field query's
            submitValue:false,
            readOnly: me.readOnly
        }, me.timeConfig));
        me.items.push(me.timeField);

        for (; i < me.items.length; i++) {
            me.items[i].on('focus', Ext.bind(me.onItemFocus, me));
            me.items[i].on('blur', Ext.bind(me.onItemBlur, me));
            me.items[i].on('specialkey', function(field, event){
                key = event.getKey();
                tab = key == event.TAB;

                if (tab && me.focussedItem == me.dateField) {
                    event.stopEvent();
                    me.timeField.focus();
                    return;
                }

                me.fireEvent('specialkey', field, event);
            });
        }

        me.callParent();

        // this dummy is necessary because Ext.Editor will not check whether an inputEl is present or not
        this.inputEl = {
            dom: document.createElement('div'),
            swallowEvent:function(){}
        };

        me.initField();
    },

    focus:function(){
        this.callParent(arguments);
        this.dateField.focus();
    },

    onItemFocus:function(item){
        if (this.blurTask){
            this.blurTask.cancel();
        }
        this.focussedItem = item;
    },

    onItemBlur:function(item, e){
        var me = this;
        if (item != me.focussedItem){ return; }
        // 100ms to focus a new item that belongs to us, otherwise we will assume the user left the field
        me.blurTask = new Ext.util.DelayedTask(function(){
            me.fireEvent('blur', me, e);
        });
        me.blurTask.delay(100);
    },

    getValue: function(){
        var value = null
            ,date = this.dateField.getSubmitValue()
            ,time = this.timeField.getSubmitValue()
            ,format;

        if (date){
            if (time){
                format = this.getFormat();
                value = Ext.Date.parse(date + ' ' + time, format);
            } else {
                value = this.dateField.getValue();
            }
        }
        return value;
    },

    getSubmitValue: function(){
//        var value = this.getValue();
//        return value ? Ext.Date.format(value, this.dateTimeFormat) : null;

        var me = this
            ,format = me.getFormat()
            ,value = me.getValue();

        return value ? Ext.Date.format(value, format) : null;
    },

    setValue: function(value){
        if (Ext.isString(value)){
            value = Ext.Date.parse(value, this.getFormat()); //this.dateTimeFormat
        }
        this.dateField.setValue(value);
        this.timeField.setValue(value);
    },

    getFormat: function(){
        return (this.dateField.submitFormat || this.dateField.format) + " " + (this.timeField.submitFormat || this.timeField.format);
    },

    // Bug? A field-mixin submits the data from getValue, not getSubmitValue
    getSubmitData: function(){
        var me = this
            ,data = null;

        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
            data = {};
            data[me.getName()] = '' + me.getSubmitValue();
        }
        return data;
    }
});
<|endoftext|>


<commit_before>
var assertScreenShotMatch = require('../../lib/testUtils').assertScreenShotMatch;

var assert = require('assert');
var expect = require('chai').expect;
var goToUrlAndSetLocalStorage = require('./../specUtils').goToUrlAndSetLocalStorage;
var waitForNetworkQuiet = require('./../specUtils').waitForNetworkQuiet;

const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, "");

function waitForGenomeNexusAnnotation() {
    browser.pause(5000);// wait for annotation
}

describe('Mutation Table', function() {
    before(function(){
        goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}`);
    });

    describe('try getting exon and hgvsc info from genome nexus', ()=>{
        before(()=>{
            var url = `${CBIOPORTAL_URL}/results/mutations?Action=Submit&Z_SCORE_THRESHOLD=1.0&cancer_study_id=gbm_tcga_pub&cancer_study_list=gbm_tcga_pub&case_ids=&case_set_id=gbm_tcga_pub_sequenced&clinicallist=PROFILED_IN_gbm_tcga_pub_cna_rae&gene_list=TP53%20MDM2%20MDM4&gene_set_choice=user-defined_list&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=gbm_tcga_pub_cna_rae&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=gbm_tcga_pub_mutations&show_samples=false`;

            goToUrlAndSetLocalStorage(url);
            // mutations table should be visiable after oncokb icon shows up,
            // also need to wait for mutations to be sorted properly
            browser.waitForVisible("tr:nth-child(1) [data-test=oncogenic-icon-image]",30000);
        });

        it('should show the exon number after adding the exon column', ()=>{
            // check if 6 appears once in COSMIC column
            browser.waitForText('//*[text()="6"]',30000);
            var textValuesWith6 = browser.getText('//*[text()="6"]');
            assert.ok(textValuesWith6.length === 1);
            // click on column button
            browser.click("button*=Columns");
            // scroll down to activated "exon" selection
            browser.scroll(1000, 1000);
            // click "exon"
            browser.click('//*[text()="Exon"]');
            // check if three exact matches for 6 appear
            browser.waitUntil(() => {
                textValuesWith6 = browser.getText('//*[text()="6"]');
                return textValuesWith6.length > 1;
            }, 60000, "Exon data not in Exon column");
            textValuesWith6 = browser.getText('//*[text()="6"]');
            assert.equal(textValuesWith6.length, 3);
        });

        it('should show more exon number after clicking "Show more"', ()=>{
            // click "show more" to add more data
            browser.click("button*=Show more");
            // check if 6 exact matches for 6 appear
            browser.waitUntil(() => {
                textValuesWith6 = browser.getText('//*[text()="6"]');
                return textValuesWith6.length === 6;
            }, 20000, "Exon data not in Exon column after clikcing show more button");
        });

        it('should show the HGVSc data after adding the HGVSc column', ()=>{
            // reopen columns
            browser.click("button*=Columns");
            // click "HGVSc"
            browser.click('//*[text()="HGVSc"]');

            // check if "ENST00000269305.4:c.817C>T" appears
            browser.waitUntil(() => {
                var hgvscValues = browser.getText('//*[text()[contains(.,"ENST00000269305.4:c.817C>T")]]');
                return hgvscValues.length > 0;
            }, 20000, "HGVSc values not in hgvs column");
        });

        it('should show more HGVSc data after clicking "Show more"', ()=>{
            // click "show more" to add more data
            browser.click("button*=Show more");
            // check if "C>T" exact matches for 12 appear
            browser.waitUntil(() => {
                var hgvscValues = browser.getText('//*[text()[contains(.,"C>T")]]');
                return hgvscValues.length > 0;
            }, 20000, "HGVSc values not in hgvs column after clicking show more button");
        });
    });

});<commit_msg>fixed broken exon column e2e test<commit_after>
var assertScreenShotMatch = require('../../lib/testUtils').assertScreenShotMatch;

var assert = require('assert');
var expect = require('chai').expect;
var goToUrlAndSetLocalStorage = require('./../specUtils').goToUrlAndSetLocalStorage;
var waitForNetworkQuiet = require('./../specUtils').waitForNetworkQuiet;

const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, "");

function waitForGenomeNexusAnnotation() {
    browser.pause(5000);// wait for annotation
}

describe('Mutation Table', function() {
    before(function(){
        goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}`);
    });

    describe('try getting exon and hgvsc info from genome nexus', ()=>{
        before(()=>{
            var url = `${CBIOPORTAL_URL}/results/mutations?Action=Submit&Z_SCORE_THRESHOLD=1.0&cancer_study_id=gbm_tcga_pub&cancer_study_list=gbm_tcga_pub&case_ids=&case_set_id=gbm_tcga_pub_sequenced&clinicallist=PROFILED_IN_gbm_tcga_pub_cna_rae&gene_list=TP53%20MDM2%20MDM4&gene_set_choice=user-defined_list&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=gbm_tcga_pub_cna_rae&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=gbm_tcga_pub_mutations&show_samples=false`;

            goToUrlAndSetLocalStorage(url);
            // mutations table should be visiable after oncokb icon shows up,
            // also need to wait for mutations to be sorted properly
            browser.waitForVisible("tr:nth-child(1) [data-test=oncogenic-icon-image]",30000);
        });

        it('should show the exon number after adding the exon column', ()=>{
            // check if 6 appears once in COSMIC column
            browser.waitForText('//*[text()="6"]',30000);
            var textValuesWith6 = browser.getText('//*[text()="6"]');
            assert.ok(textValuesWith6.length === 1);
            // click on column button
            browser.click("button*=Columns");
            // scroll down to activated "exon" selection
            browser.scroll(1000, 1000);
            // click "exon"
            browser.click('//*[text()="Exon"]');
            // check if three exact matches for 6 appear
            browser.waitUntil(() => {
                textValuesWith6 = browser.getText('//*[text()="6"]');
                return textValuesWith6.length > 1;
            }, 60000, "Exon data not in Exon column");
            textValuesWith6 = browser.getText('//*[text()="6"]');
            // 3 values from Exon column + 1 value from #Mut in Sample column
            assert.equal(textValuesWith6.length, 4);
        });

        it('should show more exon number after clicking "Show more"', ()=>{
            // click "show more" to add more data
            browser.click("button*=Show more");
            // check if 6 exact matches for 6 appear
            browser.waitUntil(() => {
                textValuesWith6 = browser.getText('//*[text()="6"]');
                // 4 values from Exon column + 1 value from #Mut in Sample column + 1 value from COSMIC column
                return textValuesWith6.length === 6;
            }, 20000, "Exon data not in Exon column after clikcing show more button");
        });

        it('should show the HGVSc data after adding the HGVSc column', ()=>{
            // reopen columns
            browser.click("button*=Columns");
            // click "HGVSc"
            browser.click('//*[text()="HGVSc"]');

            // check if "ENST00000269305.4:c.817C>T" appears
            browser.waitUntil(() => {
                var hgvscValues = browser.getText('//*[text()[contains(.,"ENST00000269305.4:c.817C>T")]]');
                return hgvscValues.length > 0;
            }, 20000, "HGVSc values not in hgvs column");
        });

        it('should show more HGVSc data after clicking "Show more"', ()=>{
            // click "show more" to add more data
            browser.click("button*=Show more");
            // check if "C>T" exact matches for 12 appear
            browser.waitUntil(() => {
                var hgvscValues = browser.getText('//*[text()[contains(.,"C>T")]]');
                return hgvscValues.length > 0;
            }, 20000, "HGVSc values not in hgvs column after clicking show more button");
        });
    });

});<|endoftext|>


<commit_before>/* global URL, Blob */

var airplay = require('airplay-js')
var chromecasts = require('chromecasts')()
var createTorrent = require('create-torrent')
var dragDrop = require('drag-drop')
var electron = require('electron')
var networkAddress = require('network-address')
var path = require('path')
var throttle = require('throttleit')
var torrentPoster = require('./lib/torrent-poster')
var WebTorrent = require('webtorrent')

var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch')

var App = require('./views/app')

var HEADER_HEIGHT = 38

// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
  .map(function (arr) {
    return arr[0]
  })
  .filter(function (url) {
    return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0
  })

var state = global.state = {
  server: null, /* local WebTorrent-to-HTTP server */
  view: {
    url: '/',
    dock: {
      badge: 0,
      progress: 0
    },
    devices: {
      airplay: null, /* airplay client. finds and manages AppleTVs */
      chromecast: null /* chromecast client. finds and manages Chromecasts */
    },
    client: null, /* the WebTorrent client */
    torrentPlaying: null, /* the torrent we're streaming. see client.torrents */
    // history: [], /* track how we got to the current view. enables Back button */
    // historyIndex: 0,
    isFocused: true,
    mainWindowBounds: null, /* x y width height */
    title: 'WebTorrent' /* current window title */
  },
  video: {
    isPaused: false,
    currentTime: 0, /* seconds */
    duration: 1 /* seconds */
  }
}

var client, currentVDom, rootElement, updateThrottled

function init () {
  client = global.client = new WebTorrent()
  client.on('warning', onWarning)
  client.on('error', onError)
  state.view.client = client

  currentVDom = App(state, dispatch)
  rootElement = createElement(currentVDom)
  document.body.appendChild(rootElement)

  updateThrottled = throttle(update, 1000)

  dragDrop('body', onFiles)

  chromecasts.on('update', function (player) {
    state.view.chromecast = player
    update()
  })

  airplay.createBrowser().on('deviceOn', function (player) {
    state.view.devices.airplay = player
  }).start()

  document.addEventListener('paste', function () {
    electron.ipcRenderer.send('addTorrentFromPaste')
  })

  document.addEventListener('keydown', function (e) {
    if (e.which === 27) { /* ESC means either exit fullscreen or go back */
      if (state.view.isFullScreen) {
        dispatch('toggleFullScreen')
      } else {
        dispatch('back')
      }
    }
  })

  window.addEventListener('focus', function () {
    state.view.isFocused = true
    if (state.view.dock.badge > 0) electron.ipcRenderer.send('setBadge', '')
    state.view.dock.badge = 0
  })

  window.addEventListener('blur', function () {
    state.view.isFocused = false
  })
}
init()

function update () {
  var newVDom = App(state, dispatch)
  var patches = diff(currentVDom, newVDom)
  rootElement = patch(rootElement, patches)
  currentVDom = newVDom

  updateDockIcon()
}

setInterval(function () {
  updateThrottled()
}, 1000)

function updateDockIcon () {
  var progress = state.view.client.progress
  var activeTorrentsExist = state.view.client.torrents.some(function (torrent) {
    return torrent.progress !== 1
  })
  // Hide progress bar when client has no torrents, or progress is 100%
  if (!activeTorrentsExist || progress === 1) {
    progress = -1
  }
  if (progress !== state.view.dock.progress) {
    state.view.dock.progress = progress
    electron.ipcRenderer.send('setProgress', progress)
  }
}

function dispatch (action, ...args) {
  console.log('dispatch: %s %o', action, args)
  if (action === 'addTorrent') {
    addTorrent(args[0] /* torrentId */)
  }
  if (action === 'seed') {
    seed(args[0] /* files */)
  }
  if (action === 'openPlayer') {
    openPlayer(args[0] /* torrent */)
  }
  if (action === 'deleteTorrent') {
    deleteTorrent(args[0] /* torrent */)
  }
  if (action === 'openChromecast') {
    openChromecast(args[0] /* torrent */)
  }
  if (action === 'openAirplay') {
    openAirplay(args[0] /* torrent */)
  }
  if (action === 'setDimensions') {
    setDimensions(args[0] /* dimensions */)
  }
  if (action === 'back') {
    if (state.view.url === '/player') {
      restoreBounds()
      closeServer()
    }
    state.view.url = '/'
    update()
  }
  if (action === 'playPause') {
    state.video.isPaused = !state.video.isPaused
    update()
  }
  if (action === 'playbackJump') {
    state.video.jumpToTime = args[0] /* seconds */
    update()
  }
  if (action === 'toggleFullScreen') {
    electron.ipcRenderer.send('toggleFullScreen')
  }
}

electron.ipcRenderer.on('addTorrent', function (e, torrentId) {
  addTorrent(torrentId)
})

electron.ipcRenderer.on('seed', function (e, files) {
  seed(files)
})

electron.ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
  state.view.isFullScreen = isFullScreen
  update()
})

function onFiles (files) {
  // .torrent file = start downloading the torrent
  files.filter(isTorrentFile).forEach(function (torrentFile) {
    dispatch('addTorrent', torrentFile)
  })

  // everything else = seed these files
  dispatch('seed', files.filter(isNotTorrentFile))
}

function isTorrentFile (file) {
  var extname = path.extname(file.name).toLowerCase()
  return extname === '.torrent'
}

function isNotTorrentFile (file) {
  return !isTorrentFile(file)
}

function addTorrent (torrentId) {
  var torrent = client.add(torrentId)
  addTorrentEvents(torrent)
}

function seed (files) {
  if (files.length === 0) return
  var torrent = client.seed(files)
  addTorrentEvents(torrent)
}

function addTorrentEvents (torrent) {
  torrent.on('infoHash', update)
  torrent.on('done', function () {
    if (!state.view.isFocused) {
      state.view.dock.badge += 1
      electron.ipcRenderer.send('setBadge', state.view.dock.badge)
    }
    update()
  })
  torrent.on('download', updateThrottled)
  torrent.on('upload', updateThrottled)
  torrent.on('ready', function () {
    torrentReady(torrent)
  })
  update()
}

function torrentReady (torrent) {
  torrentPoster(torrent, function (err, buf) {
    if (err) return onWarning(err)
    torrent.posterURL = URL.createObjectURL(new Blob([ buf ], { type: 'image/png' }))
    update()
  })
  update()
}

function startServer (torrent, cb) {
  // use largest file
  state.view.torrentPlaying = torrent.files.reduce(function (a, b) {
    return a.length > b.length ? a : b
  })
  var index = torrent.files.indexOf(state.view.torrentPlaying)

  var server = torrent.createServer()
  server.listen(0, function () {
    var port = server.address().port
    var urlSuffix = ':' + port + '/' + index
    state.server = {
      server: server,
      localURL: 'http://localhost' + urlSuffix,
      networkURL: 'http://' + networkAddress() + urlSuffix
    }
    cb()
  })
}

function closeServer () {
  state.server.server.destroy()
  state.server = null
}

function openPlayer (torrent) {
  startServer(torrent, function () {
    state.view.url = '/player'
    update()
  })
}

function deleteTorrent (torrent) {
  torrent.destroy(update)
}

function openChromecast (torrent) {
  startServer(torrent, function () {
    state.view.chromecast.play(state.server.networkURL, { title: 'WebTorrent — ' + torrent.name })
    state.view.chromecast.on('error', function (err) {
      err.message = 'Chromecast: ' + err.message
      onError(err)
    })
    update()
  })
}

function openAirplay (torrent) {
  startServer(torrent, function () {
    state.view.devices.airplay.play(state.server.networkURL, 0, function () {})
    // TODO: handle airplay errors
    update()
  })
}

function setDimensions (dimensions) {
  state.view.mainWindowBounds = electron.remote.getCurrentWindow().getBounds()

  // Limit window size to screen size
  var workAreaSize = electron.remote.screen.getPrimaryDisplay().workAreaSize
  var aspectRatio = dimensions.width / dimensions.height

  var scaleFactor = Math.min(
    Math.min(workAreaSize.width / dimensions.width, 1),
    Math.min(workAreaSize.height / dimensions.height, 1)
  )

  var width = Math.floor(dimensions.width * scaleFactor)
  var height = Math.floor(dimensions.height * scaleFactor)

  height += HEADER_HEIGHT

  // Center window on screen
  var x = Math.floor((workAreaSize.width - width) / 2)
  var y = Math.floor((workAreaSize.height - height) / 2)

  electron.ipcRenderer.send('setAspectRatio', aspectRatio, {width: 0, height: HEADER_HEIGHT})
  electron.ipcRenderer.send('setBounds', {x, y, width, height})
}

function restoreBounds () {
  electron.ipcRenderer.send('setAspectRatio', 0)
  if (state.view.mainWindowBounds) {
    electron.ipcRenderer.send('setBounds', state.view.mainWindowBounds, true)
  }
}

function onError (err) {
  console.error(err.stack)
  window.alert(err.message || err)
  update()
}

function onWarning (err) {
  console.log('warning: %s', err.message)
}
<commit_msg>fix chromecast play<commit_after>/* global URL, Blob */

var airplay = require('airplay-js')
var chromecasts = require('chromecasts')()
var createTorrent = require('create-torrent')
var dragDrop = require('drag-drop')
var electron = require('electron')
var networkAddress = require('network-address')
var path = require('path')
var throttle = require('throttleit')
var torrentPoster = require('./lib/torrent-poster')
var WebTorrent = require('webtorrent')

var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch')

var App = require('./views/app')

var HEADER_HEIGHT = 38

// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
  .map(function (arr) {
    return arr[0]
  })
  .filter(function (url) {
    return url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0
  })

var state = global.state = {
  server: null, /* local WebTorrent-to-HTTP server */
  view: {
    url: '/',
    dock: {
      badge: 0,
      progress: 0
    },
    devices: {
      airplay: null, /* airplay client. finds and manages AppleTVs */
      chromecast: null /* chromecast client. finds and manages Chromecasts */
    },
    client: null, /* the WebTorrent client */
    torrentPlaying: null, /* the torrent we're streaming. see client.torrents */
    // history: [], /* track how we got to the current view. enables Back button */
    // historyIndex: 0,
    isFocused: true,
    mainWindowBounds: null, /* x y width height */
    title: 'WebTorrent' /* current window title */
  },
  video: {
    isPaused: false,
    currentTime: 0, /* seconds */
    duration: 1 /* seconds */
  }
}

var client, currentVDom, rootElement, updateThrottled

function init () {
  client = global.client = new WebTorrent()
  client.on('warning', onWarning)
  client.on('error', onError)
  state.view.client = client

  currentVDom = App(state, dispatch)
  rootElement = createElement(currentVDom)
  document.body.appendChild(rootElement)

  updateThrottled = throttle(update, 1000)

  dragDrop('body', onFiles)

  chromecasts.on('update', function (player) {
    state.view.devices.chromecast = player
    update()
  })

  airplay.createBrowser().on('deviceOn', function (player) {
    state.view.devices.airplay = player
  }).start()

  document.addEventListener('paste', function () {
    electron.ipcRenderer.send('addTorrentFromPaste')
  })

  document.addEventListener('keydown', function (e) {
    if (e.which === 27) { /* ESC means either exit fullscreen or go back */
      if (state.view.isFullScreen) {
        dispatch('toggleFullScreen')
      } else {
        dispatch('back')
      }
    }
  })

  window.addEventListener('focus', function () {
    state.view.isFocused = true
    if (state.view.dock.badge > 0) electron.ipcRenderer.send('setBadge', '')
    state.view.dock.badge = 0
  })

  window.addEventListener('blur', function () {
    state.view.isFocused = false
  })
}
init()

function update () {
  var newVDom = App(state, dispatch)
  var patches = diff(currentVDom, newVDom)
  rootElement = patch(rootElement, patches)
  currentVDom = newVDom

  updateDockIcon()
}

setInterval(function () {
  updateThrottled()
}, 1000)

function updateDockIcon () {
  var progress = state.view.client.progress
  var activeTorrentsExist = state.view.client.torrents.some(function (torrent) {
    return torrent.progress !== 1
  })
  // Hide progress bar when client has no torrents, or progress is 100%
  if (!activeTorrentsExist || progress === 1) {
    progress = -1
  }
  if (progress !== state.view.dock.progress) {
    state.view.dock.progress = progress
    electron.ipcRenderer.send('setProgress', progress)
  }
}

function dispatch (action, ...args) {
  console.log('dispatch: %s %o', action, args)
  if (action === 'addTorrent') {
    addTorrent(args[0] /* torrentId */)
  }
  if (action === 'seed') {
    seed(args[0] /* files */)
  }
  if (action === 'openPlayer') {
    openPlayer(args[0] /* torrent */)
  }
  if (action === 'deleteTorrent') {
    deleteTorrent(args[0] /* torrent */)
  }
  if (action === 'openChromecast') {
    openChromecast(args[0] /* torrent */)
  }
  if (action === 'openAirplay') {
    openAirplay(args[0] /* torrent */)
  }
  if (action === 'setDimensions') {
    setDimensions(args[0] /* dimensions */)
  }
  if (action === 'back') {
    if (state.view.url === '/player') {
      restoreBounds()
      closeServer()
    }
    state.view.url = '/'
    update()
  }
  if (action === 'playPause') {
    state.video.isPaused = !state.video.isPaused
    update()
  }
  if (action === 'playbackJump') {
    state.video.jumpToTime = args[0] /* seconds */
    update()
  }
  if (action === 'toggleFullScreen') {
    electron.ipcRenderer.send('toggleFullScreen')
  }
}

electron.ipcRenderer.on('addTorrent', function (e, torrentId) {
  addTorrent(torrentId)
})

electron.ipcRenderer.on('seed', function (e, files) {
  seed(files)
})

electron.ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
  state.view.isFullScreen = isFullScreen
  update()
})

function onFiles (files) {
  // .torrent file = start downloading the torrent
  files.filter(isTorrentFile).forEach(function (torrentFile) {
    dispatch('addTorrent', torrentFile)
  })

  // everything else = seed these files
  dispatch('seed', files.filter(isNotTorrentFile))
}

function isTorrentFile (file) {
  var extname = path.extname(file.name).toLowerCase()
  return extname === '.torrent'
}

function isNotTorrentFile (file) {
  return !isTorrentFile(file)
}

function addTorrent (torrentId) {
  var torrent = client.add(torrentId)
  addTorrentEvents(torrent)
}

function seed (files) {
  if (files.length === 0) return
  var torrent = client.seed(files)
  addTorrentEvents(torrent)
}

function addTorrentEvents (torrent) {
  torrent.on('infoHash', update)
  torrent.on('done', function () {
    if (!state.view.isFocused) {
      state.view.dock.badge += 1
      electron.ipcRenderer.send('setBadge', state.view.dock.badge)
    }
    update()
  })
  torrent.on('download', updateThrottled)
  torrent.on('upload', updateThrottled)
  torrent.on('ready', function () {
    torrentReady(torrent)
  })
  update()
}

function torrentReady (torrent) {
  torrentPoster(torrent, function (err, buf) {
    if (err) return onWarning(err)
    torrent.posterURL = URL.createObjectURL(new Blob([ buf ], { type: 'image/png' }))
    update()
  })
  update()
}

function startServer (torrent, cb) {
  // use largest file
  state.view.torrentPlaying = torrent.files.reduce(function (a, b) {
    return a.length > b.length ? a : b
  })
  var index = torrent.files.indexOf(state.view.torrentPlaying)

  var server = torrent.createServer()
  server.listen(0, function () {
    var port = server.address().port
    var urlSuffix = ':' + port + '/' + index
    state.server = {
      server: server,
      localURL: 'http://localhost' + urlSuffix,
      networkURL: 'http://' + networkAddress() + urlSuffix
    }
    cb()
  })
}

function closeServer () {
  state.server.server.destroy()
  state.server = null
}

function openPlayer (torrent) {
  startServer(torrent, function () {
    state.view.url = '/player'
    update()
  })
}

function deleteTorrent (torrent) {
  torrent.destroy(update)
}

function openChromecast (torrent) {
  startServer(torrent, function () {
    state.view.devices.chromecast.play(state.server.networkURL, { title: 'WebTorrent — ' + torrent.name })
    state.view.devices.chromecast.on('error', function (err) {
      err.message = 'Chromecast: ' + err.message
      onError(err)
    })
    update()
  })
}

function openAirplay (torrent) {
  startServer(torrent, function () {
    state.view.devices.airplay.play(state.server.networkURL, 0, function () {})
    // TODO: handle airplay errors
    update()
  })
}

function setDimensions (dimensions) {
  state.view.mainWindowBounds = electron.remote.getCurrentWindow().getBounds()

  // Limit window size to screen size
  var workAreaSize = electron.remote.screen.getPrimaryDisplay().workAreaSize
  var aspectRatio = dimensions.width / dimensions.height

  var scaleFactor = Math.min(
    Math.min(workAreaSize.width / dimensions.width, 1),
    Math.min(workAreaSize.height / dimensions.height, 1)
  )

  var width = Math.floor(dimensions.width * scaleFactor)
  var height = Math.floor(dimensions.height * scaleFactor)

  height += HEADER_HEIGHT

  // Center window on screen
  var x = Math.floor((workAreaSize.width - width) / 2)
  var y = Math.floor((workAreaSize.height - height) / 2)

  electron.ipcRenderer.send('setAspectRatio', aspectRatio, {width: 0, height: HEADER_HEIGHT})
  electron.ipcRenderer.send('setBounds', {x, y, width, height})
}

function restoreBounds () {
  electron.ipcRenderer.send('setAspectRatio', 0)
  if (state.view.mainWindowBounds) {
    electron.ipcRenderer.send('setBounds', state.view.mainWindowBounds, true)
  }
}

function onError (err) {
  console.error(err.stack)
  window.alert(err.message || err)
  update()
}

function onWarning (err) {
  console.log('warning: %s', err.message)
}
<|endoftext|>


<commit_before>import {BehaviorSubject, Subject, debounceTime, distinctUntilChanged, filter, finalize, from, map, merge, of, switchMap, zip} from 'rxjs'
import {Loader} from 'google-maps'
import {SepalMap} from './sepalMap'
import {compose} from 'compose'
import {connect} from 'store'
import {getLogger} from 'log'
import {mapTag, mapViewTag} from 'tag'
import {v4 as uuid} from 'uuid'
import {withContext} from 'context'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import api from 'api'
import withSubscriptions from 'subscription'

const log = getLogger('maps')

const GOOGLE_MAPS_VERSION = '3.44'
const GOOGLE_MAPS_LIBRARIES = ['drawing', 'places']

const DEFAULT_ZOOM = 3
export const MIN_ZOOM = 3
export const MAX_ZOOM = 23

export const MapsContext = React.createContext()

export const withMapsContext = withContext(MapsContext, 'mapsContext')

class _Maps extends React.Component {
    state = {
        mapsContext: null
    }

    view$ = new BehaviorSubject()
    linkedMaps = new Set()

    constructor(props) {
        super(props)
        this.createGoogleMap = this.createGoogleMap.bind(this)
        this.createSepalMap = this.createSepalMap.bind(this)
        this.createMapContext = this.createMapContext.bind(this)
        this.initialize()
    }

    initialize() {
        const {stream} = this.props
        stream('INIT_MAPS',
            this.initialize$(),
            providers => this.setState({...providers, initialized: true}),
            error => this.handleError(error)
        )
    }

    initialize$() {
        return api.map.loadApiKeys$().pipe(
            switchMap(
                ({google: googleMapsApiKey, norwayPlanet: norwayPlanetApiKey}) =>
                    zip(
                        this.initGoogleMaps$(googleMapsApiKey),
                        this.initNorwayPlanet$(norwayPlanetApiKey)
                    )
            ),
            map(([google, norwayPlanet]) => ({google, norwayPlanet}))
        )
    }

    getGoogleMapsLoader(googleMapsApiKey) {
        return new Loader(googleMapsApiKey, {
            version: GOOGLE_MAPS_VERSION,
            libraries: GOOGLE_MAPS_LIBRARIES
        })
    }

    initGoogleMaps$(googleMapsApiKey) {
        const loader = this.getGoogleMapsLoader(googleMapsApiKey)
        return from(loader.load()).pipe(
            switchMap(google =>
                of({google, googleMapsApiKey})
            )
        )
    }

    initNorwayPlanet$(norwayPlanetApiKey) {
        return of({norwayPlanetApiKey})
    }

    handleError(error) {
        const {onError} = this.props
        onError && onError(error)
        this.setState({error})
    }

    getStyleOptions(style = 'sepalStyle') {
        // https://developers.google.com/maps/documentation/javascript/style-reference
        switch (style) {
        case 'sepalStyle':
            return [
                {stylers: [{visibility: 'simplified'}]},
                {stylers: [{color: '#131314'}]},
                {featureType: 'transit.station', stylers: [{visibility: 'off'}]},
                {featureType: 'poi', stylers: [{visibility: 'off'}]},
                {featureType: 'water', stylers: [{color: '#191919'}, {lightness: 4}]},
                {elementType: 'labels.text.fill', stylers: [{visibility: 'off'}, {lightness: 25}]}
            ]
        case 'overlayStyle':
            return [
                {stylers: [{visibility: 'off'}]}
            ]
        default:
            throw Error(`Unsupported map style ${style}`)
        }
    }

    createGoogleMap(mapElement, options = {}, style = 'sepalStyle') {
        const {google: {google}} = this.state
        const mapOptions = {
            zoom: DEFAULT_ZOOM,
            minZoom: MIN_ZOOM,
            maxZoom: MAX_ZOOM,
            center: new google.maps.LatLng(16.7794913, 9.6771556),
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            zoomControl: false,
            mapTypeControl: false,
            scaleControl: false,
            streetViewControl: false,
            rotateControl: false,
            fullscreenControl: false,
            backgroundColor: '#131314',
            gestureHandling: 'greedy',
            draggableCursor: 'pointer',
            ...options
        }

        const googleMap = new google.maps.Map(mapElement, mapOptions)

        const styledMapTypeStyles = this.getStyleOptions(style)
        const styledMapTypeOptions = {
            name: 'map',
            maxZoom: MAX_ZOOM
        }
        const styledMapType = new google.maps.StyledMapType(styledMapTypeStyles, styledMapTypeOptions)

        googleMap.mapTypes.set('style', styledMapType)
        googleMap.setMapTypeId('style')

        return googleMap
    }

    createSepalMap(mapElement, options, style) {
        const {google: {google}} = this.state
        const googleMap = this.createGoogleMap(mapElement, options, style)
        return new SepalMap(google, googleMap)
    }

    getCurrentView() {
        const {view$} = this
        const update = view$.getValue()
        return update && update.view
    }

    createMapContext(mapId = uuid()) {
        const {addSubscription} = this.props
        const {google: {googleMapsApiKey}, norwayPlanet: {norwayPlanetApiKey}} = this.state
        const requestedView$ = new Subject()

        const view$ = merge(
            this.view$.pipe(
                distinctUntilChanged(),
                filter(value => value),
                filter(({mapId: id}) => this.linkedMaps.has(mapId) && mapId !== id),
                map(({view}) => view)
            ),
            requestedView$
        )

        const updateView$ = new Subject()
        const linked$ = new Subject()

        const setLinked = linked => {
            const currentView = this.getCurrentView()
            if (linked) {
                this.linkedMaps.add(mapId)
            } else {
                this.linkedMaps.delete(mapId)
            }
            log.debug(() => `${mapTag(mapId)} ${linked ? 'linked' : 'unlinked'}, now ${this.linkedMaps.size} linked.`)
            if (linked && this.linkedMaps.size > 1 && currentView) {
                requestedView$.next(currentView)
            }
        }

        const updateView = view => {
            if (this.linkedMaps.has(mapId)) {
                const currentView = this.getCurrentView()
                const {center, zoom} = view
                if (center && zoom) {
                    if (currentView && _.isEqual(currentView.center, center) && currentView.zoom === zoom) {
                        log.trace(() => `View update from linked ${mapTag(mapId)} ignored`)
                    } else {
                        log.debug(() => `View update from linked ${mapTag(mapId)} accepted: ${mapViewTag(view)}`)
                        this.view$.next({mapId, view})
                    }
                }
            } else {
                log.trace(() => `View update from unlinked ${mapTag(mapId)} discarded`)
            }
        }

        addSubscription(
            linked$.pipe(
                distinctUntilChanged(),
                finalize(() => setLinked(false))
            ).subscribe(
                linked => setLinked(linked)
            ),
            updateView$.pipe(
                debounceTime(100),
                distinctUntilChanged()
            ).subscribe(
                view => updateView(view)
            )
        )

        return {mapId, googleMapsApiKey, norwayPlanetApiKey, view$, updateView$, linked$}
    }

    render() {
        const {children} = this.props
        const {error, initialized} = this.state
        return (
            <MapsContext.Provider value={{
                createGoogleMap: this.createGoogleMap,
                createSepalMap: this.createSepalMap,
                createMapContext: this.createMapContext
            }}>
                {children(initialized, error)}
            </MapsContext.Provider>
        )
    }
}

export const Maps = compose(
    _Maps,
    connect(),
    withSubscriptions()
)

Maps.propTypes = {
    children: PropTypes.any.isRequired,
    onError: PropTypes.func.isRequired
}
<commit_msg>Updated to Google Maps 3.47.<commit_after>import {BehaviorSubject, Subject, debounceTime, distinctUntilChanged, filter, finalize, from, map, merge, of, switchMap, zip} from 'rxjs'
import {Loader} from 'google-maps'
import {SepalMap} from './sepalMap'
import {compose} from 'compose'
import {connect} from 'store'
import {getLogger} from 'log'
import {mapTag, mapViewTag} from 'tag'
import {v4 as uuid} from 'uuid'
import {withContext} from 'context'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import api from 'api'
import withSubscriptions from 'subscription'

const log = getLogger('maps')

const GOOGLE_MAPS_VERSION = '3.47'
const GOOGLE_MAPS_LIBRARIES = ['drawing', 'places']

const DEFAULT_ZOOM = 3
export const MIN_ZOOM = 3
export const MAX_ZOOM = 23

export const MapsContext = React.createContext()

export const withMapsContext = withContext(MapsContext, 'mapsContext')

class _Maps extends React.Component {
    state = {
        mapsContext: null
    }

    view$ = new BehaviorSubject()
    linkedMaps = new Set()

    constructor(props) {
        super(props)
        this.createGoogleMap = this.createGoogleMap.bind(this)
        this.createSepalMap = this.createSepalMap.bind(this)
        this.createMapContext = this.createMapContext.bind(this)
        this.initialize()
    }

    initialize() {
        const {stream} = this.props
        stream('INIT_MAPS',
            this.initialize$(),
            providers => this.setState({...providers, initialized: true}),
            error => this.handleError(error)
        )
    }

    initialize$() {
        return api.map.loadApiKeys$().pipe(
            switchMap(
                ({google: googleMapsApiKey, norwayPlanet: norwayPlanetApiKey}) =>
                    zip(
                        this.initGoogleMaps$(googleMapsApiKey),
                        this.initNorwayPlanet$(norwayPlanetApiKey)
                    )
            ),
            map(([google, norwayPlanet]) => ({google, norwayPlanet}))
        )
    }

    getGoogleMapsLoader(googleMapsApiKey) {
        return new Loader(googleMapsApiKey, {
            version: GOOGLE_MAPS_VERSION,
            libraries: GOOGLE_MAPS_LIBRARIES
        })
    }

    initGoogleMaps$(googleMapsApiKey) {
        const loader = this.getGoogleMapsLoader(googleMapsApiKey)
        return from(loader.load()).pipe(
            switchMap(google =>
                of({google, googleMapsApiKey})
            )
        )
    }

    initNorwayPlanet$(norwayPlanetApiKey) {
        return of({norwayPlanetApiKey})
    }

    handleError(error) {
        const {onError} = this.props
        onError && onError(error)
        this.setState({error})
    }

    getStyleOptions(style = 'sepalStyle') {
        // https://developers.google.com/maps/documentation/javascript/style-reference
        switch (style) {
        case 'sepalStyle':
            return [
                {stylers: [{visibility: 'simplified'}]},
                {stylers: [{color: '#131314'}]},
                {featureType: 'transit.station', stylers: [{visibility: 'off'}]},
                {featureType: 'poi', stylers: [{visibility: 'off'}]},
                {featureType: 'water', stylers: [{color: '#191919'}, {lightness: 4}]},
                {elementType: 'labels.text.fill', stylers: [{visibility: 'off'}, {lightness: 25}]}
            ]
        case 'overlayStyle':
            return [
                {stylers: [{visibility: 'off'}]}
            ]
        default:
            throw Error(`Unsupported map style ${style}`)
        }
    }

    createGoogleMap(mapElement, options = {}, style = 'sepalStyle') {
        const {google: {google}} = this.state
        const mapOptions = {
            zoom: DEFAULT_ZOOM,
            minZoom: MIN_ZOOM,
            maxZoom: MAX_ZOOM,
            center: new google.maps.LatLng(16.7794913, 9.6771556),
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            zoomControl: false,
            mapTypeControl: false,
            scaleControl: false,
            streetViewControl: false,
            rotateControl: false,
            fullscreenControl: false,
            backgroundColor: '#131314',
            gestureHandling: 'greedy',
            draggableCursor: 'pointer',
            ...options
        }

        const googleMap = new google.maps.Map(mapElement, mapOptions)

        const styledMapTypeStyles = this.getStyleOptions(style)
        const styledMapTypeOptions = {
            name: 'map',
            maxZoom: MAX_ZOOM
        }
        const styledMapType = new google.maps.StyledMapType(styledMapTypeStyles, styledMapTypeOptions)

        googleMap.mapTypes.set('style', styledMapType)
        googleMap.setMapTypeId('style')

        return googleMap
    }

    createSepalMap(mapElement, options, style) {
        const {google: {google}} = this.state
        const googleMap = this.createGoogleMap(mapElement, options, style)
        return new SepalMap(google, googleMap)
    }

    getCurrentView() {
        const {view$} = this
        const update = view$.getValue()
        return update && update.view
    }

    createMapContext(mapId = uuid()) {
        const {addSubscription} = this.props
        const {google: {googleMapsApiKey}, norwayPlanet: {norwayPlanetApiKey}} = this.state
        const requestedView$ = new Subject()

        const view$ = merge(
            this.view$.pipe(
                distinctUntilChanged(),
                filter(value => value),
                filter(({mapId: id}) => this.linkedMaps.has(mapId) && mapId !== id),
                map(({view}) => view)
            ),
            requestedView$
        )

        const updateView$ = new Subject()
        const linked$ = new Subject()

        const setLinked = linked => {
            const currentView = this.getCurrentView()
            if (linked) {
                this.linkedMaps.add(mapId)
            } else {
                this.linkedMaps.delete(mapId)
            }
            log.debug(() => `${mapTag(mapId)} ${linked ? 'linked' : 'unlinked'}, now ${this.linkedMaps.size} linked.`)
            if (linked && this.linkedMaps.size > 1 && currentView) {
                requestedView$.next(currentView)
            }
        }

        const updateView = view => {
            if (this.linkedMaps.has(mapId)) {
                const currentView = this.getCurrentView()
                const {center, zoom} = view
                if (center && zoom) {
                    if (currentView && _.isEqual(currentView.center, center) && currentView.zoom === zoom) {
                        log.trace(() => `View update from linked ${mapTag(mapId)} ignored`)
                    } else {
                        log.debug(() => `View update from linked ${mapTag(mapId)} accepted: ${mapViewTag(view)}`)
                        this.view$.next({mapId, view})
                    }
                }
            } else {
                log.trace(() => `View update from unlinked ${mapTag(mapId)} discarded`)
            }
        }

        addSubscription(
            linked$.pipe(
                distinctUntilChanged(),
                finalize(() => setLinked(false))
            ).subscribe(
                linked => setLinked(linked)
            ),
            updateView$.pipe(
                debounceTime(100),
                distinctUntilChanged()
            ).subscribe(
                view => updateView(view)
            )
        )

        return {mapId, googleMapsApiKey, norwayPlanetApiKey, view$, updateView$, linked$}
    }

    render() {
        const {children} = this.props
        const {error, initialized} = this.state
        return (
            <MapsContext.Provider value={{
                createGoogleMap: this.createGoogleMap,
                createSepalMap: this.createSepalMap,
                createMapContext: this.createMapContext
            }}>
                {children(initialized, error)}
            </MapsContext.Provider>
        )
    }
}

export const Maps = compose(
    _Maps,
    connect(),
    withSubscriptions()
)

Maps.propTypes = {
    children: PropTypes.any.isRequired,
    onError: PropTypes.func.isRequired
}
<|endoftext|>


<commit_before>const BaseCard = require('../../../build/server/game/basecard.js');

describe('BaseCard', function () {
    beforeEach(function () {
        this.testCard = { id: '111', label: 'test 1(some pack)', name: 'test 1' };
        this.limitedCard = { id: '1234', text: 'Limited.' };
        this.nonLimitedCard = { id: '2222', text: 'Covert.' };
        this.game = jasmine.createSpyObj('game', ['raiseEvent', 'getCurrentAbilityContext']);
        this.owner = jasmine.createSpyObj('owner', ['getCardSelectionState', 'allowGameAction']);
        this.owner.getCardSelectionState.and.returnValue({});
        this.owner.allowGameAction.and.returnValue(true);
        this.owner.game = this.game;
        this.card = new BaseCard(this.owner, this.testCard);
    });

    describe('when new instance created', function() {
        it('should generate a new uuid', function() {
            expect(this.card.uuid).not.toBeUndefined();
        });
    });

    describe('getSummary', function() {
        describe('when is active player', function() {
            beforeEach(function () {
                this.summary = this.card.getSummary(this.owner);
            });

            describe('and card is faceup', function() {
                it('should return card data', function() {
                    // TODO: Come back and check that this.testCard.name shouldn't be this.testCard.label
                    expect(this.summary.uuid).toEqual(this.card.uuid);
                    expect(this.summary.name).toEqual(this.testCard.name);
                    expect(this.summary.code).toEqual(this.testCard.code);
                });

                it('should not return facedown', function() {
                    expect(this.summary.facedown).toBeFalsy();
                });
            });

            describe('and card is facedown', function() {
                beforeEach(function () {
                    this.card.facedown = true;
                    this.summary = this.card.getSummary(this.owner);
                });

                it('should return no card data', function () {
                    expect(this.summary.uuid).toBeUndefined();
                    expect(this.summary.name).toBeUndefined();
                });

                it('should return facedown', function() {
                    expect(this.summary.facedown).toBe(true);
                });
            });
        });

        describe('when is not active player', function() {
            beforeEach(function () {
                this.anotherPlayer = jasmine.createSpyObj('owner', ['getCardSelectionState']);
                this.anotherPlayer.getCardSelectionState.and.returnValue({});
                this.summary = this.card.getSummary(this.anotherPlayer);
            });

            describe('and card is faceup', function() {
                describe('and hiding facedown cards', function() {
                    beforeEach(function() {
                        this.summary = this.card.getSummary(this.anotherPlayer, true);
                    });

                    it('should return no card data', function () {
                        expect(this.summary.uuid).toBeUndefined();
                        expect(this.summary.name).toBeUndefined();
                        expect(this.summary.code).toBeUndefined();
                    });

                    it('should return facedown', function() {
                        expect(this.summary.facedown).toBe(true);
                    });
                });

                it('should return card data', function () {
                    expect(this.summary.uuid).toEqual(this.card.uuid);
                    expect(this.summary.name).toEqual(this.testCard.name);
                    expect(this.summary.code).toEqual(this.testCard.code);
                });

                it('should not return facedown', function() {
                    expect(this.summary.facedown).toBe(false);
                });
            });

            describe('and card is facedown', function() {
                beforeEach(function () {
                    this.card.facedown = true;
                    this.summary = this.card.getSummary(this.anotherPlayer);
                });

                it('should return no card data', function() {
                    expect(this.summary.uuid).toBeUndefined();
                    expect(this.summary.name).toBeUndefined();
                    expect(this.summary.code).toBeUndefined();
                });

                it('should return facedown', function() {
                    expect(this.summary.facedown).toBe(true);
                });
            });
        });
    });
});
<commit_msg>Test fixes for facedown getSummary (#3297)<commit_after>const BaseCard = require('../../../build/server/game/basecard.js');

describe('BaseCard', function () {
    beforeEach(function () {
        this.testCard = { id: '111', label: 'test 1(some pack)', name: 'test 1' };
        this.limitedCard = { id: '1234', text: 'Limited.' };
        this.nonLimitedCard = { id: '2222', text: 'Covert.' };
        this.game = jasmine.createSpyObj('game', ['raiseEvent', 'getCurrentAbilityContext']);
        this.owner = jasmine.createSpyObj('owner', ['getCardSelectionState', 'allowGameAction', 'getShortSummary']);
        this.owner.getCardSelectionState.and.returnValue({});
        this.owner.allowGameAction.and.returnValue(true);
        this.owner.game = this.game;
        this.card = new BaseCard(this.owner, this.testCard);
    });

    describe('when new instance created', function() {
        it('should generate a new uuid', function() {
            expect(this.card.uuid).not.toBeUndefined();
        });
    });

    describe('getSummary', function() {
        describe('when is active player', function() {
            beforeEach(function () {
                this.summary = this.card.getSummary(this.owner);
            });

            describe('and card is faceup', function() {
                it('should return card data', function() {
                    // TODO: Come back and check that this.testCard.name shouldn't be this.testCard.label
                    expect(this.summary.uuid).toEqual(this.card.uuid);
                    expect(this.summary.name).toEqual(this.testCard.name);
                    expect(this.summary.code).toEqual(this.testCard.code);
                });

                it('should not return facedown', function() {
                    expect(this.summary.facedown).toBeFalsy();
                });
            });

            describe('and card is facedown', function() {
                beforeEach(function () {
                    this.card.facedown = true;
                    this.summary = this.card.getSummary(this.owner);
                });

                it('should return no card data', function () {
                    expect(this.summary.uuid).toBeUndefined();
                    expect(this.summary.name).toBeUndefined();
                });

                it('should return facedown', function() {
                    expect(this.summary.facedown).toBe(true);
                });
            });
        });

        describe('when is not active player', function() {
            beforeEach(function () {
                this.anotherPlayer = jasmine.createSpyObj('owner', ['getCardSelectionState']);
                this.anotherPlayer.getCardSelectionState.and.returnValue({});
                this.summary = this.card.getSummary(this.anotherPlayer);
            });

            describe('and card is faceup', function() {
                describe('and hiding facedown cards', function() {
                    beforeEach(function() {
                        this.summary = this.card.getSummary(this.anotherPlayer, true);
                    });

                    it('should return no card data', function () {
                        expect(this.summary.uuid).toBeUndefined();
                        expect(this.summary.name).toBeUndefined();
                        expect(this.summary.code).toBeUndefined();
                    });

                    it('should return facedown', function() {
                        expect(this.summary.facedown).toBe(true);
                    });
                });

                it('should return card data', function () {
                    expect(this.summary.uuid).toEqual(this.card.uuid);
                    expect(this.summary.name).toEqual(this.testCard.name);
                    expect(this.summary.code).toEqual(this.testCard.code);
                });

                it('should not return facedown', function() {
                    expect(this.summary.facedown).toBe(false);
                });
            });

            describe('and card is facedown', function() {
                beforeEach(function () {
                    this.card.facedown = true;
                    this.summary = this.card.getSummary(this.anotherPlayer);
                });

                it('should return no card data', function() {
                    expect(this.summary.uuid).toBeUndefined();
                    expect(this.summary.name).toBeUndefined();
                    expect(this.summary.code).toBeUndefined();
                });

                it('should return facedown', function() {
                    expect(this.summary.facedown).toBe(true);
                });
            });
        });
    });
});
<|endoftext|>


<commit_before>var http = require('http');
var querystring = require("querystring");

function start(response, postData){
	console.log("Request handler 'start' was called");
	//TODO: load html from template (split logic from content)
	var body = ''+
	 ''+
	 '<meta http-equiv="Content-Type" content="text/html; '+
	 'charset=UTF-8" />'+
	 ''+
	 ''+
	 '
Pilots Configuration Panel
'+
	 '
Select Matchmaker
'+
	 '
Note: Cloud4all/GPII server must be started to select a Matchmaker

'+
	 '<form action="/selectMM" method="post">'+
	 '
User token:
<input name="text" type="text" size="30" maxlength="100">
'+
	 '
For auto-configuration scenario:
'+
	 '<input type="radio" name="matchmaker" value="default">Rule-based Matchmaker (without solution selection option)
'+
	 '<input type="radio" name="matchmaker" value="statistical">Statistical Matchmaker
'+
	 '
'+
	 '
For demo solution selection scenario:
'+
	 '<input type="radio" name="matchmaker" value="ruleBased">Rule-based Matchmaker
'+
	 '
'+
	 '<input type="submit" value="Set Matchmaker strategy" />'+
	 '</form>
'+
	 '
Snapshot prefs
'+
	 '<form action="/snapshotToPrefs" method="post">'+
	 '
User token:
<input name="text" type="text" size="30" maxlength="100">'+
	 '<input type="submit" value="Snapshot" />'+
	 '</form>
	'+
	 '
Select Device Specification
'+
	 '<form action="/setDeviceinfo" method="post">'+
	 '
For platform A/B scenario:
'+
	 '<input type="radio" name="device" value="platformAB_onWindows_NVDA">Windows (NVDA and Windows Magnifier) or Linux
'+
	 '<input type="radio" name="device" value="platformAB_onWindows_Supernova">Windows (SuperNova screen reader and magnifier) or Linux

'+
	// '<input type="radio" name="device" value="platformAB_onAndroid_TalkBack">Android (TalkBack)
'+
	// '<input type="radio" name="device" value="platformAB_onAndroid_MobileAccessibility">Android (TalkBack)
'+
	 '
'+
	 '
For Demos:
'+
	 '<input type="radio" name="device" value="demo_SmartHouse">SmartHouse
'+
	 '<input type="radio" name="device" value="demo_Maavis">Maavis
'+
	 '<input type="radio" name="device" value="demo_GoogleChrome">Google Chrome

'+
	 '<input type="radio" name="device" value="demo_Library">Library
'+
	 '<input type="radio" name="device" value="demo_MultipleSolutions">Mulitple solutions in computer lab
'+
	 '
'+
	 '
Reset device specification to default:
'+
	 '<input type="radio" name="device" value="installedSolutions">Default
'+
	 '
'+
	 '<input type="submit" value="Set device configuration" />'+
	 '</form>'+
	 ''+
	 '';
	response.writeHead(200, {"Content-Type": "text/html"});
	response.write(body);
	response.end();
}

function setDeviceinfo(response, postData){
	var fs = require('fs');
	var device = querystring.parse(postData)["device"];
	var filePath = 	'../node_modules/universal/testData/deviceReporter/secondPilots/'+ device +'.json' ;
	fs.createReadStream(filePath).pipe(fs.createWriteStream('../node_modules/universal/testData/deviceReporter/installedSolutions.json'));

	response.writeHead(200, {"Content-Type": "text/html"});
	response.write("Device information successfully set!");
	response.end();
}

function selectMM(response, postData){
	var url = "";
	var preferences = "";
	var token;
	var matchmaker = querystring.parse(postData)["matchmaker"];

	if(querystring.parse(postData).text){

		token = querystring.parse(postData).text;
	}
	// TODO try feching current user from GPII

	url = "http://preferences.gpii.net/user/" + token;

	if (token){

		// fetch preference set from the preference server
		http.get(url, function(res) {

		  res.on('data', function(preferencesChunk){
			preferences += preferencesChunk;
		  });

		  res.on('end', function(){
				var preferencesObject = JSON.parse(preferences)
				// sets current matchmaker strategy as preference
				// ToDo: reset/remove preference object from preference sets
				if(preferencesObject['preferences']){
					preferencesObject['preferences']['http://registry.gpii.org/common/matchMakerType'] = [{ "value": matchmaker}];
					saveModifiedPreferences(token, preferencesObject['preferences'], "matchmaker successfully selected", response);
				}
				else{
					response.writeHead(200, {"Content-Type": "text/html"});
					response.write("No such user on preference server");
					response.end();
				}
		  });

		}).on('error', function(e) {
			console.log("Got error on fetching preferences from server: " + e.message);
		});
	}else{
		response.writeHead(200, {"Content-Type": "text/html"});
		response.write("Oops! No user token specified.");
		response.end();
	}
};

function snapshotToPrefs(response, postData) {
	var url = "";
	var snapshot = "";
	var token;
	var matchmaker = querystring.parse(postData)["matchmaker"];

	if(querystring.parse(postData).text){
		token = querystring.parse(postData).text;
	}

	if (token) {
		var data="";
		// do snapshot
		http.get("http://localhost:8081/snapshot", function(res) {

		  res.on('data', function(preferencesChunk){
			data += preferencesChunk;
		  });

		  res.on('end', function(){
				var snapshot = JSON.parse(data)

				saveModifiedPreferences(token, snapshot, "Snapshot saved to preferences server", response);

		  });

		}).on('error', function(e) {
			console.log("Got error on doing snapshot: " + e.message);
		});

	} else {
		response.writeHead(200, {"Content-Type": "text/html"});
		response.write("Oops! No user token specified.");
		response.end();
	}
};

function saveModifiedPreferences(token, preferencesObject, usrMsg, response) {
	var data = JSON.stringify(preferencesObject);

	// object of options to indicate where to POST to
	var post_options = {
		host: 'preferences.gpii.net',
		port: '80',
		path: '/user/' + token,
		method: 'POST',
		headers: {
		'Content-Type': 'application/json',
		'Content-Length': data.length
		}
	};

	// set up the http.request POST
	var post_req = http.request(post_options, function(res) {
		res.setEncoding('utf8');
		res.on('data', function (chunk) {
			console.log('Response: ' + chunk);
		});
		res.on('end', function(){
			response.writeHead(200, {"Content-Type": "text/html"});
			response.write(usrMsg);
			response.end();
		});
	});
	// writes http request body data; Server replies and response callback 'res' gets activated
	post_req.write(data);
	post_req.end();

}

exports.start = start;
exports.selectMM = selectMM;
exports.setDeviceinfo = setDeviceinfo;
exports.snapshotToPrefs = snapshotToPrefs;<commit_msg>GPII-665: Now uses token for logged in user automatically and appends the application specific settings to the already existing NP set when snapshotting<commit_after>var http = require('http');
var querystring = require("querystring");

function start(response, postData){
	console.log("Request handler 'start' was called");
	//TODO: load html from template (split logic from content)
	var body = ''+
	 ''+
	 '<meta http-equiv="Content-Type" content="text/html; '+
	 'charset=UTF-8" />'+
	 ''+
	 ''+
	 '
Pilots Configuration Panel
'+
	 '
Select Matchmaker
'+
	 '
Note: Cloud4all/GPII server must be started to select a Matchmaker

'+
	 '<form action="/selectMM" method="post">'+
	 '
For auto-configuration scenario:
'+
	 '<input type="radio" name="matchmaker" checked="checked" value="default">Rule-based Matchmaker (without solution selection option)
'+
	 '<input type="radio" name="matchmaker" value="statistical">Statistical Matchmaker
'+
	 '
'+
	 '
For demo solution selection scenario:
'+
	 '<input type="radio" name="matchmaker" value="ruleBased">Rule-based Matchmaker
'+
	 '
'+
	 '<input type="submit" value="Set Matchmaker strategy" />'+
	 '</form>
'+
	 '
Snapshot prefs
'+
	 '<form action="/snapshotToPrefs" method="post">'+
	 '<input type="submit" value="Snapshot" />'+
	 '</form>
	'+
	 '
Select Device Specification
'+
	 '<form action="/setDeviceinfo" method="post">'+
	 '
For platform A/B scenario:
'+
	 '<input type="radio" name="device" value="platformAB_onWindows_NVDA">Windows (NVDA and Windows Magnifier) or Linux
'+
	 '<input type="radio" name="device" value="platformAB_onWindows_Supernova">Windows (SuperNova screen reader and magnifier) or Linux

'+
	// '<input type="radio" name="device" value="platformAB_onAndroid_TalkBack">Android (TalkBack)
'+
	// '<input type="radio" name="device" value="platformAB_onAndroid_MobileAccessibility">Android (TalkBack)
'+
	 '
'+
	 '
For Demos:
'+
	 '<input type="radio" name="device" value="demo_SmartHouse">SmartHouse
'+
	 '<input type="radio" name="device" value="demo_Maavis">Maavis
'+
	 '<input type="radio" name="device" value="demo_GoogleChrome">Google Chrome

'+
	 '<input type="radio" name="device" value="demo_Library">Library
'+
	 '<input type="radio" name="device" value="demo_MultipleSolutions">Mulitple solutions in computer lab
'+
	 '
'+
	 '
Reset device specification to default:
'+
	 '<input type="radio" name="device" value="installedSolutions">Default
'+
	 '
'+
	 '<input type="submit" value="Set device configuration" />'+
	 '</form>'+
	 ''+
	 '';
	response.writeHead(200, {"Content-Type": "text/html"});
	response.write(body);
	response.end();
}

function setDeviceinfo(response, postData){
	var fs = require('fs');
	var device = querystring.parse(postData)["device"];
	var filePath = 	'../node_modules/universal/testData/deviceReporter/secondPilots/'+ device +'.json' ;
	fs.createReadStream(filePath).pipe(fs.createWriteStream('../node_modules/universal/testData/deviceReporter/installedSolutions.json'));

	response.writeHead(200, {"Content-Type": "text/html"});
	response.write("Device information successfully set!");
	response.end();
}

function getJSONRequest (url, callback) {
	var	reply = "";

	// fetch preference set from the preference server
	http.get(url, function(res) {

		res.on('data', function(dat){
			reply += dat;
		});

		res.on('end', function () {
			var parsedReply = JSON.parse(reply);
			callback(parsedReply);
		});

	}).on('error', function(e) {
		console.log("Error on get request to "+url+": " + e.message);
		callback(undefined);
	});
};

function getCurrentToken (callback) {
	return getJSONRequest("http://localhost:8081/token", callback);
}

function getPreferences (token, callback) {
	return getJSONRequest("http://preferences.gpii.net/user/" + token, callback);
}

function selectMM (response, postData) {
	var matchmaker = querystring.parse(postData)["matchmaker"];

	getCurrentToken(function (token) {
		if (token) {

			getPreferences(token, function (preferences) {
				if (preferences && preferences['preferences']) {
					preferences['preferences']['http://registry.gpii.org/common/matchMakerType'] = [{ "value": matchmaker}];
					saveModifiedPreferences(token, preferences['preferences'], "matchmaker successfully selected", response);
				} else {
					response.writeHead(200, {"Content-Type": "text/html"});
					response.write("Error on fetching preferences");
					response.end();
				}
			});

		} else {
			response.writeHead(200, {"Content-Type": "text/html"});
			response.write("Error on fetching user token");
			response.end();
		}
	});
};

function snapshotToPrefs(response) {
	//read the token of the currently logged in user
	getCurrentToken(function (token) {
		if (token) {
			//if theres a user logged in, get that users preferences
			getPreferences(token, function (preferences) {
				if (preferences && preferences['preferences']) {
					//get and add the snapshotted settings to the
					getJSONRequest("http://localhost:8081/snapshot", function (snapshotted) {
						for (solution in snapshotted) {
							preferences['preferences'][solution] = snapshotted[solution];
						};
						saveModifiedPreferences(token, preferences['preferences']	, "Snapshot saved to preferences server", response);
					});
				} else {
					response.writeHead(200, {"Content-Type": "text/html"});
					response.write("Error on fetching preferences");
					response.end();
				}
			});
		} else {
			response.writeHead(200, {"Content-Type": "text/html"});
			response.write("Error on fetching user token");
			response.end();
		}
	});
};

function saveModifiedPreferences(token, preferencesObject, usrMsg, response) {
	var data = JSON.stringify(preferencesObject);

	// object of options to indicate where to POST to
	var post_options = {
		host: 'preferences.gpii.net',
		port: '80',
		path: '/user/' + token,
		method: 'POST',
		headers: {
		'Content-Type': 'application/json',
		'Content-Length': data.length
		}
	};

	// set up the http.request POST
	var post_req = http.request(post_options, function(res) {
		res.setEncoding('utf8');
		res.on('data', function (chunk) {
			console.log('Response: ' + chunk);
		});
		res.on('end', function(){
			response.writeHead(200, {"Content-Type": "text/html"});
			response.write(usrMsg);
			response.end();
		});
	});
	// writes http request body data; Server replies and response callback 'res' gets activated
	post_req.write(data);
	post_req.end();

}

exports.start = start;
exports.selectMM = selectMM;
exports.setDeviceinfo = setDeviceinfo;
exports.snapshotToPrefs = snapshotToPrefs;<|endoftext|>


<commit_before>import path from 'path';
import querystring from 'querystring';

import {remote, ipcRenderer as ipc} from 'electron';
const {BrowserWindow} = remote;
import {Emitter, Disposable, CompositeDisposable} from 'event-kit';
import {autobind} from 'core-decorators';

import {getPackageRoot} from './helpers';

export default class WorkerManager {
  static instance = null;

  static getInstance() {
    if (!this.instance) {
      this.instance = new WorkerManager();
    }
    return this.instance;
  }

  static reset() {
    if (this.instance) { this.instance.destroy(); }
    this.instance = null;
  }

  constructor() {
    this.workers = new Set();
    this.activeWorker = null;
    this.createNewWorker();
  }

  isReady() {
    return this.activeWorker.isReady();
  }

  request(data) {
    if (this.destroyed) { throw new Error('Worker is destroyed'); }
    let operation;
    const requestPromise = new Promise((resolve, reject) => {
      operation = new Operation(data, resolve, reject);
      return this.activeWorker.executeOperation(operation);
    });
    operation.setPromise(requestPromise);
    return requestPromise;
  }

  createNewWorker({operationCountLimit} = {operationCountLimit: 10}) {
    if (this.destroyed) { return; }
    this.activeWorker = new Worker({
      operationCountLimit,
      onDestroyed: this.onDestroyed,
      onCrashed: this.onCrashed,
      onSick: this.onSick,
    });
    this.workers.add(this.activeWorker);
  }

  @autobind
  onDestroyed(destroyedWorker) {
    this.workers.delete(destroyedWorker);
  }

  @autobind
  onCrashed(crashedWorker) {
    if (crashedWorker === this.getActiveWorker()) {
      this.createNewWorker({operationCountLimit: crashedWorker.getOperationCountLimit()});
    }
    crashedWorker.getRemainingOperations().forEach(operation => this.activeWorker.executeOperation(operation));
  }

  @autobind
  onSick(sickWorker) {
    if (!atom.inSpecMode()) {
      // eslint-disable-next-line no-console
      console.warn(`Sick worker detected.
        operationCountLimit: ${sickWorker.getOperationCountLimit()},
        completed operation count: ${sickWorker.getCompletedOperationCount()}`);
    }
    const operationCountLimit = this.calculateNewOperationCountLimit(sickWorker);
    return this.createNewWorker({operationCountLimit});
  }

  calculateNewOperationCountLimit(lastWorker) {
    let operationCountLimit = 10;
    if (lastWorker.getOperationCountLimit() >= lastWorker.getCompletedOperationCount()) {
      operationCountLimit = Math.min(lastWorker.getOperationCountLimit() * 2, 100);
    }
    return operationCountLimit;
  }

  getActiveWorker() {
    return this.activeWorker;
  }

  getReadyPromise() {
    return this.activeWorker.getReadyPromise();
  }

  destroy(force) {
    this.destroyed = true;
    this.workers.forEach(worker => worker.destroy(force));
  }
}

export class Worker {
  static channelName = 'github:renderer-ipc';

  constructor({operationCountLimit, onSick, onCrashed, onDestroyed}) {
    this.operationCountLimit = operationCountLimit;
    this.onSick = onSick;
    this.onCrashed = onCrashed;
    this.onDestroyed = onDestroyed;

    this.operationsById = new Map();
    this.completedOperationCount = 0;
    this.sick = false;

    this.rendererProcess = new RendererProcess({
      loadUrl: this.getLoadUrl(operationCountLimit),
      onData: this.handleDataReceived,
      onExecStarted: this.handleExecStarted,
      onSpawnError: this.handleSpawnError,
      onStdinError: this.handleStdinError,
      onSick: this.handleSick,
      onCrashed: this.handleCrashed,
      onDestroyed: this.destroy,
    });
  }

  isReady() {
    return this.rendererProcess.isReady();
  }

  getLoadUrl(operationCountLimit) {
    const htmlPath = path.join(getPackageRoot(), 'lib', 'renderer.html');
    const rendererJsPath = path.join(getPackageRoot(), 'lib', 'worker.js');
    const qs = querystring.stringify({
      js: rendererJsPath,
      managerWebContentsId: this.getWebContentsId(),
      operationCountLimit,
      channelName: Worker.channelName,
    });
    return `file://${htmlPath}?${qs}`;
  }

  getWebContentsId() {
    return remote.getCurrentWebContents().id;
  }

  executeOperation(operation) {
    this.operationsById.set(operation.id, operation);
    operation.onComplete(this.onOperationComplete);
    return this.rendererProcess.executeOperation(operation);
  }

  @autobind
  handleDataReceived({id, results}) {
    const operation = this.operationsById.get(id);
    operation.complete(results, data => {
      const {timing} = data;
      const totalInternalTime = timing.execTime + timing.spawnTime;
      const ipcTime = operation.getExecutionTime() - totalInternalTime;
      data.timing.ipcTime = ipcTime;
      return data;
    });
  }

  @autobind
  onOperationComplete(operation) {
    this.completedOperationCount++;
    this.operationsById.delete(operation.id);

    if (this.sick && this.operationsById.size === 0) {
      this.destroy();
    }
  }

  @autobind
  handleExecStarted({id}) {
    const operation = this.operationsById.get(id);
    operation.setInProgress();
  }

  @autobind
  handleSpawnError({id, err}) {
    const operation = this.operationsById.get(id);
    operation.error(err);
  }

  @autobind
  handleStdinError({id, stdin, err}) {
    const operation = this.operationsById.get(id);
    operation.error(err);
  }

  @autobind
  handleSick() {
    this.sick = true;
    this.onSick(this);
  }

  @autobind
  handleCrashed() {
    this.onCrashed(this);
    this.destroy();
  }

  getOperationCountLimit() {
    return this.operationCountLimit;
  }

  getCompletedOperationCount() {
    return this.completedOperationCount;
  }

  getRemainingOperations() {
    return Array.from(this.operationsById.values());
  }

  getPid() {
    return this.rendererProcess.getPid();
  }

  getReadyPromise() {
    return this.rendererProcess.getReadyPromise();
  }

  async destroy(force) {
    this.onDestroyed(this);
    if (this.operationsById.size > 0 && !force) {
      const remainingOperationPromises = this.getRemainingOperations()
        .map(operation => operation.getPromise().catch(() => null));
      await Promise.all(remainingOperationPromises);
    }
    this.rendererProcess.destroy();
  }
}

/*
Sends operations to renderer processes
*/
export class RendererProcess {
  constructor({loadUrl, onDestroyed, onCrashed, onSick, onData, onSpawnError, onStdinError, onExecStarted}) {
    this.onDestroyed = onDestroyed;
    this.onCrashed = onCrashed;
    this.onSick = onSick;
    this.onData = onData;
    this.onSpawnError = onSpawnError;
    this.onStdinError = onStdinError;
    this.onExecStarted = onExecStarted;

    this.win = new BrowserWindow({show: !!process.env.ATOM_GITHUB_SHOW_RENDERER_WINDOW});
    this.webContents = this.win.webContents;
    // this.webContents.openDevTools();

    this.emitter = new Emitter();
    this.subscriptions = new CompositeDisposable();
    this.registerListeners();

    this.win.loadURL(loadUrl);
    this.win.webContents.on('crashed', this.handleDestroy);
    this.win.webContents.on('destroyed', this.handleDestroy);
    this.subscriptions.add(
      new Disposable(() => {
        if (!this.win.isDestroyed()) {
          this.win.webContents.removeListener('crashed', this.handleDestroy);
          this.win.webContents.removeListener('destroyed', this.handleDestroy);
          this.win.destroy();
        }
      }),
      this.emitter,
    );

    this.ready = false;
    this.readyPromise = new Promise(resolve => { this.resolveReady = resolve; });
  }

  isReady() {
    return this.ready;
  }

  @autobind
  handleDestroy(...args) {
    this.destroy();
    this.onCrashed(...args);
  }

  registerListeners() {
    const handleMessages = (event, {sourceWebContentsId, type, data}) => {
      if (sourceWebContentsId === this.win.webContents.id) {
        this.emitter.emit(type, data);
      }
    };

    ipc.on(Worker.channelName, handleMessages);
    this.emitter.on('renderer-ready', ({pid}) => {
      this.pid = pid;
      this.ready = true;
      this.resolveReady();
    });
    this.emitter.on('git-data', this.onData);
    this.emitter.on('git-spawn-error', this.onSpawnError);
    this.emitter.on('git-stdin-error', this.onStdinError);
    this.emitter.on('slow-spawns', this.onSick);

    // not currently used to avoid clogging up ipc channel
    // keeping it around as it's potentially useful for avoiding duplicate write operations upon renderer crashing
    this.emitter.on('exec-started', this.onExecStarted);

    this.subscriptions.add(
      new Disposable(() => ipc.removeListener(Worker.channelName, handleMessages)),
    );
  }

  executeOperation(operation) {
    return operation.execute(payload => {
      if (this.destroyed) { return null; }
      return this.webContents.send(Worker.channelName, {
        type: 'git-exec',
        data: payload,
      });
    });
  }

  getPid() {
    return this.pid;
  }

  getReadyPromise() {
    return this.readyPromise;
  }

  destroy() {
    this.destroyed = true;
    this.subscriptions.dispose();
  }
}

export class Operation {
  static status = {
    PENDING: Symbol('pending'),
    INPROGRESS: Symbol('in-progress'),
    COMPLETE: Symbol('complete'),
  }

  static id = 0;

  constructor(data, resolve, reject) {
    this.id = Operation.id++;
    this.data = data;
    this.resolve = resolve;
    this.reject = reject;
    this.promise = null;
    this.startTime = null;
    this.endTime = null;
    this.status = Operation.status.PENDING;
    this.results = null;
    this.emitter = new Emitter();
  }

  onComplete(cb) {
    return this.emitter.on('complete', cb);
  }

  setPromise(promise) {
    this.promise = promise;
  }

  getPromise() {
    return this.promise;
  }

  setInProgress() {
    // after exec has been called but before results a received
    this.status = Operation.status.INPROGRESS;
  }

  getExecutionTime() {
    if (!this.startTime || !this.endTime) {
      return NaN;
    } else {
      return this.endTime - this.startTime;
    }
  }

  complete(results, mutate = data => data) {
    this.endTime = performance.now();
    this.results = results;
    this.resolve(mutate(results));
    this.status = Operation.status.COMPLETE;
    this.emitter.emit('complete', this);
    this.emitter.dispose();
  }

  error(results) {
    this.endTime = performance.now();
    const err = new Error(results.message, results.fileName, results.lineNumber);
    err.stack = results.stack;
    this.reject(err);
  }

  execute(execFn) {
    this.startTime = performance.now();
    return execFn({...this.data, id: this.id});
  }
}
<commit_msg>Cancel Operations with a {cancel} callback<commit_after>import path from 'path';
import querystring from 'querystring';

import {remote, ipcRenderer as ipc} from 'electron';
const {BrowserWindow} = remote;
import {Emitter, Disposable, CompositeDisposable} from 'event-kit';
import {autobind} from 'core-decorators';

import {getPackageRoot} from './helpers';

export default class WorkerManager {
  static instance = null;

  static getInstance() {
    if (!this.instance) {
      this.instance = new WorkerManager();
    }
    return this.instance;
  }

  static reset() {
    if (this.instance) { this.instance.destroy(); }
    this.instance = null;
  }

  constructor() {
    this.workers = new Set();
    this.activeWorker = null;
    this.createNewWorker();
  }

  isReady() {
    return this.activeWorker.isReady();
  }

  request(data) {
    if (this.destroyed) { throw new Error('Worker is destroyed'); }
    let operation;
    const requestPromise = new Promise((resolve, reject) => {
      operation = new Operation(data, resolve, reject);
      return this.activeWorker.executeOperation(operation);
    });
    operation.setPromise(requestPromise);
    return {
      cancel: () => this.activeWorker.cancelOperation(operation),
      promise: requestPromise,
    };
  }

  createNewWorker({operationCountLimit} = {operationCountLimit: 10}) {
    if (this.destroyed) { return; }
    this.activeWorker = new Worker({
      operationCountLimit,
      onDestroyed: this.onDestroyed,
      onCrashed: this.onCrashed,
      onSick: this.onSick,
    });
    this.workers.add(this.activeWorker);
  }

  @autobind
  onDestroyed(destroyedWorker) {
    this.workers.delete(destroyedWorker);
  }

  @autobind
  onCrashed(crashedWorker) {
    if (crashedWorker === this.getActiveWorker()) {
      this.createNewWorker({operationCountLimit: crashedWorker.getOperationCountLimit()});
    }
    crashedWorker.getRemainingOperations().forEach(operation => this.activeWorker.executeOperation(operation));
  }

  @autobind
  onSick(sickWorker) {
    if (!atom.inSpecMode()) {
      // eslint-disable-next-line no-console
      console.warn(`Sick worker detected.
        operationCountLimit: ${sickWorker.getOperationCountLimit()},
        completed operation count: ${sickWorker.getCompletedOperationCount()}`);
    }
    const operationCountLimit = this.calculateNewOperationCountLimit(sickWorker);
    return this.createNewWorker({operationCountLimit});
  }

  calculateNewOperationCountLimit(lastWorker) {
    let operationCountLimit = 10;
    if (lastWorker.getOperationCountLimit() >= lastWorker.getCompletedOperationCount()) {
      operationCountLimit = Math.min(lastWorker.getOperationCountLimit() * 2, 100);
    }
    return operationCountLimit;
  }

  getActiveWorker() {
    return this.activeWorker;
  }

  getReadyPromise() {
    return this.activeWorker.getReadyPromise();
  }

  destroy(force) {
    this.destroyed = true;
    this.workers.forEach(worker => worker.destroy(force));
  }
}

export class Worker {
  static channelName = 'github:renderer-ipc';

  constructor({operationCountLimit, onSick, onCrashed, onDestroyed}) {
    this.operationCountLimit = operationCountLimit;
    this.onSick = onSick;
    this.onCrashed = onCrashed;
    this.onDestroyed = onDestroyed;

    this.operationsById = new Map();
    this.completedOperationCount = 0;
    this.sick = false;

    this.rendererProcess = new RendererProcess({
      loadUrl: this.getLoadUrl(operationCountLimit),
      onData: this.handleDataReceived,
      onExecStarted: this.handleExecStarted,
      onSpawnError: this.handleSpawnError,
      onStdinError: this.handleStdinError,
      onSick: this.handleSick,
      onCrashed: this.handleCrashed,
      onDestroyed: this.destroy,
    });
  }

  isReady() {
    return this.rendererProcess.isReady();
  }

  getLoadUrl(operationCountLimit) {
    const htmlPath = path.join(getPackageRoot(), 'lib', 'renderer.html');
    const rendererJsPath = path.join(getPackageRoot(), 'lib', 'worker.js');
    const qs = querystring.stringify({
      js: rendererJsPath,
      managerWebContentsId: this.getWebContentsId(),
      operationCountLimit,
      channelName: Worker.channelName,
    });
    return `file://${htmlPath}?${qs}`;
  }

  getWebContentsId() {
    return remote.getCurrentWebContents().id;
  }

  executeOperation(operation) {
    this.operationsById.set(operation.id, operation);
    operation.onComplete(this.onOperationComplete);
    return this.rendererProcess.executeOperation(operation);
  }

  cancelOperation(operation) {
    return this.rendererProcess.cancelOperation(operation);
  }

  @autobind
  handleDataReceived({id, results}) {
    const operation = this.operationsById.get(id);
    operation.complete(results, data => {
      const {timing} = data;
      const totalInternalTime = timing.execTime + timing.spawnTime;
      const ipcTime = operation.getExecutionTime() - totalInternalTime;
      data.timing.ipcTime = ipcTime;
      return data;
    });
  }

  @autobind
  onOperationComplete(operation) {
    this.completedOperationCount++;
    this.operationsById.delete(operation.id);

    if (this.sick && this.operationsById.size === 0) {
      this.destroy();
    }
  }

  @autobind
  handleExecStarted({id}) {
    const operation = this.operationsById.get(id);
    operation.setInProgress();
  }

  @autobind
  handleSpawnError({id, err}) {
    const operation = this.operationsById.get(id);
    operation.error(err);
  }

  @autobind
  handleStdinError({id, stdin, err}) {
    const operation = this.operationsById.get(id);
    operation.error(err);
  }

  @autobind
  handleSick() {
    this.sick = true;
    this.onSick(this);
  }

  @autobind
  handleCrashed() {
    this.onCrashed(this);
    this.destroy();
  }

  getOperationCountLimit() {
    return this.operationCountLimit;
  }

  getCompletedOperationCount() {
    return this.completedOperationCount;
  }

  getRemainingOperations() {
    return Array.from(this.operationsById.values());
  }

  getPid() {
    return this.rendererProcess.getPid();
  }

  getReadyPromise() {
    return this.rendererProcess.getReadyPromise();
  }

  async destroy(force) {
    this.onDestroyed(this);
    if (this.operationsById.size > 0 && !force) {
      const remainingOperationPromises = this.getRemainingOperations()
        .map(operation => operation.getPromise().catch(() => null));
      await Promise.all(remainingOperationPromises);
    }
    this.rendererProcess.destroy();
  }
}

/*
Sends operations to renderer processes
*/
export class RendererProcess {
  constructor({loadUrl, onDestroyed, onCrashed, onSick, onData, onSpawnError, onStdinError, onExecStarted}) {
    this.onDestroyed = onDestroyed;
    this.onCrashed = onCrashed;
    this.onSick = onSick;
    this.onData = onData;
    this.onSpawnError = onSpawnError;
    this.onStdinError = onStdinError;
    this.onExecStarted = onExecStarted;

    this.win = new BrowserWindow({show: !!process.env.ATOM_GITHUB_SHOW_RENDERER_WINDOW});
    this.webContents = this.win.webContents;
    // this.webContents.openDevTools();

    this.emitter = new Emitter();
    this.subscriptions = new CompositeDisposable();
    this.registerListeners();

    this.win.loadURL(loadUrl);
    this.win.webContents.on('crashed', this.handleDestroy);
    this.win.webContents.on('destroyed', this.handleDestroy);
    this.subscriptions.add(
      new Disposable(() => {
        if (!this.win.isDestroyed()) {
          this.win.webContents.removeListener('crashed', this.handleDestroy);
          this.win.webContents.removeListener('destroyed', this.handleDestroy);
          this.win.destroy();
        }
      }),
      this.emitter,
    );

    this.ready = false;
    this.readyPromise = new Promise(resolve => { this.resolveReady = resolve; });
  }

  isReady() {
    return this.ready;
  }

  @autobind
  handleDestroy(...args) {
    this.destroy();
    this.onCrashed(...args);
  }

  registerListeners() {
    const handleMessages = (event, {sourceWebContentsId, type, data}) => {
      if (sourceWebContentsId === this.win.webContents.id) {
        this.emitter.emit(type, data);
      }
    };

    ipc.on(Worker.channelName, handleMessages);
    this.emitter.on('renderer-ready', ({pid}) => {
      this.pid = pid;
      this.ready = true;
      this.resolveReady();
    });
    this.emitter.on('git-data', this.onData);
    this.emitter.on('git-spawn-error', this.onSpawnError);
    this.emitter.on('git-stdin-error', this.onStdinError);
    this.emitter.on('slow-spawns', this.onSick);

    // not currently used to avoid clogging up ipc channel
    // keeping it around as it's potentially useful for avoiding duplicate write operations upon renderer crashing
    this.emitter.on('exec-started', this.onExecStarted);

    this.subscriptions.add(
      new Disposable(() => ipc.removeListener(Worker.channelName, handleMessages)),
    );
  }

  executeOperation(operation) {
    return operation.execute(payload => {
      if (this.destroyed) { return null; }
      return this.webContents.send(Worker.channelName, {
        type: 'git-exec',
        data: payload,
      });
    });
  }

  cancelOperation(operation) {
    return operation.cancel(payload => {
      if (this.destroyed) { return null; }
      return this.webContents.send(Worker.channelName, {
        type: 'git-cancel',
        data: payload,
      });
    });
  }

  getPid() {
    return this.pid;
  }

  getReadyPromise() {
    return this.readyPromise;
  }

  destroy() {
    this.destroyed = true;
    this.subscriptions.dispose();
  }
}

export class Operation {
  static status = {
    PENDING: Symbol('pending'),
    INPROGRESS: Symbol('in-progress'),
    COMPLETE: Symbol('complete'),
  }

  static id = 0;

  constructor(data, resolve, reject) {
    this.id = Operation.id++;
    this.data = data;
    this.resolve = resolve;
    this.reject = reject;
    this.promise = null;
    this.startTime = null;
    this.endTime = null;
    this.status = Operation.status.PENDING;
    this.results = null;
    this.emitter = new Emitter();
  }

  onComplete(cb) {
    return this.emitter.on('complete', cb);
  }

  setPromise(promise) {
    this.promise = promise;
  }

  getPromise() {
    return this.promise;
  }

  setInProgress() {
    // after exec has been called but before results a received
    this.status = Operation.status.INPROGRESS;
  }

  getExecutionTime() {
    if (!this.startTime || !this.endTime) {
      return NaN;
    } else {
      return this.endTime - this.startTime;
    }
  }

  complete(results, mutate = data => data) {
    this.endTime = performance.now();
    this.results = results;
    this.resolve(mutate(results));
    this.status = Operation.status.COMPLETE;
    this.emitter.emit('complete', this);
    this.emitter.dispose();
  }

  error(results) {
    this.endTime = performance.now();
    const err = new Error(results.message, results.fileName, results.lineNumber);
    err.stack = results.stack;
    this.reject(err);
  }

  execute(execFn) {
    this.startTime = performance.now();
    return execFn({...this.data, id: this.id});
  }

  cancel(execFn) {
    return execFn({id: this.id});
  }
}
<|endoftext|>