Created
July 4, 2015 15:20
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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