Created
January 22, 2018 18:38
-
-
Save misfo/040ebfa989a16f9b81a27286bb600c54 to your computer and use it in GitHub Desktop.
phoenix.js 1.3 compiled to a global var
This file contains 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
var Phoenix = | |
/******/ (function(modules) { // webpackBootstrap | |
/******/ // The module cache | |
/******/ var installedModules = {}; | |
/******/ | |
/******/ // The require function | |
/******/ function __webpack_require__(moduleId) { | |
/******/ | |
/******/ // Check if module is in cache | |
/******/ if(installedModules[moduleId]) { | |
/******/ return installedModules[moduleId].exports; | |
/******/ } | |
/******/ // Create a new module (and put it into the cache) | |
/******/ var module = installedModules[moduleId] = { | |
/******/ i: moduleId, | |
/******/ l: false, | |
/******/ exports: {} | |
/******/ }; | |
/******/ | |
/******/ // Execute the module function | |
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); | |
/******/ | |
/******/ // Flag the module as loaded | |
/******/ module.l = true; | |
/******/ | |
/******/ // Return the exports of the module | |
/******/ return module.exports; | |
/******/ } | |
/******/ | |
/******/ | |
/******/ // expose the modules object (__webpack_modules__) | |
/******/ __webpack_require__.m = modules; | |
/******/ | |
/******/ // expose the module cache | |
/******/ __webpack_require__.c = installedModules; | |
/******/ | |
/******/ // define getter function for harmony exports | |
/******/ __webpack_require__.d = function(exports, name, getter) { | |
/******/ if(!__webpack_require__.o(exports, name)) { | |
/******/ Object.defineProperty(exports, name, { | |
/******/ configurable: false, | |
/******/ enumerable: true, | |
/******/ get: getter | |
/******/ }); | |
/******/ } | |
/******/ }; | |
/******/ | |
/******/ // getDefaultExport function for compatibility with non-harmony modules | |
/******/ __webpack_require__.n = function(module) { | |
/******/ var getter = module && module.__esModule ? | |
/******/ function getDefault() { return module['default']; } : | |
/******/ function getModuleExports() { return module; }; | |
/******/ __webpack_require__.d(getter, 'a', getter); | |
/******/ return getter; | |
/******/ }; | |
/******/ | |
/******/ // Object.prototype.hasOwnProperty.call | |
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; | |
/******/ | |
/******/ // __webpack_public_path__ | |
/******/ __webpack_require__.p = ""; | |
/******/ | |
/******/ // Load entry module and return exports | |
/******/ return __webpack_require__(__webpack_require__.s = 0); | |
/******/ }) | |
/************************************************************************/ | |
/******/ ([ | |
/* 0 */ | |
/***/ (function(module, __webpack_exports__, __webpack_require__) { | |
"use strict"; | |
Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); | |
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Presence", function() { return Presence; }); | |
/** | |
* Phoenix Channels JavaScript client | |
* | |
* ## Socket Connection | |
* | |
* A single connection is established to the server and | |
* channels are multiplexed over the connection. | |
* Connect to the server using the `Socket` class: | |
* | |
* ```javascript | |
* let socket = new Socket("/socket", {params: {userToken: "123"}}) | |
* socket.connect() | |
* ``` | |
* | |
* The `Socket` constructor takes the mount point of the socket, | |
* the authentication params, as well as options that can be found in | |
* the Socket docs, such as configuring the `LongPoll` transport, and | |
* heartbeat. | |
* | |
* ## 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/timeout matches: | |
* | |
* ```javascript | |
* let channel = socket.channel("room:123", {token: roomToken}) | |
* channel.on("new_msg", msg => console.log("Got message", msg) ) | |
* $input.onEnter( e => { | |
* channel.push("new_msg", {body: e.target.val}, 10000) | |
* .receive("ok", (msg) => console.log("created message", msg) ) | |
* .receive("error", (reasons) => console.log("create failed", reasons) ) | |
* .receive("timeout", () => console.log("Networking issue...") ) | |
* }) | |
* channel.join() | |
* .receive("ok", ({messages}) => console.log("catching up", messages) ) | |
* .receive("error", ({reason}) => console.log("failed join", reason) ) | |
* .receive("timeout", () => console.log("Networking issue. Still waiting...") ) | |
*``` | |
* | |
* ## Joining | |
* | |
* Creating a channel with `socket.channel(topic, params)`, binds the params to | |
* `channel.params`, which are sent up on `channel.join()`. | |
* 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". | |
* | |
* ## Duplicate Join Subscriptions | |
* | |
* While the client may join any number of topics on any number of channels, | |
* the client may only hold a single subscription for each unique topic at any | |
* given time. When attempting to create a duplicate subscription, | |
* the server will close the existing channel, log a warning, and | |
* spawn a new channel for the topic. The client will have their | |
* `channel.onClose` callbacks fired for the existing channel, and the new | |
* channel join will have its receive hooks processed as normal. | |
* | |
* ## Pushing Messages | |
* | |
* From the previous example, we can see that pushing messages to the server | |
* can be done with `channel.push(eventName, payload)` and we can optionally | |
* receive responses from the push. Additionally, we can use | |
* `receive("timeout", callback)` to abort waiting for our other `receive` hooks | |
* and take action after some period of waiting. The default timeout is 5000ms. | |
* | |
* | |
* ## Socket Hooks | |
* | |
* Lifecycle events of the multiplexed connection can be hooked into via | |
* `socket.onError()` and `socket.onClose()` events, ie: | |
* | |
* ```javascript | |
* 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: | |
* | |
* ```javascript | |
* channel.onError( () => console.log("there was an error!") ) | |
* channel.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 attempted | |
* 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 | |
* `channel.leave()` | |
* | |
* | |
* ## Presence | |
* | |
* The `Presence` object provides features for syncing presence information | |
* from the server with the client and handling presences joining and leaving. | |
* | |
* ### Syncing initial state from the server | |
* | |
* `Presence.syncState` is used to sync the list of presences on the server | |
* with the client's state. An optional `onJoin` and `onLeave` callback can | |
* be provided to react to changes in the client's local presences across | |
* disconnects and reconnects with the server. | |
* | |
* `Presence.syncDiff` is used to sync a diff of presence join and leave | |
* events from the server, as they happen. Like `syncState`, `syncDiff` | |
* accepts optional `onJoin` and `onLeave` callbacks to react to a user | |
* joining or leaving from a device. | |
* | |
* ### Listing Presences | |
* | |
* `Presence.list` is used to return a list of presence information | |
* based on the local state of metadata. By default, all presence | |
* metadata is returned, but a `listBy` function can be supplied to | |
* allow the client to select which metadata to use for a given presence. | |
* For example, you may have a user online from different devices with | |
* a metadata status of "online", but they have set themselves to "away" | |
* on another device. In this case, the app may choose to use the "away" | |
* status for what appears on the UI. The example below defines a `listBy` | |
* function which prioritizes the first metadata which was registered for | |
* each user. This could be the first tab they opened, or the first device | |
* they came online from: | |
* | |
* ```javascript | |
* let state = {} | |
* state = Presence.syncState(state, stateFromServer) | |
* let listBy = (id, {metas: [first, ...rest]}) => { | |
* first.count = rest.length + 1 // count of this user's presences | |
* first.id = id | |
* return first | |
* } | |
* let onlineUsers = Presence.list(state, listBy) | |
* ``` | |
* | |
* | |
* ### Example Usage | |
*```javascript | |
* // detect if user has joined for the 1st time or from another tab/device | |
* let onJoin = (id, current, newPres) => { | |
* if(!current){ | |
* console.log("user has entered for the first time", newPres) | |
* } else { | |
* console.log("user additional presence", newPres) | |
* } | |
* } | |
* // detect if user has left from all tabs/devices, or is still present | |
* let onLeave = (id, current, leftPres) => { | |
* if(current.metas.length === 0){ | |
* console.log("user has left from all devices", leftPres) | |
* } else { | |
* console.log("user left from a device", leftPres) | |
* } | |
* } | |
* let presences = {} // client's initial empty presence state | |
* // receive initial presence data from server, sent after join | |
* myChannel.on("presence_state", state => { | |
* presences = Presence.syncState(presences, state, onJoin, onLeave) | |
* displayUsers(Presence.list(presences)) | |
* }) | |
* // receive "presence_diff" from server, containing join/leave events | |
* myChannel.on("presence_diff", diff => { | |
* presences = Presence.syncDiff(presences, diff, onJoin, onLeave) | |
* this.setState({users: Presence.list(room.presences, listBy)}) | |
* }) | |
* ``` | |
* @module phoenix | |
*/ | |
const VSN = "2.0.0" | |
const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3} | |
const DEFAULT_TIMEOUT = 10000 | |
const WS_CLOSE_NORMAL = 1000 | |
const CHANNEL_STATES = { | |
closed: "closed", | |
errored: "errored", | |
joined: "joined", | |
joining: "joining", | |
leaving: "leaving", | |
} | |
const CHANNEL_EVENTS = { | |
close: "phx_close", | |
error: "phx_error", | |
join: "phx_join", | |
reply: "phx_reply", | |
leave: "phx_leave" | |
} | |
const CHANNEL_LIFECYCLE_EVENTS = [ | |
CHANNEL_EVENTS.close, | |
CHANNEL_EVENTS.error, | |
CHANNEL_EVENTS.join, | |
CHANNEL_EVENTS.reply, | |
CHANNEL_EVENTS.leave | |
] | |
const TRANSPORTS = { | |
longpoll: "longpoll", | |
websocket: "websocket" | |
} | |
/** | |
* Initializes the Push | |
* @param {Channel} channel - The Channel | |
* @param {string} event - The event, for example `"phx_join"` | |
* @param {Object} payload - The payload, for example `{user_id: 123}` | |
* @param {number} timeout - The push timeout in milliseconds | |
*/ | |
class Push { | |
constructor(channel, event, payload, timeout){ | |
this.channel = channel | |
this.event = event | |
this.payload = payload || {} | |
this.receivedResp = null | |
this.timeout = timeout | |
this.timeoutTimer = null | |
this.recHooks = [] | |
this.sent = false | |
} | |
/** | |
* | |
* @param {number} timeout | |
*/ | |
resend(timeout){ | |
this.timeout = timeout | |
this.reset() | |
this.send() | |
} | |
/** | |
* | |
*/ | |
send(){ if(this.hasReceived("timeout")){ return } | |
this.startTimeout() | |
this.sent = true | |
this.channel.socket.push({ | |
topic: this.channel.topic, | |
event: this.event, | |
payload: this.payload, | |
ref: this.ref, | |
join_ref: this.channel.joinRef() | |
}) | |
} | |
/** | |
* | |
* @param {*} status | |
* @param {*} callback | |
*/ | |
receive(status, callback){ | |
if(this.hasReceived(status)){ | |
callback(this.receivedResp.response) | |
} | |
this.recHooks.push({status, callback}) | |
return this | |
} | |
// private | |
reset(){ | |
this.cancelRefEvent() | |
this.ref = null | |
this.refEvent = null | |
this.receivedResp = null | |
this.sent = false | |
} | |
matchReceive({status, response, ref}){ | |
this.recHooks.filter( h => h.status === status ) | |
.forEach( h => h.callback(response) ) | |
} | |
cancelRefEvent(){ if(!this.refEvent){ return } | |
this.channel.off(this.refEvent) | |
} | |
cancelTimeout(){ | |
clearTimeout(this.timeoutTimer) | |
this.timeoutTimer = null | |
} | |
startTimeout(){ if(this.timeoutTimer){ this.cancelTimeout() } | |
this.ref = this.channel.socket.makeRef() | |
this.refEvent = this.channel.replyEventName(this.ref) | |
this.channel.on(this.refEvent, payload => { | |
this.cancelRefEvent() | |
this.cancelTimeout() | |
this.receivedResp = payload | |
this.matchReceive(payload) | |
}) | |
this.timeoutTimer = setTimeout(() => { | |
this.trigger("timeout", {}) | |
}, this.timeout) | |
} | |
hasReceived(status){ | |
return this.receivedResp && this.receivedResp.status === status | |
} | |
trigger(status, response){ | |
this.channel.trigger(this.refEvent, {status, response}) | |
} | |
} | |
/** | |
* | |
* @param {string} topic | |
* @param {Object} params | |
* @param {Socket} socket | |
*/ | |
class Channel { | |
constructor(topic, params, socket) { | |
this.state = CHANNEL_STATES.closed | |
this.topic = topic | |
this.params = params || {} | |
this.socket = socket | |
this.bindings = [] | |
this.timeout = this.socket.timeout | |
this.joinedOnce = false | |
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout) | |
this.pushBuffer = [] | |
this.rejoinTimer = new Timer( | |
() => this.rejoinUntilConnected(), | |
this.socket.reconnectAfterMs | |
) | |
this.joinPush.receive("ok", () => { | |
this.state = CHANNEL_STATES.joined | |
this.rejoinTimer.reset() | |
this.pushBuffer.forEach( pushEvent => pushEvent.send() ) | |
this.pushBuffer = [] | |
}) | |
this.onClose( () => { | |
this.rejoinTimer.reset() | |
this.socket.log("channel", `close ${this.topic} ${this.joinRef()}`) | |
this.state = CHANNEL_STATES.closed | |
this.socket.remove(this) | |
}) | |
this.onError( reason => { if(this.isLeaving() || this.isClosed()){ return } | |
this.socket.log("channel", `error ${this.topic}`, reason) | |
this.state = CHANNEL_STATES.errored | |
this.rejoinTimer.scheduleTimeout() | |
}) | |
this.joinPush.receive("timeout", () => { if(!this.isJoining()){ return } | |
this.socket.log("channel", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout) | |
let leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, this.timeout) | |
leavePush.send() | |
this.state = CHANNEL_STATES.errored | |
this.joinPush.reset() | |
this.rejoinTimer.scheduleTimeout() | |
}) | |
this.on(CHANNEL_EVENTS.reply, (payload, ref) => { | |
this.trigger(this.replyEventName(ref), payload) | |
}) | |
} | |
rejoinUntilConnected(){ | |
this.rejoinTimer.scheduleTimeout() | |
if(this.socket.isConnected()){ | |
this.rejoin() | |
} | |
} | |
join(timeout = this.timeout){ | |
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.rejoin(timeout) | |
return this.joinPush | |
} | |
} | |
onClose(callback){ this.on(CHANNEL_EVENTS.close, callback) } | |
onError(callback){ | |
this.on(CHANNEL_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.isJoined() } | |
push(event, payload, timeout = this.timeout){ | |
if(!this.joinedOnce){ | |
throw(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`) | |
} | |
let pushEvent = new Push(this, event, payload, timeout) | |
if(this.canPush()){ | |
pushEvent.send() | |
} else { | |
pushEvent.startTimeout() | |
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: | |
* | |
* ```javascript | |
* channel.leave().receive("ok", () => alert("left!") ) | |
* ``` | |
*/ | |
leave(timeout = this.timeout){ | |
this.state = CHANNEL_STATES.leaving | |
let onClose = () => { | |
this.socket.log("channel", `leave ${this.topic}`) | |
this.trigger(CHANNEL_EVENTS.close, "leave") | |
} | |
let leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout) | |
leavePush.receive("ok", () => onClose() ) | |
.receive("timeout", () => onClose() ) | |
leavePush.send() | |
if(!this.canPush()){ leavePush.trigger("ok", {}) } | |
return leavePush | |
} | |
/** | |
* Overridable message hook | |
* | |
* Receives all events for specialized message handling | |
* before dispatching to the channel callbacks. | |
* | |
* Must return the payload, modified or unmodified | |
*/ | |
onMessage(event, payload, ref){ return payload } | |
// private | |
isMember(topic, event, payload, joinRef){ | |
if(this.topic !== topic){ return false } | |
let isLifecycleEvent = CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0 | |
if(joinRef && isLifecycleEvent && joinRef !== this.joinRef()){ | |
this.socket.log("channel", "dropping outdated message", {topic, event, payload, joinRef}) | |
return false | |
} else { | |
return true | |
} | |
} | |
joinRef(){ return this.joinPush.ref } | |
sendJoin(timeout){ | |
this.state = CHANNEL_STATES.joining | |
this.joinPush.resend(timeout) | |
} | |
rejoin(timeout = this.timeout){ if(this.isLeaving()){ return } | |
this.sendJoin(timeout) | |
} | |
trigger(event, payload, ref, joinRef){ | |
let handledPayload = this.onMessage(event, payload, ref, joinRef) | |
if(payload && !handledPayload){ throw("channel onMessage callbacks must return the payload, modified or unmodified") } | |
this.bindings.filter( bind => bind.event === event) | |
.map( bind => bind.callback(handledPayload, ref, joinRef || this.joinRef())) | |
} | |
replyEventName(ref){ return `chan_reply_${ref}` } | |
isClosed() { return this.state === CHANNEL_STATES.closed } | |
isErrored(){ return this.state === CHANNEL_STATES.errored } | |
isJoined() { return this.state === CHANNEL_STATES.joined } | |
isJoining(){ return this.state === CHANNEL_STATES.joining } | |
isLeaving(){ return this.state === CHANNEL_STATES.leaving } | |
} | |
/* harmony export (immutable) */ __webpack_exports__["Channel"] = Channel; | |
let Serializer = { | |
encode(msg, callback){ | |
let payload = [ | |
msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload | |
] | |
return callback(JSON.stringify(payload)) | |
}, | |
decode(rawPayload, callback){ | |
let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload) | |
return callback({join_ref, ref, topic, event, payload}) | |
} | |
} | |
/** Initializes the Socket | |
* | |
* | |
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim) | |
* | |
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`, | |
* `"wss://example.com"` | |
* `"/socket"` (inherited host & protocol) | |
* @param {Object} opts - Optional configuration | |
* @param {string} opts.transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll. | |
* | |
* Defaults to WebSocket with automatic LongPoll fallback. | |
* @param {Function} opts.encode - The function to encode outgoing messages. | |
* | |
* Defaults to JSON: | |
* | |
* ```javascript | |
* (payload, callback) => callback(JSON.stringify(payload)) | |
* ``` | |
* | |
* @param {Function} opts.decode - The function to decode incoming messages. | |
* | |
* Defaults to JSON: | |
* | |
* ```javascript | |
* (payload, callback) => callback(JSON.parse(payload)) | |
* ``` | |
* | |
* @param {number} opts.timeout - The default timeout in milliseconds to trigger push timeouts. | |
* | |
* Defaults `DEFAULT_TIMEOUT` | |
* @param {number} opts.heartbeatIntervalMs - The millisec interval to send a heartbeat message | |
* @param {number} opts.reconnectAfterMs - The optional function that returns the millsec reconnect interval. | |
* | |
* Defaults to stepped backoff of: | |
* | |
* ```javascript | |
* function(tries){ | |
* return [1000, 5000, 10000][tries - 1] || 10000 | |
* } | |
* ``` | |
* @param {Function} opts.logger - The optional function for specialized logging, ie: | |
* ```javascript | |
* logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } | |
* ``` | |
* | |
* @param {number} opts.longpollerTimeout - The maximum timeout of a long poll AJAX request. | |
* | |
* Defaults to 20s (double the server long poll timer). | |
* | |
* @param {Object} opts.params - The optional params to pass when connecting | |
* | |
* | |
*/ | |
class Socket { | |
constructor(endPoint, opts = {}){ | |
this.stateChangeCallbacks = {open: [], close: [], error: [], message: []} | |
this.channels = [] | |
this.sendBuffer = [] | |
this.ref = 0 | |
this.timeout = opts.timeout || DEFAULT_TIMEOUT | |
this.transport = opts.transport || window.WebSocket || LongPoll | |
this.defaultEncoder = Serializer.encode | |
this.defaultDecoder = Serializer.decode | |
if(this.transport !== LongPoll){ | |
this.encode = opts.encode || this.defaultEncoder | |
this.decode = opts.decode || this.defaultDecoder | |
} else { | |
this.encode = this.defaultEncoder | |
this.decode = this.defaultDecoder | |
} | |
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000 | |
this.reconnectAfterMs = opts.reconnectAfterMs || function(tries){ | |
return [1000, 2000, 5000, 10000][tries - 1] || 10000 | |
} | |
this.logger = opts.logger || function(){} // noop | |
this.longpollerTimeout = opts.longpollerTimeout || 20000 | |
this.params = opts.params || {} | |
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}` | |
this.heartbeatTimer = null | |
this.pendingHeartbeatRef = null | |
this.reconnectTimer = new Timer(() => { | |
this.disconnect(() => this.connect()) | |
}, this.reconnectAfterMs) | |
} | |
protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" } | |
endPointURL(){ | |
let uri = Ajax.appendParams( | |
Ajax.appendParams(this.endPoint, this.params), {vsn: VSN}) | |
if(uri.charAt(0) !== "/"){ return uri } | |
if(uri.charAt(1) === "/"){ return `${this.protocol()}:${uri}` } | |
return `${this.protocol()}://${location.host}${uri}` | |
} | |
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() | |
} | |
/** | |
* | |
* @param {Object} params - The params to send when connecting, for example `{user_id: userToken}` | |
*/ | |
connect(params){ | |
if(params){ | |
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor") | |
this.params = params | |
} | |
if(this.conn){ return } | |
this.conn = new this.transport(this.endPointURL()) | |
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 | |
* @param {string} kind | |
* @param {string} msg | |
* @param {Object} data | |
*/ | |
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.endPointURL()}`) | |
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.scheduleTimeout() | |
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( channel => channel.trigger(CHANNEL_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(channel){ | |
this.channels = this.channels.filter(c => c.joinRef() !== channel.joinRef()) | |
} | |
/** | |
* Initiates a new channel for the given topic | |
* | |
* @param {string} topic | |
* @param {Object} chanParams - Paramaters for the channel | |
* @returns {Channel} | |
*/ | |
channel(topic, chanParams = {}){ | |
let chan = new Channel(topic, chanParams, this) | |
this.channels.push(chan) | |
return chan | |
} | |
push(data){ | |
let {topic, event, payload, ref, join_ref} = data | |
let callback = () => { | |
this.encode(data, result => { | |
this.conn.send(result) | |
}) | |
} | |
this.log("push", `${topic} ${event} (${join_ref}, ${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(){ if(!this.isConnected()){ return } | |
if(this.pendingHeartbeatRef){ | |
this.pendingHeartbeatRef = null | |
this.log("transport", "heartbeat timeout. Attempting to re-establish connection") | |
this.conn.close(WS_CLOSE_NORMAL, "hearbeat timeout") | |
return | |
} | |
this.pendingHeartbeatRef = this.makeRef() | |
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef}) | |
} | |
flushSendBuffer(){ | |
if(this.isConnected() && this.sendBuffer.length > 0){ | |
this.sendBuffer.forEach( callback => callback() ) | |
this.sendBuffer = [] | |
} | |
} | |
onConnMessage(rawMessage){ | |
this.decode(rawMessage.data, msg => { | |
let {topic, event, payload, ref, join_ref} = msg | |
if(ref && ref === this.pendingHeartbeatRef){ this.pendingHeartbeatRef = null } | |
this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`, payload) | |
this.channels.filter( channel => channel.isMember(topic, event, payload, join_ref) ) | |
.forEach( channel => channel.trigger(event, payload, ref, join_ref) ) | |
this.stateChangeCallbacks.message.forEach( callback => callback(msg) ) | |
}) | |
} | |
} | |
/* harmony export (immutable) */ __webpack_exports__["Socket"] = Socket; | |
class LongPoll { | |
constructor(endPoint){ | |
this.endPoint = null | |
this.token = null | |
this.skipHeartbeat = true | |
this.onopen = function(){} // noop | |
this.onerror = function(){} // noop | |
this.onmessage = function(){} // noop | |
this.onclose = function(){} // noop | |
this.pollEndpoint = this.normalizeEndpoint(endPoint) | |
this.readyState = SOCKET_STATES.connecting | |
this.poll() | |
} | |
normalizeEndpoint(endPoint){ | |
return(endPoint | |
.replace("ws://", "http://") | |
.replace("wss://", "https://") | |
.replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll)) | |
} | |
endpointURL(){ | |
return Ajax.appendParams(this.pollEndpoint, {token: this.token}) | |
} | |
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, messages} = resp | |
this.token = token | |
} else{ | |
var status = 0 | |
} | |
switch(status){ | |
case 200: | |
messages.forEach(msg => this.onmessage({data: 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(resp && resp.status) | |
this.closeAndRetry() | |
} | |
}) | |
} | |
close(code, reason){ | |
this.readyState = SOCKET_STATES.closed | |
this.onclose() | |
} | |
} | |
/* harmony export (immutable) */ __webpack_exports__["LongPoll"] = LongPoll; | |
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 window.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.open(method, endPoint, true) | |
req.timeout = timeout | |
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){ | |
if(!resp || resp === ""){ return null } | |
try { | |
return JSON.parse(resp) | |
} catch(e) { | |
console && console.log("failed to parse JSON response", resp) | |
return null | |
} | |
} | |
static serialize(obj, parentKey){ | |
let queryStr = []; | |
for(var key in obj){ if(!obj.hasOwnProperty(key)){ continue } | |
let paramKey = parentKey ? `${parentKey}[${key}]` : key | |
let paramVal = obj[key] | |
if(typeof paramVal === "object"){ | |
queryStr.push(this.serialize(paramVal, paramKey)) | |
} else { | |
queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal)) | |
} | |
} | |
return queryStr.join("&") | |
} | |
static appendParams(url, params){ | |
if(Object.keys(params).length === 0){ return url } | |
let prefix = url.match(/\?/) ? "&" : "?" | |
return `${url}${prefix}${this.serialize(params)}` | |
} | |
} | |
/* harmony export (immutable) */ __webpack_exports__["Ajax"] = Ajax; | |
Ajax.states = {complete: 4} | |
var Presence = { | |
syncState(currentState, newState, onJoin, onLeave){ | |
let state = this.clone(currentState) | |
let joins = {} | |
let leaves = {} | |
this.map(state, (key, presence) => { | |
if(!newState[key]){ | |
leaves[key] = presence | |
} | |
}) | |
this.map(newState, (key, newPresence) => { | |
let currentPresence = state[key] | |
if(currentPresence){ | |
let newRefs = newPresence.metas.map(m => m.phx_ref) | |
let curRefs = currentPresence.metas.map(m => m.phx_ref) | |
let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0) | |
let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0) | |
if(joinedMetas.length > 0){ | |
joins[key] = newPresence | |
joins[key].metas = joinedMetas | |
} | |
if(leftMetas.length > 0){ | |
leaves[key] = this.clone(currentPresence) | |
leaves[key].metas = leftMetas | |
} | |
} else { | |
joins[key] = newPresence | |
} | |
}) | |
return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave) | |
}, | |
syncDiff(currentState, {joins, leaves}, onJoin, onLeave){ | |
let state = this.clone(currentState) | |
if(!onJoin){ onJoin = function(){} } | |
if(!onLeave){ onLeave = function(){} } | |
this.map(joins, (key, newPresence) => { | |
let currentPresence = state[key] | |
state[key] = newPresence | |
if(currentPresence){ | |
state[key].metas.unshift(...currentPresence.metas) | |
} | |
onJoin(key, currentPresence, newPresence) | |
}) | |
this.map(leaves, (key, leftPresence) => { | |
let currentPresence = state[key] | |
if(!currentPresence){ return } | |
let refsToRemove = leftPresence.metas.map(m => m.phx_ref) | |
currentPresence.metas = currentPresence.metas.filter(p => { | |
return refsToRemove.indexOf(p.phx_ref) < 0 | |
}) | |
onLeave(key, currentPresence, leftPresence) | |
if(currentPresence.metas.length === 0){ | |
delete state[key] | |
} | |
}) | |
return state | |
}, | |
list(presences, chooser){ | |
if(!chooser){ chooser = function(key, pres){ return pres } } | |
return this.map(presences, (key, presence) => { | |
return chooser(key, presence) | |
}) | |
}, | |
// private | |
map(obj, func){ | |
return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key])) | |
}, | |
clone(obj){ return JSON.parse(JSON.stringify(obj)) } | |
} | |
/** | |
* | |
* Creates a timer that accepts a `timerCalc` function to perform | |
* calculated timeout retries, such as exponential backoff. | |
* | |
* ## Examples | |
* | |
* ```javascript | |
* let reconnectTimer = new Timer(() => this.connect(), function(tries){ | |
* return [1000, 5000, 10000][tries - 1] || 10000 | |
* }) | |
* reconnectTimer.scheduleTimeout() // fires after 1000 | |
* reconnectTimer.scheduleTimeout() // fires after 5000 | |
* reconnectTimer.reset() | |
* reconnectTimer.scheduleTimeout() // fires after 1000 | |
* ``` | |
* @param {Function} callback | |
* @param {Function} timerCalc | |
*/ | |
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 scheduleTimeout and schedules callback | |
*/ | |
scheduleTimeout(){ | |
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