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