packages feed

linden-0: client/src/GardenClient.js

require('event-source-polyfill')

var Linden = require('./Linden')
var walkTree = require('./walkTree')
var randomWind = require('./randomWind')

var GardenClient = function GardenClient(opts) {
  this.opts = opts
  this.data = null
  this.gardenId = null
  this.gardenKey = null
}
GardenClient.prototype = {
  record: function record(ref, name) {
    new Image().src = 'http://xkcd.com/events.gif?re=' + ref + '&ev=' + name
  },

  log: function log() {
    if (this.opts.debug) {
      console.log.apply(console, arguments)  // eslint-disable-line no-console
    }
  },

  _url: function _url() {
    var serverURL = this.opts.endpoint + 'run'
    if (this.gardenId) {
      serverURL += '/' + this.gardenId
    }
    return serverURL
  },

  _connect: function() {
    var serverURL = this._url()

    this.source = new EventSource(serverURL)
    this.log('connecting to event source:', serverURL)

    this.source.addEventListener('open', this.onConnect.bind(this), false)
    this.source.addEventListener('error', this.onConnectionError.bind(this), false)
    this.source.addEventListener('linden-key', this.onLindenKey.bind(this), false)
    this.source.addEventListener('linden', this.onLindenUpdate.bind(this), false)
    this.source.addEventListener('reload', this.onReload.bind(this), false)
  },

  run: function run() {
    try {
      if (this.opts.endpoint) {
        if (location.hash) {
          this.gardenId = location.hash.substr(1)
          this.gardenKey = localStorage['key-' + this.gardenId]
        }
        this._connect()
      }

      this.linden = new Linden({
        el: this.opts.el,
        width: this.opts.width,
        height: this.opts.height,
        title: this.opts.title,
        urlPrefix: this.opts.imgPrefix,
        loading: true,
        readOnly: !this.gardenKey,
        debug: this.opts.debug,
        onLightChanging: this.handleLightChanging.bind(this),
        onSetLight: this.handleSetLight.bind(this),
        onRemoveLight: this.handleRemoveLight.bind(this),
        onPruneNode: this.handlePruneNode.bind(this),
      })

      if (this.opts.data) {
        this.linden.update(this.opts.data)
      }

      randomWind(this.linden)
    } catch (e) {
      this.record(this.opts.name, 'js_error')
      throw e
    }

    return this
  },

  onConnect: function onConnect() {
    this.record(this.opts.name, 'connect_start')
    this.dirtyLights = {}
    this.inFlightRequests = 0
    this.linden.renderUI()
  },

  onConnectionError: function onConnectionError(ev) {
    this.log('connection error', ev)
    this.record(this.opts.name, 'connect_error')
    this.linden.opts.loading = true
    this.linden.resetTree()
    this.linden.renderUI()
    if (this.source.url !== this._url()) {
      // we need to change the url to re-fetch the current tree
      this.source.close()
      this._connect()
    }
  },

  onLindenKey: function onLindenKey(ev) {
    this.log('linden key from EventSource', ev)
    var data = JSON.parse(ev.data)
    this.gardenId = location.hash = data.garden
    this.gardenKey = localStorage['key-' + data.garden] = data.key

    // TODO: this violates encapsulation
    this.linden.opts.readOnly = false
    this.linden.renderUI()
  },

  _acceptUpdate: function _acceptUpdate() {
    if (this.inFlightRequests > 0) {
      this.log('rejecting update, inflight:', this.inFlightRequests)
      return false
    }

    for (var k in this.dirtyLights) {
      if (this.dirtyLights.hasOwnProperty(k) && this.dirtyLights[k] === true) {
        this.log('rejecting update, dirty light:', k)
        return false
      }
    }

    return true
  },

  _update: function _update(data) {
    if (!this._acceptUpdate()) {
      // discard possibly conflicting updates
      return
    }
    this.data = data
    this.linden.update(data)
  },

  onLindenUpdate: function onLindenUpdate(ev) {
    this.log('linden from EventSource', ev)
    this._update(JSON.parse(ev.data))
  },

  onReload: function onReload() {
    var delay = Math.round(Math.random() * this.opts.spread.reload)
    this.log('reloading in', delay + 5, 'seconds')
    setTimeout(function doReload() {
      this.record(this.opts.name, 'reloading')

      // give the record a little time to be sent
      setTimeout(function delayedReload() {
        location.reload()
      }, 5 * 1000)
    }, delay * 1000)
  },

  request: function request(url, options) {
    var onFinished = function onRequestFinished() {
      this.inFlightRequests--
      this.log('inflight--', this.inFlightRequests)
    }.bind(this)

    var fetchOptions = {
      method: 'POST',
    }
    Object.assign(fetchOptions, options)

    fetchOptions.body = JSON.stringify(options.body)

    this.log('request', url, fetchOptions)
    var promise = fetch(url, fetchOptions)
    promise.then(onFinished, onFinished)
    this.inFlightRequests++
    this.log('inflight++', this.inFlightRequests)
    return promise
  },

  handleLightChanging: function handleLightChanging(id) {
    this.dirtyLights[id] = true
  },

  handleSetLight: function handleSetLight(id, data) {
    this.log('set light:', id, data)

    this.request(this.opts.endpoint + 'edit', {
      body: Object.assign({
        tag: 'EditLight',
        garden: this.data.garden,
        editKey: this.gardenKey,
        light: id,
      }, data),
    })
      .then(function onResponse(resp) { return resp.json() })
      .then(function onJSON(newLights) {
        this._update(Object.assign({}, this.data, {
          lights: newLights,
        }))
      }.bind(this))

    delete this.dirtyLights[id]
  },

  _sendRemove: function _sendRemove(id) {
    this.request(this.opts.endpoint + 'edit', {
      body: {
        tag: 'Del',
        garden: this.data.garden,
        editKey: this.gardenKey,
        node: id,
      },
    })
  },

  handleRemoveLight: function handleRemoveLight(id) {
    this.log('remove light:', id)

    var newLights = this.data.lights.filter(function filterLights(light) {
      return light.id !== id
    })

    this._update(Object.assign({}, this.data, {
      lights: newLights,
    }))

    this._sendRemove(id)
  },

  handlePruneNode: function handlePruneNode(id) {
    this.log('prune node:', id)

    var rootNode = { children: [] }
    var parentNode = rootNode
    walkTree(this.data.tree, function pruneVisit(node, descend) {
      var origParentNode = parentNode
      parentNode = { node: node, children: [] }
      if (node.id !== id) {
        origParentNode.children.push(parentNode)
      }
      descend()
      parentNode = origParentNode
    })

    this._update(Object.assign({}, this.data, {
      tree: rootNode.children[0],
    }))

    this._sendRemove(id)
  },
}

module.exports = GardenClient