<commit_before>$(function ($) {
  // Constants
  var flameHost = '192.168.7.101:5000'
  var pollingInterval = 1000 // in milliseconds
  var prefixes = {
    hydraulics: 'hydraulics-attract',
    poofer: 'poofer-sequence'
  }

  // Initial values
  var selectedPoofer = null
  var inverted = false
  var selectedFilenames = {}
  var toggleStates = {
    'poofer-main': true,
    'hydraulics-main': true,
    'hydraulics-attract': true
  }
  var hydraulicsAttractModeActive = false
  var pooferSequenceModeActive = false
  var hydraulicsRecordActive = false
  var hydraulicsState = {control_x:0,control_y:0,control_z:0,sculpture_x:0,sculpture_y:0,sculpture_z:0}
  var hydraulicsAttractFiles = []
  var pooferSequenceFiles = []

  // jQuery helpers
  $.fn.reduce = function() {
    return [].reduce.apply(this.toArray(), arguments)
  }

  // Helper functions
  var flameUrl = function(endpoint) {
    if (endpoint === undefined) {
      endpoint = ''
    }
    return `http://${flameHost}/flame${endpoint}`
  }
  var hydraulicsUrl = function(endpoint) {
    if (endpoint === undefined) {
      endpoint = ''
    }
    return `http://${flameHost}/hydraulics${endpoint}`
  }
  var getOrSetFilename = function(prefix, filename) {
    if (filename !== undefined) {
      selectedFilenames[prefix] = filename
    }
    return selectedFilenames[prefix]
  }
  var selectedHydraulicsFilename = function(filename) {
    return getOrSetFilename(prefixes.hydraulics, filename)
  }
  var selectedPooferFilename = function(filename) {
    return getOrSetFilename(prefixes.poofer, filename)
  }

  // jQuery helper functions
  var findPoofer = function(name) {
    return $('.poofer-' + name)
  }

  // Display state update functions
  var updateToggleState = function(prefix) {
    var $element = $(`.${prefix}-toggle-button`)
    var state = toggleStates[prefix]
    $element.prop('checked', state)
  }
  var updateToggleStates = function() {
    Object.keys(toggleStates).map(updateToggleState)
  }
  var updatePooferToggleButtonState = function() {
    var enabled = !selectedPoofer || selectedPoofer.enabled
    $('.poofer-individual-toggle-button').prop('checked', enabled)
  }
  var updateSelectedPooferName = function() {
    var name = (selectedPoofer && selectedPoofer.name) || 'None'
    $('.poofer-selected-display-field').html(name)
  }
  var updateSelectedPooferDependentState = function() {
    updateSelectedPooferName()
    updatePooferToggleButtonState()
  }
  var updatePooferDisplayState = function(poofer) {
    var $poofer = findPoofer(poofer.name)
    if (poofer.enabled) {
      $poofer.removeClass('poofer-disabled')
      $poofer.addClass('poofer-enabled')
    } else {
      $poofer.removeClass('poofer-enabled')
      $poofer.addClass('poofer-disabled')
    }
    if (selectedPoofer && poofer.name === selectedPoofer.name) {
      $poofer.addClass('selected')
    } else {
      $poofer.removeClass('selected')
    }
  }
  var updateAllPoofersDisplayState = function() {
    allPoofers.forEach(function (pooferName) {
      var poofer = allPoofersState[pooferName]
      updatePooferDisplayState(poofer)
    })
  }
  var updateHydraulicsDisplayState = function() {
    ['control_x','control_y','control_z','sculpture_x','sculpture_y','sculpture_z'].forEach(function(coord) {
      $(`.hydraulics-label-${coord}`).text(hydraulicsState[coord])
    })
  }
  var updateFilePicker = function(prefix, fileList) {
    var $files = $(`.${prefix}-file`)
    var $fileList = $(`.${prefix}-file-picker`)
    var addedFiles = []
    var removedFiles = []
    fileList.forEach(function(file) {
      var fileInList = $files.reduce(function(acc, cur) {
        var $cur = $(cur)
        return acc || $cur.data('name') === file
      }, false)
      if (!fileInList) {
        addedFiles.push(file)
      }
    })
    $files.each(function(idx, file) {
      var $file = $(file)
      var fileInList = fileList.reduce(function(acc, cur) {
        return acc || $file.data('name') === cur
      }, false)
      if (!fileInList) {
        removedFiles.push($file)
        if ($file.hasClass('selected')) {
          selectedFilenames[prefix] = null
        }
      }
    })
    removedFiles.forEach(function($file) {
      $file.remove()
    })
    addedFiles.forEach(function(file) {
      var $file = $(`<li class="${prefix}-file sequence-file" data-name="${file}">${file}</li>`)
      $fileList.append($file)
    })
  }
  var updateHydraulicsAttractFilePicker = function() {
    updateFilePicker(prefixes.hydraulics, hydraulicsAttractFiles)
  }
  var updatePooferSequenceFilePicker = function() {
    updateFilePicker(prefixes.poofer, pooferSequenceFiles)
  }
  var updateHydraulicsAttractPlayMode = function() {
    var oldMode = hydraulicsAttractModeActive
    var $button = $('.hydraulics-attract-play-stop-button')
    if (!selectedHydraulicsFilename()) {
      hydraulicsAttractModeActive = false
      $button.attr('disabled', true)
    } else if (!toggleStates['hydraulics-attract']) {
      $button.attr('disabled', true)
    } else {
      $button.attr('disabled', false)
    }
    if (hydraulicsAttractModeActive) {
      $button.removeClass('btn-play')
      $button.addClass('btn-stop')
    } else {
      $button.removeClass('btn-stop')
      $button.addClass('btn-play')
    }
    $button.attr('title', hydraulicsAttractModeActive ? 'Stop' : 'Start')
    if (hydraulicsAttractModeActive !== oldMode) {
      postHydraulicsAttractMode()
    }
  }
  var updatePooferSequencePlayMode = function() {
    //FIXME this should set the button state based on the state of the selected file XXX
    var oldMode = pooferSequenceModeActive
    var $button = $('.poofer-sequence-play-stop-button')
    if (!selectedPooferFilename()) {
      pooferSequenceModeActive = false
      $button.attr('disabled', true)
    } else {
      $button.attr('disabled', false)
    }
    if (pooferSequenceModeActive) {
      $button.removeClass('btn-play')
      $button.addClass('btn-stop')
    } else {
      $button.removeClass('btn-stop')
      $button.addClass('btn-play')
    }
    $button.attr('title', pooferSequenceModeActive ? 'Stop' : 'Start')
    if (pooferSequenceModeActive !== oldMode) {
      postPooferSequenceMode()
    }
  }
  var updateHydraulicsRecordButtonState = function() {
    var $button = $('.hydraulics-record-toggle-button')
    if (hydraulicsRecordActive) {
      $button.removeClass('btn-record')
      $button.addClass('btn-stop-record')
    } else {
      $button.removeClass('btn-stop-record')
      $button.addClass('btn-record')
    }
    $button.attr('title', hydraulicsAttractModeActive ? 'Stop Recording' : 'Record')

  }
  var updateSelectedFileDependentState = function() {
    updateHydraulicsAttractPlayMode()
    updatePooferSequencePlayMode()
  }
  var updateColorInversion = function() {
    if (inverted) {
      $('html').addClass('inverted')
    }
    else {
      $('html').removeClass('inverted')
    }
  }
  var updateAllUIState = function() {
    // Since we don't have automatic data binding and it's a pain to call update functions
    // in just the right places, this function just updates everything and is called
    // when anything changes. Where possible, functions called from here should be
    // written to not force refreshes of state that didn't actually change.
    updateToggleStates()
    updateSelectedPooferDependentState()
    updateAllPoofersDisplayState()
    updateHydraulicsDisplayState()
    updateHydraulicsAttractFilePicker()
    updatePooferSequenceFilePicker()
    updateSelectedFileDependentState()
    updateHydraulicsRecordButtonState()
    updateColorInversion()
  }

  // Data state setting functions
  var setSelectedPoofer = function(name) {
    selectedPoofer = allPoofersState[name]
    updateAllUIState()
  }
  var updatePooferData = function (data) {
    toggleStates['poofer-main'] = data.globalState
    data.poofers.forEach(function(poofer) {
      var pooferState = allPoofersState[poofer.id.toLowerCase()]
      pooferState.enabled = poofer.enabled
    })
    pooferSequenceFiles = data.patterns.map(function(p) { return p.name })
  }
  var updateHydraulicsData = function (data) {
    toggleStates['hydraulics-main'] = data.currentState === 'nomove' ? false : true
    toggleStates['hydraulics-attract'] = data.autoAttractEnabled
    hydraulicsAttractModeActive = data.currentState === 'attract' ? true : false
    hydraulicsState = data
    hydraulicsAttractFiles = data.playbacks
  }

  // Setup poofer data structures
  var allPoofers = ['nn','nw','nt','ne','wn','tn','en','ww','wt','tw','tt','te','et','ee','ws','ts','es','sw','st','se','ss','bn','bs','be','bw']
  var PooferState = function(name) {
    this.name = name
    this.enabled = true
    return this
  }
  var allPoofersState = {}
  for (var i = 0; i < allPoofers.length; i++) {
    var name = allPoofers[i]
    allPoofersState[name] = new PooferState(name)
    // Setup poofer event handlers
    var $currentPoofer = findPoofer(name)
    ;(function () {
      var tempName = name
      $currentPoofer.on('click', function () {
        setSelectedPoofer(tempName)
      })
    })()
  }

  // Setup button event handlers
  $('.invert-button').on('click', function () {
    inverted = !inverted
    updateAllUIState()
  })
  $('.poofer-individual-toggle-button').on('click', function () {
    if (selectedPoofer) {
      selectedPoofer.enabled = !selectedPoofer.enabled
      updatePooferDisplayState(selectedPoofer)
      updatePooferToggleButtonState()
      var name = selectedPoofer.name.toUpperCase()
      $.post(flameUrl(`/poofers/${name}`), { enabled: selectedPoofer.enabled })
    }
  })
  Object.keys(prefixes).forEach(function(prefixKey) {
    var prefix = prefixes[prefixKey]
    $(`.${prefix}-file-picker`).on('click', function(event) {
      var $selectedItem = $(event.target)
      if ($selectedItem.length === 0 || !$selectedItem.hasClass(`${prefix}-file`)) {
        return
      }
      selectedFilenames[prefix] = $selectedItem.data('name')
      if (!$selectedItem.hasClass('selected')) {
        $(`.${prefix}-file-picker li`).removeClass('selected')
        $selectedItem.addClass('selected')
      }
      postAttractFile(prefix)
      updateAllUIState()
    })
  })
  $('.hydraulics-attract-play-stop-button').on('click', function() {
    hydraulicsAttractModeActive = !hydraulicsAttractModeActive
    postHydraulicsAttractPlayStop()
    updateAllUIState()
  })
  $('.poofer-sequence-play-stop-button').on('click', function() {
    pooferSequenceModeActive = !pooferSequenceModeActive
    postPooferSequenceMode()
    updateAllUIState()
  })
  $('.hydraulics-record-toggle-button').on('click', function() {
    hydraulicsRecordActive = !hydraulicsRecordActive
    postHydraulicsRecordMode()
    updateAllUIState()
  })
  Object.keys(toggleStates).forEach(function(p) {
    var prefix = p
    $(`.${prefix}-toggle-button`).on('click', function() {
      toggleStates[prefix] = !toggleStates[prefix]
      postToggleState(prefix)
      updateAllUIState()
    })
  })
  $('.hydraulics-attract-rename-button').on('click', function () {
    postHydraulicsAttractRename()
  })
  $('.hydraulics-attract-trash-button').on('click', function () {
    deleteHydraulicsAttractFile()
  })

  // Ajax functions
  var postHydraulicsAttractMode = function() {
    if (hydraulicsAttractModeActive && selectedHydraulicsFilename()) {
      $.post(hydraulicsUrl(), {
        state: 'attract',
        currentPlayback: selectedHydraulicsFilename()
      })
    } else {
      // just in case, make state consistent
      hydraulicsAttractModeActive = false
      $.post(hydraulicsUrl(), {
        state: 'passthrough'
      })
    }
  }
  var postPooferSequenceMode = function() {
    var name = selectedPooferFilename()
    if (name) {
      $.post(flameUrl(`/patterns/${name}`), {
        active: pooferSequenceModeActive
      })
    } else {
      // just in case, make state consistent
      pooferSequenceModeActive = false
    }
  }
  postHydraulicsRecordMode = function() {
    $.post(hydraulicsUrl('/playbacks'), {
      record: '' + hydraulicsRecordActive
    })
  }
  var postHydraulicsAttractRename = function() {
    var oldName = selectedHydraulicsFilename()
    var newName = $('.hydraulics-attract-name-field').val()
    if (!oldName || !newName) {
      return
    }
    $.post(hydraulicsUrl(`/playbacks/${oldName}`), {
      newName: newName
    })
  }
  var deleteHydraulicsAttractFile = function() {
    var filename = selectedHydraulicsFilename()
    if (!filename) {
      return
    }
    $.ajax(hydraulicsUrl(`/playbacks/${filename}`), {
      method: 'DELETE'
    })
  }
  var postAttractFile = function(prefix) {
    var filename = selectedFilenames[prefix]
    if (filename) {
      if (prefix === prefixes.hydraulics) {
        $.post(hydraulicsUrl(), {
          currentPlayback: filename
        })
      } else if (prefix === prefixes.poofer) {
        // Do nothing - poofer filenames work differently
      }
    }
  }
  var postToggleState = function(prefix) {
    if (prefix === 'poofer-main') {
      $.post(flameUrl(), {
        playState: toggleStates[prefix] ? 'play' : 'pause'
      })
    } else if (prefix === 'hydraulics-main') {
      $.post(hydraulicsUrl(), {
        state: toggleStates[prefix] ? 'passthrough' : 'nomove'
      })
    } else if (prefix === 'hydraulics-attract') {
      if (toggleStates['hydraulics-main']) {
        $.post(hydraulicsUrl(), {
          autoAttractEnabled: toggleStates[prefix]
        })
      }
    }
  }
  var postHydraulicsAttractPlayStop = function() {
    $.post(hydraulicsUrl(), {
      state: hydraulicsAttractModeActive ? 'attract' : 'passthrough'
    })
  }

  // Ajax callbacks
  var flameDataCallback = function(data) {
    updatePooferData(data)
    updateAllUIState()
  }
  var hydraulicsDataCallback = function(data) {
    updateHydraulicsData(data)
    updateAllUIState()
  }

  // Setup polling
  var pollingFunction = function() {
    $.getJSON(flameUrl()).done(flameDataCallback)
    $.getJSON(hydraulicsUrl()).done(hydraulicsDataCallback)
  }
  setInterval(pollingFunction, pollingInterval)

  // Make sure initial UI state is correct
  updateAllUIState()
})
<commit_msg>Fix host url<commit_after>$(function ($) {
  // Constants
  var flameHost = '//:5000'
  var pollingInterval = 1000 // in milliseconds
  var prefixes = {
    hydraulics: 'hydraulics-attract',
    poofer: 'poofer-sequence'
  }

  // Initial values
  var selectedPoofer = null
  var inverted = false
  var selectedFilenames = {}
  var toggleStates = {
    'poofer-main': true,
    'hydraulics-main': true,
    'hydraulics-attract': true
  }
  var hydraulicsAttractModeActive = false
  var pooferSequenceModeActive = false
  var hydraulicsRecordActive = false
  var hydraulicsState = {control_x:0,control_y:0,control_z:0,sculpture_x:0,sculpture_y:0,sculpture_z:0}
  var hydraulicsAttractFiles = []
  var pooferSequenceFiles = []

  // jQuery helpers
  $.fn.reduce = function() {
    return [].reduce.apply(this.toArray(), arguments)
  }

  // Helper functions
  var flameUrl = function(endpoint) {
    if (endpoint === undefined) {
      endpoint = ''
    }
    return `http://${flameHost}/flame${endpoint}`
  }
  var hydraulicsUrl = function(endpoint) {
    if (endpoint === undefined) {
      endpoint = ''
    }
    return `http://${flameHost}/hydraulics${endpoint}`
  }
  var getOrSetFilename = function(prefix, filename) {
    if (filename !== undefined) {
      selectedFilenames[prefix] = filename
    }
    return selectedFilenames[prefix]
  }
  var selectedHydraulicsFilename = function(filename) {
    return getOrSetFilename(prefixes.hydraulics, filename)
  }
  var selectedPooferFilename = function(filename) {
    return getOrSetFilename(prefixes.poofer, filename)
  }

  // jQuery helper functions
  var findPoofer = function(name) {
    return $('.poofer-' + name)
  }

  // Display state update functions
  var updateToggleState = function(prefix) {
    var $element = $(`.${prefix}-toggle-button`)
    var state = toggleStates[prefix]
    $element.prop('checked', state)
  }
  var updateToggleStates = function() {
    Object.keys(toggleStates).map(updateToggleState)
  }
  var updatePooferToggleButtonState = function() {
    var enabled = !selectedPoofer || selectedPoofer.enabled
    $('.poofer-individual-toggle-button').prop('checked', enabled)
  }
  var updateSelectedPooferName = function() {
    var name = (selectedPoofer && selectedPoofer.name) || 'None'
    $('.poofer-selected-display-field').html(name)
  }
  var updateSelectedPooferDependentState = function() {
    updateSelectedPooferName()
    updatePooferToggleButtonState()
  }
  var updatePooferDisplayState = function(poofer) {
    var $poofer = findPoofer(poofer.name)
    if (poofer.enabled) {
      $poofer.removeClass('poofer-disabled')
      $poofer.addClass('poofer-enabled')
    } else {
      $poofer.removeClass('poofer-enabled')
      $poofer.addClass('poofer-disabled')
    }
    if (selectedPoofer && poofer.name === selectedPoofer.name) {
      $poofer.addClass('selected')
    } else {
      $poofer.removeClass('selected')
    }
  }
  var updateAllPoofersDisplayState = function() {
    allPoofers.forEach(function (pooferName) {
      var poofer = allPoofersState[pooferName]
      updatePooferDisplayState(poofer)
    })
  }
  var updateHydraulicsDisplayState = function() {
    ['control_x','control_y','control_z','sculpture_x','sculpture_y','sculpture_z'].forEach(function(coord) {
      $(`.hydraulics-label-${coord}`).text(hydraulicsState[coord])
    })
  }
  var updateFilePicker = function(prefix, fileList) {
    var $files = $(`.${prefix}-file`)
    var $fileList = $(`.${prefix}-file-picker`)
    var addedFiles = []
    var removedFiles = []
    fileList.forEach(function(file) {
      var fileInList = $files.reduce(function(acc, cur) {
        var $cur = $(cur)
        return acc || $cur.data('name') === file
      }, false)
      if (!fileInList) {
        addedFiles.push(file)
      }
    })
    $files.each(function(idx, file) {
      var $file = $(file)
      var fileInList = fileList.reduce(function(acc, cur) {
        return acc || $file.data('name') === cur
      }, false)
      if (!fileInList) {
        removedFiles.push($file)
        if ($file.hasClass('selected')) {
          selectedFilenames[prefix] = null
        }
      }
    })
    removedFiles.forEach(function($file) {
      $file.remove()
    })
    addedFiles.forEach(function(file) {
      var $file = $(`<li class="${prefix}-file sequence-file" data-name="${file}">${file}</li>`)
      $fileList.append($file)
    })
  }
  var updateHydraulicsAttractFilePicker = function() {
    updateFilePicker(prefixes.hydraulics, hydraulicsAttractFiles)
  }
  var updatePooferSequenceFilePicker = function() {
    updateFilePicker(prefixes.poofer, pooferSequenceFiles)
  }
  var updateHydraulicsAttractPlayMode = function() {
    var oldMode = hydraulicsAttractModeActive
    var $button = $('.hydraulics-attract-play-stop-button')
    if (!selectedHydraulicsFilename()) {
      hydraulicsAttractModeActive = false
      $button.attr('disabled', true)
    } else if (!toggleStates['hydraulics-attract']) {
      $button.attr('disabled', true)
    } else {
      $button.attr('disabled', false)
    }
    if (hydraulicsAttractModeActive) {
      $button.removeClass('btn-play')
      $button.addClass('btn-stop')
    } else {
      $button.removeClass('btn-stop')
      $button.addClass('btn-play')
    }
    $button.attr('title', hydraulicsAttractModeActive ? 'Stop' : 'Start')
    if (hydraulicsAttractModeActive !== oldMode) {
      postHydraulicsAttractMode()
    }
  }
  var updatePooferSequencePlayMode = function() {
    //FIXME this should set the button state based on the state of the selected file XXX
    var oldMode = pooferSequenceModeActive
    var $button = $('.poofer-sequence-play-stop-button')
    if (!selectedPooferFilename()) {
      pooferSequenceModeActive = false
      $button.attr('disabled', true)
    } else {
      $button.attr('disabled', false)
    }
    if (pooferSequenceModeActive) {
      $button.removeClass('btn-play')
      $button.addClass('btn-stop')
    } else {
      $button.removeClass('btn-stop')
      $button.addClass('btn-play')
    }
    $button.attr('title', pooferSequenceModeActive ? 'Stop' : 'Start')
    if (pooferSequenceModeActive !== oldMode) {
      postPooferSequenceMode()
    }
  }
  var updateHydraulicsRecordButtonState = function() {
    var $button = $('.hydraulics-record-toggle-button')
    if (hydraulicsRecordActive) {
      $button.removeClass('btn-record')
      $button.addClass('btn-stop-record')
    } else {
      $button.removeClass('btn-stop-record')
      $button.addClass('btn-record')
    }
    $button.attr('title', hydraulicsAttractModeActive ? 'Stop Recording' : 'Record')

  }
  var updateSelectedFileDependentState = function() {
    updateHydraulicsAttractPlayMode()
    updatePooferSequencePlayMode()
  }
  var updateColorInversion = function() {
    if (inverted) {
      $('html').addClass('inverted')
    }
    else {
      $('html').removeClass('inverted')
    }
  }
  var updateAllUIState = function() {
    // Since we don't have automatic data binding and it's a pain to call update functions
    // in just the right places, this function just updates everything and is called
    // when anything changes. Where possible, functions called from here should be
    // written to not force refreshes of state that didn't actually change.
    updateToggleStates()
    updateSelectedPooferDependentState()
    updateAllPoofersDisplayState()
    updateHydraulicsDisplayState()
    updateHydraulicsAttractFilePicker()
    updatePooferSequenceFilePicker()
    updateSelectedFileDependentState()
    updateHydraulicsRecordButtonState()
    updateColorInversion()
  }

  // Data state setting functions
  var setSelectedPoofer = function(name) {
    selectedPoofer = allPoofersState[name]
    updateAllUIState()
  }
  var updatePooferData = function (data) {
    toggleStates['poofer-main'] = data.globalState
    data.poofers.forEach(function(poofer) {
      var pooferState = allPoofersState[poofer.id.toLowerCase()]
      pooferState.enabled = poofer.enabled
    })
    pooferSequenceFiles = data.patterns.map(function(p) { return p.name })
  }
  var updateHydraulicsData = function (data) {
    toggleStates['hydraulics-main'] = data.currentState === 'nomove' ? false : true
    toggleStates['hydraulics-attract'] = data.autoAttractEnabled
    hydraulicsAttractModeActive = data.currentState === 'attract' ? true : false
    hydraulicsState = data
    hydraulicsAttractFiles = data.playbacks
  }

  // Setup poofer data structures
  var allPoofers = ['nn','nw','nt','ne','wn','tn','en','ww','wt','tw','tt','te','et','ee','ws','ts','es','sw','st','se','ss','bn','bs','be','bw']
  var PooferState = function(name) {
    this.name = name
    this.enabled = true
    return this
  }
  var allPoofersState = {}
  for (var i = 0; i < allPoofers.length; i++) {
    var name = allPoofers[i]
    allPoofersState[name] = new PooferState(name)
    // Setup poofer event handlers
    var $currentPoofer = findPoofer(name)
    ;(function () {
      var tempName = name
      $currentPoofer.on('click', function () {
        setSelectedPoofer(tempName)
      })
    })()
  }

  // Setup button event handlers
  $('.invert-button').on('click', function () {
    inverted = !inverted
    updateAllUIState()
  })
  $('.poofer-individual-toggle-button').on('click', function () {
    if (selectedPoofer) {
      selectedPoofer.enabled = !selectedPoofer.enabled
      updatePooferDisplayState(selectedPoofer)
      updatePooferToggleButtonState()
      var name = selectedPoofer.name.toUpperCase()
      $.post(flameUrl(`/poofers/${name}`), { enabled: selectedPoofer.enabled })
    }
  })
  Object.keys(prefixes).forEach(function(prefixKey) {
    var prefix = prefixes[prefixKey]
    $(`.${prefix}-file-picker`).on('click', function(event) {
      var $selectedItem = $(event.target)
      if ($selectedItem.length === 0 || !$selectedItem.hasClass(`${prefix}-file`)) {
        return
      }
      selectedFilenames[prefix] = $selectedItem.data('name')
      if (!$selectedItem.hasClass('selected')) {
        $(`.${prefix}-file-picker li`).removeClass('selected')
        $selectedItem.addClass('selected')
      }
      postAttractFile(prefix)
      updateAllUIState()
    })
  })
  $('.hydraulics-attract-play-stop-button').on('click', function() {
    hydraulicsAttractModeActive = !hydraulicsAttractModeActive
    postHydraulicsAttractPlayStop()
    updateAllUIState()
  })
  $('.poofer-sequence-play-stop-button').on('click', function() {
    pooferSequenceModeActive = !pooferSequenceModeActive
    postPooferSequenceMode()
    updateAllUIState()
  })
  $('.hydraulics-record-toggle-button').on('click', function() {
    hydraulicsRecordActive = !hydraulicsRecordActive
    postHydraulicsRecordMode()
    updateAllUIState()
  })
  Object.keys(toggleStates).forEach(function(p) {
    var prefix = p
    $(`.${prefix}-toggle-button`).on('click', function() {
      toggleStates[prefix] = !toggleStates[prefix]
      postToggleState(prefix)
      updateAllUIState()
    })
  })
  $('.hydraulics-attract-rename-button').on('click', function () {
    postHydraulicsAttractRename()
  })
  $('.hydraulics-attract-trash-button').on('click', function () {
    deleteHydraulicsAttractFile()
  })

  // Ajax functions
  var postHydraulicsAttractMode = function() {
    if (hydraulicsAttractModeActive && selectedHydraulicsFilename()) {
      $.post(hydraulicsUrl(), {
        state: 'attract',
        currentPlayback: selectedHydraulicsFilename()
      })
    } else {
      // just in case, make state consistent
      hydraulicsAttractModeActive = false
      $.post(hydraulicsUrl(), {
        state: 'passthrough'
      })
    }
  }
  var postPooferSequenceMode = function() {
    var name = selectedPooferFilename()
    if (name) {
      $.post(flameUrl(`/patterns/${name}`), {
        active: pooferSequenceModeActive
      })
    } else {
      // just in case, make state consistent
      pooferSequenceModeActive = false
    }
  }
  postHydraulicsRecordMode = function() {
    $.post(hydraulicsUrl('/playbacks'), {
      record: '' + hydraulicsRecordActive
    })
  }
  var postHydraulicsAttractRename = function() {
    var oldName = selectedHydraulicsFilename()
    var newName = $('.hydraulics-attract-name-field').val()
    if (!oldName || !newName) {
      return
    }
    $.post(hydraulicsUrl(`/playbacks/${oldName}`), {
      newName: newName
    })
  }
  var deleteHydraulicsAttractFile = function() {
    var filename = selectedHydraulicsFilename()
    if (!filename) {
      return
    }
    $.ajax(hydraulicsUrl(`/playbacks/${filename}`), {
      method: 'DELETE'
    })
  }
  var postAttractFile = function(prefix) {
    var filename = selectedFilenames[prefix]
    if (filename) {
      if (prefix === prefixes.hydraulics) {
        $.post(hydraulicsUrl(), {
          currentPlayback: filename
        })
      } else if (prefix === prefixes.poofer) {
        // Do nothing - poofer filenames work differently
      }
    }
  }
  var postToggleState = function(prefix) {
    if (prefix === 'poofer-main') {
      $.post(flameUrl(), {
        playState: toggleStates[prefix] ? 'play' : 'pause'
      })
    } else if (prefix === 'hydraulics-main') {
      $.post(hydraulicsUrl(), {
        state: toggleStates[prefix] ? 'passthrough' : 'nomove'
      })
    } else if (prefix === 'hydraulics-attract') {
      if (toggleStates['hydraulics-main']) {
        $.post(hydraulicsUrl(), {
          autoAttractEnabled: toggleStates[prefix]
        })
      }
    }
  }
  var postHydraulicsAttractPlayStop = function() {
    $.post(hydraulicsUrl(), {
      state: hydraulicsAttractModeActive ? 'attract' : 'passthrough'
    })
  }

  // Ajax callbacks
  var flameDataCallback = function(data) {
    updatePooferData(data)
    updateAllUIState()
  }
  var hydraulicsDataCallback = function(data) {
    updateHydraulicsData(data)
    updateAllUIState()
  }

  // Setup polling
  var pollingFunction = function() {
    $.getJSON(flameUrl()).done(flameDataCallback)
    $.getJSON(hydraulicsUrl()).done(hydraulicsDataCallback)
  }
  setInterval(pollingFunction, pollingInterval)

  // Make sure initial UI state is correct
  updateAllUIState()
})
<|endoftext|>


<commit_before>const request       = require('request'),
      mustache      = require('mustache'),
      AWS           = require('aws-sdk'),
      Promise       = require('promise'),
      SamsungRemote = require('samsung-remote');

process.on('uncaughtException', function (err) {
  console.log(err);
})

AWS.config.update({
  region:           process.env.AWS_DEFAULT_REGION,
  accessKeyId:      process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey:  process.env.AWS_SECRET_KEY
});

const recieverIP  = process.env.RECEIVER_IP,
      recieverAPI = "YamahaRemoteControl/ctrl",
      sqsQueueUrl = process.env.AWS_SQS_URL,
      sqs         = new AWS.SQS(),
      remote      = new SamsungRemote({ ip: process.env.TV_IP });

const commands = {
  power:        '<YAMAHA_AV cmd="PUT"><Main_Zone><Power_Control><Power>{{value}}</Power></Power_Control></Main_Zone></YAMAHA_AV>',
  volume:       '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Lvl><Val>{{value}} {{value2}} dB</Val><Exp></Exp><Unit></Unit></Lvl></Volume></Main_Zone></YAMAHA_AV>', // <Val>Up 1 dB</Val>
  volumeLevel:  '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Lvl><Val>{{value}}</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume></Main_Zone></YAMAHA_AV>',
  mute:         '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Mute>{{value}}</Mute></Volume></Main_Zone></YAMAHA_AV>',
  input:        '<YAMAHA_AV cmd="PUT"><Main_Zone><Input><Input_Sel>{{value}}</Input_Sel></Input></Main_Zone></YAMAHA_AV>',
  mode:         '<YAMAHA_AV cmd="PUT"><Main_Zone><Surround><Program_Sel><Current><Sound_Program>{{value}}</Sound_Program></Current></Program_Sel></Surround></Main_Zone></YAMAHA_AV>'
};

var num     = 0,
    command;

sqsRequest();

// Poll SQS queue
function sqsRequest() {
  return new Promise(function (resolve, reject) {
    var params = {
      QueueUrl: sqsQueueUrl,
      MaxNumberOfMessages:  1, // how many messages do we wanna retrieve?
      VisibilityTimeout:    60, // seconds - how long we want a lock on this job
      WaitTimeSeconds:      20 // seconds - how long should we wait for a message?
    };
    sqs.receiveMessage(params, function(err, data) {
      if (err) {
        console.log(err);
        reject(err);
      } else if (data.Messages) {
        var message = data.Messages[0];
        sendReciever(message);
        removeFromQueue(message);
        resolve(data);
      } else {
        resolve(data);
      }
      console.log(num += 1);
    });
  }).then(sqsRequest);
}

// Remove the message from queue
function removeFromQueue (message) {
  return new Promise(function (resolve, reject) {
    var params = {
      QueueUrl:       sqsQueueUrl,
      ReceiptHandle:  message.ReceiptHandle
    };
    sqs.deleteMessage(params, function(err, data) {
      if (err) reject(err);
      else     resolve(data);
    });
  });
}

// Send data to the receiver
function sendReciever(message) {
  return new Promise(function (resolve, reject) {
    command = JSON.parse(message.Body);
    console.log(command);

    if (command.action === "power") {
      if (tvUp()) {
        tvOff();
      }
    }

    request.post(
      {url:     recieverIP+recieverAPI,
      body :    mustache.render(commands[command.action], command),
      headers:  {'Content-Type': 'text/xml'}
      },
      function (err, response, data) {
        if (err) reject(err);
        else     resolve(data);
      }
    );
  });
}

function tvUp() {
  return new Promise(function (resolve, reject) {
    remote.isAlive(function(err) {
      if (err) reject(false);
      else     resolve(true);
    });
  });
}

function tvOff() {
  return new Promise(function (resolve, reject) {
    remote.send('KEY_POWEROFF', function callback(err) {
      if (err) reject(err);
      else     resolve();
    });
  });
}

function tvOn() {
  return new Promise(function (resolve, reject) {
    remote.send('KEY_POWERON', function callback(err) {
      if (err) reject(err);
      else     resolve();
    });
  });
}
// ** build in the npm lookup function for the receiver ip<commit_msg>updates to samsung tv<commit_after>const request       = require('request'),
      mustache      = require('mustache'),
      AWS           = require('aws-sdk'),
      Promise       = require('promise'),
      SamsungRemote = require('samsung-remote');

process.on('uncaughtException', function (err) {
  console.log(err);
});

AWS.config.update({
  region:           process.env.AWS_DEFAULT_REGION,
  accessKeyId:      process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey:  process.env.AWS_SECRET_KEY
});

const recieverIP  = process.env.RECEIVER_IP,
      recieverAPI = "YamahaRemoteControl/ctrl",
      sqsQueueUrl = process.env.AWS_SQS_URL,
      sqs         = new AWS.SQS(),
      remote      = new SamsungRemote({ ip: process.env.TV_IP });

const commands = {
  power:        '<YAMAHA_AV cmd="PUT"><Main_Zone><Power_Control><Power>{{value}}</Power></Power_Control></Main_Zone></YAMAHA_AV>',
  volume:       '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Lvl><Val>{{value}} {{value2}} dB</Val><Exp></Exp><Unit></Unit></Lvl></Volume></Main_Zone></YAMAHA_AV>', // <Val>Up 1 dB</Val>
  volumeLevel:  '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Lvl><Val>{{value}}</Val><Exp>1</Exp><Unit>dB</Unit></Lvl></Volume></Main_Zone></YAMAHA_AV>',
  mute:         '<YAMAHA_AV cmd="PUT"><Main_Zone><Volume><Mute>{{value}}</Mute></Volume></Main_Zone></YAMAHA_AV>',
  input:        '<YAMAHA_AV cmd="PUT"><Main_Zone><Input><Input_Sel>{{value}}</Input_Sel></Input></Main_Zone></YAMAHA_AV>',
  mode:         '<YAMAHA_AV cmd="PUT"><Main_Zone><Surround><Program_Sel><Current><Sound_Program>{{value}}</Sound_Program></Current></Program_Sel></Surround></Main_Zone></YAMAHA_AV>'
};

var num     = 0,
    command;

sqsRequest();

// Poll SQS queue
function sqsRequest() {
  return new Promise(function (resolve, reject) {
    var params = {
      QueueUrl: sqsQueueUrl,
      MaxNumberOfMessages:  1, // how many messages do we wanna retrieve?
      VisibilityTimeout:    60, // seconds - how long we want a lock on this job
      WaitTimeSeconds:      20 // seconds - how long should we wait for a message?
    };
    sqs.receiveMessage(params, function(err, data) {
      if (err) {
        console.log(err);
        reject(err);
      } else if (data.Messages) {
        var message = data.Messages[0];
        sendReciever(message);
        removeFromQueue(message);
        resolve(data);
      } else {
        resolve(data);
      }
      console.log(num += 1);
    });
  }).then(sqsRequest);
}

// Remove the message from queue
function removeFromQueue (message) {
  return new Promise(function (resolve, reject) {
    var params = {
      QueueUrl:       sqsQueueUrl,
      ReceiptHandle:  message.ReceiptHandle
    };
    sqs.deleteMessage(params, function(err, data) {
      if (err) reject(err);
      else     resolve(data);
    });
  });
}

// Send data to the receiver
function sendReciever(message) {
  return new Promise(function (resolve, reject) {
    command = JSON.parse(message.Body);
    console.log(command);

    if (command.action === "power") {
      if (command.value === "Standby") {
        tvOff();
      }
    }

    request.post(
      {url:     recieverIP+recieverAPI,
      body :    mustache.render(commands[command.action], command),
      headers:  {'Content-Type': 'text/xml'}
      },
      function (err, response, data) {
        if (err) reject(err);
        else     resolve(data);
      }
    );
  });
}

function tvUp() {
  return new Promise(function (resolve, reject) {
    remote.isAlive(function(err) {
      if (err) reject(false);
      else     resolve(true);
    });
  });
}

function tvOff() {
  return new Promise(function (resolve, reject) {
    remote.send('KEY_POWEROFF', function callback(err) {
      if (err) reject(err);
      else     resolve();
    });
  });
}

function tvOn() {
  return new Promise(function (resolve, reject) {
    remote.send('KEY_POWERON', function callback(err) {
      if (err) reject(err);
      else     resolve();
    });
  });
}
// ** build in the npm lookup function for the receiver ip<|endoftext|>


<commit_before>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
                http://www.apache.org/licenses/LICENSE-2.0
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
     limitations under the License.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

"use strict";

var Bridge = require('./PhantomBridge');

function WidgetSnapshotProvider (id, workerPool, onKill) {
    this.id = id;
    this.creationDate = new Date();
    this.isReady = false;
    this.bridge = null;
    this.currentJob = null;
    this.currentJobs = 0;
    this.maxJobs = 0;
    this.jobs = {};
    this.timeoutId = null;
    this.workerPool = workerPool;
    this.onKill = onKill;

    this.workerPool.add(this);
}

WidgetSnapshotProvider.prototype = {
    constructor: WidgetSnapshotProvider

    /**
     * Initialize the web capturer.
     * @param onReady
     */
    , init: function(requireJsWidgetList, api_url, onReady) {

        if(typeof onReady !== 'function') {
            throw new Error(this.msg('A callback function must be specified in "init" method.'));
        }

        this.bridge = new Bridge(this.id);
        this.bridge.initFramework(requireJsWidgetList, api_url, phantomWebMessageHandler.bind(this), onReady);

        this.isReady = true;

    }

    , destroy: function() {
        if(this.isReady) {
            this.isReady = false;
            this.bridge.close();
            this.bridge = null;
        }
    }

    , getCreationDate: function() {
        return this.creationDate;
    }

    , isReady: function() {
        return this.isReady;
    }

    , getChartImage: function(job) {

        if(typeof job.data.onImageReady !== 'function')
            throw new Error(job.worker.msg('A onImageReady function must be specified in "getChartImage" method.'));

        if(job.worker.currentJob != null) {
            throw new Error(job.worker.msg('The WidgetSnapshotProvider instance is busy!'));
        }

        if(!job.worker.isReady) {
            throw new Error(job.worker.msg('The WidgetSnapshotProvider instance is not initialized!'));
        }

        job.worker.currentJob = job;

        //Set the viewport
        job.worker.bridge.getPage().viewportSize = job.data.viewport;

        //Set the clipRect to be sure that the image generated is of the size we want
        job.worker.bridge.getPage().clipRect = {
            top: 0,
            left: 0,
            width: job.data.viewport.width,
            height: job.data.viewport.height
        };

        //Load the chart in the web executor
        //The execution of the job will continue when the DATA_RECEIVED event is received
        var success = job.worker.bridge.getPage().evaluate(chartCreateWebFunction, job.data.chart, job.data.metrics, job.data.config); //TODO: maybe asynchronous?

        if(!success) {
            processErrorEvent("Some king of error happened.");
        }

    }

    , msg: function(txt) {
        return 'WidgetSnapshotProvider('+this.id+') ' + txt;
    },

    kill: function () {
        if(this.isReady) {
            this.destroy();
            this.currentJob.abort(408, "Max execution time reached");
            this.currentJobs = 0;
            this.onKill(this);
        }
    },

    setJobFinished: function(job) {
        if(this.isReady && this.currentJobs > 0) {

            this.currentJobs--;
            this.currentJob = null;

            this.workerPool.setIdle(this);

            var jobInfo = this.jobs[job.id];
            delete this.jobs[job.id];

            clearTimeout(jobInfo.timeout);
        }
    },

    startJob: function(job) {
        if(this.isReady && this.currentJobs < this.maxJobs) {

            // Add the job to the list of jobs
            this.jobs[job.id] = {
                job: job,
                timeout: setTimeout(this.kill.bind(this), job.maxExecTime)
            };

            this.currentJobs++;

            // Run the job
            job.start(this);

        } else {
            throw new Error("This worker has reached the maximum of jobs");
        }
    },

    isCompletellyBusy: function() {
        return this.currentJobs >= this.maxJobs;
    },

    decreaseMaxJobs: function(amount) {
        if(amount > 0) {
            this.maxJobs -= amount;
            if (this.maxJobs < 0) {
                this.maxJobs = 0;
            }
            this.workerPool.refresh(this);
        }
    },

    increaseMaxJobs: function(amount) {
        if(amount > 0) {
            this.maxJobs += amount;
            this.workerPool.refresh(this);
        }

    }

};

var processChartReadyEvent = function(causedByTimeout) {

    if(this.currentJob == null) {
        console.warn(this.msg("A job in the WidgetSnapshotProvider was lost!!!"));
        return;
    }

    if(this.timeoutId != null) {
        clearTimeout(this.timeoutId);
        this.timeoutId = null;
    }

    // If no CHART_READY event was received, the make sure to flush the animation frames before taking the snapshot
    if(causedByTimeout) {
        this.bridge.getPage().evaluate(function() {
            flushAnimationFrames();
        });
    }

    var fileName = '/tmp/' + Math.round(Math.random() * 1000000000) + ".png"; //TODO: compatibility with Windows?
    var success = this.bridge.getPage().render(fileName);

    //Clear the chart
    this.bridge.getPage().evaluate(chartDeleteWebFunction);

    // Execute the callback
    this.currentJob.data.onImageReady(this.currentJob, success ? fileName : undefined);

    // Clear the current job information
    this.currentJob.setFinished();

};

var processDataReceivedEvent = function() {
    if(this.currentJob != null) {
        this.timeoutId = setTimeout(processChartReadyEvent.bind(this, true), 200);
    }
};

var processErrorEvent = function(msg) {

    console.error(msg);

    //Clear the chart
    this.bridge.getPage().evaluate(chartDeleteWebFunction);

    this.currentJob.abort(500, msg);

};

/**
 * This method handles all the messages sent from the webExecutor to Phantom
 * @param data
 */
var phantomWebMessageHandler = function(data) {

    switch (data.type) {
        case 'DATA_RECEIVED': processDataReceivedEvent.call(this); break;
        case 'CHART_READY': processChartReadyEvent.call(this); break;
        case 'ERROR': processErrorEvent.call(this, data.data); break;
        default:
            console.warn(this.msg("Received unknown type in phantomWebMessageHandler!"));
            break;
    }
};

var chartDeleteWebFunction = function() {

    window.resetOptimizations();

    //Clear previous chart
    if(window.chart != null) {
        Bridge.stopEventTransmission(window.chart, "DATA_RECEIVED");
        window.chart.delete();
    }

    $("body").empty();

};

/**
 * This method is evaluated in the WebExecutor and creates the requested chart in the page
 * @param chartType Name of the class of the chart (the name with which the chart is registered in the sdh-framework).
 * @param metrics Metrics array to pass to the chart constructor.
 * @param config Configuration to pass to the chart constructor.
 * @returns {boolean}
 */
var chartCreateWebFunction = function(chartType, metrics, config) {

    //TODO: if no chart ready is generated, flushAnimationFrames

    for(var param in config) {
        var val = config[param];
        if(typeof val === 'string' && isAFunctionString(val)) {
            config[param] = createSandboxedFuntion(val)();
        }
    }

    for(var i = 0; i < metrics.length; i++) {
        var metric = metrics[i];
        if(metric instanceof Object) {
            for(var param in metric) {
                var val = metric[param];
                if(typeof val === 'string' && isAFunctionString(val)) {
                    metric[param] = createSandboxedFuntion(val)();
                }
            }
        }
    }

    $("body").append("

");

    var domElement = document.getElementById("chart");

    try {
        var constructor = framework.widgets[chartType];
        if(constructor != null) {

            //Handle errors
            constructor.prototype.onError = function(msg) {
                Bridge.sendToPhantom("ERROR", msg);
            };

            window.chart = new constructor(domElement, metrics, [], config);
            Bridge.transmitEventAndExecute(window.chart, "DATA_RECEIVED", false);

            return true;
        }

    } catch(e) {
        console.error(e);
    }

    return false;

};

module.exports = WidgetSnapshotProvider;<commit_msg>Yield before taking the snapshot to make sure that rendering has been done.<commit_after>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
                http://www.apache.org/licenses/LICENSE-2.0
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
     limitations under the License.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

"use strict";

var Bridge = require('./PhantomBridge');

function WidgetSnapshotProvider (id, workerPool, onKill) {
    this.id = id;
    this.creationDate = new Date();
    this.isReady = false;
    this.bridge = null;
    this.currentJob = null;
    this.currentJobs = 0;
    this.maxJobs = 0;
    this.jobs = {};
    this.timeoutId = null;
    this.workerPool = workerPool;
    this.onKill = onKill;

    this.workerPool.add(this);
}

WidgetSnapshotProvider.prototype = {
    constructor: WidgetSnapshotProvider

    /**
     * Initialize the web capturer.
     * @param onReady
     */
    , init: function(requireJsWidgetList, api_url, onReady) {

        if(typeof onReady !== 'function') {
            throw new Error(this.msg('A callback function must be specified in "init" method.'));
        }

        this.bridge = new Bridge(this.id);
        this.bridge.initFramework(requireJsWidgetList, api_url, phantomWebMessageHandler.bind(this), onReady);

        this.isReady = true;

    }

    , destroy: function() {
        if(this.isReady) {
            this.isReady = false;
            this.bridge.close();
            this.bridge = null;
        }
    }

    , getCreationDate: function() {
        return this.creationDate;
    }

    , isReady: function() {
        return this.isReady;
    }

    , getChartImage: function(job) {

        if(typeof job.data.onImageReady !== 'function')
            throw new Error(job.worker.msg('A onImageReady function must be specified in "getChartImage" method.'));

        if(job.worker.currentJob != null) {
            throw new Error(job.worker.msg('The WidgetSnapshotProvider instance is busy!'));
        }

        if(!job.worker.isReady) {
            throw new Error(job.worker.msg('The WidgetSnapshotProvider instance is not initialized!'));
        }

        job.worker.currentJob = job;

        //Set the viewport
        job.worker.bridge.getPage().viewportSize = job.data.viewport;

        //Set the clipRect to be sure that the image generated is of the size we want
        job.worker.bridge.getPage().clipRect = {
            top: 0,
            left: 0,
            width: job.data.viewport.width,
            height: job.data.viewport.height
        };

        //Load the chart in the web executor
        //The execution of the job will continue when the DATA_RECEIVED event is received
        var success = job.worker.bridge.getPage().evaluate(chartCreateWebFunction, job.data.chart, job.data.metrics, job.data.config); //TODO: maybe asynchronous?

        if(!success) {
            processErrorEvent("Some king of error happened.");
        }

    }

    , msg: function(txt) {
        return 'WidgetSnapshotProvider('+this.id+') ' + txt;
    },

    kill: function () {
        if(this.isReady) {
            this.destroy();
            this.currentJob.abort(408, "Max execution time reached");
            this.currentJobs = 0;
            this.onKill(this);
        }
    },

    setJobFinished: function(job) {
        if(this.isReady && this.currentJobs > 0) {

            this.currentJobs--;
            this.currentJob = null;

            this.workerPool.setIdle(this);

            var jobInfo = this.jobs[job.id];
            delete this.jobs[job.id];

            clearTimeout(jobInfo.timeout);
        }
    },

    startJob: function(job) {
        if(this.isReady && this.currentJobs < this.maxJobs) {

            // Add the job to the list of jobs
            this.jobs[job.id] = {
                job: job,
                timeout: setTimeout(this.kill.bind(this), job.maxExecTime)
            };

            this.currentJobs++;

            // Run the job
            job.start(this);

        } else {
            throw new Error("This worker has reached the maximum of jobs");
        }
    },

    isCompletellyBusy: function() {
        return this.currentJobs >= this.maxJobs;
    },

    decreaseMaxJobs: function(amount) {
        if(amount > 0) {
            this.maxJobs -= amount;
            if (this.maxJobs < 0) {
                this.maxJobs = 0;
            }
            this.workerPool.refresh(this);
        }
    },

    increaseMaxJobs: function(amount) {
        if(amount > 0) {
            this.maxJobs += amount;
            this.workerPool.refresh(this);
        }

    }

};

var processChartReadyEvent = function(causedByTimeout) {

    if(this.currentJob == null) {
        console.warn(this.msg("A job in the WidgetSnapshotProvider was lost!!!"));
        return;
    }

    if(this.timeoutId != null) {
        clearTimeout(this.timeoutId);
        this.timeoutId = null;
    }

    // If no CHART_READY event was received, the make sure to flush the animation frames before taking the snapshot
    if(causedByTimeout) {
        this.bridge.getPage().evaluate(function() {
            flushAnimationFrames();
        });
    }

    // Yield to make sure that rendering has been done
    setTimeout(function() {
        var fileName = '/tmp/' + Math.round(Math.random() * 1000000000) + ".png"; //TODO: compatibility with Windows?
        var success = this.bridge.getPage().render(fileName);

        //Clear the chart
        this.bridge.getPage().evaluate(chartDeleteWebFunction);

        // Execute the callback
        this.currentJob.data.onImageReady(this.currentJob, success ? fileName : undefined);

        // Clear the current job information
        this.currentJob.setFinished();
    }.bind(this), 0);

};

var processDataReceivedEvent = function() {
    if(this.currentJob != null) {
        this.timeoutId = setTimeout(processChartReadyEvent.bind(this, true), 200);
    }
};

var processErrorEvent = function(msg) {

    console.error(msg);

    //Clear the chart
    this.bridge.getPage().evaluate(chartDeleteWebFunction);

    this.currentJob.abort(500, msg);

};

/**
 * This method handles all the messages sent from the webExecutor to Phantom
 * @param data
 */
var phantomWebMessageHandler = function(data) {

    switch (data.type) {
        case 'DATA_RECEIVED': processDataReceivedEvent.call(this); break;
        case 'CHART_READY': processChartReadyEvent.call(this); break;
        case 'ERROR': processErrorEvent.call(this, data.data); break;
        default:
            console.warn(this.msg("Received unknown type in phantomWebMessageHandler!"));
            break;
    }
};

var chartDeleteWebFunction = function() {

    window.resetOptimizations();

    //Clear previous chart
    if(window.chart != null) {
        Bridge.stopEventTransmission(window.chart, "DATA_RECEIVED");
        window.chart.delete();
    }

    $("body").empty();

};

/**
 * This method is evaluated in the WebExecutor and creates the requested chart in the page
 * @param chartType Name of the class of the chart (the name with which the chart is registered in the sdh-framework).
 * @param metrics Metrics array to pass to the chart constructor.
 * @param config Configuration to pass to the chart constructor.
 * @returns {boolean}
 */
var chartCreateWebFunction = function(chartType, metrics, config) {

    //TODO: if no chart ready is generated, flushAnimationFrames

    for(var param in config) {
        var val = config[param];
        if(typeof val === 'string' && isAFunctionString(val)) {
            config[param] = createSandboxedFuntion(val)();
        }
    }

    for(var i = 0; i < metrics.length; i++) {
        var metric = metrics[i];
        if(metric instanceof Object) {
            for(var param in metric) {
                var val = metric[param];
                if(typeof val === 'string' && isAFunctionString(val)) {
                    metric[param] = createSandboxedFuntion(val)();
                }
            }
        }
    }

    $("body").append("

");

    var domElement = document.getElementById("chart");

    try {
        var constructor = framework.widgets[chartType];
        if(constructor != null) {

            //Handle errors
            constructor.prototype.onError = function(msg) {
                Bridge.sendToPhantom("ERROR", msg);
            };

            window.chart = new constructor(domElement, metrics, [], config);
            Bridge.transmitEventAndExecute(window.chart, "DATA_RECEIVED", false);

            return true;
        }

    } catch(e) {
        console.error(e);
    }

    return false;

};

module.exports = WidgetSnapshotProvider;<|endoftext|>


<commit_before>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
                http://www.apache.org/licenses/LICENSE-2.0
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

/**
 * The code for the optimizations that has to be executed after the framework has been loaded
 */
(function() {

    // For nvd3: overwrite the addGraph method to make sure that the CHART_READY event is triggered
    if(nv != null) {
        window._addGraph = window._addGraph || nv.addGraph;
        nv.addGraph = function(generator, callback) {
            window._addGraph(generator, function(c) {
                flushAnimationFrames();
                Bridge.sendToPhantom("CHART_READY", null);
                if (typeof callback === 'function') callback(c);
            })
        };
    }

    // Deactivate animations and wait for the animation to be completed
    if(Chart != null) {
        var count = 2; //We need to wait for the second one (the first one is the creaion of the chart)
        Chart.defaults.global.animation = false;
        Chart.defaults.global.onAnimationComplete = function() {
            if(--count === 0) {
                Bridge.sendToPhantom("CHART_READY", null);
            }
        }
    }

    window.resetOptimizations = function resetOptimizations() {
        count = 2; //Reset counter for the ChartJs optimization
    }

})();
<commit_msg>Optimization: do not use the loading transition of widget common.<commit_after>/*
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      This file is part of the Smart Developer Hub Project:
        http://www.smartdeveloperhub.org/
      Center for Open Middleware
            http://www.centeropenmiddleware.com/
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Copyright (C) 2015 Center for Open Middleware.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at
                http://www.apache.org/licenses/LICENSE-2.0
      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.
    #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
*/

/**
 * The code for the optimizations that has to be executed after the framework has been loaded
 */
(function() {

    // For nvd3: overwrite the addGraph method to make sure that the CHART_READY event is triggered
    if(nv != null) {
        window._addGraph = window._addGraph || nv.addGraph;
        nv.addGraph = function(generator, callback) {
            window._addGraph(generator, function(c) {
                flushAnimationFrames();
                Bridge.sendToPhantom("CHART_READY", null);
                if (typeof callback === 'function') callback(c);
            })
        };
    }

    // Deactivate animations and wait for the animation to be completed
    if(Chart != null) {
        var count = 2; //We need to wait for the second one (the first one is the creaion of the chart)
        Chart.defaults.global.animation = false;
        Chart.defaults.global.onAnimationComplete = function() {
            if(--count === 0) {
                Bridge.sendToPhantom("CHART_READY", null);
            }
        }
    }

    // Deactivate common widget loading transition
    framework.widgets.CommonWidget.prototype.startLoading = function() { };
    framework.widgets.CommonWidget.prototype.endLoading = function(cb) {
        cb();
    };

    window.resetOptimizations = function resetOptimizations() {
        count = 2; //Reset counter for the ChartJs optimization
    }

})();
<|endoftext|>


<commit_before>/* ex: set tabstop=2 softtabstop=2 shiftwidth=2 : */
/*global newArray:true, hasProperty:true */

var ftcCookie = (function() {
  function getCookiesName() {
    return document.cookie.match( /\w+(?==)/g );
  }

  function deleteCookieByName(arr) {
    var cookieNames = newArray( arr ),
    cookieName, i,
    _epoch = new Date( 0 ).toGMTString();

    for ( i = 0; cookieName = cookieNames[i]; i++ ) {
      document.cookie = cookieName + "=;expires=" + _epoch;
    }
  }

  return {
    // {json}, 30 days, host, path
    set: function(obj, _days, _host, _path) {
      _days = _days || 30;
      _host = _host || location.host;
      _path = "/";

      var c = new Date(), prop;
      // 24*3600000 = 86400,000
      c.setTime( c.getTime() + ( _days *86400000 ) );

      var _expiry_time = "; expires=" + c.toGMTString();

      for( prop in obj ) {
        if ( hasProperty( prop, obj ) ) {
          document.cookie = prop + "=" + obj[prop] +_expiry_time +
            "; domain=" + _host +
            "; path=" + _path;
        }
      }
    },
    get: function(prop) {
      var prop_equals = prop + "=",
          item = document.cookie.split( ";" ),
          i, g;
      for ( i = 0; g = item[i]; i++ ) {
        while ( g.charAt(0) === " " ) {
          g = g.substring( 1, g.length );
        }
        if ( g.indexOf( prop_equals ) === 0 ) {
          return g.substring( prop_equals.length, g.length ).replace( /\"/g, "" );
        }
      }
      return null;
    },
    remove: deleteCookieByName,
    clear: function() {
      deleteCookieByName( getCookiesName() );
    }
  };
}());

<commit_msg>removed hasProperty<commit_after>/* ex: set tabstop=2 softtabstop=2 shiftwidth=2 : */
/*global newArray:true */

var ftcCookie = (function() {
  function getCookiesName() {
    return document.cookie.match( /\w+(?==)/g );
  }

  function deleteCookieByName(arr) {
    var cookieNames = newArray( arr ),
    cookieName, i,
    _epoch = new Date( 0 ).toGMTString();

    for ( i = 0; cookieName = cookieNames[i]; i++ ) {
      document.cookie = cookieName + "=;expires=" + _epoch;
    }
  }

  return {
    // {json}, 30 days, host, path
    set: function(obj, _days, _host, _path) {
      _days = _days || 30;
      _host = _host || location.host;
      _path = "/";

      var c = new Date(), prop;
      // 24*3600000 = 86400,000
      c.setTime( c.getTime() + ( _days *86400000 ) );

      var _expiry_time = "; expires=" + c.toGMTString();

      for( prop in obj ) {
        if ( obj.hasOwnProperty( prop ) ) {
          document.cookie = prop + "=" + obj[prop] +_expiry_time +
            "; domain=" + _host +
            "; path=" + _path;
        }
      }
    },
    get: function(prop) {
      var prop_equals = prop + "=",
          item = document.cookie.split( ";" ),
          i, g;
      for ( i = 0; g = item[i]; i++ ) {
        while ( g.charAt(0) === " " ) {
          g = g.substring( 1, g.length );
        }
        if ( g.indexOf( prop_equals ) === 0 ) {
          return g.substring( prop_equals.length, g.length ).replace( /\"/g, "" );
        }
      }
      return null;
    },
    remove: deleteCookieByName,
    clear: function() {
      deleteCookieByName( getCookiesName() );
    }
  };
}());

<|endoftext|>


<commit_before>// async cookies API polyfill for document contexts
if (self.document) (function() {
  'use strict';
  const DISALLOWED_IN_COOKIE_NAME_RE = /[()<>@,;:\\""\/\[\]?={} \0-\x1f\x7f]|[^\x21-\x7e\u00A0-\u{10FFFF}/u;
  const DISALLOWED_IN_COOKIE_VALUE_RE = /[^\x2D-\x3A\x21\x23-\x2B\x3C-\x5B\x5D-\x7E\u00A0-\u{10FFFF}/u;
  // based on https://tools.ietf.org/html/rfc3986#appendix-B
  const uriRegExp = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/;
  const uriSchemeGroup = 2;
  const uriAuthorityGroup = 4;
  const uriPathGroup = 5;
  const defUriGroups = String(location.href).match(uriRegExp) || [];
  const dirName = path => path.replace(/(^|\/)\.\.$/, '$1../').replace(/[^\/]+$/, '');
  const defReadPath = dirName(defUriGroups[uriPathGroup] || '/');
  const defOrigin = defUriGroups[uriSchemeGroup] + '://' + defUriGroups[uriAuthorityGroup];
  const defSecure = defUriGroups[uriSchemeGroup] === 'https';
  const resolvePath = path => {
    path = String(path || defReadPath);
    if (path[0] !== '/') path = defReadPath + path;
    let pathSegments = path.split('/');
    let resolvedPathSegments = [];
    pathSegments.forEach(segment => {
      if (segment === '..') resolvedPathSegments.pop();
      else resolvedPathSegments.push(segment);
    });
    return resolvedPathSegments.join('/');
  };
  const readPathForUrl = url => {
    if (url == null) url = defReadPath;
    url = String(url);
    const urlGroups = url.match(uriRegExp) || [];
    const urlPath = resolvePath(dirName(String(urlGroups[uriPathGroup] || defReadPath)));
    const origin = [
      urlGroups[uriSchemeGroup] || defUriGroups[uriSchemeGroup],
      urlGroups[uriAuthorityGroup] || defUriGroups[uriAuthorityGroup]].join('://').toLowerCase();
    if ((origin + urlPath) !== (defOrigin + defReadPath)) {
      // TODO: iframe with unpredictable long pathname inside
      // requested path, maybe by appending as many
      // randomly-selected '$' and ':' as needed to reach 2083 chars
      // in the URL could be used to read same-origin, different-path cookies.
      throw new SyntaxError([
        'Cannot read cookies at requested URL ',
        JSON.stringify(url),
        ' from ',
        JSON.stringify(defOrigin + defReadPath),
        ': not yet implemented.'].join(''));
    }
    return defReadPath;
  };
  class AsyncCookieJar_ {
    constructor(document) {
      this.doc_ = document;
    }
    async set(setCookieString) {
      console.info('Set-Cookie: %s', setCookieString);
      this.doc_.cookie = setCookieString;
    }
    async get() {
      return this.doc_.cookie;
    }
  }
  const SECURE_PREFIX = '__Secure-', HOST_PREFIX = '__Host-';
  class CookieStore {
    constructor(asyncCookieJar) {
      this.asyncCookieJar_ = asyncCookieJar;
    }
    async has(nameOrOptions, moreOptions) {
      let cookieList = await this.getAll(nameOrOptions, moreOptions);
      return cookieList.length >= 1;
    }
    async get(nameOrOptions, moreOptions) {
      let cookieList = await this.getAll(nameOrOptions, moreOptions);
      return cookieList[0];
    }
    async getAll(nameOrOptions, moreOptions) {
      let options = Object.assign(
        {},
        (typeof nameOrOptions === 'object') ? nameOrOptions : {name: nameOrOptions},
        moreOptions);
      let {name, url = defReadPath, matchType} = options || {};
      if (name != null) {
      	name = String(name);
        if (name.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie name');
        if (name.indexOf('=') !== -1) throw new SyntaxError('Character "=" is not allowed in cookie name');
      } else {
        name = '';
        if (matchType == null) matchType = 'startsWith';
      }
      if (matchType == null) matchType = 'equals';
      matchType = String(matchType);
      if (matchType !== 'equals' && matchType !== 'startsWith') {
        throw new SyntaxError('Unrecognized matchType ' + JSON.stringify(matchType));
      }
      url = readPathForUrl(url);
      let cookieList = [];
      let jar = String(await this.asyncCookieJar_.get() || '');
      for (let i = 0, j = jar.length, k = jar.indexOf(';');
           k = (k == -1) ? j : k, i < j;
           i = k + 1 + (jar[k + 1] == ' ' ? 1 : 0), k = jar.indexOf(';', i)) {
       	let nv = jar.substr(i, k - i), c = nv.indexOf('=');
        if (c !== -1) {
          let n = nv.substr(0, c);
          // NOTE: some of the reported names and/or values will not be allowed in write operations.
          if (name === n || matchType === 'startsWith' && n.substr(0, name.length) === name) {
            let v = nv.substr(c + 1);
            cookieList.push({name: n, value: v});
          }
        }
      }
      return cookieList;
    }
    async delete(name, options) {
      await this.set(name, undefined, options);
    }
    async set(name, value, options) {
      name = String(name || '') || undefined;
      if (!name) throw new SyntaxError('Cookie name is required');
      if (name.match(DISALLOWED_IN_COOKIE_NAME_RE) || name !== decodeURIComponent(encodeURIComponent(name))) {
        throw new SyntaxError('Unsupported character in cookie name');
      }
      options = options || {};
      let expires = null;
      let maxAge = null;
      let expiresAsNumber;
      if (options.expires != null && !isNaN(expiresAsNumber = Number(options.expires))) {
        maxAge = (expiresAsNumber - Date.now()) / 1000;
      } else {
        expires = String(options.expires || '') || undefined;
      }
      if (expires && expires.indexOf(';') !== -1) {
        throw new SyntaxError('Character ";" is not allowed in cookie "expires" attribute');
      }
      let domain = String(options.domain || '') || undefined;
      if (domain && domain.indexOf(';') !== -1) {
        throw new SyntaxError('Character ";" is not allowed in cookie "domain" attribute');
      }
      let path = String(options.path || '/');
      if (path[0] !== '/') throw new SyntaxError('The "path" attribute must begin with "/"');
      if (path.match(/(^|\/)\.\.(\/|$)/)) {
        throw new SyntaxError('The cookie "path" attribute must not contain ".." path segments');
      }
      if (path.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie "path" attribute');
      if (path.indexOf('?') !== -1) throw new SyntaxError('Character "?" is not allowed in cookie "path" attribute');
      if (path.indexOf('#') !== -1) throw new SyntaxError('Character "#" is not allowed in cookie "path" attribute');
      // Add implicit trailing '/' if omitted. Does not match IE Set-Cookie behavior.
      path = path.replace(/([^\/]$)/, '$1/');
      let secure = options.secure;
      if (secure == null) secure = defSecure;
      secure = !!secure;
      if (secure && !defSecure) throw new SyntaxError('Secure cookies can only be modified from secure contexts');
      let httpOnly = !!options.httpOnly;
      if (value == null && maxAge == null && expires == null) maxAge = 0;
      value = String(value || '');
      if (value.match(DISALLOWED_IN_COOKIE_VALUE_RE) || value !== decodeURIComponent(encodeURIComponent(value))) {
        // Does not match document.cookie behavior!
        throw new SyntaxError('Unsupported character in cookie value');
      }
      if (name.substr(0, SECURE_PREFIX.length) === SECURE_PREFIX) {
        if (!defSecure) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(SECURE_PREFIX),
            ' prefix can only be modified from secure contexts'].join(''));
        }
        if (!secure) {
          throw new SyntaxError('Cookies with the ' + JSON.stringify(SECURE_PREFIX) + ' prefix must use the Secure flag');
        }
      } else if (name.substr(0, HOST_PREFIX.length) === HOST_PREFIX) {
        if (!defSecure) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix can only be modified from secure contexts'].join(''));
        }
        if (!secure) {
          throw new SyntaxError('Cookies with the ' + JSON.stringify(HOST_PREFIX) + ' prefix must use the secure flag');
        }
        if (path !== '/') {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix must have path ',
            JSON.stringify('/')].join(''));
        }
        if (domain != null) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix cannot have the domain parameter'].join(''));
        }
      }
      let setCookieParts = [name, '=', value];
      if (domain != null) setCookieParts.push('; domain=', domain);
      if (path != null) setCookieParts.push('; path=', path);
      if (expires != null) setCookieParts.push('; expires=', expires);
      if (maxAge != null) setCookieParts.push('; max-age=', maxAge);
      if (secure) setCookieParts.push('; secure');
      if (httpOnly) setCookieParts.push('; httpOnly');
      let setCookie = setCookieParts.join('');
      await this.asyncCookieJar_.set(setCookie);
    }
  };
  const FAST_OBSERVER_INTERVAL = 200, SLOW_OBSERVER_INTERVAL = 5000; // ms
  const VERY_LOW_BATTERY_THRESHOLD = 0.05, LOW_BATTERY_THRESHOLD = 0.15; // 1.0 = charged
  class CookieObserver {
    constructor(callback) {
      this.callback_ = callback;
      this.timer_ = null;
      this.interests_ = [];
    }
    observe(cookieStore, interests) {
      const copiedInterests = [];
      (interests || [{}]).forEach(({name, url = defReadPath, matchType} = {}) => {
        if (name != null) {
        	name = String(name);
          if (name.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie name');
          if (name.indexOf('=') !== -1) throw new SyntaxError('Character "=" is not allowed in cookie name');
        } else {
          name = '';
          if (matchType == null) matchType = 'startsWith';
        }
        url = String(url || defReadPath);
        let path = readPathForUrl(url);
        // readPathForUrl will throw or return defReadPath so we can ignore path past this point
        if (matchType == null) matchType = 'equals';
        matchType = String(matchType);
        if (matchType !== 'equals' && matchType !== 'startsWith') {
          throw new SyntaxError('Unrecognized matchType ' + JSON.stringify(matchType));
        }
        copiedInterests.push({name: name, url: url, matchType: matchType});
      });
      if (!copiedInterests.length) return;
      this.interests_.push({cookieStore: cookieStore, interests: copiedInterests, cookies_: null});
      this.schedule_(true);
    }
    disconnect() {
      this.interests_ = null;
      this.snapshots_ = null;
      if (this.timer_) {
        clearTimeout(this.timer_);
        this.timer_ = null;
      }
    }
    schedule_(skipWaiting) {
      navigator.getBattery().then(batteryManager => {
        if (this.timer_) return;
        let interval = SLOW_OBSERVER_INTERVAL;
        if (self.document.visibilityState === 'visible') {
          const level = batteryManager.level;
          if (level > VERY_LOW_BATTERY_THRESHOLD && level > LOW_BATTERY_THRESHOLD || batteryManager.charging) {
            interval = FAST_OBSERVER_INTERVAL;
          }
        }
        if (skipWaiting) interval = 0;
        this.timer_ = setTimeout(() => this.tick_().then(() => this.schedule_(false)), interval);
      });
    }
    async tick_() {
      this.timer_ = null;
      let reported = {};
      let observed = [];
      let allCookies = await cookieStore.getAll();
      let forceReport = false;
      this.interests_.forEach(interestEntry => {
        let {cookieStore, interests, cookies_} = interestEntry;
        let oldCookies = cookies_;
        let newCookies = interestEntry.cookies_ = allCookies;
        let oldCookiesFlat = {};
        (oldCookies || []).forEach(({name, value}) => {
          oldCookiesFlat[name + '='] = oldCookiesFlat[name + '='] || [];
          oldCookiesFlat[name + '='].push(name + '=' + value);
        });
        let newCookiesFlat = {};
        newCookies.forEach(({name, value}) => {
          newCookiesFlat[name + '='] = newCookiesFlat[name + '='] || [];
          newCookiesFlat[name + '='].push(name + '=' + value);
        });
        let changes = [];
        let newSame = {};
        (oldCookies || []).forEach(({name, value}, index) => {
          if (oldCookiesFlat[name + '='].join(';') === (newCookiesFlat[name + '='] || []).join(';')) {
            newSame[name + '='] = true;
          } else {
            changes.push({type: 'hidden', name: name, value: value, index: index});
          }
        });
        newCookies.forEach(({name, value}, index) => {
          if (!newSame[name + '=']) changes.push({type: 'visible', name: name, value: value, index: index});
        });
        if (oldCookies == null || changes.length > 0) {
          interests.forEach(({name, url, matchType}) => {
            let matching =
              changes.filter(change => name === change.name || matchType === 'startsWith' && change.name.startsWith(name));
            matching.forEach(({type, name, value, index}) => {
              let serialized = type + ';' + name + '=' + value + ';' + index + ';' + url;
              if (reported[serialized]) return;
              observed.push({type: type, name: name, value: value, url: url, cookieStore: cookieStore, index: index});
              reported[serialized] = true;
            });
          });
        }
        if (oldCookies == null) forceReport = true;
      });
      if (forceReport || observed.length) this.callback_.call(null, observed, this);
    }
  };
  if (!self.cookieStore) self.cookieStore = new CookieStore(new AsyncCookieJar_(self.document));
  if (!self.CookieObserver) self.CookieObserver = CookieObserver;
})();
<commit_msg>typofix<commit_after>// async cookies API polyfill for document contexts
if (self.document) (function() {
  'use strict';

  const DISALLOWED_IN_COOKIE_NAME_RE = /[()<>@,;:\\""\/\[\]?={} \0-\x1f\x7f]|[^\x21-\x7e\u00A0-\u{10FFFF}]/u;
  const DISALLOWED_IN_COOKIE_VALUE_RE = /[^\x2D-\x3A\x21\x23-\x2B\x3C-\x5B\x5D-\x7E\u00A0-\u{10FFFF}]/u;
  // based on https://tools.ietf.org/html/rfc3986#appendix-B
  const uriRegExp = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/;
  const uriSchemeGroup = 2;
  const uriAuthorityGroup = 4;
  const uriPathGroup = 5;
  const defUriGroups = String(location.href).match(uriRegExp) || [];
  const dirName = path => path.replace(/(^|\/)\.\.$/, '$1../').replace(/[^\/]+$/, '');
  const defReadPath = dirName(defUriGroups[uriPathGroup] || '/');
  const defOrigin = defUriGroups[uriSchemeGroup] + '://' + defUriGroups[uriAuthorityGroup];
  const defSecure = defUriGroups[uriSchemeGroup] === 'https';
  const resolvePath = path => {
    path = String(path || defReadPath);
    if (path[0] !== '/') path = defReadPath + path;
    let pathSegments = path.split('/');
    let resolvedPathSegments = [];
    pathSegments.forEach(segment => {
      if (segment === '..') resolvedPathSegments.pop();
      else resolvedPathSegments.push(segment);
    });
    return resolvedPathSegments.join('/');
  };
  const readPathForUrl = url => {
    if (url == null) url = defReadPath;
    url = String(url);
    const urlGroups = url.match(uriRegExp) || [];
    const urlPath = resolvePath(dirName(String(urlGroups[uriPathGroup] || defReadPath)));
    const origin = [
      urlGroups[uriSchemeGroup] || defUriGroups[uriSchemeGroup],
      urlGroups[uriAuthorityGroup] || defUriGroups[uriAuthorityGroup]].join('://').toLowerCase();
    if ((origin + urlPath) !== (defOrigin + defReadPath)) {
      // TODO: iframe with unpredictable long pathname inside
      // requested path, maybe by appending as many
      // randomly-selected '$' and ':' as needed to reach 2083 chars
      // in the URL could be used to read same-origin, different-path cookies.
      throw new SyntaxError([
        'Cannot read cookies at requested URL ',
        JSON.stringify(url),
        ' from ',
        JSON.stringify(defOrigin + defReadPath),
        ': not yet implemented.'].join(''));
    }
    return defReadPath;
  };
  class AsyncCookieJar_ {
    constructor(document) {
      this.doc_ = document;
    }
    async set(setCookieString) {
      console.info('Set-Cookie: %s', setCookieString);
      this.doc_.cookie = setCookieString;
    }
    async get() {
      return this.doc_.cookie;
    }
  }
  const SECURE_PREFIX = '__Secure-', HOST_PREFIX = '__Host-';
  class CookieStore {
    constructor(asyncCookieJar) {
      this.asyncCookieJar_ = asyncCookieJar;
    }
    async has(nameOrOptions, moreOptions) {
      let cookieList = await this.getAll(nameOrOptions, moreOptions);
      return cookieList.length >= 1;
    }
    async get(nameOrOptions, moreOptions) {
      let cookieList = await this.getAll(nameOrOptions, moreOptions);
      return cookieList[0];
    }
    async getAll(nameOrOptions, moreOptions) {
      let options = Object.assign(
        {},
        (typeof nameOrOptions === 'object') ? nameOrOptions : {name: nameOrOptions},
        moreOptions);
      let {name, url = defReadPath, matchType} = options || {};
      if (name != null) {
      	name = String(name);
        if (name.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie name');
        if (name.indexOf('=') !== -1) throw new SyntaxError('Character "=" is not allowed in cookie name');
      } else {
        name = '';
        if (matchType == null) matchType = 'startsWith';
      }
      if (matchType == null) matchType = 'equals';
      matchType = String(matchType);
      if (matchType !== 'equals' && matchType !== 'startsWith') {
        throw new SyntaxError('Unrecognized matchType ' + JSON.stringify(matchType));
      }
      url = readPathForUrl(url);
      let cookieList = [];
      let jar = String(await this.asyncCookieJar_.get() || '');
      for (let i = 0, j = jar.length, k = jar.indexOf(';');
           k = (k == -1) ? j : k, i < j;
           i = k + 1 + (jar[k + 1] == ' ' ? 1 : 0), k = jar.indexOf(';', i)) {
       	let nv = jar.substr(i, k - i), c = nv.indexOf('=');
        if (c !== -1) {
          let n = nv.substr(0, c);
          // NOTE: some of the reported names and/or values will not be allowed in write operations.
          if (name === n || matchType === 'startsWith' && n.substr(0, name.length) === name) {
            let v = nv.substr(c + 1);
            cookieList.push({name: n, value: v});
          }
        }
      }
      return cookieList;
    }
    async delete(name, options) {
      await this.set(name, undefined, options);
    }
    async set(name, value, options) {
      name = String(name || '') || undefined;
      if (!name) throw new SyntaxError('Cookie name is required');
      if (name.match(DISALLOWED_IN_COOKIE_NAME_RE) || name !== decodeURIComponent(encodeURIComponent(name))) {
        throw new SyntaxError('Unsupported character in cookie name');
      }
      options = options || {};
      let expires = null;
      let maxAge = null;
      let expiresAsNumber;
      if (options.expires != null && !isNaN(expiresAsNumber = Number(options.expires))) {
        maxAge = (expiresAsNumber - Date.now()) / 1000;
      } else {
        expires = String(options.expires || '') || undefined;
      }
      if (expires && expires.indexOf(';') !== -1) {
        throw new SyntaxError('Character ";" is not allowed in cookie "expires" attribute');
      }
      let domain = String(options.domain || '') || undefined;
      if (domain && domain.indexOf(';') !== -1) {
        throw new SyntaxError('Character ";" is not allowed in cookie "domain" attribute');
      }
      let path = String(options.path || '/');
      if (path[0] !== '/') throw new SyntaxError('The "path" attribute must begin with "/"');
      if (path.match(/(^|\/)\.\.(\/|$)/)) {
        throw new SyntaxError('The cookie "path" attribute must not contain ".." path segments');
      }
      if (path.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie "path" attribute');
      if (path.indexOf('?') !== -1) throw new SyntaxError('Character "?" is not allowed in cookie "path" attribute');
      if (path.indexOf('#') !== -1) throw new SyntaxError('Character "#" is not allowed in cookie "path" attribute');
      // Add implicit trailing '/' if omitted. Does not match IE Set-Cookie behavior.
      path = path.replace(/([^\/]$)/, '$1/');
      let secure = options.secure;
      if (secure == null) secure = defSecure;
      secure = !!secure;
      if (secure && !defSecure) throw new SyntaxError('Secure cookies can only be modified from secure contexts');
      let httpOnly = !!options.httpOnly;
      if (value == null && maxAge == null && expires == null) maxAge = 0;
      value = String(value || '');
      if (value.match(DISALLOWED_IN_COOKIE_VALUE_RE) || value !== decodeURIComponent(encodeURIComponent(value))) {
        // Does not match document.cookie behavior!
        throw new SyntaxError('Unsupported character in cookie value');
      }
      if (name.substr(0, SECURE_PREFIX.length) === SECURE_PREFIX) {
        if (!defSecure) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(SECURE_PREFIX),
            ' prefix can only be modified from secure contexts'].join(''));
        }
        if (!secure) {
          throw new SyntaxError('Cookies with the ' + JSON.stringify(SECURE_PREFIX) + ' prefix must use the Secure flag');
        }
      } else if (name.substr(0, HOST_PREFIX.length) === HOST_PREFIX) {
        if (!defSecure) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix can only be modified from secure contexts'].join(''));
        }
        if (!secure) {
          throw new SyntaxError('Cookies with the ' + JSON.stringify(HOST_PREFIX) + ' prefix must use the secure flag');
        }
        if (path !== '/') {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix must have path ',
            JSON.stringify('/')].join(''));
        }
        if (domain != null) {
          throw new SyntaxError([
            'Cookies with the ',
            JSON.stringify(HOST_PREFIX),
            ' prefix cannot have the domain parameter'].join(''));
        }
      }
      let setCookieParts = [name, '=', value];
      if (domain != null) setCookieParts.push('; domain=', domain);
      if (path != null) setCookieParts.push('; path=', path);
      if (expires != null) setCookieParts.push('; expires=', expires);
      if (maxAge != null) setCookieParts.push('; max-age=', maxAge);
      if (secure) setCookieParts.push('; secure');
      if (httpOnly) setCookieParts.push('; httpOnly');
      let setCookie = setCookieParts.join('');
      await this.asyncCookieJar_.set(setCookie);
    }
  };
  const FAST_OBSERVER_INTERVAL = 200, SLOW_OBSERVER_INTERVAL = 5000; // ms
  const VERY_LOW_BATTERY_THRESHOLD = 0.05, LOW_BATTERY_THRESHOLD = 0.15; // 1.0 = charged
  class CookieObserver {
    constructor(callback) {
      this.callback_ = callback;
      this.timer_ = null;
      this.interests_ = [];
    }
    observe(cookieStore, interests) {
      const copiedInterests = [];
      (interests || [{}]).forEach(({name, url = defReadPath, matchType} = {}) => {
        if (name != null) {
        	name = String(name);
          if (name.indexOf(';') !== -1) throw new SyntaxError('Character ";" is not allowed in cookie name');
          if (name.indexOf('=') !== -1) throw new SyntaxError('Character "=" is not allowed in cookie name');
        } else {
          name = '';
          if (matchType == null) matchType = 'startsWith';
        }
        url = String(url || defReadPath);
        let path = readPathForUrl(url);
        // readPathForUrl will throw or return defReadPath so we can ignore path past this point
        if (matchType == null) matchType = 'equals';
        matchType = String(matchType);
        if (matchType !== 'equals' && matchType !== 'startsWith') {
          throw new SyntaxError('Unrecognized matchType ' + JSON.stringify(matchType));
        }
        copiedInterests.push({name: name, url: url, matchType: matchType});
      });
      if (!copiedInterests.length) return;
      this.interests_.push({cookieStore: cookieStore, interests: copiedInterests, cookies_: null});
      this.schedule_(true);
    }
    disconnect() {
      this.interests_ = null;
      this.snapshots_ = null;
      if (this.timer_) {
        clearTimeout(this.timer_);
        this.timer_ = null;
      }
    }
    schedule_(skipWaiting) {
      navigator.getBattery().then(batteryManager => {
        if (this.timer_) return;
        let interval = SLOW_OBSERVER_INTERVAL;
        if (self.document.visibilityState === 'visible') {
          const level = batteryManager.level;
          if (level > VERY_LOW_BATTERY_THRESHOLD && level > LOW_BATTERY_THRESHOLD || batteryManager.charging) {
            interval = FAST_OBSERVER_INTERVAL;
          }
        }
        if (skipWaiting) interval = 0;
        this.timer_ = setTimeout(() => this.tick_().then(() => this.schedule_(false)), interval);
      });
    }
    async tick_() {
      this.timer_ = null;
      let reported = {};
      let observed = [];
      let allCookies = await cookieStore.getAll();
      let forceReport = false;
      this.interests_.forEach(interestEntry => {
        let {cookieStore, interests, cookies_} = interestEntry;
        let oldCookies = cookies_;
        let newCookies = interestEntry.cookies_ = allCookies;
        let oldCookiesFlat = {};
        (oldCookies || []).forEach(({name, value}) => {
          oldCookiesFlat[name + '='] = oldCookiesFlat[name + '='] || [];
          oldCookiesFlat[name + '='].push(name + '=' + value);
        });
        let newCookiesFlat = {};
        newCookies.forEach(({name, value}) => {
          newCookiesFlat[name + '='] = newCookiesFlat[name + '='] || [];
          newCookiesFlat[name + '='].push(name + '=' + value);
        });
        let changes = [];
        let newSame = {};
        (oldCookies || []).forEach(({name, value}, index) => {
          if (oldCookiesFlat[name + '='].join(';') === (newCookiesFlat[name + '='] || []).join(';')) {
            newSame[name + '='] = true;
          } else {
            changes.push({type: 'hidden', name: name, value: value, index: index});
          }
        });
        newCookies.forEach(({name, value}, index) => {
          if (!newSame[name + '=']) changes.push({type: 'visible', name: name, value: value, index: index});
        });
        if (oldCookies == null || changes.length > 0) {
          interests.forEach(({name, url, matchType}) => {
            let matching =
              changes.filter(change => name === change.name || matchType === 'startsWith' && change.name.startsWith(name));
            matching.forEach(({type, name, value, index}) => {
              let serialized = type + ';' + name + '=' + value + ';' + index + ';' + url;
              if (reported[serialized]) return;
              observed.push({type: type, name: name, value: value, url: url, cookieStore: cookieStore, index: index});
              reported[serialized] = true;
            });
          });
        }
        if (oldCookies == null) forceReport = true;
      });
      if (forceReport || observed.length) this.callback_.call(null, observed, this);
    }
  };
  if (!self.cookieStore) self.cookieStore = new CookieStore(new AsyncCookieJar_(self.document));
  if (!self.CookieObserver) self.CookieObserver = CookieObserver;
})();
<|endoftext|>


<commit_before>import Em from 'ember';
import WithConfigMixin from 'ember-idx-utils/mixin/with-config'

/**
 * Button component
 *
 * Styled button with async support.
 *
 * @class Button
 */

export default Em.Component.extend(WithConfigMixin, {

  /**
   * The tag name the component is rendered as.
   * This theoretically can be a div or anything else.
   * @property tagName
   * @private
   */
  tagName: 'button',

  /**
   * Bind the specified properties as DOM attributes.
   * @property attributeBindings
   * @private
   */
  attributeBindings: ['disabled', 'state'],

  /**
   * Bind the specified properties as the classes of the DOM element.
   */
  classNameBindings: ['class'],

  /**
   * True if the button is disabled and cannot be clicked.
   * @property disabled
   * @public
   */
  disabled: Em.computed.equal('state', 'executing'),

  /**
   * The state of the button, can be one of the following:
   * default - The button is enabled and ready to be clicked.
   * executing - The promise bound to the button was sent and the promise is still executing
   * resolved - The promise was resolved properly.
   * rejected - The promise bound to the button was finished as rejected.
   *
   * The state is also bound to the DOM as `state` property, this allows to easily change styles for every
   * state by using `.em-button[state=resolved]` syntax.
   *
   * The label of the button will change to the value of the component properties that correspond to the
   * states mentioned above.
   *
   * @property state
   * @private
   */
  state: 'default',

  /**
   * The action name to invoke on the controller when the button is clicked.
   * @property on-click
   * @public
   */
  'on-click': void 0,

  /**
   * If set, an icon tag will be added as apart of the button and the given value here will be set
   * as the icon's `class` attribute.
   * @property icon-classes
   * @public
   */
  'icon-classes': (function() {
    var propName;
    propName = "icon-" + this.state;
    return this.getWithDefault(propName, this.get('icon-default'));
  }).property('state', 'icon-default', 'icon-executing', 'icon-resolved', 'icon-rejected'),

  /*
   * The label of the button, calculated according to the state of the button
   * See the `state` property documentation for more info.
   * @property label
   * @private
   */
  label: (function() {
    return this.getWithDefault(this.state, this.get('default'));
  }).property('state', 'default', 'executing', 'resolved', 'rejected'),

  /**
   * Set by the `onClick` callback, if set, the promise will be observed and the button's state will be
   * changed accordingly.
   * @property promise
   * @private
   */
  promise: void 0,

  /**
   * Triggered when the button is clicked
   * Invoke the action name on the controller defined in the `action` property, default is `onClick`.
   * The action on the controller recieves a property that should be set to the promise being invoked (if there is one)
   * If a promise was set, the button will move to 'executing' state until the promise will be resolved
   * @method onClick
   * @private
   */
  onClick: (function() {
    this.sendAction('on-click', (function(_this) {
      return function(promise) {
        _this.set('promise', promise);
        return _this.set('state', 'executing');
      };
    })(this));
    return false;
  }).on('click'),

  /*
   * Observes the promise property
   * @property changeStateByPromise
   * @private
   */
  changeStateByPromise: (function() {
    return this.get('promise').then((function(_this) {
      return function() {
        return _this.set('state', 'resolved');
      };
    })(this), (function(_this) {
      return function(err) {
        _this.set('state', 'rejected');
        return _this.set('error', err);
      };
    })(this));
  }).observes('promise')
});<commit_msg>Pass button instance to clicked functions.<commit_after>import Em from 'ember';
import WithConfigMixin from 'ember-idx-utils/mixin/with-config'

/**
 * Button component
 *
 * Styled button with async support.
 *
 * @class Button
 */

export default Em.Component.extend(WithConfigMixin, {

  /**
   * The tag name the component is rendered as.
   * This theoretically can be a div or anything else.
   * @property tagName
   * @private
   */
  tagName: 'button',

  /**
   * Bind the specified properties as DOM attributes.
   * @property attributeBindings
   * @private
   */
  attributeBindings: ['disabled', 'state'],

  /**
   * Bind the specified properties as the classes of the DOM element.
   */
  classNameBindings: ['class'],

  /**
   * True if the button is disabled and cannot be clicked.
   * @property disabled
   * @public
   */
  disabled: Em.computed.equal('state', 'executing'),

  /**
   * The state of the button, can be one of the following:
   * default - The button is enabled and ready to be clicked.
   * executing - The promise bound to the button was sent and the promise is still executing
   * resolved - The promise was resolved properly.
   * rejected - The promise bound to the button was finished as rejected.
   *
   * The state is also bound to the DOM as `state` property, this allows to easily change styles for every
   * state by using `.em-button[state=resolved]` syntax.
   *
   * The label of the button will change to the value of the component properties that correspond to the
   * states mentioned above.
   *
   * @property state
   * @private
   */
  state: 'default',

  /**
   * The action name to invoke on the controller when the button is clicked.
   * @property on-click
   * @public
   */
  'on-click': void 0,

  /**
   * If set, an icon tag will be added as apart of the button and the given value here will be set
   * as the icon's `class` attribute.
   * @property icon-classes
   * @public
   */
  'icon-classes': (function() {
    var propName;
    propName = "icon-" + this.state;
    return this.getWithDefault(propName, this.get('icon-default'));
  }).property('state', 'icon-default', 'icon-executing', 'icon-resolved', 'icon-rejected'),

  /*
   * The label of the button, calculated according to the state of the button
   * See the `state` property documentation for more info.
   * @property label
   * @private
   */
  label: (function() {
    return this.getWithDefault(this.state, this.get('default'));
  }).property('state', 'default', 'executing', 'resolved', 'rejected'),

  /**
   * Set by the `onClick` callback, if set, the promise will be observed and the button's state will be
   * changed accordingly.
   * @property promise
   * @private
   */
  promise: void 0,

  /**
   * Triggered when the button is clicked
   * Invoke the action name on the controller defined in the `action` property, default is `onClick`.
   * The action on the controller recieves a property that should be set to the promise being invoked (if there is one)
   * If a promise was set, the button will move to 'executing' state until the promise will be resolved
   * @method onClick
   * @private
   */
  onClick: (function() {
    this.sendAction('on-click', (function(_this) {
      return function(promise) {
        _this.set('promise', promise);
        return _this.set('state', 'executing');
      };
    })(this), this);
    return false;
  }).on('click'),

  /*
   * Observes the promise property
   * @property changeStateByPromise
   * @private
   */
  changeStateByPromise: (function() {
    return this.get('promise').then((function(_this) {
      return function() {
        return _this.set('state', 'resolved');
      };
    })(this), (function(_this) {
      return function(err) {
        _this.set('state', 'rejected');
        return _this.set('error', err);
      };
    })(this));
  }).observes('promise')
});<|endoftext|>