Repository: phoenixframework/phoenix
Pull request #2345 (2017-06-07 09:17:37)

Brunch: Update deps


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5f732fb4..90fb99d71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 1.3.0-dev
+
+* Enhancements
+  * [ChannelTest] Subscribe `connect` to `UserSocket.id` to support testing forceful disconnects
+
 ## 1.3.0-rc.2 (2017-05-15)
 
 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.
diff --git a/README.md b/README.md
index c06d2c626..26632a2bb 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@ See the official site at http://www.phoenixframework.org/
 
 API documentation is available at [https://hexdocs.pm/phoenix](https://hexdocs.pm/phoenix)
 
+Phoenix.js documentation is available at [https://hexdocs.pm/phoenix/js](https://hexdocs.pm/phoenix/js)
+
 ## Contributing
 
 We appreciate any contribution to Phoenix. Check our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) and [CONTRIBUTING.md](CONTRIBUTING.md) guides for more information. We usually keep a list of features and bugs [in the issue tracker][4].
@@ -43,8 +45,7 @@ To create projects outside of the `installer/` directory, add the latest archive
 
 ```bash
 $ npm install
-$ npm install -g brunch
-$ brunch watch
+$ npm run watch
 ```
 
 ### Building docs from source
diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js
index ec73a7adc..8de18a422 100644
--- a/assets/js/phoenix.js
+++ b/assets/js/phoenix.js
@@ -1,171 +1,184 @@
-// 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:
-//
-//     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:
-//
-//     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:
-//
-//     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:
-//
-//     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:
-//
-//     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
-//
-//     // 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)})
-//     })
-//
+/**
+ * Phoenix Channels JavaScript client
+ *
+ * ## Socket Connection
+ *
+ * A single connection is established to the server and
+ * channels are multiplexed over the connection.
+ * Connect to the server using the `Socket` class:
+ *
+ * ```javascript
+ *     let socket = new Socket("/socket", {params: {userToken: "123"}})
+ *     socket.connect()
+ * ```
+ *
+ * The `Socket` constructor takes the mount point of the socket,
+ * the authentication params, as well as options that can be found in
+ * the Socket docs, such as configuring the `LongPoll` transport, and
+ * heartbeat.
+ *
+ * ## Channels
+ *
+ * Channels are isolated, concurrent processes on the server that
+ * subscribe to topics and broker events between the client and server.
+ * To join a channel, you must provide the topic, and channel params for
+ * authorization. Here's an example chat room example where `"new_msg"`
+ * events are listened for, messages are pushed to the server, and
+ * the channel is joined with ok/error/timeout matches:
+ *
+ * ```javascript
+ *     let channel = socket.channel("room:123", {token: roomToken})
+ *     channel.on("new_msg", msg => console.log("Got message", msg) )
+ *     $input.onEnter( e => {
+ *       channel.push("new_msg", {body: e.target.val}, 10000)
+ *        .receive("ok", (msg) => console.log("created message", msg) )
+ *        .receive("error", (reasons) => console.log("create failed", reasons) )
+ *        .receive("timeout", () => console.log("Networking issue...") )
+ *     })
+ *     channel.join()
+ *       .receive("ok", ({messages}) => console.log("catching up", messages) )
+ *       .receive("error", ({reason}) => console.log("failed join", reason) )
+ *       .receive("timeout", () => console.log("Networking issue. Still waiting...") )
+ *```
+ *
+ * ## Joining
+ *
+ * Creating a channel with `socket.channel(topic, params)`, binds the params to
+ * `channel.params`, which are sent up on `channel.join()`.
+ * Subsequent rejoins will send up the modified params for
+ * updating authorization params, or passing up last_message_id information.
+ * Successful joins receive an "ok" status, while unsuccessful joins
+ * receive "error".
+ *
+ * ## Duplicate Join Subscriptions
+ *
+ * While the client may join any number of topics on any number of channels,
+ * the client may only hold a single subscription for each unique topic at any
+ * given time. When attempting to create a duplicate subscription,
+ * the server will close the existing channel, log a warning, and
+ * spawn a new channel for the topic. The client will have their
+ * `channel.onClose` callbacks fired for the existing channel, and the new
+ * channel join will have its receive hooks processed as normal.
+ *
+ * ## Pushing Messages
+ *
+ * From the previous example, we can see that pushing messages to the server
+ * can be done with `channel.push(eventName, payload)` and we can optionally
+ * receive responses from the push. Additionally, we can use
+ * `receive("timeout", callback)` to abort waiting for our other `receive` hooks
+ *  and take action after some period of waiting. The default timeout is 5000ms.
+ *
+ *
+ * ## Socket Hooks
+ *
+ * Lifecycle events of the multiplexed connection can be hooked into via
+ * `socket.onError()` and `socket.onClose()` events, ie:
+ *
+ * ```javascript
+ *     socket.onError( () => console.log("there was an error with the connection!") )
+ *     socket.onClose( () => console.log("the connection dropped") )
+ * ```
+ *
+ *
+ * ## Channel Hooks
+ *
+ * For each joined channel, you can bind to `onError` and `onClose` events
+ * to monitor the channel lifecycle, ie:
+ *
+ * ```javascript
+ *     channel.onError( () => console.log("there was an error!") )
+ *     channel.onClose( () => console.log("the channel has gone away gracefully") )
+ * ```
+ *
+ * ### onError hooks
+ *
+ * `onError` hooks are invoked if the socket connection drops, or the channel
+ * crashes on the server. In either case, a channel rejoin is attempted
+ * automatically in an exponential backoff manner.
+ *
+ * ### onClose hooks
+ *
+ * `onClose` hooks are invoked only in two cases. 1) the channel explicitly
+ * closed on the server, or 2). The client explicitly closed, by calling
+ * `channel.leave()`
+ *
+ *
+ * ## Presence
+ *
+ * The `Presence` object provides features for syncing presence information
+ * from the server with the client and handling presences joining and leaving.
+ *
+ * ### Syncing initial state from the server
+ *
+ * `Presence.syncState` is used to sync the list of presences on the server
+ * with the client's state. An optional `onJoin` and `onLeave` callback can
+ * be provided to react to changes in the client's local presences across
+ * disconnects and reconnects with the server.
+ *
+ * `Presence.syncDiff` is used to sync a diff of presence join and leave
+ * events from the server, as they happen. Like `syncState`, `syncDiff`
+ * accepts optional `onJoin` and `onLeave` callbacks to react to a user
+ * joining or leaving from a device.
+ *
+ * ### Listing Presences
+ *
+ * `Presence.list` is used to return a list of presence information
+ * based on the local state of metadata. By default, all presence
+ * metadata is returned, but a `listBy` function can be supplied to
+ * allow the client to select which metadata to use for a given presence.
+ * For example, you may have a user online from different devices with
+ * a metadata status of "online", but they have set themselves to "away"
+ * on another device. In this case, the app may choose to use the "away"
+ * status for what appears on the UI. The example below defines a `listBy`
+ * function which prioritizes the first metadata which was registered for
+ * each user. This could be the first tab they opened, or the first device
+ * they came online from:
+ *
+ * ```javascript
+ *     let state = {}
+ *     state = Presence.syncState(state, stateFromServer)
+ *     let listBy = (id, {metas: [first, ...rest]}) => {
+ *       first.count = rest.length + 1 // count of this user's presences
+ *       first.id = id
+ *       return first
+ *     }
+ *     let onlineUsers = Presence.list(state, listBy)
+ * ```
+ *
+ *
+ * ### Example Usage
+ *```javascript
+ *     // detect if user has joined for the 1st time or from another tab/device
+ *     let onJoin = (id, current, newPres) => {
+ *       if(!current){
+ *         console.log("user has entered for the first time", newPres)
+ *       } else {
+ *         console.log("user additional presence", newPres)
+ *       }
+ *     }
+ *     // detect if user has left from all tabs/devices, or is still present
+ *     let onLeave = (id, current, leftPres) => {
+ *       if(current.metas.length === 0){
+ *         console.log("user has left from all devices", leftPres)
+ *       } else {
+ *         console.log("user left from a device", leftPres)
+ *       }
+ *     }
+ *     let presences = {} // client's initial empty presence state
+ *     // receive initial presence data from server, sent after join
+ *     myChannel.on("presence_state", state => {
+ *       presences = Presence.syncState(presences, state, onJoin, onLeave)
+ *       displayUsers(Presence.list(presences))
+ *     })
+ *     // receive "presence_diff" from server, containing join/leave events
+ *     myChannel.on("presence_diff", diff => {
+ *       presences = Presence.syncDiff(presences, diff, onJoin, onLeave)
+ *       this.setState({users: Presence.list(room.presences, listBy)})
+ *     })
+ * ```
+ * @module phoenix
+ */
+
 const VSN = "1.0.0"
 const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
 const DEFAULT_TIMEOUT = 10000
@@ -189,15 +202,15 @@ const TRANSPORTS = {
   websocket: "websocket"
 }
 
+/**
+ * Initializes the Push
+ * @param {Channel} channel - The Channel
+ * @param {string} event - The event, for example `"phx_join"`
+ * @param {Object} payload - The payload, for example `{user_id: 123}`
+ * @param {number} timeout - The push timeout in milliseconds
+ */
 class Push {
 
-  // Initializes the Push
-  //
-  // channel - The Channel
-  // event - The event, for example `"phx_join"`
-  // payload - The payload, for example `{user_id: 123}`
-  // timeout - The push timeout in milliseconds
-  //
   constructor(channel, event, payload, timeout){
     this.channel      = channel
     this.event        = event
@@ -209,6 +222,10 @@ class Push {
     this.sent         = false
   }
 
+  /**
+   *
+   * @param {number} timeout
+   */
   resend(timeout){
     this.timeout = timeout
     this.cancelRefEvent()
@@ -219,6 +236,9 @@ class Push {
     this.send()
   }
 
+  /**
+   *
+   */
   send(){ if(this.hasReceived("timeout")){ return }
     this.startTimeout()
     this.sent = true
@@ -230,6 +250,11 @@ class Push {
     })
   }
 
+  /**
+   *
+   * @param {*} status
+   * @param {*} callback
+   */
   receive(status, callback){
     if(this.hasReceived(status)){
       callback(this.receivedResp.response)
@@ -281,6 +306,12 @@ class Push {
   }
 }
 
+/**
+ *
+ * @param {string} topic
+ * @param {Object} params
+ * @param {Socket} socket
+ */
 export class Channel {
   constructor(topic, params, socket) {
     this.state       = CHANNEL_STATES.closed
@@ -367,18 +398,20 @@ export class Channel {
     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:
-  //
-  //     channel.leave().receive("ok", () => alert("left!") )
-  //
+  /** Leaves the channel
+   *
+   * Unsubscribes from server events, and
+   * instructs channel to terminate on server
+   *
+   * Triggers onClose() hooks
+   *
+   * To receive leave acknowledgements, use the a `receive`
+   * hook to bind to the server ack, ie:
+   *
+   * ```javascript
+   *     channel.leave().receive("ok", () => alert("left!") )
+   * ```
+   */
   leave(timeout = this.timeout){
     this.state = CHANNEL_STATES.leaving
     let onClose = () => {
@@ -394,12 +427,14 @@ export class Channel {
     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
+  /**
+   * Overridable message hook
+   *
+   * Receives all events for specialized message handling
+   * before dispatching to the channel callbacks.
+   *
+   * Must return the payload, modified or unmodified
+   */
   onMessage(event, payload, ref){ return payload }
 
   // private
@@ -438,44 +473,63 @@ export class Channel {
   isLeaving(){ return this.state === CHANNEL_STATES.leaving }
 }
 
+  /** 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
+   *
+   *
+  */
 export class Socket {
 
-  // Initializes the Socket
-  //
-  // endPoint - The string WebSocket endpoint, ie, "ws://example.com/socket",
-  //                                               "wss://example.com"
-  //                                               "/socket" (inherited host & protocol)
-  // opts - Optional configuration
-  //   transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
-  //               Defaults to WebSocket with automatic LongPoll fallback.
-  //   encode - The function to encode outgoing messages. Defaults to JSON:
-  //
-  //     (payload, callback) => callback(JSON.stringify(payload))
-  //
-  //   decode - The function to decode incoming messages. Defaults to JSON:
-  //
-  //     (payload, callback) => callback(JSON.parse(payload))
-  //
-  //   timeout - The default timeout in milliseconds to trigger push timeouts.
-  //             Defaults `DEFAULT_TIMEOUT`
-  //   heartbeatIntervalMs - The millisec interval to send a heartbeat message
-  //   reconnectAfterMs - The optional function that returns the millsec
-  //                      reconnect interval. Defaults to stepped backoff of:
-  //
-  //     function(tries){
-  //       return [1000, 5000, 10000][tries - 1] || 10000
-  //     }
-  //
-  //   logger - The optional function for specialized logging, ie:
-  //     `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
-  //
-  //   longpollerTimeout - The maximum timeout of a long poll AJAX request.
-  //                        Defaults to 20s (double the server long poll timer).
-  //
-  //   params - The optional params to pass when connecting
-  //
-  // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
-  //
+
   constructor(endPoint, opts = {}){
     this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}
     this.channels             = []
@@ -527,7 +581,10 @@ export class Socket {
     callback && callback()
   }
 
-  // params - The params to send when connecting, for example `{user_id: userToken}`
+  /**
+   *
+   * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
+   */
   connect(params){
     if(params){
       console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor")
@@ -543,7 +600,12 @@ export class Socket {
     this.conn.onclose   = event => this.onConnClose(event)
   }
 
-  // Logs the message. Override `this.logger` for specialized logging. noops by default
+  /**
+   * Logs the message. Override `this.logger` for specialized logging. noops by default
+   * @param {string} kind
+   * @param {string} msg
+   * @param {Object} data
+   */
   log(kind, msg, data){ this.logger(kind, msg, data) }
 
   // Registers callbacks for connection state change events
@@ -623,7 +685,9 @@ export class Socket {
     }
   }
 
-  // Return the next message ref, accounting for overflows
+  /**
+   * Return the next message ref, accounting for overflows
+   */
   makeRef(){
     let newRef = this.ref + 1
     if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }
@@ -749,7 +813,6 @@ export class LongPoll {
   }
 }
 
-
 export class Ajax {
 
   static request(method, endPoint, accept, body, timeout, ontimeout, callback){
@@ -912,19 +975,25 @@ export var Presence = {
 }
 
 
-// Creates a timer that accepts a `timerCalc` function to perform
-// calculated timeout retries, such as exponential backoff.
-//
-// ## Examples
-//
-//    let reconnectTimer = new Timer(() => this.connect(), function(tries){
-//      return [1000, 5000, 10000][tries - 1] || 10000
-//    })
-//    reconnectTimer.scheduleTimeout() // fires after 1000
-//    reconnectTimer.scheduleTimeout() // fires after 5000
-//    reconnectTimer.reset()
-//    reconnectTimer.scheduleTimeout() // fires after 1000
-//
+/**
+ *
+ * Creates a timer that accepts a `timerCalc` function to perform
+ * calculated timeout retries, such as exponential backoff.
+ *
+ * ## Examples
+ *
+ * ```javascript
+ *    let reconnectTimer = new Timer(() => this.connect(), function(tries){
+ *      return [1000, 5000, 10000][tries - 1] || 10000
+ *    })
+ *    reconnectTimer.scheduleTimeout() // fires after 1000
+ *    reconnectTimer.scheduleTimeout() // fires after 5000
+ *    reconnectTimer.reset()
+ *    reconnectTimer.scheduleTimeout() // fires after 1000
+ * ```
+ * @param {Function} callback
+ * @param {Function} timerCalc
+ */
 class Timer {
   constructor(callback, timerCalc){
     this.callback  = callback
@@ -938,7 +1007,9 @@ class Timer {
     clearTimeout(this.timer)
   }
 
-  // Cancels any previous scheduleTimeout and schedules callback
+  /**
+   * Cancels any previous scheduleTimeout and schedules callback
+   */
   scheduleTimeout(){
     clearTimeout(this.timer)
 
diff --git a/installer/mix.exs b/installer/mix.exs
index 23090f698..d675a7671 100644
--- a/installer/mix.exs
+++ b/installer/mix.exs
@@ -2,10 +2,12 @@ defmodule Phx.New.Mixfile do
   use Mix.Project
 
   def project do
-    [app: :phx_new,
-     start_permanent: Mix.env == :prod,
-     version: "1.3.0-rc.2",
-     elixir: "~> 1.3 or ~> 1.4"]
+    [
+      app: :phx_new,
+      start_permanent: Mix.env == :prod,
+      version: "1.3.0-rc.2",
+      elixir: "~> 1.3 or ~> 1.4"
+    ]
   end
 
   # Configuration for the OTP application
diff --git a/installer/templates/new/mix.exs b/installer/templates/new/mix.exs
index faf4ea255..6a3c34a59 100644
--- a/installer/templates/new/mix.exs
+++ b/installer/templates/new/mix.exs
@@ -2,26 +2,30 @@ defmodule <%= app_module %>.Mixfile do
   use Mix.Project
 
   def project do
-    [app: :<%= app_name %>,
-     version: "0.0.1",<%= if in_umbrella do %>
-     build_path: "../../_build",
-     config_path: "../../config/config.exs",
-     deps_path: "../../deps",
-     lockfile: "../../mix.lock",<% end %>
-     elixir: "~> 1.4",
-     elixirc_paths: elixirc_paths(Mix.env),
-     compilers: [:phoenix, :gettext] ++ Mix.compilers,
-     start_permanent: Mix.env == :prod,<%= if ecto do %>
-     aliases: aliases(),<% end %>
-     deps: deps()]
+    [
+      app: :<%= app_name %>,
+      version: "0.0.1",<%= if in_umbrella do %>
+      build_path: "../../_build",
+      config_path: "../../config/config.exs",
+      deps_path: "../../deps",
+      lockfile: "../../mix.lock",<% end %>
+      elixir: "~> 1.4",
+      elixirc_paths: elixirc_paths(Mix.env),
+      compilers: [:phoenix, :gettext] ++ Mix.compilers,
+      start_permanent: Mix.env == :prod,<%= if ecto do %>
+      aliases: aliases(),<% end %>
+      deps: deps()
+    ]
   end
 
   # Configuration for the OTP application.
   #
   # Type `mix help compile.app` for more information.
   def application do
-    [mod: {<%= app_module %>, []},
-     extra_applications: [:logger]]
+    [
+      mod: {<%= app_module %>, []},
+      extra_applications: [:logger]
+    ]
   end
 
   # Specifies which paths to compile per environment.
@@ -32,14 +36,16 @@ defmodule <%= app_module %>.Mixfile do
   #
   # Type `mix help deps` for examples and options.
   defp deps do
-    [<%= phoenix_dep %>,
-     {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
-     {:phoenix_ecto, "~> 3.2"},
-     {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %>
-     {:phoenix_html, "~> 2.6"},
-     {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
-     {:gettext, "~> 0.11"},
-     {:cowboy, "~> 1.0"}]
+    [
+      <%= phoenix_dep %>,
+      {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
+      {:phoenix_ecto, "~> 3.2"},
+      {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %>
+      {:phoenix_html, "~> 2.6"},
+      {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
+      {:gettext, "~> 0.11"},
+      {:cowboy, "~> 1.0"}
+    ]
   end<%= if ecto do %>
 
   # Aliases are shortcuts or tasks specific to the current project.
@@ -49,8 +55,10 @@ defmodule <%= app_module %>.Mixfile do
   #
   # See the documentation for `Mix` for more info on aliases.
   defp aliases do
-    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
-     "ecto.reset": ["ecto.drop", "ecto.setup"],
-     "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
+    [
+      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+      "ecto.reset": ["ecto.drop", "ecto.setup"],
+      "test": ["ecto.create --quiet", "ecto.migrate", "test"]
+    ]
   end<% end %>
 end
diff --git a/installer/templates/phx_assets/brunch/package.json b/installer/templates/phx_assets/brunch/package.json
index df18961bd..f6d34c8d6 100644
--- a/installer/templates/phx_assets/brunch/package.json
+++ b/installer/templates/phx_assets/brunch/package.json
@@ -10,9 +10,9 @@
     "phoenix_html": "file:<%= phoenix_html_brunch_path %>"<% end %>
   },
   "devDependencies": {
-    "babel-brunch": "6.0.6",
-    "brunch": "2.10.7",
+    "babel-brunch": "6.1.1",
+    "brunch": "2.10.9",
     "clean-css-brunch": "2.10.0",
-    "uglify-js-brunch": "2.1.1"
+    "uglify-js-brunch": "2.10.0"
   }
 }
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index f7391967b..e820f8fdb 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -2,26 +2,30 @@ defmodule <%= app_module %>.Mixfile do
   use Mix.Project
 
   def project do
-    [app: :<%= app_name %>,
-     version: "0.0.1",<%= if in_umbrella do %>
-     build_path: "../../_build",
-     config_path: "../../config/config.exs",
-     deps_path: "../../deps",
-     lockfile: "../../mix.lock",<% end %>
-     elixir: "~> 1.4",
-     elixirc_paths: elixirc_paths(Mix.env),
-     compilers: [:phoenix, :gettext] ++ Mix.compilers,
-     start_permanent: Mix.env == :prod,<%= if ecto do %>
-     aliases: aliases(),<% end %>
-     deps: deps()]
+    [
+      app: :<%= app_name %>,
+      version: "0.0.1",<%= if in_umbrella do %>
+      build_path: "../../_build",
+      config_path: "../../config/config.exs",
+      deps_path: "../../deps",
+      lockfile: "../../mix.lock",<% end %>
+      elixir: "~> 1.4",
+      elixirc_paths: elixirc_paths(Mix.env),
+      compilers: [:phoenix, :gettext] ++ Mix.compilers,
+      start_permanent: Mix.env == :prod,<%= if ecto do %>
+      aliases: aliases(),<% end %>
+      deps: deps()
+    ]
   end
 
   # Configuration for the OTP application.
   #
   # Type `mix help compile.app` for more information.
   def application do
-    [mod: {<%= app_module %>.Application, []},
-     extra_applications: [:logger, :runtime_tools]]
+    [
+      mod: {<%= app_module %>.Application, []},
+      extra_applications: [:logger, :runtime_tools]
+    ]
   end
 
   # Specifies which paths to compile per environment.
@@ -32,14 +36,16 @@ defmodule <%= app_module %>.Mixfile do
   #
   # Type `mix help deps` for examples and options.
   defp deps do
-    [<%= phoenix_dep %>,
-     {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
-     {:phoenix_ecto, "~> 3.2"},
-     {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %>
-     {:phoenix_html, "~> 2.6"},
-     {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
-     {:gettext, "~> 0.11"},
-     {:cowboy, "~> 1.0"}]
+    [
+      <%= phoenix_dep %>,
+      {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
+      {:phoenix_ecto, "~> 3.2"},
+      {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %>
+      {:phoenix_html, "~> 2.6"},
+      {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
+      {:gettext, "~> 0.11"},
+      {:cowboy, "~> 1.0"}
+    ]
   end<%= if ecto do %>
 
   # Aliases are shortcuts or tasks specific to the current project.
@@ -49,8 +55,10 @@ defmodule <%= app_module %>.Mixfile do
   #
   # See the documentation for `Mix` for more info on aliases.
   defp aliases do
-    ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
-     "ecto.reset": ["ecto.drop", "ecto.setup"],
-     "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
+    [
+      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+      "ecto.reset": ["ecto.drop", "ecto.setup"],
+      "test": ["ecto.create --quiet", "ecto.migrate", "test"]
+    ]
   end<% end %>
 end
diff --git a/installer/templates/phx_umbrella/apps/app_name/mix.exs b/installer/templates/phx_umbrella/apps/app_name/mix.exs
index 3f55dd6a1..921c56d46 100644
--- a/installer/templates/phx_umbrella/apps/app_name/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/mix.exs
@@ -2,25 +2,29 @@ defmodule <%= app_module %>.Mixfile do
   use Mix.Project
 
   def project do
-    [app: :<%= app_name %>,
-     version: "0.0.1",
-     build_path: "../../_build",
-     config_path: "../../config/config.exs",
-     deps_path: "../../deps",
-     lockfile: "../../mix.lock",
-     elixir: "~> 1.4",
-     elixirc_paths: elixirc_paths(Mix.env),
-     start_permanent: Mix.env == :prod,
-     aliases: aliases(),
-     deps: deps()]
+    [
+      app: :<%= app_name %>,
+      version: "0.0.1",
+      build_path: "../../_build",
+      config_path: "../../config/config.exs",
+      deps_path: "../../deps",
+      lockfile: "../../mix.lock",
+      elixir: "~> 1.4",
+      elixirc_paths: elixirc_paths(Mix.env),
+      start_permanent: Mix.env == :prod,
+      aliases: aliases(),
+      deps: deps()
+    ]
   end
 
   # Configuration for the OTP application.
   #
   # Type `mix help compile.app` for more information.
   def application do
-    [mod: {<%= app_module %>.Application, []},
-     extra_applications: [:logger, :runtime_tools]]
+    [
+      mod: {<%= app_module %>.Application, []},
+      extra_applications: [:logger, :runtime_tools]
+    ]
   end
 
   # Specifies which paths to compile per environment.
@@ -31,8 +35,10 @@ defmodule <%= app_module %>.Mixfile do
   #
   # Type `mix help deps` for examples and options.
   defp deps do
-    [<%= if ecto do %>{:<%= adapter_app %>, ">= 0.0.0"},
-     {:ecto, "~> 2.1"}<% end %>]
+    [<%= if ecto do %>
+      {:<%= adapter_app %>, ">= 0.0.0"},
+      {:ecto, "~> 2.1"}
+    <% end %>]
   end
 
   # Aliases are shortcuts or tasks specific to the current project.<%= if ecto do %>
@@ -42,8 +48,10 @@ defmodule <%= app_module %>.Mixfile do
   #
   # See the documentation for `Mix` for more info on aliases.
   defp aliases do
-    [<%= if ecto do %>"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
-     "ecto.reset": ["ecto.drop", "ecto.setup"],
-     "test": ["ecto.create --quiet", "ecto.migrate", "test"]<% end %>]
+    [<%= if ecto do %>
+      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
+      "ecto.reset": ["ecto.drop", "ecto.setup"],
+      "test": ["ecto.create --quiet", "ecto.migrate", "test"]
+    <% end %>]
   end
 end
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index 2a51a288e..4d001dfa0 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -2,26 +2,30 @@ defmodule <%= web_namespace %>.Mixfile do
   use Mix.Project
 
   def project do
-    [app: :<%= web_app_name %>,
-     version: "0.0.1",
-     build_path: "../../_build",
-     config_path: "../../config/config.exs",
-     deps_path: "../../deps",
-     lockfile: "../../mix.lock",
-     elixir: "~> 1.4",
-     elixirc_paths: elixirc_paths(Mix.env),
-     compilers: [:phoenix, :gettext] ++ Mix.compilers,
-     start_permanent: Mix.env == :prod,
-     aliases: aliases(),
-     deps: deps()]
+    [
+      app: :<%= web_app_name %>,
+      version: "0.0.1",
+      build_path: "../../_build",
+      config_path: "../../config/config.exs",
+      deps_path: "../../deps",
+      lockfile: "../../mix.lock",
+      elixir: "~> 1.4",
+      elixirc_paths: elixirc_paths(Mix.env),
+      compilers: [:phoenix, :gettext] ++ Mix.compilers,
+      start_permanent: Mix.env == :prod,
+      aliases: aliases(),
+      deps: deps()
+    ]
   end
 
   # Configuration for the OTP application.
   #
   # Type `mix help compile.app` for more information.
   def application do
-    [mod: {<%= web_namespace %>.Application, []},
-     extra_applications: [:logger, :runtime_tools]]
+    [
+      mod: {<%= web_namespace %>.Application, []},
+      extra_applications: [:logger, :runtime_tools]
+    ]
   end
 
   # Specifies which paths to compile per environment.
@@ -32,14 +36,16 @@ defmodule <%= web_namespace %>.Mixfile do
   #
   # Type `mix help deps` for examples and options.
   defp deps do
-    [<%= phoenix_dep %>,
-     {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
-     {:phoenix_ecto, "~> 3.2"},<% end %><%= if html do %>
-     {:phoenix_html, "~> 2.6"},
-     {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
-     {:gettext, "~> 0.11"},<%= if app_name != web_app_name do %>
-     {:<%= app_name %>, in_umbrella: true},<% end %>
-     {:cowboy, "~> 1.0"}]
+    [
+      <%= phoenix_dep %>,
+      {:phoenix_pubsub, "~> 1.0"},<%= if ecto do %>
+      {:phoenix_ecto, "~> 3.2"},<% end %><%= if html do %>
+      {:phoenix_html, "~> 2.6"},
+      {:phoenix_live_reload, "~> 1.0", only: :dev},<% end %>
+      {:gettext, "~> 0.11"},<%= if app_name != web_app_name do %>
+      {:<%= app_name %>, in_umbrella: true},<% end %>
+      {:cowboy, "~> 1.0"}
+    ]
   end
 
   # Aliases are shortcuts or tasks specific to the current project.<%= if ecto do %>
diff --git a/installer/templates/phx_umbrella/mix.exs b/installer/templates/phx_umbrella/mix.exs
index a32dc8ccf..a008cb376 100644
--- a/installer/templates/phx_umbrella/mix.exs
+++ b/installer/templates/phx_umbrella/mix.exs
@@ -2,9 +2,11 @@ defmodule <%= root_app_module %>.Mixfile do
   use Mix.Project
 
   def project do
-    [apps_path: "apps",
-     start_permanent: Mix.env == :prod,
-     deps: deps()]
+    [
+      apps_path: "apps",
+      start_permanent: Mix.env == :prod,
+      deps: deps()
+    ]
   end
 
   # Dependencies can be Hex packages:
diff --git a/installer/templates/static/brunch/package.json b/installer/templates/static/brunch/package.json
index e72199704..080c9af79 100644
--- a/installer/templates/static/brunch/package.json
+++ b/installer/templates/static/brunch/package.json
@@ -10,9 +10,9 @@
     "phoenix_html": "file:<%= brunch_deps_prefix %>deps/phoenix_html"<% end %>
   },
   "devDependencies": {
-    "babel-brunch": "6.0.6",
-    "brunch": "2.10.7",
+    "babel-brunch": "6.1.1",
+    "brunch": "2.10.9",
     "clean-css-brunch": "2.10.0",
-    "uglify-js-brunch": "2.1.1"
+    "uglify-js-brunch": "2.10.0"
   }
 }
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 88b429627..66ecd75bf 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -21,7 +21,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   Overall, this generator will add the following files to lib/your_app:
 
     * a context module in accounts/accounts.ex, serving as the API boundary
-    * a schema in accounts/user.ex, with an `accounts_users` table
+    * a schema in accounts/user.ex, with a `users` table
 
   A migration file for the repository and test files for the context
   will also be generated.
diff --git a/lib/phoenix.ex b/lib/phoenix.ex
index d82dd517d..5c58f62f6 100644
--- a/lib/phoenix.ex
+++ b/lib/phoenix.ex
@@ -10,6 +10,8 @@ defmodule Phoenix do
     * [Phoenix](https://hexdocs.pm/phoenix) - the Phoenix web framework
       (these docs)
 
+    * [Phoenix.js](js) - Phoenix Channels JavaScript client
+
     * [Phoenix Pubsub](https://hexdocs.pm/phoenix_pubsub) - a distributed
       pubsub system with presence support
 
diff --git a/lib/phoenix/channel/server.ex b/lib/phoenix/channel/server.ex
index da29060dc..981fa846e 100644
--- a/lib/phoenix/channel/server.ex
+++ b/lib/phoenix/channel/server.ex
@@ -77,7 +77,9 @@ defmodule Phoenix.Channel.Server do
       payload: payload
     }
   end
-  def broadcast(_, _, _, _), do: raise_invalid_message()
+  def broadcast(_, topic, event, payload) do
+    raise_invalid_message(topic, event, payload)
+  end
 
   @doc """
   Broadcasts on the given pubsub server with the given
@@ -93,7 +95,10 @@ defmodule Phoenix.Channel.Server do
       payload: payload
     }
   end
-  def broadcast!(_, _, _, _), do: raise_invalid_message()
+  def broadcast!(_, topic, event, payload) do
+    raise_invalid_message(topic, event, payload)
+  end
+
 
   @doc """
   Broadcasts on the given pubsub server with the given
@@ -109,7 +114,9 @@ defmodule Phoenix.Channel.Server do
       payload: payload
     }
   end
-  def broadcast_from(_, _, _, _, _), do: raise_invalid_message()
+  def broadcast_from(_, _from, topic, event, payload) do
+    raise_invalid_message(topic, event, payload)
+  end
 
   @doc """
   Broadcasts on the given pubsub server with the given
@@ -125,7 +132,9 @@ defmodule Phoenix.Channel.Server do
       payload: payload
     }
   end
-  def broadcast_from!(_, _, _, _, _), do: raise_invalid_message()
+  def broadcast_from!(_, _from, topic, event, payload) do
+    raise_invalid_message(topic, event, payload)
+  end
 
   @doc """
   Pushes a message with the given topic, event and payload
@@ -140,7 +149,9 @@ defmodule Phoenix.Channel.Server do
     send pid, encoded_msg
     :ok
   end
-  def push(_, _, _, _, _), do: raise_invalid_message()
+  def push(_, topic, event, payload, _) do
+    raise_invalid_message(topic, event, payload)
+  end
 
   @doc """
   Replies to a given ref to the transport process.
@@ -153,12 +164,20 @@ defmodule Phoenix.Channel.Server do
     )
     :ok
   end
-  def reply(_, _, _, _, _), do: raise_invalid_message()
+  def reply(_, _, topic, {_status, payload}, _) do
+    raise_invalid_message(topic, "phx_reply", payload)
+  end
 
 
-  @spec raise_invalid_message() :: no_return()
-  defp raise_invalid_message do
-    raise ArgumentError, "topic and event must be strings, message must be a map"
+  @spec raise_invalid_message(topic :: term, event :: term, payload :: term) :: no_return()
+  defp raise_invalid_message(topic, event, payload) do
+    raise ArgumentError, """
+    topic and event must be strings, message must be a map, got:
+
+      topic: #{inspect topic}
+      event: #{inspect event}
+      payload: #{inspect payload}
+    """
   end
 
   ## Callbacks
diff --git a/mix.exs b/mix.exs
index 2e99f5d52..f55e3617e 100644
--- a/mix.exs
+++ b/mix.exs
@@ -6,111 +6,138 @@ defmodule Phoenix.Mixfile do
   # TODO v1.4: Update to elixir 1.4 + new application inference
 
   def project do
-    [app: :phoenix,
-     version: @version,
-     elixir: "~> 1.3",
-     deps: deps(),
-     package: package(),
-     preferred_cli_env: [docs: :docs],
-
-     # Because we define protocols on the fly to test
-     # Phoenix.Param, we need to disable consolidation
-     # for the test environment for Elixir v1.2 onward.
-     consolidate_protocols: Mix.env != :test,
-     xref: [exclude: [Ecto.Type]],
-
-     name: "Phoenix",
-     docs: [source_ref: "v#{@version}",
-            main: "overview",
-            logo: "logo.png",
-            extra_section: "GUIDES",
-            assets: "deps/phoenix_guides/images",
-            extras: extras()],
-     source_url: "https://github.com/phoenixframework/phoenix",
-     homepage_url: "http://www.phoenixframework.org",
-     description: """
-     Productive. Reliable. Fast. A productive web framework that
-     does not compromise speed and maintainability.
-     """]
+    [
+      app: :phoenix,
+      version: @version,
+      elixir: "~> 1.3",
+      deps: deps(),
+      package: package(),
+      preferred_cli_env: [docs: :docs],
+
+      # Because we define protocols on the fly to test
+      # Phoenix.Param, we need to disable consolidation
+      # for the test environment for Elixir v1.2 onward.
+      consolidate_protocols: Mix.env != :test,
+      xref: [exclude: [Ecto.Type]],
+
+      name: "Phoenix",
+      docs: [
+        source_ref: "v#{@version}",
+        main: "overview",
+        logo: "logo.png",
+        extra_section: "GUIDES",
+        assets: "deps/phoenix_guides/images",
+        extras: extras()
+      ],
+      aliases: aliases(),
+      source_url: "https://github.com/phoenixframework/phoenix",
+      homepage_url: "http://www.phoenixframework.org",
+      description: """
+      Productive. Reliable. Fast. A productive web framework that
+      does not compromise speed and maintainability.
+      """
+    ]
   end
 
   def application do
-    [mod: {Phoenix, []},
-     applications: [:plug, :poison, :logger, :eex],
-     env: [stacktrace_depth: nil,
-           template_engines: [],
-           format_encoders: [],
-           filter_parameters: ["password"],
-           serve_endpoints: false,
-           gzippable_exts: ~w(.js .css .txt .text .html .json .svg)]]
+    [
+      mod: {Phoenix, []},
+      applications: [:plug, :poison, :logger, :eex, :phoenix_pubsub, :crypto],
+      env: [
+        stacktrace_depth: nil,
+        template_engines: [],
+        format_encoders: [],
+        filter_parameters: ["password"],
+        serve_endpoints: false,
+        gzippable_exts: ~w(.js .css .txt .text .html .json .svg)
+      ]
+    ]
   end
 
   defp deps do
-    [{:cowboy, "~> 1.0", optional: true},
-     {:plug, "~> 1.3.2 or ~> 1.4"},
-     {:phoenix_pubsub, "~> 1.0"},
-     {:poison, "~> 2.2 or ~> 3.0"},
-     {:gettext, "~> 0.8", only: :test},
-
-     # Docs dependencies
-     {:ex_doc, "~> 0.16", only: :docs},
-     {:inch_ex, "~> 0.2", only: :docs},
-     {:phoenix_guides, git: "https://github.com/phoenixframework/phoenix_guides.git", compile: false, app: false, only: :docs},
-
-     # Test dependencies
-     {:phoenix_html, "~> 2.6", only: :test},
-     {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}]
+    [
+      {:cowboy, "~> 1.0", optional: true},
+      {:plug, "~> 1.3.2 or ~> 1.4"},
+      {:phoenix_pubsub, "~> 1.0"},
+      {:poison, "~> 2.2 or ~> 3.0"},
+      {:gettext, "~> 0.8", only: :test},
+
+      # Docs dependencies
+      {:ex_doc, "~> 0.16", only: :docs},
+      {:inch_ex, "~> 0.2", only: :docs},
+      {:phoenix_guides, git: "https://github.com/phoenixframework/phoenix_guides.git", compile: false, app: false, only: :docs},
+
+      # Test dependencies
+      {:phoenix_html, "~> 2.6", only: :test},
+      {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
+    ]
   end
 
   defp package do
-    [maintainers: [
-      "Chris McCord", "José Valim", "Lance Halvorsen", "Gary Rennie",
-      "Jason Stiebs", "Eric Meadows-Jönsson", "Sonny Scroggin"],
-     licenses: ["MIT"],
-     links: %{github: "https://github.com/phoenixframework/phoenix"},
-     files: ~w(assets lib priv) ++
-            ~w(brunch-config.js CHANGELOG.md LICENSE.md mix.exs package.json README.md)]
+    [
+      maintainers: [
+        "Chris McCord", "José Valim", "Lance Halvorsen", "Gary Rennie",
+        "Jason Stiebs", "Eric Meadows-Jönsson", "Sonny Scroggin"
+      ],
+      licenses: ["MIT"],
+      links: %{github: "https://github.com/phoenixframework/phoenix"},
+      files: ~w(assets lib priv) ++
+        ~w(brunch-config.js CHANGELOG.md LICENSE.md mix.exs package.json README.md)
+    ]
   end
 
   defp extras do
-    ["introduction/overview.md": [group: "Introduction"],
-     "introduction/installation.md": [group: "Introduction"],
-     "introduction/learning.md": [group: "Introduction"],
-     "introduction/community.md": [group: "Introduction"],
-
-     "up_and_running.md": [group: "Guides"],
-     "adding_pages.md": [group: "Guides"],
-     "routing.md": [group: "Guides"],
-     "plug.md": [group: "Guides"],
-     "controllers.md": [group: "Guides"],
-     "views.md": [group: "Guides"],
-     "templates.md": [group: "Guides"],
-     "channels.md": [group: "Guides"],
-     "ecto_models.md": [group: "Guides"],
-
-     "testing/testing.md": [group: "Testing"],
-     "testing/testing_models.md": [group: "Testing"],
-     "testing/testing_controllers.md": [group: "Testing"],
-     "testing/testing_views.md": [group: "Testing"],
-     "testing/testing_channels.md": [group: "Testing"],
-
-     "deployment/deployment.md": [group: "Deployment"],
-     "deployment/heroku.md": [group: "Deployment"],
-
-     "bonus_guides/upgrading_phoenix.md": [group: "Bonus Guides"],
-     "bonus_guides/custom_primary_key.md": [group: "Bonus Guides"],
-     "bonus_guides/using_mysql.md": [group: "Bonus Guides"],
-     "bonus_guides/static_assets.md": [group: "Bonus Guides"],
-     "bonus_guides/file_uploads.md": [group: "Bonus Guides"],
-     "bonus_guides/sending_email_with_mailgun.md": [group: "Bonus Guides"],
-     "bonus_guides/sending_email_with_smtp.md": [group: "Bonus Guides"],
-     "bonus_guides/sessions.md": [group: "Bonus Guides"],
-     "bonus_guides/custom_errors.md": [group: "Bonus Guides"],
-     "bonus_guides/using_ssl.md": [group: "Bonus Guides"],
-     "bonus_guides/phoenix_behind_proxy.md": [group: "Bonus Guides"],
-     "bonus_guides/config.md": [group: "Bonus Guides"],
-     "bonus_guides/learning_elixir.md": [group: "Bonus Guides"],
-     "bonus_guides/seeding_data.md": [group: "Bonus Guides"]]
+    [
+      "introduction/overview.md": [group: "Introduction"],
+      "introduction/installation.md": [group: "Introduction"],
+      "introduction/learning.md": [group: "Introduction"],
+      "introduction/community.md": [group: "Introduction"],
+
+      "up_and_running.md": [group: "Guides"],
+      "adding_pages.md": [group: "Guides"],
+      "routing.md": [group: "Guides"],
+      "plug.md": [group: "Guides"],
+      "controllers.md": [group: "Guides"],
+      "views.md": [group: "Guides"],
+      "templates.md": [group: "Guides"],
+      "channels.md": [group: "Guides"],
+      "ecto_models.md": [group: "Guides"],
+
+      "testing/testing.md": [group: "Testing"],
+      "testing/testing_models.md": [group: "Testing"],
+      "testing/testing_controllers.md": [group: "Testing"],
+      "testing/testing_views.md": [group: "Testing"],
+      "testing/testing_channels.md": [group: "Testing"],
+
+      "deployment/deployment.md": [group: "Deployment"],
+      "deployment/heroku.md": [group: "Deployment"],
+
+      "bonus_guides/upgrading_phoenix.md": [group: "Bonus Guides"],
+      "bonus_guides/custom_primary_key.md": [group: "Bonus Guides"],
+      "bonus_guides/using_mysql.md": [group: "Bonus Guides"],
+      "bonus_guides/static_assets.md": [group: "Bonus Guides"],
+      "bonus_guides/file_uploads.md": [group: "Bonus Guides"],
+      "bonus_guides/sending_email_with_mailgun.md": [group: "Bonus Guides"],
+      "bonus_guides/sending_email_with_smtp.md": [group: "Bonus Guides"],
+      "bonus_guides/sessions.md": [group: "Bonus Guides"],
+      "bonus_guides/custom_errors.md": [group: "Bonus Guides"],
+      "bonus_guides/using_ssl.md": [group: "Bonus Guides"],
+      "bonus_guides/phoenix_behind_proxy.md": [group: "Bonus Guides"],
+      "bonus_guides/config.md": [group: "Bonus Guides"],
+      "bonus_guides/learning_elixir.md": [group: "Bonus Guides"],
+      "bonus_guides/seeding_data.md": [group: "Bonus Guides"]
+    ]
     |> Enum.map(fn {file, opts} -> {:"deps/phoenix_guides/docs/#{file}", opts} end)
   end
+
+  defp aliases do
+    [
+      "docs": ["docs", &generate_js_docs/1]
+    ]
+  end
+
+  def generate_js_docs(_) do
+    Mix.Task.run "app.start"
+    System.cmd("npm", ["run", "docs"])
+  end
 end
diff --git a/package.json b/package.json
index 3ff729e5c..6759ca413 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
   "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",
@@ -21,6 +22,9 @@
   },
   "files": ["README.md", "LICENSE.md", "package.json", "priv/static/phoenix.js", "assets/js/phoenix.js"],
   "scripts": {
-    "test": "./node_modules/.bin/mocha ./assets/test/**/*.js --compilers js:babel-register -r jsdom-global/register"
+    "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"
   }
 }
diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js
index 382ba0d53..baad88088 100644
--- a/priv/static/phoenix.js
+++ b/priv/static/phoenix.js
@@ -17,174 +17,187 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, 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:
-//
-//     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:
-//
-//     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:
-//
-//     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:
-//
-//     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:
-//
-//     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
-//
-//     // 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)})
-//     })
-//
+/**
+ * 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 = "1.0.0";
 var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
 var DEFAULT_TIMEOUT = 10000;
@@ -208,15 +221,15 @@ var TRANSPORTS = {
   websocket: "websocket"
 };
 
-var Push = function () {
+/**
+ * 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
+ */
 
-  // Initializes the Push
-  //
-  // channel - The Channel
-  // event - The event, for example `"phx_join"`
-  // payload - The payload, for example `{user_id: 123}`
-  // timeout - The push timeout in milliseconds
-  //
+var Push = function () {
   function Push(channel, event, payload, timeout) {
     _classCallCheck(this, Push);
 
@@ -230,6 +243,12 @@ var Push = function () {
     this.sent = false;
   }
 
+  /**
+   *
+   * @param {number} timeout
+   */
+
+
   _createClass(Push, [{
     key: "resend",
     value: function resend(timeout) {
@@ -241,6 +260,11 @@ var Push = function () {
       this.sent = false;
       this.send();
     }
+
+    /**
+     *
+     */
+
   }, {
     key: "send",
     value: function send() {
@@ -256,6 +280,13 @@ var Push = function () {
         ref: this.ref
       });
     }
+
+    /**
+     *
+     * @param {*} status
+     * @param {*} callback
+     */
+
   }, {
     key: "receive",
     value: function receive(status, callback) {
@@ -333,6 +364,11 @@ var Push = function () {
   return Push;
 }();
 
+/**
+ *
+ */
+
+
 var Channel = exports.Channel = function () {
   function Channel(topic, params, socket) {
     var _this2 = this;
@@ -455,18 +491,20 @@ var Channel = exports.Channel = function () {
       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:
-    //
-    //     channel.leave().receive("ok", () => alert("left!") )
-    //
+    /** 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",
@@ -494,12 +532,14 @@ var Channel = exports.Channel = function () {
       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
+    /**
+     * 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",
@@ -591,44 +631,62 @@ var Channel = exports.Channel = function () {
   return Channel;
 }();
 
-var Socket = exports.Socket = function () {
+/** Initializes the Socket
+ *
+ *
+ * @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
+ *
+ *
+ * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
+*/
 
-  // Initializes the Socket
-  //
-  // endPoint - The string WebSocket endpoint, ie, "ws://example.com/socket",
-  //                                               "wss://example.com"
-  //                                               "/socket" (inherited host & protocol)
-  // opts - Optional configuration
-  //   transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
-  //               Defaults to WebSocket with automatic LongPoll fallback.
-  //   encode - The function to encode outgoing messages. Defaults to JSON:
-  //
-  //     (payload, callback) => callback(JSON.stringify(payload))
-  //
-  //   decode - The function to decode incoming messages. Defaults to JSON:
-  //
-  //     (payload, callback) => callback(JSON.parse(payload))
-  //
-  //   timeout - The default timeout in milliseconds to trigger push timeouts.
-  //             Defaults `DEFAULT_TIMEOUT`
-  //   heartbeatIntervalMs - The millisec interval to send a heartbeat message
-  //   reconnectAfterMs - The optional function that returns the millsec
-  //                      reconnect interval. Defaults to stepped backoff of:
-  //
-  //     function(tries){
-  //       return [1000, 5000, 10000][tries - 1] || 10000
-  //     }
-  //
-  //   logger - The optional function for specialized logging, ie:
-  //     `logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
-  //
-  //   longpollerTimeout - The maximum timeout of a long poll AJAX request.
-  //                        Defaults to 20s (double the server long poll timer).
-  //
-  //   params - The optional params to pass when connecting
-  //
-  // For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
-  //
+
+var Socket = exports.Socket = function () {
   function Socket(endPoint) {
     var _this4 = this;
 
@@ -705,7 +763,10 @@ var Socket = exports.Socket = function () {
       callback && callback();
     }
 
-    // params - The params to send when connecting, for example `{user_id: userToken}`
+    /**
+     *
+     * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
+     */
 
   }, {
     key: "connect",
@@ -736,7 +797,12 @@ var Socket = exports.Socket = function () {
       };
     }
 
-    // Logs the message. Override `this.logger` for specialized logging. noops by default
+    /**
+     * Logs the message. Override `this.logger` for specialized logging. noops by default
+     * @param {*} kind
+     * @param {*} msg
+     * @param {*} data
+     */
 
   }, {
     key: "log",
@@ -1267,19 +1333,25 @@ var Presence = exports.Presence = {
   }
 };
 
-// Creates a timer that accepts a `timerCalc` function to perform
-// calculated timeout retries, such as exponential backoff.
-//
-// ## Examples
-//
-//    let reconnectTimer = new Timer(() => this.connect(), function(tries){
-//      return [1000, 5000, 10000][tries - 1] || 10000
-//    })
-//    reconnectTimer.scheduleTimeout() // fires after 1000
-//    reconnectTimer.scheduleTimeout() // fires after 5000
-//    reconnectTimer.reset()
-//    reconnectTimer.scheduleTimeout() // fires after 1000
-//
+/**
+ *
+ * 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) {
@@ -1298,7 +1370,9 @@ var Timer = function () {
       clearTimeout(this.timer);
     }
 
-    // Cancels any previous scheduleTimeout and schedules callback
+    /**
+     * Cancels any previous scheduleTimeout and schedules callback
+     */
 
   }, {
     key: "scheduleTimeout",


Test patch:
diff --git a/installer/test/mix_helper.exs b/installer/test/mix_helper.exs
index 2b7293240..4bf1796e5 100644
--- a/installer/test/mix_helper.exs
+++ b/installer/test/mix_helper.exs
@@ -92,18 +92,20 @@ defmodule MixHelper do
 
   def umbrella_mixfile_contents do
     """
-defmodule Umbrella.Mixfile do
-  use Mix.Project
-
-  def project do
-    [apps_path: "apps",
-     deps: deps()]
-  end
-
-  defp deps do
-    []
-  end
-end
+    defmodule Umbrella.Mixfile do
+      use Mix.Project
+
+      def project do
+        [
+          apps_path: "apps",
+          deps: deps()
+        ]
+      end
+
+      defp deps do
+        []
+      end
+    end
     """
   end
 
diff --git a/lib/phoenix/test/channel_test.ex b/lib/phoenix/test/channel_test.ex
index e3c80392f..0cc0d3946 100644
--- a/lib/phoenix/test/channel_test.ex
+++ b/lib/phoenix/test/channel_test.ex
@@ -240,14 +240,27 @@ defmodule Phoenix.ChannelTest do
   defmacro connect(handler, params) do
     if endpoint = Module.get_attribute(__CALLER__.module, :endpoint) do
       quote do
-        Transport.connect(unquote(endpoint), unquote(handler), :channel_test,
-                          unquote(__MODULE__), NoopSerializer, Phoenix.ChannelTest.__stringify__(unquote(params)))
+        unquote(__MODULE__).__connect__(unquote(endpoint), unquote(handler), unquote(params))
       end
     else
       raise "module attribute @endpoint not set for socket/2"
     end
   end
 
+  @doc false
+  def __connect__(endpoint, handler, params) do
+    endpoint
+    |> Transport.connect(handler, :channel_test, __MODULE__, NoopSerializer, __stringify__(params))
+    |> subscribe_to_socket_id(endpoint, handler)
+  end
+  defp subscribe_to_socket_id({:ok, socket}, endpoint, handler) do
+    if topic = handler.id(socket) do
+      :ok = endpoint.subscribe(topic)
+    end
+    {:ok, socket}
+  end
+  defp subscribe_to_socket_id(error, _endpoint, _handler), do: error
+
   @doc "See `subscribe_and_join!/4`."
   def subscribe_and_join!(%Socket{} = socket, topic) when is_binary(topic) do
     subscribe_and_join!(socket, topic, %{})
diff --git a/test/phoenix/test/channel_test.exs b/test/phoenix/test/channel_test.exs
index 8f2af1b73..9eed0b7c5 100644
--- a/test/phoenix/test/channel_test.exs
+++ b/test/phoenix/test/channel_test.exs
@@ -445,4 +445,10 @@ defmodule Phoenix.Test.ChannelTest do
       assert_reply ref, :ok
     end) =~ "received unexpected message in handle_info/2: :unhandled"
   end
+
+  test "subscribes to socket.id and receives disconnects" do
+    {:ok, socket} = connect(UserSocket, %{})
+    socket.endpoint.broadcast!(socket.id, "disconnect", %{})
+    assert_broadcast "disconnect", %{}
+  end
 end