Skip to content

Instantly share code, notes, and snippets.

@chrismcg
Created July 4, 2015 15:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrismcg/9984897345b846c58d35 to your computer and use it in GitHub Desktop.
Save chrismcg/9984897345b846c58d35 to your computer and use it in GitHub Desktop.
Patch for How to get ES6 source of phoenix.js into an ember-cli app
diff --git a/Brocfile.js b/Brocfile.js
index 7fa2af9..3718742 100644
--- a/Brocfile.js
+++ b/Brocfile.js
@@ -1,6 +1,9 @@
/* global require, module */
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
+var ES6Modules = require('broccoli-es6modules');
+var esTranspiler = require('broccoli-babel-transpiler');
+var mergeTrees = require('broccoli-merge-trees');
var app = new EmberApp();
@@ -39,4 +42,15 @@ app.import('bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.w
});
app.import('bower_components/bootstrap/dist/js/bootstrap.js');
-module.exports = app.toTree();
+
+var phoenixTree = "./vendor/phoenix";
+var phoenixAmdFiles = new ES6Modules(phoenixTree, {
+ format: 'amd',
+ esperantoOptions: {
+ strict: true,
+ amdName: "phoenix"
+ }
+});
+var phoenixTranspiledFiles = esTranspiler(phoenixAmdFiles, {});
+
+module.exports = mergeTrees([app.toTree(), phoenixTranspiledFiles]);
diff --git a/app/index.html b/app/index.html
index 2d8173c..75dee7b 100644
--- a/app/index.html
+++ b/app/index.html
@@ -18,6 +18,7 @@
{{content-for 'body'}}
<script src="assets/vendor.js"></script>
+ <script src="phoenix.js"></script>
<script src="assets/wordstream-bingo.js"></script>
{{content-for 'body-footer'}}
diff --git a/app/initializers/game-server.js b/app/initializers/game-server.js
new file mode 100644
index 0000000..5d199af
--- /dev/null
+++ b/app/initializers/game-server.js
@@ -0,0 +1,16 @@
+import {Socket} from "phoenix"
+
+export function initialize(/* container, application */) {
+ let socket = new Socket("ws://localhost:4000/ws");
+ socket.connect();
+ let chan = socket.chan("bingo:lobby", {});
+ chan.join().receive("ok", chan => {
+ console.log("Success!")
+ });
+}
+
+
+export default {
+ name: 'game-server',
+ initialize: initialize
+};
diff --git a/package.json b/package.json
index bf1143e..cc325db 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,9 @@
"license": "MIT",
"devDependencies": {
"broccoli-asset-rev": "^2.0.2",
+ "broccoli-es6modules": "1.0.0",
+ "broccoli-merge-trees": "0.2.1",
+ "broccoli-babel-transpiler": "5.2.3",
"ember-cli": "0.2.7",
"ember-cli-app-version": "0.3.3",
"ember-cli-babel": "^5.0.0",
diff --git a/tests/unit/initializers/game-server-test.js b/tests/unit/initializers/game-server-test.js
new file mode 100644
index 0000000..a323e92
--- /dev/null
+++ b/tests/unit/initializers/game-server-test.js
@@ -0,0 +1,23 @@
+import Ember from 'ember';
+import { initialize } from '../../../initializers/game-server';
+import { module, test } from 'qunit';
+
+var container, application;
+
+module('Unit | Initializer | game server', {
+ beforeEach: function() {
+ Ember.run(function() {
+ application = Ember.Application.create();
+ container = application.__container__;
+ application.deferReadiness();
+ });
+ }
+});
+
+// Replace this with your real tests.
+test('it works', function(assert) {
+ initialize(container, application);
+
+ // you would normally confirm the results of the initializer here
+ assert.ok(true);
+});
diff --git a/vendor/phoenix/phoenix.js b/vendor/phoenix/phoenix.js
new file mode 100644
index 0000000..e6ede0e
--- /dev/null
+++ b/vendor/phoenix/phoenix.js
@@ -0,0 +1,671 @@
+// Phoenix Channels JavaScript client
+//
+// ## Socket Connection
+//
+// A single connection is established to the server and
+// channels are mulitplexed over the connection.
+// Connect to the server using the `Socket` class:
+//
+// let socket = new Socket("/ws")
+// socket.connect()
+//
+// The `Socket` constructor takes the mount point of the socket
+// as well as options that can be found in the Socket docs,
+// such as configuring the `LongPoller` transport, and heartbeat.
+// Socket params can also be passed as an option for default, but
+// overridable channel params to apply to all channels.
+//
+//
+// ## Channels
+//
+// Channels are isolated, concurrent processes on the server that
+// subscribe to topics and broker events between the client and server.
+// To join a channel, you must provide the topic, and channel params for
+// authorization. Here's an example chat room example where `"new_msg"`
+// events are listened for, messages are pushed to the server, and
+// the channel is joined with ok/error matches, and `after` hook:
+//
+// let chan = socket.chan("rooms:123", {token: roomToken})
+// chan.on("new_msg", msg => console.log("Got message", msg) )
+// $input.onEnter( e => {
+// chan.push("new_msg", {body: e.target.val})
+// .receive("ok", (message) => console.log("created message", message) )
+// .receive("error", (reasons) => console.log("create failed", reasons) )
+// .after(10000, () => console.log("Networking issue. Still waiting...") )
+// })
+// chan.join()
+// .receive("ok", ({messages}) => console.log("catching up", messages) )
+// .receive("error", ({reason}) => console.log("failed join", reason) )
+// .after(10000, () => console.log("Networking issue. Still waiting...") )
+//
+//
+// ## Joining
+//
+// Joining a channel with `chan.join(topic, params)`, binds the params to
+// `chan.params`. Subsequent rejoins will send up the modified params for
+// updating authorization params, or passing up last_message_id information.
+// Successful joins receive an "ok" status, while unsuccessful joins
+// receive "error".
+//
+//
+// ## Pushing Messages
+//
+// From the previous example, we can see that pushing messages to the server
+// can be done with `chan.push(eventName, payload)` and we can optionally
+// receive responses from the push. Additionally, we can use
+// `after(millsec, callback)` to abort waiting for our `receive` hooks and
+// take action after some period of waiting.
+//
+//
+// ## Socket Hooks
+//
+// Lifecycle events of the multiplexed connection can be hooked into via
+// `socket.onError()` and `socket.onClose()` events, ie:
+//
+// socket.onError( () => console.log("there was an error with the connection!") )
+// socket.onClose( () => console.log("the connection dropped") )
+//
+//
+// ## Channel Hooks
+//
+// For each joined channel, you can bind to `onError` and `onClose` events
+// to monitor the channel lifecycle, ie:
+//
+// chan.onError( () => console.log("there was an error!") )
+// chan.onClose( () => console.log("the channel has gone away gracefully") )
+//
+// ### onError hooks
+//
+// `onError` hooks are invoked if the socket connection drops, or the channel
+// crashes on the server. In either case, a channel rejoin is attemtped
+// automatically in an exponential backoff manner.
+//
+// ### onClose hooks
+//
+// `onClose` hooks are invoked only in two cases. 1) the channel explicitly
+// closed on the server, or 2). The client explicitly closed, by calling
+// `chan.leave()`
+//
+
+const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
+const CHAN_STATES = {
+ closed: "closed",
+ errored: "errored",
+ joined: "joined",
+ joining: "joining",
+}
+const CHAN_EVENTS = {
+ close: "phx_close",
+ error: "phx_error",
+ join: "phx_join",
+ reply: "phx_reply",
+ leave: "phx_leave"
+}
+
+class Push {
+
+ // Initializes the Push
+ //
+ // chan - The Channel
+ // event - The event, ie `"phx_join"`
+ // payload - The payload, ie `{user_id: 123}`
+ //
+ constructor(chan, event, payload){
+ this.chan = chan
+ this.event = event
+ this.payload = payload || {}
+ this.receivedResp = null
+ this.afterHook = null
+ this.recHooks = []
+ this.sent = false
+ }
+
+ send(){
+ const ref = this.chan.socket.makeRef()
+ this.refEvent = this.chan.replyEventName(ref)
+ this.receivedResp = null
+ this.sent = false
+
+ this.chan.on(this.refEvent, payload => {
+ this.receivedResp = payload
+ this.matchReceive(payload)
+ this.cancelRefEvent()
+ this.cancelAfter()
+ })
+
+ this.startAfter()
+ this.sent = true
+ this.chan.socket.push({
+ topic: this.chan.topic,
+ event: this.event,
+ payload: this.payload,
+ ref: ref
+ })
+ }
+
+ receive(status, callback){
+ if(this.receivedResp && this.receivedResp.status === status){
+ callback(this.receivedResp.response)
+ }
+
+ this.recHooks.push({status, callback})
+ return this
+ }
+
+ after(ms, callback){
+ if(this.afterHook){ throw(`only a single after hook can be applied to a push`) }
+ let timer = null
+ if(this.sent){ timer = setTimeout(callback, ms) }
+ this.afterHook = {ms: ms, callback: callback, timer: timer}
+ return this
+ }
+
+
+ // private
+
+ matchReceive({status, response, ref}){
+ this.recHooks.filter( h => h.status === status )
+ .forEach( h => h.callback(response) )
+ }
+
+ cancelRefEvent(){ this.chan.off(this.refEvent) }
+
+ cancelAfter(){ if(!this.afterHook){ return }
+ clearTimeout(this.afterHook.timer)
+ this.afterHook.timer = null
+ }
+
+ startAfter(){ if(!this.afterHook){ return }
+ let callback = () => {
+ this.cancelRefEvent()
+ this.afterHook.callback()
+ }
+ this.afterHook.timer = setTimeout(callback, this.afterHook.ms)
+ }
+}
+
+export class Channel {
+ constructor(topic, params, socket) {
+ this.state = CHAN_STATES.closed
+ this.topic = topic
+ this.params = params || {}
+ this.socket = socket
+ this.bindings = []
+ this.joinedOnce = false
+ this.joinPush = new Push(this, CHAN_EVENTS.join, this.params)
+ this.pushBuffer = []
+ this.rejoinTimer = new Timer(
+ () => this.rejoinUntilConnected(),
+ this.socket.reconnectAfterMs
+ )
+ this.joinPush.receive("ok", () => {
+ this.state = CHAN_STATES.joined
+ this.rejoinTimer.reset()
+ })
+ this.onClose( () => {
+ this.socket.log("channel", `close ${this.topic}`)
+ this.state = CHAN_STATES.closed
+ this.socket.remove(this)
+ })
+ this.onError( reason => {
+ this.socket.log("channel", `error ${this.topic}`, reason)
+ this.state = CHAN_STATES.errored
+ this.rejoinTimer.setTimeout()
+ })
+ this.on(CHAN_EVENTS.reply, (payload, ref) => {
+ this.trigger(this.replyEventName(ref), payload)
+ })
+ }
+
+ rejoinUntilConnected(){
+ this.rejoinTimer.setTimeout()
+ if(this.socket.isConnected()){
+ this.rejoin()
+ }
+ }
+
+ join(){
+ if(this.joinedOnce){
+ throw(`tried to join multiple times. 'join' can only be called a single time per channel instance`)
+ } else {
+ this.joinedOnce = true
+ }
+ this.sendJoin()
+ return this.joinPush
+ }
+
+ onClose(callback){ this.on(CHAN_EVENTS.close, callback) }
+
+ onError(callback){
+ this.on(CHAN_EVENTS.error, reason => callback(reason) )
+ }
+
+ on(event, callback){ this.bindings.push({event, callback}) }
+
+ off(event){ this.bindings = this.bindings.filter( bind => bind.event !== event ) }
+
+ canPush(){ return this.socket.isConnected() && this.state === CHAN_STATES.joined }
+
+ push(event, payload){
+ if(!this.joinedOnce){
+ throw(`tried to push '${event}' to '${this.topic}' before joining. Use chan.join() before pushing events`)
+ }
+ let pushEvent = new Push(this, event, payload)
+ if(this.canPush()){
+ pushEvent.send()
+ } else {
+ this.pushBuffer.push(pushEvent)
+ }
+
+ return pushEvent
+ }
+
+ // Leaves the channel
+ //
+ // Unsubscribes from server events, and
+ // instructs channel to terminate on server
+ //
+ // Triggers onClose() hooks
+ //
+ // To receive leave acknowledgements, use the a `receive`
+ // hook to bind to the server ack, ie:
+ //
+ // chan.leave().receive("ok", () => alert("left!") )
+ //
+ leave(){
+ return this.push(CHAN_EVENTS.leave).receive("ok", () => {
+ this.log("channel", `leave ${this.topic}`)
+ this.trigger(CHAN_EVENTS.close, "leave")
+ })
+ }
+
+ // Overridable message hook
+ //
+ // Receives all events for specialized message handling
+ onMessage(event, payload, ref){}
+
+ // private
+
+ isMember(topic){ return this.topic === topic }
+
+ sendJoin(){
+ this.state = CHAN_STATES.joining
+ this.joinPush.send()
+ }
+
+ rejoin(){
+ this.sendJoin()
+ this.pushBuffer.forEach( pushEvent => pushEvent.send() )
+ this.pushBuffer = []
+ }
+
+ trigger(triggerEvent, payload, ref){
+ this.onMessage(triggerEvent, payload, ref)
+ this.bindings.filter( bind => bind.event === triggerEvent )
+ .map( bind => bind.callback(payload, ref) )
+ }
+
+ replyEventName(ref){ return `chan_reply_${ref}` }
+}
+
+export class Socket {
+
+ // Initializes the Socket
+ //
+ // endPoint - The string WebSocket endpoint, ie, "ws://example.com/ws",
+ // "wss://example.com"
+ // "/ws" (inherited host & protocol)
+ // opts - Optional configuration
+ // transport - The Websocket Transport, ie WebSocket, Phoenix.LongPoller.
+ // Defaults to WebSocket with automatic LongPoller fallback.
+ // params - The defaults for all channel params, ie `{user_id: userToken}`
+ // heartbeatIntervalMs - The millisec interval to send a heartbeat message
+ // reconnectAfterMs - The optional function that returns the millsec
+ // reconnect interval. Defaults to stepped backoff of:
+ //
+ // function(tries){
+ // return [1000, 5000, 10000][tries - 1] || 10000
+ // }
+ //
+ // logger - The optional function for specialized logging, ie:
+ // `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
+ //
+ // longpollerTimeout - The maximum timeout of a long poll AJAX request.
+ // Defaults to 20s (double the server long poll timer).
+ //
+ // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
+ //
+ constructor(endPoint, opts = {}){
+ this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}
+ this.channels = []
+ this.sendBuffer = []
+ this.ref = 0
+ this.transport = opts.transport || window.WebSocket || LongPoller
+ this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000
+ this.reconnectAfterMs = opts.reconnectAfterMs || function(tries){
+ return [1000, 5000, 10000][tries - 1] || 10000
+ }
+ this.reconnectTimer = new Timer(() => this.connect(), this.reconnectAfterMs)
+ this.logger = opts.logger || function(){} // noop
+ this.longpollerTimeout = opts.longpollerTimeout || 20000
+ this.endPoint = this.expandEndpoint(endPoint)
+ this.params = opts.params || {}
+ }
+
+ protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" }
+
+ expandEndpoint(endPoint){
+ if(endPoint.charAt(0) !== "/"){ return endPoint }
+ if(endPoint.charAt(1) === "/"){ return `${this.protocol()}:${endPoint}` }
+
+ return `${this.protocol()}://${location.host}${endPoint}`
+ }
+
+ disconnect(callback, code, reason){
+ if(this.conn){
+ this.conn.onclose = function(){} // noop
+ if(code){ this.conn.close(code, reason || "") } else { this.conn.close() }
+ this.conn = null
+ }
+ callback && callback()
+ }
+
+ connect(){
+ this.disconnect(() => {
+ this.conn = new this.transport(this.endPoint)
+ this.conn.timeout = this.longpollerTimeout
+ this.conn.onopen = () => this.onConnOpen()
+ this.conn.onerror = error => this.onConnError(error)
+ this.conn.onmessage = event => this.onConnMessage(event)
+ this.conn.onclose = event => this.onConnClose(event)
+ })
+ }
+
+ // Logs the message. Override `this.logger` for specialized logging. noops by default
+ log(kind, msg, data){ this.logger(kind, msg, data) }
+
+ // Registers callbacks for connection state change events
+ //
+ // Examples
+ //
+ // socket.onError(function(error){ alert("An error occurred") })
+ //
+ onOpen (callback){ this.stateChangeCallbacks.open.push(callback) }
+ onClose (callback){ this.stateChangeCallbacks.close.push(callback) }
+ onError (callback){ this.stateChangeCallbacks.error.push(callback) }
+ onMessage (callback){ this.stateChangeCallbacks.message.push(callback) }
+
+ onConnOpen(){
+ this.log("transport", `connected to ${this.endPoint}`, this.transport)
+ this.flushSendBuffer()
+ this.reconnectTimer.reset()
+ if(!this.conn.skipHeartbeat){
+ clearInterval(this.heartbeatTimer)
+ this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
+ }
+ this.stateChangeCallbacks.open.forEach( callback => callback() )
+ }
+
+ onConnClose(event){
+ this.log("transport", "close", event)
+ this.triggerChanError()
+ clearInterval(this.heartbeatTimer)
+ this.reconnectTimer.setTimeout()
+ this.stateChangeCallbacks.close.forEach( callback => callback(event) )
+ }
+
+ onConnError(error){
+ this.log("transport", error)
+ this.triggerChanError()
+ this.stateChangeCallbacks.error.forEach( callback => callback(error) )
+ }
+
+ triggerChanError(){
+ this.channels.forEach( chan => chan.trigger(CHAN_EVENTS.error) )
+ }
+
+ connectionState(){
+ switch(this.conn && this.conn.readyState){
+ case SOCKET_STATES.connecting: return "connecting"
+ case SOCKET_STATES.open: return "open"
+ case SOCKET_STATES.closing: return "closing"
+ default: return "closed"
+ }
+ }
+
+ isConnected(){ return this.connectionState() === "open" }
+
+ remove(chan){
+ this.channels = this.channels.filter( c => !c.isMember(chan.topic) )
+ }
+
+ chan(topic, chanParams = {}){
+ let mergedParams = {}
+ for(var key in this.params){ mergedParams[key] = this.params[key] }
+ for(var key in chanParams){ mergedParams[key] = chanParams[key] }
+
+ let chan = new Channel(topic, mergedParams, this)
+ this.channels.push(chan)
+ return chan
+ }
+
+ push(data){
+ let {topic, event, payload, ref} = data
+ let callback = () => this.conn.send(JSON.stringify(data))
+ this.log("push", `${topic} ${event} (${ref})`, payload)
+ if(this.isConnected()){
+ callback()
+ }
+ else {
+ this.sendBuffer.push(callback)
+ }
+ }
+
+ // Return the next message ref, accounting for overflows
+ makeRef(){
+ let newRef = this.ref + 1
+ if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }
+
+ return this.ref.toString()
+ }
+
+ sendHeartbeat(){
+ this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: this.makeRef()})
+ }
+
+ flushSendBuffer(){
+ if(this.isConnected() && this.sendBuffer.length > 0){
+ this.sendBuffer.forEach( callback => callback() )
+ this.sendBuffer = []
+ }
+ }
+
+ onConnMessage(rawMessage){
+ let msg = JSON.parse(rawMessage.data)
+ let {topic, event, payload, ref} = msg
+ this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`, payload)
+ this.channels.filter( chan => chan.isMember(topic) )
+ .forEach( chan => chan.trigger(event, payload, ref) )
+ this.stateChangeCallbacks.message.forEach( callback => callback(msg) )
+ }
+}
+
+
+export class LongPoller {
+
+ constructor(endPoint){
+ this.endPoint = null
+ this.token = null
+ this.sig = null
+ this.skipHeartbeat = true
+ this.onopen = function(){} // noop
+ this.onerror = function(){} // noop
+ this.onmessage = function(){} // noop
+ this.onclose = function(){} // noop
+ this.upgradeEndpoint = this.normalizeEndpoint(endPoint)
+ this.pollEndpoint = this.upgradeEndpoint + (/\/$/.test(endPoint) ? "poll" : "/poll")
+ this.readyState = SOCKET_STATES.connecting
+
+ this.poll()
+ }
+
+ normalizeEndpoint(endPoint){
+ return endPoint.replace("ws://", "http://").replace("wss://", "https://")
+ }
+
+ endpointURL(){
+ return this.pollEndpoint + `?token=${encodeURIComponent(this.token)}&sig=${encodeURIComponent(this.sig)}&format=json`
+ }
+
+ closeAndRetry(){
+ this.close()
+ this.readyState = SOCKET_STATES.connecting
+ }
+
+ ontimeout(){
+ this.onerror("timeout")
+ this.closeAndRetry()
+ }
+
+ poll(){
+ if(!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)){ return }
+
+ Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), (resp) => {
+ if(resp){
+ var {status, token, sig, messages} = resp
+ this.token = token
+ this.sig = sig
+ } else{
+ var status = 0
+ }
+
+ switch(status){
+ case 200:
+ messages.forEach( msg => this.onmessage({data: JSON.stringify(msg)}) )
+ this.poll()
+ break
+ case 204:
+ this.poll()
+ break
+ case 410:
+ this.readyState = SOCKET_STATES.open
+ this.onopen()
+ this.poll()
+ break
+ case 0:
+ case 500:
+ this.onerror()
+ this.closeAndRetry()
+ break
+ default: throw(`unhandled poll status ${status}`)
+ }
+ })
+ }
+
+ send(body){
+ Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), (resp) => {
+ if(!resp || resp.status !== 200){
+ this.onerror(status)
+ this.closeAndRetry()
+ }
+ })
+ }
+
+ close(code, reason){
+ this.readyState = SOCKET_STATES.closed
+ this.onclose()
+ }
+}
+
+
+export class Ajax {
+
+ static request(method, endPoint, accept, body, timeout, ontimeout, callback){
+ if(window.XDomainRequest){
+ let req = new XDomainRequest() // IE8, IE9
+ this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
+ } else {
+ let req = window.XMLHttpRequest ?
+ new XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
+ new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5
+ this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
+ }
+ }
+
+ static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){
+ req.timeout = timeout
+ req.open(method, endPoint)
+ req.onload = () => {
+ let response = this.parseJSON(req.responseText)
+ callback && callback(response)
+ }
+ if(ontimeout){ req.ontimeout = ontimeout }
+
+ // Work around bug in IE9 that requires an attached onprogress handler
+ req.onprogress = () => {}
+
+ req.send(body)
+ }
+
+ static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){
+ req.timeout = timeout
+ req.open(method, endPoint, true)
+ req.setRequestHeader("Content-Type", accept)
+ req.onerror = () => { callback && callback(null) }
+ req.onreadystatechange = () => {
+ if(req.readyState === this.states.complete && callback){
+ let response = this.parseJSON(req.responseText)
+ callback(response)
+ }
+ }
+ if(ontimeout){ req.ontimeout = ontimeout }
+
+ req.send(body)
+ }
+
+ static parseJSON(resp){
+ return (resp && resp !== "") ?
+ JSON.parse(resp) :
+ null
+ }
+}
+
+Ajax.states = {complete: 4}
+
+
+// Creates a timer that accepts a `timerCalc` function to perform
+// calculated timeout retries, such as exponential backoff.
+//
+// ## Examples
+//
+// let reconnectTimer = new Timer(() => this.connect(), function(tries){
+// return [1000, 5000, 10000][tries - 1] || 10000
+// })
+// reconnectTimer.setTimeout() // fires after 1000
+// reconnectTimer.setTimeout() // fires after 5000
+// reconnectTimer.reset()
+// reconnectTimer.setTimeout() // fires after 1000
+//
+class Timer {
+ constructor(callback, timerCalc){
+ this.callback = callback
+ this.timerCalc = timerCalc
+ this.timer = null
+ this.tries = 0
+ }
+
+ reset(){
+ this.tries = 0
+ clearTimeout(this.timer)
+ }
+
+ // Cancels any previous setTimeout and schedules callback
+ setTimeout(){
+ clearTimeout(this.timer)
+
+ this.timer = setTimeout(() => {
+ this.tries = this.tries + 1
+ this.callback()
+ }, this.timerCalc(this.tries + 1))
+ }
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment