Socket
Socket
Sign inDemoInstall

phoenix

Package Overview
Dependencies
0
Maintainers
2
Versions
79
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.3.4 to 1.4.0

assets/package.json

689

assets/js/phoenix.js

@@ -11,4 +11,4 @@ /**

* ```javascript
* let socket = new Socket("/socket", {params: {userToken: "123"}})
* socket.connect()
* let socket = new Socket("/socket", {params: {userToken: "123"}})
* socket.connect()
* ```

@@ -31,14 +31,15 @@ *

* ```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...") )
* 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..."))
*```

@@ -71,3 +72,3 @@ *

* `receive("timeout", callback)` to abort waiting for our other `receive` hooks
* and take action after some period of waiting. The default timeout is 5000ms.
* and take action after some period of waiting. The default timeout is 10000ms.
*

@@ -81,4 +82,4 @@ *

* ```javascript
* socket.onError( () => console.log("there was an error with the connection!") )
* socket.onClose( () => console.log("the connection dropped") )
* socket.onError( () => console.log("there was an error with the connection!") )
* socket.onClose( () => console.log("the connection dropped") )
* ```

@@ -93,4 +94,4 @@ *

* ```javascript
* channel.onError( () => console.log("there was an error!") )
* channel.onClose( () => console.log("the channel has gone away gracefully") )
* channel.onError( () => console.log("there was an error!") )
* channel.onClose( () => console.log("the channel has gone away gracefully") )
* ```

@@ -116,17 +117,25 @@ *

*
* ### Syncing initial state from the server
* ### Syncing 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.
* To sync presence state from the server, first instantiate an object and
* pass your channel in to track lifecycle events:
*
* `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.
* ```javascript
* let channel = new socket.channel("some:topic")
* let presence = new Presence(channel)
* ```
*
* Next, use the `presence.onSync` callback to react to state changes
* from the server. For example, to render the list of users every time
* the list changes, you could write:
*
* ```javascript
* presence.onSync(() => {
* myRenderUsersFunction(presence.list())
* })
* ```
*
* ### Listing Presences
*
* `Presence.list` is used to return a list of presence information
* `presence.list` is used to return a list of presence information
* based on the local state of metadata. By default, all presence

@@ -144,42 +153,39 @@ * metadata is returned, but a `listBy` function can be supplied to

* ```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)
* 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(listBy)
* ```
*
* ### Handling individual presence join and leave events
*
* ### 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)})
* })
* The `presence.onJoin` and `presence.onLeave` callbacks can be used to
* react to individual presences joining and leaving the app. For example:
*
* ```javascript
* let presence = new Presence(channel)
*
* // detect if user has joined for the 1st time or from another tab/device
* presence.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
* presence.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)
* }
* })
* // receive presence data from server
* presence.onSync(() => {
* displayUsers(presence.list())
* })
* ```

@@ -189,2 +195,3 @@ * @module phoenix

const global = typeof self !== "undefined" ? self : window
const VSN = "2.0.0"

@@ -220,2 +227,12 @@ const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}

// wraps value in closure or returns closure
let closure = (value) => {
if(typeof value === "function"){
return value
} else {
let closure = function(){ return value }
return closure
}
}
/**

@@ -229,7 +246,6 @@ * Initializes the Push

class Push {
constructor(channel, event, payload, timeout){
this.channel = channel
this.event = event
this.payload = payload || {}
this.payload = payload || function(){ return {} }
this.receivedResp = null

@@ -261,3 +277,3 @@ this.timeout = timeout

event: this.event,
payload: this.payload,
payload: this.payload(),
ref: this.ref,

@@ -282,5 +298,5 @@ join_ref: this.channel.joinRef()

// private
/**
* @private
*/
reset(){

@@ -294,2 +310,5 @@ this.cancelRefEvent()

/**
* @private
*/
matchReceive({status, response, ref}){

@@ -300,2 +319,5 @@ this.recHooks.filter( h => h.status === status )

/**
* @private
*/
cancelRefEvent(){ if(!this.refEvent){ return }

@@ -305,2 +327,5 @@ this.channel.off(this.refEvent)

/**
* @private
*/
cancelTimeout(){

@@ -311,2 +336,5 @@ clearTimeout(this.timeoutTimer)

/**
* @private
*/
startTimeout(){ if(this.timeoutTimer){ this.cancelTimeout() }

@@ -328,2 +356,5 @@ this.ref = this.channel.socket.makeRef()

/**
* @private
*/
hasReceived(status){

@@ -333,2 +364,5 @@ return this.receivedResp && this.receivedResp.status === status

/**
* @private
*/
trigger(status, response){

@@ -342,3 +376,3 @@ this.channel.trigger(this.refEvent, {status, response})

* @param {string} topic
* @param {Object} params
* @param {(Object|function)} params
* @param {Socket} socket

@@ -350,5 +384,6 @@ */

this.topic = topic
this.params = params || {}
this.params = closure(params || {})
this.socket = socket
this.bindings = []
this.bindingRef = 0
this.timeout = this.socket.timeout

@@ -370,3 +405,3 @@ this.joinedOnce = false

this.rejoinTimer.reset()
this.socket.log("channel", `close ${this.topic} ${this.joinRef()}`)
if (this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic} ${this.joinRef()}`)
this.state = CHANNEL_STATES.closed

@@ -376,3 +411,3 @@ this.socket.remove(this)

this.onError( reason => { if(this.isLeaving() || this.isClosed()){ return }
this.socket.log("channel", `error ${this.topic}`, reason)
if (this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
this.state = CHANNEL_STATES.errored

@@ -382,4 +417,4 @@ 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)
if (this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)
leavePush.send()

@@ -395,2 +430,5 @@ this.state = CHANNEL_STATES.errored

/**
* @private
*/
rejoinUntilConnected(){

@@ -403,2 +441,7 @@ this.rejoinTimer.scheduleTimeout()

/**
* Join the channel
* @param {integer} timeout
* @returns {Push}
*/
join(timeout = this.timeout){

@@ -414,14 +457,62 @@ if(this.joinedOnce){

onClose(callback){ this.on(CHANNEL_EVENTS.close, callback) }
/**
* Hook into channel close
* @param {Function} callback
*/
onClose(callback){
this.on(CHANNEL_EVENTS.close, callback)
}
/**
* Hook into channel errors
* @param {Function} callback
*/
onError(callback){
this.on(CHANNEL_EVENTS.error, reason => callback(reason) )
return this.on(CHANNEL_EVENTS.error, reason => callback(reason))
}
on(event, callback){ this.bindings.push({event, callback}) }
/**
* Subscribes on channel events
*
* Subscription returns a ref counter, which can be used later to
* unsubscribe the exact event listener
*
* @example
* const ref1 = channel.on("event", do_stuff)
* const ref2 = channel.on("event", do_other_stuff)
* channel.off("event", ref1)
* // Since unsubscription, do_stuff won't fire,
* // while do_other_stuff will keep firing on the "event"
*
* @param {string} event
* @param {Function} callback
* @returns {integer} ref
*/
on(event, callback){
let ref = this.bindingRef++
this.bindings.push({event, ref, callback})
return ref
}
off(event){ this.bindings = this.bindings.filter( bind => bind.event !== event ) }
/**
* @param {string} event
* @param {integer} ref
*/
off(event, ref){
this.bindings = this.bindings.filter((bind) => {
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref))
})
}
/**
* @private
*/
canPush(){ return this.socket.isConnected() && this.isJoined() }
/**
* @param {string} event
* @param {Object} payload
* @param {number} [timeout]
* @returns {Push}
*/
push(event, payload, timeout = this.timeout){

@@ -431,3 +522,3 @@ if(!this.joinedOnce){

}
let pushEvent = new Push(this, event, payload, timeout)
let pushEvent = new Push(this, event, function(){ return payload }, timeout)
if(this.canPush()){

@@ -453,5 +544,7 @@ pushEvent.send()

*
* ```javascript
* channel.leave().receive("ok", () => alert("left!") )
* ```
* @example
* channel.leave().receive("ok", () => alert("left!") )
*
* @param {integer} timeout
* @returns {Push}
*/

@@ -461,6 +554,6 @@ leave(timeout = this.timeout){

let onClose = () => {
this.socket.log("channel", `leave ${this.topic}`)
if (this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`)
this.trigger(CHANNEL_EVENTS.close, "leave")
}
let leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout)
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)
leavePush.receive("ok", () => onClose() )

@@ -481,14 +574,22 @@ .receive("timeout", () => onClose() )

* Must return the payload, modified or unmodified
* @param {string} event
* @param {Object} payload
* @param {integer} ref
* @returns {Object}
*/
onMessage(event, payload, ref){ return payload }
/**
* @private
*/
isLifecycleEvent(event) { return CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0 }
// private
/**
* @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})
if(joinRef && joinRef !== this.joinRef() && this.isLifecycleEvent(event)){
if (this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {topic, event, payload, joinRef})
return false

@@ -500,4 +601,10 @@ } else {

/**
* @private
*/
joinRef(){ return this.joinPush.ref }
/**
* @private
*/
sendJoin(timeout){

@@ -508,2 +615,5 @@ this.state = CHANNEL_STATES.joining

/**
* @private
*/
rejoin(timeout = this.timeout){ if(this.isLeaving()){ return }

@@ -513,2 +623,5 @@ this.sendJoin(timeout)

/**
* @private
*/
trigger(event, payload, ref, joinRef){

@@ -518,16 +631,41 @@ let handledPayload = this.onMessage(event, payload, ref, joinRef)

this.bindings.filter( bind => bind.event === event)
.map( bind => bind.callback(handledPayload, ref, joinRef || this.joinRef()))
for (let i = 0; i < this.bindings.length; i++) {
const bind = this.bindings[i]
if(bind.event !== event){ continue }
bind.callback(handledPayload, ref, joinRef || this.joinRef())
}
}
/**
* @private
*/
replyEventName(ref){ return `chan_reply_${ref}` }
/**
* @private
*/
isClosed() { return this.state === CHANNEL_STATES.closed }
/**
* @private
*/
isErrored(){ return this.state === CHANNEL_STATES.errored }
/**
* @private
*/
isJoined() { return this.state === CHANNEL_STATES.joined }
/**
* @private
*/
isJoining(){ return this.state === CHANNEL_STATES.joining }
/**
* @private
*/
isLeaving(){ return this.state === CHANNEL_STATES.leaving }
}
let Serializer = {
const Serializer = {
encode(msg, callback){

@@ -556,7 +694,7 @@ let payload = [

* `"/socket"` (inherited host & protocol)
* @param {Object} opts - Optional configuration
* @param {string} opts.transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
* @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.
* @param {Function} [opts.encode] - The function to encode outgoing messages.
*

@@ -569,3 +707,3 @@ * Defaults to JSON:

*
* @param {Function} opts.decode - The function to decode incoming messages.
* @param {Function} [opts.decode] - The function to decode incoming messages.
*

@@ -578,7 +716,7 @@ * Defaults to JSON:

*
* @param {number} opts.timeout - The default timeout in milliseconds to trigger push timeouts.
* @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.
* @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.
*

@@ -588,16 +726,18 @@ * Defaults to stepped backoff of:

* ```javascript
* function(tries){
* return [1000, 5000, 10000][tries - 1] || 10000
* }
* function(tries){
* return [1000, 5000, 10000][tries - 1] || 10000
* }
* ```
* @param {Function} opts.logger - The optional function for specialized logging, ie:
* @param {Function} [opts.logger] - The optional function for specialized logging, ie:
* ```javascript
* logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
* function(kind, msg, data) {
* console.log(`${kind}: ${msg}`, data)
* }
* ```
*
* @param {number} opts.longpollerTimeout - The maximum timeout of a long poll AJAX request.
* @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
* @param {{Object|function)} [opts.params] - The optional params to pass when connecting
*

@@ -607,3 +747,2 @@ *

export class Socket {
constructor(endPoint, opts = {}){

@@ -615,3 +754,3 @@ this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}

this.timeout = opts.timeout || DEFAULT_TIMEOUT
this.transport = opts.transport || window.WebSocket || LongPoll
this.transport = opts.transport || global.WebSocket || LongPoll
this.defaultEncoder = Serializer.encode

@@ -630,5 +769,5 @@ this.defaultDecoder = Serializer.decode

}
this.logger = opts.logger || function(){} // noop
this.logger = opts.logger || null
this.longpollerTimeout = opts.longpollerTimeout || 20000
this.params = opts.params || {}
this.params = closure(opts.params || {})
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`

@@ -638,11 +777,21 @@ this.heartbeatTimer = null

this.reconnectTimer = new Timer(() => {
this.disconnect(() => this.connect())
this.teardown(() => this.connect())
}, this.reconnectAfterMs)
}
/**
* Returns the socket protocol
*
* @returns {string}
*/
protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" }
/**
* The fully qualifed socket url
*
* @returns {string}
*/
endPointURL(){
let uri = Ajax.appendParams(
Ajax.appendParams(this.endPoint, this.params), {vsn: VSN})
Ajax.appendParams(this.endPoint, this.params()), {vsn: VSN})
if(uri.charAt(0) !== "/"){ return uri }

@@ -654,9 +803,10 @@ if(uri.charAt(1) === "/"){ return `${this.protocol()}:${uri}` }

/**
* @param {Function} callback
* @param {integer} code
* @param {string} reason
*/
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()
this.reconnectTimer.reset()
this.teardown(callback, code, reason)
}

@@ -671,3 +821,3 @@

console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor")
this.params = params
this.params = closure(params)
}

@@ -692,34 +842,83 @@ if(this.conn){ return }

// 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) }
/**
* Returns true if a logger has been set on this socket.
*/
hasLogger(){ return this.logger !== null }
/**
* Registers callbacks for connection open events
*
* @example socket.onOpen(function(){ console.info("the socket was opened") })
*
* @param {Function} callback
*/
onOpen(callback){ this.stateChangeCallbacks.open.push(callback) }
/**
* Registers callbacks for connection close events
* @param {Function} callback
*/
onClose(callback){ this.stateChangeCallbacks.close.push(callback) }
/**
* Registers callbacks for connection error events
*
* @example socket.onError(function(error){ alert("An error occurred") })
*
* @param {Function} callback
*/
onError(callback){ this.stateChangeCallbacks.error.push(callback) }
/**
* Registers callbacks for connection message events
* @param {Function} callback
*/
onMessage(callback){ this.stateChangeCallbacks.message.push(callback) }
/**
* @private
*/
onConnOpen(){
this.log("transport", `connected to ${this.endPointURL()}`)
if (this.hasLogger()) 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.resetHeartbeat()
this.resetChannelTimers()
this.stateChangeCallbacks.open.forEach( callback => callback() )
}
/**
* @private
*/
resetHeartbeat(){ if(this.conn.skipHeartbeat){ return }
this.pendingHeartbeatRef = null
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
}
teardown(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()
}
onConnClose(event){
this.log("transport", "close", event)
if (this.hasLogger()) this.log("transport", "close", event)
this.triggerChanError()
clearInterval(this.heartbeatTimer)
this.reconnectTimer.scheduleTimeout()
if(event && event.code !== WS_CLOSE_NORMAL) {
this.reconnectTimer.scheduleTimeout()
}
this.stateChangeCallbacks.close.forEach( callback => callback(event) )
}
/**
* @private
*/
onConnError(error){
this.log("transport", error)
if (this.hasLogger()) this.log("transport", error)
this.triggerChanError()

@@ -729,2 +928,5 @@ this.stateChangeCallbacks.error.forEach( callback => callback(error) )

/**
* @private
*/
triggerChanError(){

@@ -734,2 +936,5 @@ this.channels.forEach( channel => channel.trigger(CHANNEL_EVENTS.error) )

/**
* @returns {string}
*/
connectionState(){

@@ -744,4 +949,10 @@ switch(this.conn && this.conn.readyState){

/**
* @returns {boolean}
*/
isConnected(){ return this.connectionState() === "open" }
/**
* @param {Channel}
*/
remove(channel){

@@ -755,3 +966,3 @@ this.channels = this.channels.filter(c => c.joinRef() !== channel.joinRef())

* @param {string} topic
* @param {Object} chanParams - Paramaters for the channel
* @param {Object} chanParams - Parameters for the channel
* @returns {Channel}

@@ -765,16 +976,16 @@ */

/**
* @param {Object} data
*/
push(data){
let {topic, event, payload, ref, join_ref} = data
let callback = () => {
this.encode(data, result => {
this.conn.send(result)
})
if (this.hasLogger()) {
let {topic, event, payload, ref, join_ref} = data
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload)
}
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload)
if(this.isConnected()){
callback()
this.encode(data, result => this.conn.send(result))
} else {
this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))
}
else {
this.sendBuffer.push(callback)
}
}

@@ -784,2 +995,3 @@

* Return the next message ref, accounting for overflows
* @returns {string}
*/

@@ -796,3 +1008,3 @@ makeRef(){

this.pendingHeartbeatRef = null
this.log("transport", "heartbeat timeout. Attempting to re-establish connection")
if (this.hasLogger()) this.log("transport", "heartbeat timeout. Attempting to re-establish connection")
this.conn.close(WS_CLOSE_NORMAL, "hearbeat timeout")

@@ -817,8 +1029,24 @@ return

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) )
if (this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`, payload)
for (let i = 0; i < this.channels.length; i++) {
const channel = this.channels[i]
if(!channel.isMember(topic, event, payload, join_ref)){ continue }
channel.trigger(event, payload, ref, join_ref)
}
for (let i = 0; i < this.stateChangeCallbacks.message.length; i++) {
this.stateChangeCallbacks.message[i](msg)
}
})
}
/**
* @private
*/
resetChannelTimers() {
this.channels.forEach(channel => {
channel.rejoinTimer.restart()
})
}
}

@@ -916,8 +1144,8 @@

static request(method, endPoint, accept, body, timeout, ontimeout, callback){
if(window.XDomainRequest){
if(global.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
let req = global.XMLHttpRequest ?
new global.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5

@@ -971,3 +1199,3 @@ this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)

static serialize(obj, parentKey){
let queryStr = [];
let queryStr = []
for(var key in obj){ if(!obj.hasOwnProperty(key)){ continue }

@@ -995,7 +1223,70 @@ let paramKey = parentKey ? `${parentKey}[${key}]` : key

/**
* Initializes the Presence
* @param {Channel} channel - The Channel
* @param {Object} opts - The options,
* for example `{events: {state: "state", diff: "diff"}}`
*/
export class Presence {
constructor(channel, opts = {}){
let events = opts.events || {state: "presence_state", diff: "presence_diff"}
this.state = {}
this.pendingDiffs = []
this.channel = channel
this.joinRef = null
this.caller = {
onJoin: function(){},
onLeave: function(){},
onSync: function(){}
}
export var Presence = {
this.channel.on(events.state, newState => {
let {onJoin, onLeave, onSync} = this.caller
syncState(currentState, newState, onJoin, onLeave){
this.joinRef = this.channel.joinRef()
this.state = Presence.syncState(this.state, newState, onJoin, onLeave)
this.pendingDiffs.forEach(diff => {
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
})
this.pendingDiffs = []
onSync()
})
this.channel.on(events.diff, diff => {
let {onJoin, onLeave, onSync} = this.caller
if(this.inPendingSyncState()){
this.pendingDiffs.push(diff)
} else {
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
onSync()
}
})
}
onJoin(callback){ this.caller.onJoin = callback }
onLeave(callback){ this.caller.onLeave = callback }
onSync(callback){ this.caller.onSync = callback }
list(by){ return Presence.list(this.state, by) }
inPendingSyncState(){
return !this.joinRef || (this.joinRef !== this.channel.joinRef())
}
// lower-level public static API
/**
* 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.
*
* @returns {Presence}
*/
static syncState(currentState, newState, onJoin, onLeave){
let state = this.clone(currentState)

@@ -1030,5 +1321,14 @@ let joins = {}

return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)
},
}
syncDiff(currentState, {joins, leaves}, onJoin, onLeave){
/**
*
* 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.
*
* @returns {Presence}
*/
static syncDiff(currentState, {joins, leaves}, onJoin, onLeave){
let state = this.clone(currentState)

@@ -1042,3 +1342,5 @@ if(!onJoin){ onJoin = function(){} }

if(currentPresence){
state[key].metas.unshift(...currentPresence.metas)
let joinedRefs = state[key].metas.map(m => m.phx_ref)
let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)
state[key].metas.unshift(...curMetas)
}

@@ -1060,5 +1362,13 @@ onJoin(key, currentPresence, newPresence)

return state
},
}
list(presences, chooser){
/**
* Returns the array of presences, with selected metadata.
*
* @param {Object} presences
* @param {Function} chooser
*
* @returns {Presence}
*/
static list(presences, chooser){
if(!chooser){ chooser = function(key, pres){ return pres } }

@@ -1069,11 +1379,11 @@

})
},
}
// private
map(obj, func){
static map(obj, func){
return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))
},
}
clone(obj){ return JSON.parse(JSON.stringify(obj)) }
static clone(obj){ return JSON.parse(JSON.stringify(obj)) }
}

@@ -1087,13 +1397,11 @@

*
* ## Examples
* @example
* 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
*
* ```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

@@ -1112,5 +1420,13 @@ * @param {Function} timerCalc

this.tries = 0
clearTimeout(this.timer)
this.clearTimer()
}
restart(){
const processing = this.timer !== null
this.reset()
if (processing){
this.scheduleTimeout()
}
}
/**

@@ -1120,3 +1436,3 @@ * Cancels any previous scheduleTimeout and schedules callback

scheduleTimeout(){
clearTimeout(this.timer)
this.clearTimer()

@@ -1128,2 +1444,7 @@ this.timer = setTimeout(() => {

}
clearTimer() {
clearTimeout(this.timer)
this.timer = null
}
}

@@ -1,785 +0,87 @@

# Changelog
# Changelog for v1.4
## 1.3.4 (2018-08-06)
See the [upgrade guides](https://gist.github.com/chrismccord/bb1f8b136f5a9e4abc0bfc07b832257e) to bring your Phoenix 1.3.x apps up to speed, including instructions on upgrading to Cowboy 2 for HTTP support.
* Bug Fixes
* [CodeReloader] Fix code reloader failing to properly report Elixir 1.6+ compiler errors
## The Socket <-> Transport contract
## 1.3.3 (2018-06-15)
We have used the opportunity of writing the new Cowboy 2 adapter to do an overhaul in how `Phoenix.Socket` interacts with transports. The result is a new API that makes it very easy to implement new transports and also allows developers to provide custom socket implementations without ceremony. For example, if you would like to have direct control of the socket and bypass the channel implementation completely, it is now very straight-forward to do so. See the `Phoenix.Socket.Transport` behaviour for more information.
* Enhancements
* [Router] cache pipeline compilation to improve performance
This overhaul means that the `transport/3` macro in `Phoenix.Socket` is deprecated. Instead of defining transports in your socket.ex file:
* Bug Fixes
* [Phoenix.Router] Fix reraise deprecation warnings when using Plug 1.5 and above
* [Digester] Fix option parser warnings
transport :websocket, Phoenix.Transport.Websocket,
key1: value1, key2: value2, key3: value3
* Deprecations
* [Socket] deprecate via opton in channel/3 macro
transport :longpoll, Phoenix.Transport.LongPoll,
key1: value1, key2: value2, key3: value3
## 1.3.2 (2018-03-08)
Configurations must be applied directly in your endpoint file via the `Phoenix.Endpoint.socket/3` macro:
* Bug Fixes
* [CodeReloader] Fix error when running `phx.server` from umbrella root
socket "/socket", MyApp.UserSocket,
websocket: [key1: value1, key2: value2, key3: value3],
longpoll: [key1: value1, key2: value2, key3: value3]
## 1.3.1 (2018-03-02)
Note the websocket/longpoll configuration given to socket/3 will only apply after you remove all `transport/3` calls from your socket definition. If you have explicitly upgraded to Cowboy 2, any transport defined with the `transport/3` macro will be ignored.
* Enhancements
* [Router] Raise on unknown `:only` and `:except` in `resources`.
* [Router] Include line numbers in generated routes for better reporting
* [Logger] Add parameter keep strategy to filter all parameters by default and selectively keep parameters
* [Token] Support `:infinity` for token max age
* [Presence] Generate `child_spec` for Elixir 1.5+ support
The old APIs for building transports are also deprecated. The good news is: adapting an existing transport to the new API is a less error prone process where you should mostly remove code.
* Bug Fixes
* [Transports.WebSocketSerializer] Do not encode `:join_ref` in v1 messages
## 1.3.0 (2017-07-28)
## 1.4.0 (2018-11-07)
See these [`1.2.x` to `1.3.x` upgrade instructions](https://gist.github.com/chrismccord/71ab10d433c98b714b75c886eff17357) to bring your existing apps up to speed.
### Enhancements
* [phx.new] Update Ecto deps with the release of Ecto 3.0 including `phoenix_ecto` 4.0
* [phx.new] Import Ecto's `.formatter.exs` in new projects
* [phx.new] Use Ecto 3.0RC, with `ecto_sql` in new project deps
* [phx.new] Use Plug 1.7 with new `:plug_cowboy` dependency for cowboy adapter
* [phx.gen.html|json|schema|context] Support new Ecto 3.0 usec datetime types
* [Phoenix] Add `Phoenix.json_library/0` and replace `Poison` with `Jason` for JSON encoding in new projects
* [Endpoint] Add `Cowboy2Adapter` for HTTP2 support with cowboy2
* [Endpoint] The `socket/3` macro now accepts direct configuration about websockets and longpoll
* [Endpoint] Support MFA function in `:check_origin` config for custom origin checking
* [Endpoint] Add new `:phoenix_error_render` instrumentation callback
* [Endpoint] Log the configured url instead of raw IP when booting endpoint webserver
* [Endpoint] Allow custom keyword pairs to be passed to the socket `:connect_info` options.
* [Router] Display list of available routes on debugger 404 error page
* [Router] Raise on duplicate plugs in `pipe_through` scopes
* [Controller] Support partial file downloads with `:offset` and `:length` options to `send_download/3`
* [Controller] Add additional security headers to `put_secure_browser_headers` (`x-content-type-options`, `x-download-options`, and `x-permitted-cross-domain-policies`)
* [Controller] Add `put_router_url/2` to override the default URL generation pulled from endpoint configuration
* [Logger] Add whitelist support to `filter_parameters` logger configuration, via new `:keep` tuple format
* [Socket] Add new `phoenix_socket_connect` instrumentation
* [Socket] Improve error message when missing socket mount in endpoint
* [Logger] Log calls to user socket connect
* [Presence] Add `Presence.get_by_key` to fetch presences for specific user
* [CodeReloader] Add `:reloadable_apps` endpoint configuration option to allow recompiling local dependencies
* [ChannelTest] Respect user's configured ExUnit `:assert_receive_timeout` for macro assertions
* Enhancements
* [Generator] Add new `phx.new`, `phx.new.web`, `phx.new.ecto` project generators with improved application structure and support for umbrella applications
* [Generator] Add new `phx.gen.html` and `phx.gen.json` resource generators with improved isolation of API boundaries
* [Controller] Add `current_path` and `current_url` to generate a connection's path and url
* [Controller] Introduce `action_fallback` to registers a plug to call as a fallback to the controller action
* [Controller] Wrap exceptions at controller to maintain connection state
* [Channel] Add ability to configure channel event logging with `:log_join` and `:log_handle_in` options
* [Channel] Warn on unhandled `handle_info/2` messages
* [Channel] Channels now distinguish from graceful exits and application restarts, allowing clients to enter error mode and reconnected after cold deploys.
* [Channel] Add V2 of wire channel wire protocol with resolved race conditions and compacted payloads
* [ChannelTest] Subscribe `connect` to `UserSocket.id` to support testing forceful disconnects
* [Socket] Support static `:assigns` when defining channel routes
* [Router] Document `match` support for matching on any HTTP method with the special `:*` argument
* [Router] Populate `conn.path_params` with path parameters for the route
* [ConnTest] Add `redirected_params/1` to return the named params matched in the router for the redirected URL
* [Digester] Add `mix phx.digest.clean` to remove old versions of compiled assets
* [phx.new] Add Erlang 20 support in `phx.new` installer archive
* [phx.new] Use new `lib/my_app` and `lib/my_app_web` directory structure
* [phx.new] Use new `MyAppWeb` alias convention for web modules
* Bug Fixes
* [Controller] Harden local redirect against arbitrary URL redirection
* [Controller] Fix issue causing flash session to remain when using `clear_flash/1`
### Bug Fixes
* Add missing `.formatter.exs` to hex package for proper elixir formatter integration
* [phx.gen.cert] Fix usage inside umbrella applications
* [phx.new] Revert `Routes.static_url` in app layout in favor of original `Routes.static_path`
* [phx.new] Use phoenix_live_reload 1.2-rc to fix hex version errors
* [phx.gen.json|html] Fix generator tests incorrectly encoding datetimes
* [phx.gen.cert] Fix generation of cert inside umbrella projects
* [Channel] Fix issue with WebSocket transport sending wrong ContentLength header with 403 response
* [Router] Fix forward aliases failing to expand within scope block
* [Router] Fix regression in router compilation failing to escape plug options
* Deprecations
* [Generator] All `phoenix.*` mix tasks have been deprecated in favor of new `phx.*` tasks
### phx.new installer
* Generate new Elixir 1.5+ child spec (therefore new apps require Elixir v1.5)
* Use webpack for asset bundling
* JavaScript client enhancements
* Use V2 channel wire protocol support
* Add ability to pass `encode` and `decode` functions to socket constructor for custom encoding and decoding of outgoing and incoming messages.
* Detect heartbeat timeouts on client to handle ungraceful connection loss for faster socket error detection
* Add support for AMD/RequireJS
### Deprecations
* [Controller] Passing a view in `render/3` and `render/4` is deprecated in favor of `put_view/2`
* [Endpoint] The `:handler` option in the endpoint is deprecated in favor of `:adapter`
* [Socket] `transport/3` is deprecated. The transport is now specified in the endpoint
* [Transport] The transport system has seen an overhaul and been drastically simplified. The previous mechanism for building transports is still supported but it is deprecated. Please see `Phoenix.Socket.Transport` for more information
* JavaScript client bug fixes
* Resolve race conditions when join timeouts occur on client, while server channel successfully joins
### JavaScript client
* Add new instance-based Presence API with simplified synchronization callbacks
* Accept a function for socket and channel `params` for dynamic parameter generation when connecting and joining
* Fix race condition when presence diff arrives before state
* Immediately rejoin channels on socket reconnect for faster recovery after reconnection
* Fix reconnect caused by pending heartbeat
## 1.2.2 (2017-3-14)
## v1.3
* Big Fixes
* [Controller] Harden local redirect against arbitrary URL redirection
## 1.2.1 (2016-8-11)
* Enhancements
* [Router] Improve errors for invalid route paths
* [Plug] Include new development error pages
* Bug Fixes
* [Endpoint] Fixed issue where endpoint would fail to code reload on next request after an endpoint compilation error
## 1.2.0 (2016-6-23)
See these [`1.1.x` to `1.2.x` upgrade instructions](https://gist.github.com/chrismccord/29100e16d3990469c47f851e3142f766) to bring your existing apps up to speed.
* Enhancements
* [CodeReloader] The `lib/` directory is now code reloaded by default along with `web/` in development
* [Channel] Add `subscribe/2` and `unsubscribe/2` to handle external topic subscriptions for a socket
* [Channel] Add `:phoenix_channel_join` instrumentation hook
* [View] Generate private `render_template/2` clauses for views to allow overriding `render/2` clauses before rendering templates
* [View] Add `:path` and `:pattern` options to allow wildcard template inclusion as well as customized template directory locations
* Deprecations
* [Endpoint] Generated `subscribe/3` and `unsubscribe/2` clauses have been deprecated in favor of `subscribe/2` and `unsubscribe/1` which uses the caller's pid
* [PubSub] `Phoenix.PubSub.subscribe/3` and `Phoenix.PubSub.unsubscribe/2` have been deprecated in favor of `subscribe/2` and `unsubscribe/1` which uses the caller's pid
* [Watcher] Using the `:root` endpoint configuration for watchers is deprecated. Pass the :cd option at the end of your watcher argument list in config/dev.exs. For example:
```elixir
watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
cd: Path.expand("../", __DIR__)]]
```
* Bug Fixes
* [Template] Prevent infinite call stack when rendering a non-existent template from `template_not_found`
* Backward incompatible changes
* [Channel] `subscribe/1` and `unsubscribe/1` have been removed in favor of calling subscribe and unsubscribe off the endpoint directly
* JavaScript client enhancements
* Add Presence object for syncing presence state between client and server
* Use return value of channel onMessage callback for specialized message transformations before dispatching to the channel
* JavaScript client backward incompatible changes
* `Presence.syncState` and `Presence.syncDiff` now return a copy of the state instead of mutating it
## 1.1.6 (2016-6-03)
* Enhancements
* Add Erlang 19 compatibility
## 1.1.5 (2016-6-01)
* Enhancements
* Fix warnings for Elixir 1.3
## 1.1.4 (2016-1-25)
* Enhancements
* [phoenix.new] Update dependencies and solve problem where Mix may take too long to resolve deps
* [phoenix.new] Be more conservative regarding brunch dependencies
* [phoenix.new] Provide `local.phoenix` task
* [phoenix.digest] Add `?vsn=d` to digested assets
## 1.1.3 (2016-1-20)
* Enhancements
* [phoenix.gen] Support `--binary-id` option when generating scaffold
* [phoenix.new] Don't include Ecto gettext translations if `--no-ecto` is passed
* JavaScript client bug fixes
* Ensure exports variable does not leak
* Fix `setTimeout` scoping issue for Babel
## 1.1.2 (2016-1-8)
See these *optional* [`1.1.1` to `1.1.2` upgrade instructions](https://gist.github.com/chrismccord/d5bc5f8e38c8f76cad33) to bring your existing apps up to speed.
* Enhancements
* [Cowboy] Improve log report for errors from the UserSocket
* [ChannelTest] Add `refute_push` and `refute_reply`
* [Router] Improve error messages when calling Router helpers without matching clauses
* [phoenix.new] Use brunch 2.1.1 npm integration to load `phoenix` and `phoenix_html` js deps
## 1.1.1 (2015-12-26)
* Bug fixes
* Fix `--no-html` flag on `phoenix.new` task failing to generate ErrorHelpers module
## 1.1.0 (2015-12-16)
See these [`1.0.x` to `1.1.0` upgrade instructions](https://gist.github.com/chrismccord/557ef22e2a03a7992624) to bring your existing apps up to speed.
* Enhancements
* [Router] Enable defining routes for custom http methods with a new `match` macro
* [CodeReloader] The socket transports now trigger the code reloader when enabled for external clients that only connect to channels without trigger a recompile through the normal page request.
* [phoenix.digest] The `phoenix.digest` task now digests asset urls in stylesheets automatically
* [Channel] Add `Phoenix.Channel.reply/3` to reply asynchronously to a channel push
* [Channel] `code_change/3` is now supported to upgrade channel servers
* [Endpoint] `check_origin` now supports wildcard hosts, ie `check_origin: ["//*.example.com"]`
* [Endpoint] `check_origin` treats invalid origin hosts as missing for misbehaving clients
* [Endpoint] Add `Phoenix.Endpoint.server?/2` to check if webserver has been configured to start
* [ConnTest] Add `assert_error_sent` to assert an error was wrapped and sent with a given status
* Backward incompatible changes
* [View] The `@inner` assign has been removed in favor of explicit rendering with `render/3` and the new `@view_module` and `view_template` assigns, for example: `<%= @inner %>` is replaced by `<%= render @view_module, @view_template, assigns %>`
## 1.0.4 (2015-11-30)
* Enhancements
* [ConnTest] Add `bypass_through` to pass a connection through a Router and pipelines while bypassing route dispatch.
* Bug fixes
* [LongPoll] force application/json content-type to fix blank JSON bodies on older IE clients using xdomain
## 1.0.3 (2015-9-28)
* Enhancements
* [Controller] Transform FunctionClauseError's from controller actions into ActionClauseError, and send 400 response
* [Router] Allow plugs to be passed to `pipe_through`
* [Channel] WebSocket transport now sends server heartbeats and shutdowns if client heartbeats stop. Fixes timeout issues when clients keep connection open, but hang with suspended js runtimes
* JavaScript client deprecations
* Passing params to socket.connect() has been deprecated in favor of the `:params` option of the Socket constructor
## 1.0.2 (2015-9-6)
* Enhancements
* [Installer] Support `--database mongodb` when generating new apps
* [Installer] Support `binary_id` and `migration` configuration for models
* Bug fixes
* [Digest] Ensure Phoenix app is loaded before digesting
* [Generator] Ensure proper keys are generated in JSON views and tests
* [Generator] Ensure proper titles are generated in HTML views and tests
* [Mix] Ensure app is compiled before showing routes with `mix phoenix.routes`
* [Token] Ensure max age is counted in seconds and not in milliseconds
## 1.0.1 (2015-9-3)
* Enhancements
* [Controller] `phoenix.gen.json` generator now excludes `:new` and `:edit` actions
* [Endpoint] Set hostname to "localhost" by default for dev and test
* [ConnTest] Support multiple json mime types in `json_response/2`
## 1.0.0 (2015-8-28) 🚀
## v0.17.1 (2015-8-26)
* Enhancements
* [ChannelTest] Add `connect/2` helper for test UserSocket handlers
* [Endpoint] Expose `struct_url/0` in the endpoint that returns the URL as struct for further manipulation
* [Router] Allow `URI` structs to be given to generated `url/1` and `path/2` helpers
* Bug fixes
* [Endpoint] Pass port configuration when configuring force_ssl
* [Mix] By default include all attributes in generated JSON views
* [Router] Fix `pipe_through` not respecting halting when piping through multiple pipelines
## v0.17.0 (2015-8-19)
See these [`0.16.x` to `0.17.0` upgrade instructions](https://gist.github.com/chrismccord/ee5ae90b949a9768b871) to bring your existing apps up to speed.
* Enhancements
* [Endpoint] Allow `check_origin` and `force_ssl` to be config in transports and fallback to endpoint config
* [Transport] Log when `check_origin` fails
* Bug fixes
* [Mix] Properly humanize names in the generator
* Deprecations
* [Endpoint] `render_errors: [default_format: "html"]` is deprecated in favor of `render_errors: [accepts: ["html"]]`
* Backward incompatible changes
* [Controller] The "format" param for overriding the accept header has been renamed to "_format" and is no longer injected into the params when parsing the Accept headers. Use `get_format/1` to access the negotiated format.
* [ChannelTest] In order to test channels, one must now explicitly create a socket and pass it to `subscribe_and_join`. For example, `subscribe_and_join(MyChannel, "my_topic")` should now become `socket() |> subscribe_and_join(MyChannel, "my_topic")` or `socket("user:id", %{user_id: 13}) |> subscribe_and_join(MyChannel, "my_topic")`.
## v0.16.1 (2015-8-6)
* JavaScript client bug fixes
* Pass socket params on reconnect
## v0.16.0 (2015-8-5)
See these [`0.15.x` to `0.16.0` upgrade instructions](https://gist.github.com/chrismccord/969d75d1562979a3ae37) to bring your existing apps up to speed.
* Enhancements
* [Brunch] No longer ship with `sass-brunch` dependency
* [Endpoint] Add `force_ssl` support
* [Mix] Allow `phoenix.gen.*` tasks templates to be customized by the target application by placing copies at `priv/template/phoenix.gen.*`
* [Mix] Support `mix phoenix.gen.model Comment comment post_id:references:posts`
* [Mix] Add `mix phoenix.gen.secret`
* [Router] Provide `put_secure_browser_headers/2` and use it by default in the browser pipeline
* [Socket] Automatically check origins on socket transports
* [Token] Add `Phoenix.Token` for easy signing and verification of tokens
* Bug fixes
* [Cowboy] Ensure we print proper URL when starting the server with both http and https
* [Digest] Do not gzip binary files like png and jpg. Default only to known text files and make them configurable via `config :phoenix, :gzippable_exts, ~w(.txt .html .js .css)` and so on
* Backward incompatible changes
* [Controller] `jsonp/3` function has been removed in favor of the `plug :allow_jsonp`
* [Controller] `controller_template/1` has been renamed to `view_template/1`
* [HTML] Use `phoenix_html ~> 2.0` which includes its own `phoenix_html.js` version
* [Socket] `:origins` transport option has been renamed to `:check_origin`
* [View] `render_one` and `render_many` no longer inflect the view module from the model in favor of explicitly passing the view
* JavaScript client backwards incompatible changes
* Socket params are now passed to `socket.connect()` instead of an option on the constructor.
* Socket params are no longer merged as default params for channel params. Use `connect/2` on the server to wire up default channel assigns.
* Socket `chan` has been renamed to `channel`, for example `socket.channel("some:topic")`
## v0.15.0 (2015-7-27)
See these [`0.14.x` to `0.15.0` upgrade instructions](https://gist.github.com/chrismccord/931373940f320bf41a50) to bring your existing apps up to speed.
* Enhancements
* [Socket] Introduce `Phoenix.Socket` behaviour that allows socket authentication, termination, and default channel socket assigns
* [PubSub] Use ETS dispatch table for increased broadcast performance
* [Channel] Use event intercept for increased broadcast performance
* Backward incompatible changes
* [Router] channel routes are now defined on a socket handler module instead of the Router
* [Router] `socket` mounts have been moved from the Router to the Endpoint
* [Channel] `handle_out` callbacks now require explicit event intercept for callback to be invoked, with `Phoenix.Channel.intercept/1`
* [Transports] WebSocket and LongPoll transport configuration has been moved from mix config to the UserSocket
* JavaScript client backwards incompatible changes
* `Phoenix.LongPoller` has been renamed to `Phoenix.LongPoll`
* A new client version is required to accommodate server changes
## v0.14.0 (2015-06-29)
See these [`0.13.x` to `0.14.0` upgrade instructions](https://gist.github.com/chrismccord/57805158f463d3369103) to bring your existing apps up to speed.
* Enhancements
* [Phoenix.HTML] Update to phoenix_html 1.1.0 which raises on missing assigns
* [Controller] Add `jsonp/2` for handling JSONP responses
* [Channel] Enhance logging with join information
* [Router] Add `forward` macro to forward a requests to a Plug, invoking the pipeline
* Javascript client enhancements
* Add socket params to apply default, overridable params to all channel params.
* Enchance logging
* Bug fixes
* [Channel] Fix xdomain content type not being treated as JSON requests
* Javascript client backwards incompatible changes
* `logger` option to `Phoenix.Socket`, now uses three arguments, ie: `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }`
* Backward incompatible changes
* [Controller] `plug :action` is now called automatically
* [Endpoint] The `:format` option in `:render_errors` has been renamed to `:default_format`
* [PubSub.Redis] The Redis PubSub adapter has been extracted into its own project. If using redis, see the [project's readme](https://github.com/phoenixframework/phoenix_pubsub_redis) for instructions
* [View] The default template `web/templates/layout/application.html.eex` has been renamed to `app.html.eex`
## v0.13.1 (2015-05-16)
See these [`0.13.0` to `0.13.1` upgrade instructions](https://gist.github.com/chrismccord/4a62780056b08c60542d) to bring your existing apps up to speed.
* Enhancements
* [Channel] Add `phoenix.new.channel Channel topic`
* [Channel] Add `Phoenix.ChannelCase`
* [Controller] Assert changes in the repository on generated controller tests
* [Endpoint] Add `static_url` to endpoint to configure host, port and more for static urls
* [phoenix.new] Generate a channel case for new apps
* [phoenix.new] Improve installation workflow by asking to install and fetch dependencies once
* [phoenix.new] Add `errors_on/1` to generated model case
## v0.13.0 (2015-05-11)
See these [`0.12.x` to `0.13.0` upgrade instructions](https://gist.github.com/chrismccord/0a3bf5229801d61f219b) to bring your existing apps up to speed.
* Enhancements
* [Channel] Allow router helpers to work in channels by passing a socket (instead of connection), for example: `user_path(socket, :index)`
* [Channel] Support replies in `join/3`
* [HTML] `Phoenix.HTML` has been extracted to its own project. You need to explicitly depend on it by adding `{:phoenix_html, "~> 1.0"}` to `deps` in your `mix.exs` file
* [HTML] `safe/1` in views is deprecated in favor of `raw/1`
* [Generators] Allow `belongs_to` in model generator which supports associations and indexes
* Bug fixes
* [HTML] `select` no longer inverses the key and values in the given options
* [phoenix.new] Do not run `deps.get` if there is no Hex
* Backward incompatible changes
* [Channel] To refuse joining a channel, `join/3` now requires `{:error, reason}`
* Javascript client backward incompatible changes
* channel instances are now created from the `socket`
* channel joins are now called explicitly off channel instances
* channel onClose now only triggered on explicit client `leave` or server `:stop`
* Examples:
```javascript
let socket = new Phoenix.Socket("/ws")
let chan = socket.chan("rooms:123", {})
chan.join().receive("ok", ({resp} => ...).receive("error", ({reason}) => ...)
```
## v0.12.0 (2015-04-30)
See these [`0.11.x` to `0.12.0` upgrade instructions](https://gist.github.com/chrismccord/b3975ba356dba902ec88) to bring your existing apps up to speed.
* Enhancements
* [Channel] Leaving the channel or closing the client will now trigger terminate on the channel, regardless of traping exits, with reasons `{:shutdown, :left}` and `{:shutdown, :closed}` respectively
* [Controller] Support `:namespace` option in controllers in order to use proper layout in namespaced applications
* [Controller] Add `controller_template/1` to lookup the template rendered from the controller
* [Generators] Add `phoenix.gen.json`
* [Generators] Allow models to be skipped on `phoenix.gen.json` and `phoenix.gen.html` generators
* [Generators] Generate test files in `phoenix.gen.html`, `phoenix.gen.json` and `phoenix.gen.model`
* [HTML] Add `search_input/3`, `telephone_input/3`, `url_input/3` and `range_input/3` to `Phoenix.HTML.Form`
* [Installer] Add the generated `config/prod.secret.exs` file to `.gitignore` by default
* [Static] Add a `mix phoenix.digest` task to run during deploys to generate digest filenames and gzip static files. A new configuration called `cache_static_manifest` was added which should be set to "priv/static/manifest.json" in production which will preload the manifest file generated by the mix task in order to point to the digested files when generating static paths
* [Test] Add `html_response/2`, `json_response/2`, `text_response/2` and `response/2` to aid connection-based testing
* [View] Add `render_existing/3` to render a template only if it exists without raising an error
* [View] Add `render_many/4` and `render_one/4` to make it easier to render collection and optional data respctivelly
* Bug fixes
* [Channel] Ensure channels are terminated when WebSocket and LongPoller transports exit normally
* [Installer] Declare missing applications in generated phoenix.new app
* [Installer] No longer generate encryption salt in generated phoenix.new app
* [Installer] Generate proper credentials in phoenix.new for different databases
* [Mix] Ensure the serve endpoints configuration is persistent
* [Router] Ensure URL helpers know how to call `to_param` on query parameters
## v0.11.0 (2015-04-07)
See these [`0.10.x` to `0.11.0` upgrade instructions](https://gist.github.com/chrismccord/3603fd2735019f86c74b) to bring your existing apps up to speed.
* Javascript client enhancements
* Joins are now synchronous, removing the prior issues of client race conditions
* Events can now be replied to from the server, for request/response style messaging
* Clients can now detect and react to individual channel errors and terminations
* Javascript client backward incompatible changes
* The `Socket` instance no long connects automatically. You must explicitly call `connect()`
* `close()` has been renamed to `disconnect()`
* `send` has been renamed to `push` to unify client and server messaging commands
* The `join` API has changed to use synchronous messaging. Check the upgrade guide for details
* Backwards incompatible changes
* [Generator] `mix phoenix.gen.resource` renamed to `mix phoenix.gen.html`
* [Channel] `reply` has been renamed to `push` to better signify we are only push a message down the socket, not replying to a specific request
* [Channel] The return signatures for `handle_in/3` and `handle_out/3` have changed, ie:
handle_in(event :: String.t, msg :: map, Socket.t) ::
{:noreply, Socket.t} |
{:reply, {status :: atom, response :: map}, Socket.t} |
{:reply, status :: atom, Socket.t} |
{:stop, reason :: term, Socket.t} |
{:stop, reason :: term, reply :: {status :: atom, response :: map}, Socket.t} |
{:stop, reason :: term, reply :: status :: atom, Socket.t}
handle_out(event :: String.t, msg :: map, Socket.t) ::
{:ok, Socket.t} |
{:noreply, Socket.t} |
{:error, reason :: term, Socket.t} |
{:stop, reason :: term, Socket.t}
* [Channel] The `leave/2` callback has been removed. If you need to cleanup/teardown when a client disconnects, trap exits and handle in `terminate/2`, ie:
def join(topic, auth_msg, socket) do
Process.flag(:trap_exit, true)
{:ok, socket}
end
def terminate({:shutdown, :client_left}, socket) do
# client left intentionally
end
def terminate(reason, socket) do
# terminating for another reason (connection drop, crash, etc)
end
* [HTML] `use Phoenix.HTML` no longer imports controller functions. You must add `import Phoenix.Controller, only: [get_flash: 2]` manually to your views or your `web.ex`
* [Endpoint] Code reloader must now be configured in your endpoint instead of Phoenix. Therefore, upgrade your `config/dev.exs` replacing
config :phoenix, :code_reloader, true
by
config :your_app, Your.Endpoint, code_reloader: true
* [Endpoint] Live reloader is now a dependency instead of being shipped with Phoenix. Please add `{:phoenix_live_reload, "~> 0.3"}` to your dependencies
* [Endpoint] The `live_reload` configuration has changed to allow a `:url` option and work with `:patterns` instead of paths:
config :your_app, Your.Endpoint,
code_reloader: true,
live_reload: [
url: "ws://localhost:4000",
patterns: [~r{priv/static/.*(js|css|png|jpeg|jpg|gif)$},
~r{web/views/.*(ex)$},
~r{web/templates/.*(eex)$}]]
* [Endpoint] Code and live reloader must now be explicitly plugged in your endpoint. Wrap them inside `lib/your_app/endpoint.ex` in a `code_reloading?` block:
if code_reloading? do
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
* Enhancements
* [Endpoint] Allow the default format used when rendering errors to be customized in the `render_views` configuration
* [HTML] Add `button/2` function to `Phoenix.HTML`
* [HTML] Add `textarea/3` function to `Phoenix.HTML.Form`
* [Controller] `render/3` and `render/4` allows a view to be specified
directly.
* Bug fixes
* [HTML] Fix out of order hours, minutes and days in date/time select
## v0.10.0 (2015-03-08)
See these [`0.9.x` to `0.10.0` upgrade instructions](https://gist.github.com/chrismccord/cf51346c6636b5052885) to bring your existing apps up to speed.
* Enhancements
* [CLI] Make `phoenix.new` in sync with `mix new` by making the project directory optional
* [Controller] Add `scrub_params/2` which makes it easy to remove and prune blank string values from parameters (usually sent by forms)
* [Endpoint] Runtime evaluation of `:port` configuration is now supported. When given a tuple like `{:system, "PORT"}`, the port will be referenced from `System.get_env("PORT")` at runtime as a workaround for releases where environment specific information is loaded only at compile-time
* [HTML] Provide `tag/2`, `content_tag/2` and `content_tag/3` helpers to make tag generation easier and safer
* [Router] Speed up router compilation
* Backwards incompatible changes
* [Plug] Update to Plug 0.10.0 which moves CSRF tokens from cookies back to sessions. To avoid future bumps on the road, a `get_csrf_token/0` function has been added to controllers
* [PubSub] Remove the option `:options` from `:pubsub`. Just define the options alongside the pubsub configuration
* [Pubsub] Require the `:name` option when configuring a pubsub adapter
## v0.9.0 (2015-02-12)
See these [`0.8.x` to `0.9.0` upgrade instructions](https://gist.github.com/chrismccord/def6f4dc444b6a8f8d8b) to bring your existing apps up to speed.
* Enhancements
* [PubSub/Channels] The PubSub layer now supports Redis, and is opened up to other third party adapters. It still defaults to PG2, but other adapters are convenient for non-distributed deployments or durable messaging.
* Bug fixes
* [Plug] Ensure session and flash are serializable to JSON
* Backwards incompatible changes
* [PubSub] The new PubSub system requires the adapter's configuration to be added to your Endpoint's mix config.
* [PubSub] The `Phoenix.PubSub` API now requires a registered server name, ie `Phoenix.PubSub.broadcast(MyApp.PubSub, "foo:bar", %{baz: :bang})`
* [Channel] Channel broadcasts from outside a socket connection now must be called from an Endpoint module directly, ie: `MyApp.Endpoint.broadcast("topic", "event", %{...})`
* [Channel] The error return signature has been changed from `{:error, socket, reason}` to `{:error, reason, socket}`
* [Plug] `Plug.CSRFProtection` now uses a cookie instead of session and expects a `"_csrf_token"` parameter instead of `"csrf_token"`
* [Router/Controller] The `destroy` action has been renamed to `delete`, update your controller actions and url builders accordingly
## v0.8.0 (2015-01-11)
See these [`0.7.x` to `0.8.0` upgrade instructions](https://gist.github.com/chrismccord/9434b8fa208b3aae22b6) to bring your existing apps up to speed.
* Enhancements
* [Router] `protect_from_forgery` has been added to the Router for CSRF protection. This is automatically plugged in new projects. See [this example](https://github.com/phoenixframework/phoenix/blob/ce5ebf3d9de4412a18e6325cd0d34e1b9699fb5a/priv/template/web/router.ex#L7) for plugging in your existing router pipeline(s)
* [Router] New `socket` macro allows scoping channels to different transports and mounting multiple socket endpoints
* [Channels] The "topic" abstraction has been refined to be a simple string identifier to provide more direct integration with the `Phoenix.PubSub` layer
* [Channels] Channels can now intercept outgoing messages and customize the broadcast for a socket-by-socket customization, message dropping, etc
* [Channels] A channel can be left by returning `{:leave, socket}` from a channel callback to unsubscribe from the channel
* [Channels] Channel Serializer can now use binary protocol over websockets instead of just text
* [Endpoint] Allow the reloadable paths to be configured in the endpoint
* [Mix] Allow the code generation namespace to be configured with the `:namespace` option
* [Mix] Allow `:reloadable_paths` in Endpoint configuration to reload directories other than `"web"` in development
* Bug Fixes
* [Channel] Fix WebSocket heartbeat causing unnecessary `%Phoenix.Socket{}`'s to be tracked and leave errors on disconnect
* [Mix] Ensure Phoenix can serve and code reload inside umbrella apps
* Backwards incompatible changes
* [Endpoint] Endpoints should now be explicitly started in your application supervision tree. Just add `supervisor(YourApp.Endpoint, [])` to your supervision tree in `lib/your_app.ex`
* `mix phoenix.start` was renamed to `mix phoenix.server`
* [Endpoint] The `YourApp.Endpoint.start/0` function was removed. You can simply remove it from your `test/test_helper.ex` file
* [Router] Generated named paths now expect a conn arg. For example, `MyApp.Router.Helpers.page_path(conn, :show, "hello")` instead of `MyApp.Router.Helpers.page_path(:show, "hello")`
* [Controller] `Phoenix.Controller.Flash` has been removed in favor of `fetch_flash/2`, `get_flash/2`, and `put_flash/2` functions on `Phoenix.Controller`
* [Router] `Phoenix.Router.Socket` has been removed in favor of new `Phoenix.Router.socket/2` macro.
* [Router] The `channel` macro now requires a topic pattern to be used to match incoming channel messages to a channel handler. See `Phoenix.Router.channel/2` for details.
* [Channel] The `event/3` callback has been renamed to `handle_in/3` and the argument order has changed to `def handle_in("some:event", msg, socket)`
* [Channel] Channel callback return signatures have changed and now require `{:ok, socket} | {:leave, socket| | {:error, socket, reason}`. `terminate/2` and `hibernate/2` have also been removed.
## v0.7.2 (2014-12-11)
* Enhancements
* [Mix] Update Plug to `0.9.0`. You can now remove the Plug git dep from your `mix.exs`.
* Bug fixes
* [Endpoint] Ensure CodeReloader is removed fron Endpoint when disabled
## v0.7.1 (2014-12-09)
* Bug fixes
* [Phoenix] Include Plug dep in new project generation since it's a github dep until next Plug release.
## v0.7.0 (2014-12-09)
See these [`0.6.x` to `0.7.0` upgrade instructions](https://gist.github.com/chrismccord/c24b2b516066d987f4fe) to bring your existing apps up to speed.
* Enhancements
* [Endpoint] Introduce the concept of endpoints which removes some of the responsibilities from the router
* [Endpoint] Move configuration from the :phoenix application to the user own OTP app
* Bug fixes
* [Router] Fix a bug where the error rendering layer was not picking JSON changes
* [CodeReloader] Fix a bug where the code reloader was unable to recompile when the router could not compile
* Backwards incompatible changes
* [I18n] `Linguist` has been removed as a dependency, and an `I18n` module is no longer generated in your project
* [View] `ErrorsView` has been renamed to `ErrorView`, update your `MyApp.ErrorsView` accordingly
* [Controller] `html/2`, `json/2`, `text/2`, `redirect/2` and
`render/3` no longer halt automatically
* [Router] Configuration is no longer stored in the router but in the application endpoint. The before pipeline was also removed and moved to the endpoint itself
## v0.6.2 (2014-12-07)
* Bug fixes
* [Mix] Fix phoenix dep reference in new project generator
## v0.6.1 (2014-11-30)
* Enhancements
* [Controller] Allow sensitive parameters to be filtered from logs
* [Router] Add ability for routes to be scoped by hostname via the :host option
* [Router] Add `Plug.Debugger` that shows helpful error pages in case of failures
* [Router] Add `Phoenix.Router.RenderErrors` which dispatches to a view for rendering in case of crashes
* [Router] Log which pipelines were triggered during a request
* [Channel] Allows custom serializers to be configured for WebSocket Transport
## v0.6.0 (2014-11-22)
See the [`0.5.x` to `0.6.0` upgrade instructions](https://gist.github.com/chrismccord/e774e6ab5220e6505a03) for upgrading your
existing applications.
* Enhancements
* [Controller] Support `put_view/2` to configure which view to use
when rendering in the controller
* [Controller] Support templates as an atom in
`Phoenix.Controller.render/3` as a way to explicitly render templates
based on the request format
* [Controller] Split paths from external urls in `redirect/2`
* [Controller] `json/2` automatically encodes the data to JSON by
using the registered `:format_encoders`
* [Controller] `html/2`, `json/2`, `text/2`, `redirect/2` and
`render/3` now halt automatically
* [Controller] Add `accepts/2` for content negotiation
* [Controller] Add `put_layout_formats/2` and `layout_formats/1` to
configure and read which formats have a layout when rendering
* [View] Assigns are always guaranteed to be maps
* [View] Add support to `format_encoders` that automatically encodes
rendered templates. This means a "user.json" template only needs to
return a map (or any structure encodable to JSON) and it will be
automatically encoded to JSON by Phoenix
* [View] Add a .exs template engine
* [Channel] Add a `Transport` contract for custom Channel backends
* [Channel] Add a `LongPoller` transport with automatic LP fallback
in `phoenix.js`
* [phoenix.js] Add long-polling support with automatic LP fallback
for older browsers
* Deprecations
* [Controller] `html/3`, `json/3`, `text/3` and `redirect/3` were
deprecated in favor of using `put_status/2`
* [Controller] `redirect(conn, url)` was deprecated in favor of
`redirect(conn, to: url)`
* Backwards incompatible changes
* [Controller] Passing a string to render without format in the
controller, as in `render(conn, "show")` no longer works. You should
either make the format explicit `render(conn, "show.html")` or use an
atom `render(conn, :show)` to dynamically render based on the format
* [View] Using `:within` was renamed in favor of `:layout` for
rendering with layouts
* [View] Your application should now directly use Phoenix.View in
its main view and specify further configuration in the `using(...)`
section
* [View] Template engines now should implement compile and simply
return the quoted expression of the function body instead of the
quoted expression of the render function
* [Router] `PUT` route generation for the `:update` action has been
dropped in favor of `PATCH`, but `PUT` still matches requests to maintain compatibility with proxies.
* [Router] Router no longer defines default :browser and :api
pipelines
* Bug fixes
* [Router] Generate correct route for helper path on root
## v0.5.0
* Enhancements
* [Router] Named helpers are now automatically generated for every
route based on the controller name
* [Router] Named helpers have been optimized to do as little work as
possible at runtime
* [Router] Support multiple pipelines at the router level
* [Channels] The `phoenix.js` channel client now sends a
configurable heartbeat every 30s to maintain connections
* Deprecations
* [Controller] `assign_private` is deprecated in favor of
`put_private`
* [Controller] `assign_status` is deprecated in favor of
`put_status`
* Backwards incompatible changes
* [Controller] Remove default, injected aliases: `Flash`, `JSON`
* [Controller] Controllers now require `plug :action` to be
explicitly invoked
* [Router] `*path` identifiers in routers are now returned as a list
* [Router] Named helpers are now defined in a explicit module nested
to your router. For example, if your router is named `MyApp.Router`,
the named helpers will be available at `MyApp.Router.Helpers`
* [Router] `session_secret` configuration is deprecated in favor of
`secret_key_base`
* [Router] Plugs can now only be defined inside pipelines. All
routers now need to explicitly declare which pipeline they want to use
* [Router] Router configuration was revamped, static configuration
has been moved into `:static`, session configuration into `:session`,
parsers configuration into `:parsers`, the http server configuration
has been moved into `:http`, the https configuration into `:https` and
the URI information for generating URIs into `:uri`
* [CodeReloaer] Code reloading now requires the `:phoenix` compiler
to be added to the list of compilers in your `mix.exs` project config,
ie: `compilers: [:phoenix] ++ Mix.compilers`. Additionally, the
`Phoenix.CodeReloader.reload!` invocation should be removed from your
`test_helper.exs` for applications generated on `0.4.x`.
* [Topic] `Phoenix.Topic` has been renamed to `Phoenix.PubSub`. If you were calling into the topic layer directly, update your module references.
## v0.4.1 (2014-09-08)
* Bug fixes
* [Project Generation] Fix project template dependencies pointing to
incorrect phoenix and elixir versions
## v0.4.0 (2014-08-30)
* Enhancements
* [Controller] Controllers are now Plugs and can be plugged as a
"second layer" plug stack from the Router plug stack
* [Controller] Elixir Logger Integration - Improved request logger,
durations, params, etc
* [Controller] Custom 404/500 page handling,
[details](https://github.com/phoenixframework/phoenix/blob/0b6bdffab45fc46bc1455860f2d3971d0224eeb5/README.md#custom-not-found-and-error-pages)
* [Controller] Ability to halt Plug stacks with Plug 0.7.0 `halt/1`
* [Controller] Add `assign_layout/2` and `assign_status/2`
* [Controller] Flash messages for one-time message support across
redirects
* [View] Internationalization support
* [View] New `Template.Engine` behaviour for third-party template
engines. See
[PhoenixHaml](https://github.com/chrismccord/phoenix_haml) for haml
support via Calliope.
* `render/2` can be explicitly plugged for automatic rendering of
actions based on action name
* [Channel] Assign API for Sockets allows ephemeral state to be
stored on the multiplexed socket, similar to conn assigns
* [Config] Add `proxy_port` Router config option for deployments
where public facing port differs from local port
* [Router] Add nested generated `Helpers` module to Routers for easy
imports of named route helpers, ie `import MyApp.Router.Helpers`
* Bug fixes
* Various bug fixes and improvements
* Backwards incompatible changes
* [Config] ExConf Configuration has been replaced by Mix Config
* Directory and naming conventions have changed. A `web/` directory
now lives at root of the project and holds routers, controllers,
channels, views & templates, where all `web/` files are recompiled by
the code reloader during development. Modules that cannot be simply
recompiled in process are placed in lib as normal and require a server
restart to take effect. Follow
[this guide](https://gist.github.com/dgoldie/2fdc90fe09ecdddb78f4) for
upgrade steps from 0.3.x.
* Naming conventions now use singular form for module names,
directory names, and named route helpers
* [Router] Named route helpers have been reworked to use single
function name with pattern matched arguments. See the
[readme examples](https://github.com/phoenixframework/phoenix/blob/0b6bdffab45fc46bc1455860f2d3971d0224eeb5/README.md#resources)
* [Controller] `layout: nil` render option has been replaced by
`assign_layout(conn, :none)`
* [Plugs] `Plugs.JSON` now adds parsed params under "_json" key when
the JSON object is an array
## v0.3.1 (2014-07-04)
* Enhancements
* Various performance improvements
## v0.3.0 (2014-06-30)
* Enhancements
* Add Precompiled EEx Templating Engine and View layer
* Add JSON Plug parser
* Update Plug to 0.5.2 with Cookie Session support
* URL helpers ie, `Router.page_path`, now properly encode nested
query string params
* Bug fixes
* Auto template compilation has been fixed for Elixir 0.14.2
`@external_resource` changes
* Backwards incompatible changes
* Controller action arity has changed. All actions now receive the
Plug conn and params as arguments, ie `def show(conn, %{"id" => id})`
* Channel and Topic `reply` and `broadcast` functions now require a
map instead of an arbitrary dict
The CHANGELOG for v1.3 releases can be found [in the v1.3 branch](https://github.com/phoenixframework/phoenix/blob/v1.3/CHANGELOG.md).
{
"name": "phoenix",
"version": "1.3.4",
"version": "1.4.0",
"description": "The official JavaScript client for the Phoenix web framework.",

@@ -12,13 +12,2 @@ "license": "MIT",

"author": "Chris McCord <chris@chrismccord.com> (http://www.phoenixframework.org)",
"devDependencies": {
"babel-brunch": "~6.0.0",
"brunch": "~2.6.5",
"documentation": "^4.0.0-rc.1",
"jsdom": "9.8.3",
"jsdom-global": "2.1.0",
"mocha": "~2.4.4",
"mock-socket": "^6.0.1",
"sinon": "^1.17.6",
"uglify-js-brunch": "~2.0.1"
},
"files": [

@@ -30,9 +19,3 @@ "README.md",

"assets/js/phoenix.js"
],
"scripts": {
"test": "./node_modules/.bin/mocha ./assets/test/**/*.js --compilers js:babel-register -r jsdom-global/register",
"docs": "documentation build assets/js/phoenix.js -f html -o doc/js",
"watch": "brunch watch",
"build": "brunch build"
}
]
}

@@ -1,1431 +0,1 @@

(function (global, factory) {
typeof exports === 'object' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
factory(global.Phoenix = global.Phoenix || {});
}(this, (function (exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* 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
*/
var VSN = "2.0.0";
var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
var DEFAULT_TIMEOUT = 10000;
var WS_CLOSE_NORMAL = 1000;
var CHANNEL_STATES = {
closed: "closed",
errored: "errored",
joined: "joined",
joining: "joining",
leaving: "leaving"
};
var CHANNEL_EVENTS = {
close: "phx_close",
error: "phx_error",
join: "phx_join",
reply: "phx_reply",
leave: "phx_leave"
};
var CHANNEL_LIFECYCLE_EVENTS = [CHANNEL_EVENTS.close, CHANNEL_EVENTS.error, CHANNEL_EVENTS.join, CHANNEL_EVENTS.reply, CHANNEL_EVENTS.leave];
var 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
*/
var Push = function () {
function Push(channel, event, payload, timeout) {
_classCallCheck(this, Push);
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
*/
_createClass(Push, [{
key: "resend",
value: function resend(timeout) {
this.timeout = timeout;
this.reset();
this.send();
}
/**
*
*/
}, {
key: "send",
value: function 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
*/
}, {
key: "receive",
value: function receive(status, callback) {
if (this.hasReceived(status)) {
callback(this.receivedResp.response);
}
this.recHooks.push({ status: status, callback: callback });
return this;
}
// private
}, {
key: "reset",
value: function reset() {
this.cancelRefEvent();
this.ref = null;
this.refEvent = null;
this.receivedResp = null;
this.sent = false;
}
}, {
key: "matchReceive",
value: function matchReceive(_ref) {
var status = _ref.status,
response = _ref.response,
ref = _ref.ref;
this.recHooks.filter(function (h) {
return h.status === status;
}).forEach(function (h) {
return h.callback(response);
});
}
}, {
key: "cancelRefEvent",
value: function cancelRefEvent() {
if (!this.refEvent) {
return;
}
this.channel.off(this.refEvent);
}
}, {
key: "cancelTimeout",
value: function cancelTimeout() {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
}, {
key: "startTimeout",
value: function startTimeout() {
var _this = this;
if (this.timeoutTimer) {
this.cancelTimeout();
}
this.ref = this.channel.socket.makeRef();
this.refEvent = this.channel.replyEventName(this.ref);
this.channel.on(this.refEvent, function (payload) {
_this.cancelRefEvent();
_this.cancelTimeout();
_this.receivedResp = payload;
_this.matchReceive(payload);
});
this.timeoutTimer = setTimeout(function () {
_this.trigger("timeout", {});
}, this.timeout);
}
}, {
key: "hasReceived",
value: function hasReceived(status) {
return this.receivedResp && this.receivedResp.status === status;
}
}, {
key: "trigger",
value: function trigger(status, response) {
this.channel.trigger(this.refEvent, { status: status, response: response });
}
}]);
return Push;
}();
/**
*
* @param {string} topic
* @param {Object} params
* @param {Socket} socket
*/
var Channel = exports.Channel = function () {
function Channel(topic, params, socket) {
var _this2 = this;
_classCallCheck(this, Channel);
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(function () {
return _this2.rejoinUntilConnected();
}, this.socket.reconnectAfterMs);
this.joinPush.receive("ok", function () {
_this2.state = CHANNEL_STATES.joined;
_this2.rejoinTimer.reset();
_this2.pushBuffer.forEach(function (pushEvent) {
return pushEvent.send();
});
_this2.pushBuffer = [];
});
this.onClose(function () {
_this2.rejoinTimer.reset();
_this2.socket.log("channel", "close " + _this2.topic + " " + _this2.joinRef());
_this2.state = CHANNEL_STATES.closed;
_this2.socket.remove(_this2);
});
this.onError(function (reason) {
if (_this2.isLeaving() || _this2.isClosed()) {
return;
}
_this2.socket.log("channel", "error " + _this2.topic, reason);
_this2.state = CHANNEL_STATES.errored;
_this2.rejoinTimer.scheduleTimeout();
});
this.joinPush.receive("timeout", function () {
if (!_this2.isJoining()) {
return;
}
_this2.socket.log("channel", "timeout " + _this2.topic + " (" + _this2.joinRef() + ")", _this2.joinPush.timeout);
var leavePush = new Push(_this2, CHANNEL_EVENTS.leave, {}, _this2.timeout);
leavePush.send();
_this2.state = CHANNEL_STATES.errored;
_this2.joinPush.reset();
_this2.rejoinTimer.scheduleTimeout();
});
this.on(CHANNEL_EVENTS.reply, function (payload, ref) {
_this2.trigger(_this2.replyEventName(ref), payload);
});
}
_createClass(Channel, [{
key: "rejoinUntilConnected",
value: function rejoinUntilConnected() {
this.rejoinTimer.scheduleTimeout();
if (this.socket.isConnected()) {
this.rejoin();
}
}
}, {
key: "join",
value: function join() {
var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 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;
}
}
}, {
key: "onClose",
value: function onClose(callback) {
this.on(CHANNEL_EVENTS.close, callback);
}
}, {
key: "onError",
value: function onError(callback) {
this.on(CHANNEL_EVENTS.error, function (reason) {
return callback(reason);
});
}
}, {
key: "on",
value: function on(event, callback) {
this.bindings.push({ event: event, callback: callback });
}
}, {
key: "off",
value: function off(event) {
this.bindings = this.bindings.filter(function (bind) {
return bind.event !== event;
});
}
}, {
key: "canPush",
value: function canPush() {
return this.socket.isConnected() && this.isJoined();
}
}, {
key: "push",
value: function push(event, payload) {
var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.timeout;
if (!this.joinedOnce) {
throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events";
}
var 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!") )
* ```
*/
}, {
key: "leave",
value: function leave() {
var _this3 = this;
var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
this.state = CHANNEL_STATES.leaving;
var onClose = function onClose() {
_this3.socket.log("channel", "leave " + _this3.topic);
_this3.trigger(CHANNEL_EVENTS.close, "leave");
};
var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
leavePush.receive("ok", function () {
return onClose();
}).receive("timeout", function () {
return 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
*/
}, {
key: "onMessage",
value: function onMessage(event, payload, ref) {
return payload;
}
// private
}, {
key: "isMember",
value: function isMember(topic, event, payload, joinRef) {
if (this.topic !== topic) {
return false;
}
var isLifecycleEvent = CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0;
if (joinRef && isLifecycleEvent && joinRef !== this.joinRef()) {
this.socket.log("channel", "dropping outdated message", { topic: topic, event: event, payload: payload, joinRef: joinRef });
return false;
} else {
return true;
}
}
}, {
key: "joinRef",
value: function joinRef() {
return this.joinPush.ref;
}
}, {
key: "sendJoin",
value: function sendJoin(timeout) {
this.state = CHANNEL_STATES.joining;
this.joinPush.resend(timeout);
}
}, {
key: "rejoin",
value: function rejoin() {
var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
if (this.isLeaving()) {
return;
}
this.sendJoin(timeout);
}
}, {
key: "trigger",
value: function trigger(event, payload, ref, joinRef) {
var _this4 = this;
var handledPayload = this.onMessage(event, payload, ref, joinRef);
if (payload && !handledPayload) {
throw "channel onMessage callbacks must return the payload, modified or unmodified";
}
this.bindings.filter(function (bind) {
return bind.event === event;
}).map(function (bind) {
return bind.callback(handledPayload, ref, joinRef || _this4.joinRef());
});
}
}, {
key: "replyEventName",
value: function replyEventName(ref) {
return "chan_reply_" + ref;
}
}, {
key: "isClosed",
value: function isClosed() {
return this.state === CHANNEL_STATES.closed;
}
}, {
key: "isErrored",
value: function isErrored() {
return this.state === CHANNEL_STATES.errored;
}
}, {
key: "isJoined",
value: function isJoined() {
return this.state === CHANNEL_STATES.joined;
}
}, {
key: "isJoining",
value: function isJoining() {
return this.state === CHANNEL_STATES.joining;
}
}, {
key: "isLeaving",
value: function isLeaving() {
return this.state === CHANNEL_STATES.leaving;
}
}]);
return Channel;
}();
var Serializer = {
encode: function encode(msg, callback) {
var payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload];
return callback(JSON.stringify(payload));
},
decode: function decode(rawPayload, callback) {
var _JSON$parse = JSON.parse(rawPayload),
_JSON$parse2 = _slicedToArray(_JSON$parse, 5),
join_ref = _JSON$parse2[0],
ref = _JSON$parse2[1],
topic = _JSON$parse2[2],
event = _JSON$parse2[3],
payload = _JSON$parse2[4];
return callback({ join_ref: join_ref, ref: ref, topic: topic, event: event, payload: 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
*
*
*/
var Socket = exports.Socket = function () {
function Socket(endPoint) {
var _this5 = this;
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, Socket);
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(function () {
_this5.disconnect(function () {
return _this5.connect();
});
}, this.reconnectAfterMs);
}
_createClass(Socket, [{
key: "protocol",
value: function protocol() {
return location.protocol.match(/^https/) ? "wss" : "ws";
}
}, {
key: "endPointURL",
value: function endPointURL() {
var 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;
}
}, {
key: "disconnect",
value: function 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}`
*/
}, {
key: "connect",
value: function connect(params) {
var _this6 = this;
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 = function () {
return _this6.onConnOpen();
};
this.conn.onerror = function (error) {
return _this6.onConnError(error);
};
this.conn.onmessage = function (event) {
return _this6.onConnMessage(event);
};
this.conn.onclose = function (event) {
return _this6.onConnClose(event);
};
}
/**
* Logs the message. Override `this.logger` for specialized logging. noops by default
* @param {string} kind
* @param {string} msg
* @param {Object} data
*/
}, {
key: "log",
value: function 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") })
//
}, {
key: "onOpen",
value: function onOpen(callback) {
this.stateChangeCallbacks.open.push(callback);
}
}, {
key: "onClose",
value: function onClose(callback) {
this.stateChangeCallbacks.close.push(callback);
}
}, {
key: "onError",
value: function onError(callback) {
this.stateChangeCallbacks.error.push(callback);
}
}, {
key: "onMessage",
value: function onMessage(callback) {
this.stateChangeCallbacks.message.push(callback);
}
}, {
key: "onConnOpen",
value: function onConnOpen() {
var _this7 = this;
this.log("transport", "connected to " + this.endPointURL());
this.flushSendBuffer();
this.reconnectTimer.reset();
if (!this.conn.skipHeartbeat) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(function () {
return _this7.sendHeartbeat();
}, this.heartbeatIntervalMs);
}
this.stateChangeCallbacks.open.forEach(function (callback) {
return callback();
});
}
}, {
key: "onConnClose",
value: function onConnClose(event) {
this.log("transport", "close", event);
this.triggerChanError();
clearInterval(this.heartbeatTimer);
this.reconnectTimer.scheduleTimeout();
this.stateChangeCallbacks.close.forEach(function (callback) {
return callback(event);
});
}
}, {
key: "onConnError",
value: function onConnError(error) {
this.log("transport", error);
this.triggerChanError();
this.stateChangeCallbacks.error.forEach(function (callback) {
return callback(error);
});
}
}, {
key: "triggerChanError",
value: function triggerChanError() {
this.channels.forEach(function (channel) {
return channel.trigger(CHANNEL_EVENTS.error);
});
}
}, {
key: "connectionState",
value: function 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";
}
}
}, {
key: "isConnected",
value: function isConnected() {
return this.connectionState() === "open";
}
}, {
key: "remove",
value: function remove(channel) {
this.channels = this.channels.filter(function (c) {
return c.joinRef() !== channel.joinRef();
});
}
}, {
key: "channel",
value: function channel(topic) {
var chanParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var chan = new Channel(topic, chanParams, this);
this.channels.push(chan);
return chan;
}
}, {
key: "push",
value: function push(data) {
var _this8 = this;
var topic = data.topic,
event = data.event,
payload = data.payload,
ref = data.ref,
join_ref = data.join_ref;
var callback = function callback() {
_this8.encode(data, function (result) {
_this8.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
*/
}, {
key: "makeRef",
value: function makeRef() {
var newRef = this.ref + 1;
if (newRef === this.ref) {
this.ref = 0;
} else {
this.ref = newRef;
}
return this.ref.toString();
}
}, {
key: "sendHeartbeat",
value: function 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 });
}
}, {
key: "flushSendBuffer",
value: function flushSendBuffer() {
if (this.isConnected() && this.sendBuffer.length > 0) {
this.sendBuffer.forEach(function (callback) {
return callback();
});
this.sendBuffer = [];
}
}
}, {
key: "onConnMessage",
value: function onConnMessage(rawMessage) {
var _this9 = this;
this.decode(rawMessage.data, function (msg) {
var topic = msg.topic,
event = msg.event,
payload = msg.payload,
ref = msg.ref,
join_ref = msg.join_ref;
if (ref && ref === _this9.pendingHeartbeatRef) {
_this9.pendingHeartbeatRef = null;
}
_this9.log("receive", (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload);
_this9.channels.filter(function (channel) {
return channel.isMember(topic, event, payload, join_ref);
}).forEach(function (channel) {
return channel.trigger(event, payload, ref, join_ref);
});
_this9.stateChangeCallbacks.message.forEach(function (callback) {
return callback(msg);
});
});
}
}]);
return Socket;
}();
var LongPoll = exports.LongPoll = function () {
function LongPoll(endPoint) {
_classCallCheck(this, LongPoll);
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();
}
_createClass(LongPoll, [{
key: "normalizeEndpoint",
value: function normalizeEndpoint(endPoint) {
return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
}
}, {
key: "endpointURL",
value: function endpointURL() {
return Ajax.appendParams(this.pollEndpoint, { token: this.token });
}
}, {
key: "closeAndRetry",
value: function closeAndRetry() {
this.close();
this.readyState = SOCKET_STATES.connecting;
}
}, {
key: "ontimeout",
value: function ontimeout() {
this.onerror("timeout");
this.closeAndRetry();
}
}, {
key: "poll",
value: function poll() {
var _this10 = this;
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), function (resp) {
if (resp) {
var status = resp.status,
token = resp.token,
messages = resp.messages;
_this10.token = token;
} else {
var status = 0;
}
switch (status) {
case 200:
messages.forEach(function (msg) {
return _this10.onmessage({ data: msg });
});
_this10.poll();
break;
case 204:
_this10.poll();
break;
case 410:
_this10.readyState = SOCKET_STATES.open;
_this10.onopen();
_this10.poll();
break;
case 0:
case 500:
_this10.onerror();
_this10.closeAndRetry();
break;
default:
throw "unhandled poll status " + status;
}
});
}
}, {
key: "send",
value: function send(body) {
var _this11 = this;
Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
if (!resp || resp.status !== 200) {
_this11.onerror(resp && resp.status);
_this11.closeAndRetry();
}
});
}
}, {
key: "close",
value: function close(code, reason) {
this.readyState = SOCKET_STATES.closed;
this.onclose();
}
}]);
return LongPoll;
}();
var Ajax = exports.Ajax = function () {
function Ajax() {
_classCallCheck(this, Ajax);
}
_createClass(Ajax, null, [{
key: "request",
value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
if (window.XDomainRequest) {
var req = new XDomainRequest(); // IE8, IE9
this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
} else {
var _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);
}
}
}, {
key: "xdomainRequest",
value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
var _this12 = this;
req.timeout = timeout;
req.open(method, endPoint);
req.onload = function () {
var response = _this12.parseJSON(req.responseText);
callback && callback(response);
};
if (ontimeout) {
req.ontimeout = ontimeout;
}
// Work around bug in IE9 that requires an attached onprogress handler
req.onprogress = function () {};
req.send(body);
}
}, {
key: "xhrRequest",
value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
var _this13 = this;
req.open(method, endPoint, true);
req.timeout = timeout;
req.setRequestHeader("Content-Type", accept);
req.onerror = function () {
callback && callback(null);
};
req.onreadystatechange = function () {
if (req.readyState === _this13.states.complete && callback) {
var response = _this13.parseJSON(req.responseText);
callback(response);
}
};
if (ontimeout) {
req.ontimeout = ontimeout;
}
req.send(body);
}
}, {
key: "parseJSON",
value: function 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;
}
}
}, {
key: "serialize",
value: function serialize(obj, parentKey) {
var queryStr = [];
for (var key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
var paramKey = parentKey ? parentKey + "[" + key + "]" : key;
var paramVal = obj[key];
if ((typeof paramVal === "undefined" ? "undefined" : _typeof(paramVal)) === "object") {
queryStr.push(this.serialize(paramVal, paramKey));
} else {
queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
}
}
return queryStr.join("&");
}
}, {
key: "appendParams",
value: function appendParams(url, params) {
if (Object.keys(params).length === 0) {
return url;
}
var prefix = url.match(/\?/) ? "&" : "?";
return "" + url + prefix + this.serialize(params);
}
}]);
return Ajax;
}();
Ajax.states = { complete: 4 };
var Presence = exports.Presence = {
syncState: function syncState(currentState, newState, onJoin, onLeave) {
var _this14 = this;
var state = this.clone(currentState);
var joins = {};
var leaves = {};
this.map(state, function (key, presence) {
if (!newState[key]) {
leaves[key] = presence;
}
});
this.map(newState, function (key, newPresence) {
var currentPresence = state[key];
if (currentPresence) {
var newRefs = newPresence.metas.map(function (m) {
return m.phx_ref;
});
var curRefs = currentPresence.metas.map(function (m) {
return m.phx_ref;
});
var joinedMetas = newPresence.metas.filter(function (m) {
return curRefs.indexOf(m.phx_ref) < 0;
});
var leftMetas = currentPresence.metas.filter(function (m) {
return newRefs.indexOf(m.phx_ref) < 0;
});
if (joinedMetas.length > 0) {
joins[key] = newPresence;
joins[key].metas = joinedMetas;
}
if (leftMetas.length > 0) {
leaves[key] = _this14.clone(currentPresence);
leaves[key].metas = leftMetas;
}
} else {
joins[key] = newPresence;
}
});
return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave);
},
syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) {
var joins = _ref2.joins,
leaves = _ref2.leaves;
var state = this.clone(currentState);
if (!onJoin) {
onJoin = function onJoin() {};
}
if (!onLeave) {
onLeave = function onLeave() {};
}
this.map(joins, function (key, newPresence) {
var currentPresence = state[key];
state[key] = newPresence;
if (currentPresence) {
var _state$key$metas;
(_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas));
}
onJoin(key, currentPresence, newPresence);
});
this.map(leaves, function (key, leftPresence) {
var currentPresence = state[key];
if (!currentPresence) {
return;
}
var refsToRemove = leftPresence.metas.map(function (m) {
return m.phx_ref;
});
currentPresence.metas = currentPresence.metas.filter(function (p) {
return refsToRemove.indexOf(p.phx_ref) < 0;
});
onLeave(key, currentPresence, leftPresence);
if (currentPresence.metas.length === 0) {
delete state[key];
}
});
return state;
},
list: function list(presences, chooser) {
if (!chooser) {
chooser = function chooser(key, pres) {
return pres;
};
}
return this.map(presences, function (key, presence) {
return chooser(key, presence);
});
},
// private
map: function map(obj, func) {
return Object.getOwnPropertyNames(obj).map(function (key) {
return func(key, obj[key]);
});
},
clone: function 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
*/
var Timer = function () {
function Timer(callback, timerCalc) {
_classCallCheck(this, Timer);
this.callback = callback;
this.timerCalc = timerCalc;
this.timer = null;
this.tries = 0;
}
_createClass(Timer, [{
key: "reset",
value: function reset() {
this.tries = 0;
clearTimeout(this.timer);
}
/**
* Cancels any previous scheduleTimeout and schedules callback
*/
}, {
key: "scheduleTimeout",
value: function scheduleTimeout() {
var _this15 = this;
clearTimeout(this.timer);
this.timer = setTimeout(function () {
_this15.tries = _this15.tries + 1;
_this15.callback();
}, this.timerCalc(this.tries + 1));
}
}]);
return Timer;
}();
})));
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(window,function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e)){for(var t=0,n=new Array(e.length);t<e.length;t++)n[t]=e[t];return n}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function a(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}function c(e,t,n){return t&&a(e.prototype,t),n&&a(e,n),e}n.r(t),n.d(t,"Channel",function(){return g}),n.d(t,"Socket",function(){return b}),n.d(t,"LongPoll",function(){return j}),n.d(t,"Ajax",function(){return R}),n.d(t,"Presence",function(){return T});var u="undefined"!=typeof self?self:window,h={connecting:0,open:1,closing:2,closed:3},l=1e4,f={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},p={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},d=[p.close,p.error,p.join,p.reply,p.leave],v={longpoll:"longpoll",websocket:"websocket"},y=function(e){if("function"==typeof e)return e;return function(){return e}},m=function(){function e(t,n,i,o){s(this,e),this.channel=t,this.event=n,this.payload=i||function(){return{}},this.receivedResp=null,this.timeout=o,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}return c(e,[{key:"resend",value:function(e){this.timeout=e,this.reset(),this.send()}},{key:"send",value:function(){this.hasReceived("timeout")||(this.startTimeout(),this.sent=!0,this.channel.socket.push({topic:this.channel.topic,event:this.event,payload:this.payload(),ref:this.ref,join_ref:this.channel.joinRef()}))}},{key:"receive",value:function(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}},{key:"reset",value:function(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}},{key:"matchReceive",value:function(e){var t=e.status,n=e.response;e.ref;this.recHooks.filter(function(e){return e.status===t}).forEach(function(e){return e.callback(n)})}},{key:"cancelRefEvent",value:function(){this.refEvent&&this.channel.off(this.refEvent)}},{key:"cancelTimeout",value:function(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}},{key:"startTimeout",value:function(){var e=this;this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,function(t){e.cancelRefEvent(),e.cancelTimeout(),e.receivedResp=t,e.matchReceive(t)}),this.timeoutTimer=setTimeout(function(){e.trigger("timeout",{})},this.timeout)}},{key:"hasReceived",value:function(e){return this.receivedResp&&this.receivedResp.status===e}},{key:"trigger",value:function(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}}]),e}(),g=function(){function e(t,n,i){var o=this;s(this,e),this.state=f.closed,this.topic=t,this.params=y(n||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new m(this,p.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new C(function(){return o.rejoinUntilConnected()},this.socket.reconnectAfterMs),this.joinPush.receive("ok",function(){o.state=f.joined,o.rejoinTimer.reset(),o.pushBuffer.forEach(function(e){return e.send()}),o.pushBuffer=[]}),this.onClose(function(){o.rejoinTimer.reset(),o.socket.hasLogger()&&o.socket.log("channel","close ".concat(o.topic," ").concat(o.joinRef())),o.state=f.closed,o.socket.remove(o)}),this.onError(function(e){o.isLeaving()||o.isClosed()||(o.socket.hasLogger()&&o.socket.log("channel","error ".concat(o.topic),e),o.state=f.errored,o.rejoinTimer.scheduleTimeout())}),this.joinPush.receive("timeout",function(){o.isJoining()&&(o.socket.hasLogger()&&o.socket.log("channel","timeout ".concat(o.topic," (").concat(o.joinRef(),")"),o.joinPush.timeout),new m(o,p.leave,y({}),o.timeout).send(),o.state=f.errored,o.joinPush.reset(),o.rejoinTimer.scheduleTimeout())}),this.on(p.reply,function(e,t){o.trigger(o.replyEventName(t),e)})}return c(e,[{key:"rejoinUntilConnected",value:function(){this.rejoinTimer.scheduleTimeout(),this.socket.isConnected()&&this.rejoin()}},{key:"join",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw"tried to join multiple times. 'join' can only be called a single time per channel instance";return this.joinedOnce=!0,this.rejoin(e),this.joinPush}},{key:"onClose",value:function(e){this.on(p.close,e)}},{key:"onError",value:function(e){return this.on(p.error,function(t){return e(t)})}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter(function(n){return!(n.event===e&&(void 0===t||t===n.ref))})}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw"tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events");var i=new m(this,e,function(){return t},n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.state=f.leaving;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(p.close,"leave")},i=new m(this,p.leave,y({}),t);return i.receive("ok",function(){return n()}).receive("timeout",function(){return n()}),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return d.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"sendJoin",value:function(e){this.state=f.joining,this.joinPush.resend(e)}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||this.sendJoin(e)}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw"channel onMessage callbacks must return the payload, modified or unmodified";for(var r=0;r<this.bindings.length;r++){var s=this.bindings[r];s.event===e&&s.callback(o,n,i||this.joinRef())}}},{key:"replyEventName",value:function(e){return"chan_reply_".concat(e)}},{key:"isClosed",value:function(){return this.state===f.closed}},{key:"isErrored",value:function(){return this.state===f.errored}},{key:"isJoined",value:function(){return this.state===f.joined}},{key:"isJoining",value:function(){return this.state===f.joining}},{key:"isLeaving",value:function(){return this.state===f.leaving}}]),e}(),k={encode:function(e,t){var n=[e.join_ref,e.ref,e.topic,e.event,e.payload];return t(JSON.stringify(n))},decode:function(e,t){var n=r(JSON.parse(e),5);return t({join_ref:n[0],ref:n[1],topic:n[2],event:n[3],payload:n[4]})}},b=function(){function e(t){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};s(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||l,this.transport=i.transport||u.WebSocket||j,this.defaultEncoder=k.encode,this.defaultDecoder=k.decode,this.transport!==j?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.reconnectAfterMs=i.reconnectAfterMs||function(e){return[1e3,2e3,5e3,1e4][e-1]||1e4},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=y(i.params||{}),this.endPoint="".concat(t,"/").concat(v.websocket),this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new C(function(){n.teardown(function(){return n.connect()})},this.reconnectAfterMs)}return c(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=R.appendParams(R.appendParams(this.endPoint,this.params()),{vsn:"2.0.0"});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=y(e)),this.conn||(this.conn=new this.transport(this.endPointURL()),this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){this.stateChangeCallbacks.open.push(e)}},{key:"onClose",value:function(e){this.stateChangeCallbacks.close.push(e)}},{key:"onError",value:function(e){this.stateChangeCallbacks.error.push(e)}},{key:"onMessage",value:function(e){this.stateChangeCallbacks.message.push(e)}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.resetChannelTimers(),this.stateChangeCallbacks.open.forEach(function(e){return e()})}},{key:"resetHeartbeat",value:function(){var e=this;this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(function(){return e.sendHeartbeat()},this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){this.conn&&(this.conn.onclose=function(){},t?this.conn.close(t,n||""):this.conn.close(),this.conn=null),e&&e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),e&&1e3!==e.code&&this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(function(t){return t(e)})}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach(function(t){return t(e)})}},{key:"triggerChanError",value:function(){this.channels.forEach(function(e){return e.trigger(p.error)})}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case h.connecting:return"connecting";case h.open:return"open";case h.closing:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.channels=this.channels.filter(function(t){return t.joinRef()!==e.joinRef()})}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new g(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,function(e){return t.conn.send(e)}):this.sendBuffer.push(function(){return t.encode(e,function(e){return t.conn.send(e)})})}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.conn.close(1e3,"hearbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(function(e){return e()}),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,function(e){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;r&&r===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(r&&"("+r+")"||""),o);for(var a=0;a<t.channels.length;a++){var c=t.channels[a];c.isMember(n,i,o,s)&&c.trigger(i,o,r,s)}for(var u=0;u<t.stateChangeCallbacks.message.length;u++)t.stateChangeCallbacks.message[u](e)})}},{key:"resetChannelTimers",value:function(){this.channels.forEach(function(e){e.rejoinTimer.restart()})}}]),e}(),j=function(){function e(t){s(this,e),this.endPoint=null,this.token=null,this.skipHeartbeat=!0,this.onopen=function(){},this.onerror=function(){},this.onmessage=function(){},this.onclose=function(){},this.pollEndpoint=this.normalizeEndpoint(t),this.readyState=h.connecting,this.poll()}return c(e,[{key:"normalizeEndpoint",value:function(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+v.websocket),"$1/"+v.longpoll)}},{key:"endpointURL",value:function(){return R.appendParams(this.pollEndpoint,{token:this.token})}},{key:"closeAndRetry",value:function(){this.close(),this.readyState=h.connecting}},{key:"ontimeout",value:function(){this.onerror("timeout"),this.closeAndRetry()}},{key:"poll",value:function(){var e=this;this.readyState!==h.open&&this.readyState!==h.connecting||R.request("GET",this.endpointURL(),"application/json",null,this.timeout,this.ontimeout.bind(this),function(t){if(t){var n=t.status,i=t.token,o=t.messages;e.token=i}else n=0;switch(n){case 200:o.forEach(function(t){return e.onmessage({data:t})}),e.poll();break;case 204:e.poll();break;case 410:e.readyState=h.open,e.onopen(),e.poll();break;case 0:case 500:e.onerror(),e.closeAndRetry();break;default:throw"unhandled poll status ".concat(n)}})}},{key:"send",value:function(e){var t=this;R.request("POST",this.endpointURL(),"application/json",e,this.timeout,this.onerror.bind(this,"timeout"),function(e){e&&200===e.status||(t.onerror(e&&e.status),t.closeAndRetry())})}},{key:"close",value:function(e,t){this.readyState=h.closed,this.onclose()}}]),e}(),R=function(){function e(){s(this,e)}return c(e,null,[{key:"request",value:function(e,t,n,i,o,r,s){if(u.XDomainRequest){var a=new XDomainRequest;this.xdomainRequest(a,e,t,i,o,r,s)}else{var c=u.XMLHttpRequest?new u.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");this.xhrRequest(c,e,t,n,i,o,r,s)}}},{key:"xdomainRequest",value:function(e,t,n,i,o,r,s){var a=this;e.timeout=o,e.open(t,n),e.onload=function(){var t=a.parseJSON(e.responseText);s&&s(t)},r&&(e.ontimeout=r),e.onprogress=function(){},e.send(i)}},{key:"xhrRequest",value:function(e,t,n,i,o,r,s,a){var c=this;e.open(t,n,!0),e.timeout=r,e.setRequestHeader("Content-Type",i),e.onerror=function(){a&&a(null)},e.onreadystatechange=function(){if(e.readyState===c.states.complete&&a){var t=c.parseJSON(e.responseText);a(t)}},s&&(e.ontimeout=s),e.send(o)}},{key:"parseJSON",value:function(e){if(!e||""===e)return null;try{return JSON.parse(e)}catch(t){return console&&console.log("failed to parse JSON response",e),null}}},{key:"serialize",value:function(e,t){var n=[];for(var i in e)if(e.hasOwnProperty(i)){var r=t?"".concat(t,"[").concat(i,"]"):i,s=e[i];"object"===o(s)?n.push(this.serialize(s,r)):n.push(encodeURIComponent(r)+"="+encodeURIComponent(s))}return n.join("&")}},{key:"appendParams",value:function(e,t){if(0===Object.keys(t).length)return e;var n=e.match(/\?/)?"&":"?";return"".concat(e).concat(n).concat(this.serialize(t))}}]),e}();R.states={complete:4};var T=function(){function e(t){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};s(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach(function(t){n.state=e.syncDiff(n.state,t,o,r)}),n.pendingDiffs=[],s()}),this.channel.on(o.diff,function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())})}return c(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,function(e,n){t[e]||(a[e]=n)}),this.map(t,function(e,t){var n=r[e];if(n){var i=t.metas.map(function(e){return e.phx_ref}),c=n.metas.map(function(e){return e.phx_ref}),u=t.metas.filter(function(e){return c.indexOf(e.phx_ref)<0}),h=n.metas.filter(function(e){return i.indexOf(e.phx_ref)<0});u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t}),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map(function(e){return e.phx_ref}),c=o.metas.filter(function(e){return s.indexOf(e.phx_ref)<0});(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)}),this.map(s,function(e,t){var n=a[e];if(n){var i=t.metas.map(function(e){return e.phx_ref});n.metas=n.metas.filter(function(e){return i.indexOf(e.phx_ref)<0}),o(e,n,t),0===n.metas.length&&delete a[e]}}),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,function(e,n){return t(e,n)})}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map(function(n){return t(n,e[n])})}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),C=function(){function e(t,n){s(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return c(e,[{key:"reset",value:function(){this.tries=0,this.clearTimer()}},{key:"restart",value:function(){var e=null!==this.timer;this.reset(),e&&this.scheduleTimeout()}},{key:"scheduleTimeout",value:function(){var e=this;this.clearTimer(),this.timer=setTimeout(function(){e.tries=e.tries+1,e.callback()},this.timerCalc(this.tries+1))}},{key:"clearTimer",value:function(){clearTimeout(this.timer),this.timer=null}}]),e}()}])});

@@ -5,3 +5,3 @@ ![phoenix logo](https://raw.githubusercontent.com/phoenixframework/phoenix/master/priv/static/phoenix.png)

[![Build Status](https://api.travis-ci.org/phoenixframework/phoenix.svg)](https://travis-ci.org/phoenixframework/phoenix)
[![Build Status](https://api.travis-ci.org/phoenixframework/phoenix.svg?branch=master)](https://travis-ci.org/phoenixframework/phoenix)
[![Inline docs](http://inch-ci.org/github/phoenixframework/phoenix.svg)](http://inch-ci.org/github/phoenixframework/phoenix)

@@ -13,2 +13,4 @@

Install the latest version of Phoenix by following the instructions at https://hexdocs.pm/phoenix/installation.html#phoenix
## Documentation

@@ -46,15 +48,34 @@

### Building phoenix.js
To build the documentation from source:
```bash
$ cd assets
$ npm install
$ npm run watch
$ cd ..
$ MIX_ENV=docs mix docs
```
### Building docs from source
To build Phoenix from source:
```bash
$ MIX_ENV=docs mix docs
$ mix deps.get
$ mix compile
```
To build the Phoenix installer from source:
```bash
$ mix deps.get
$ mix compile
$ mix archive.build
```
### Building phoenix.js
```bash
$ cd assets
$ npm install
$ npm run watch
```
## Important links

@@ -65,3 +86,3 @@

* [Issue tracker][4]
* [phoenix-talk Mailing list (questions)][5]
* [Phoenix Forum (questions)][5]
* [phoenix-core Mailing list (development)][6]

@@ -75,3 +96,3 @@ * Visit Phoenix's sponsor, DockYard, for expert [phoenix consulting](https://dockyard.com/phoenix-consulting)

[4]: https://github.com/phoenixframework/phoenix/issues
[5]: http://groups.google.com/group/phoenix-talk
[5]: https://elixirforum.com/c/phoenix-forum
[6]: http://groups.google.com/group/phoenix-core

@@ -78,0 +99,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc