Repository: phoenixframework/phoenix
Pull request #3264 (2019-01-29 19:34:16)

Allow the app configuration to specify a custom migration module for the generated migration code.
If the current app configuration specifies a custom migration module the generated migration code will use that rather than the default
  Ecto.Migration
     config :ecto_sql, migration_module: MyApplication.CustomMigrationModule

cf.  https://github.com/elixir-ecto/ecto_sql/pull/76

I think I have all the bases covered, if not please advise and I will correct.

Thank you!


Patch:
diff --git a/README.md b/README.md
index ac62e3383..b572f66b9 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 ![phoenix logo](https://raw.githubusercontent.com/phoenixframework/phoenix/master/priv/static/phoenix.png)
 > ### Productive. Reliable. Fast.
-> A productive web framework that does not compromise speed and maintainability.
+> A productive web framework that does not compromise speed or maintainability.
 
 [![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)
diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js
index de56fe015..2bc742947 100644
--- a/assets/js/phoenix.js
+++ b/assets/js/phoenix.js
@@ -115,7 +115,7 @@
  * pass your channel in to track lifecycle events:
  *
  * ```javascript
- * let channel = new socket.channel("some:topic")
+ * let channel = socket.channel("some:topic")
  * let presence = new Presence(channel)
  * ```
  *
@@ -773,6 +773,9 @@ export class Socket {
   /**
    *
    * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
+   *
+   * Passing params to connect is deprecated; pass them in the Socket constructor instead:
+   * `new Socket("/socket", {params: {user_id: userToken}})`.
    */
   connect(params){
     if(params){
@@ -840,7 +843,6 @@ export class Socket {
     this.flushSendBuffer()
     this.reconnectTimer.reset()
     this.resetHeartbeat()
-    this.resetChannelTimers()
     this.stateChangeCallbacks.open.forEach( callback => callback() )
   }
 
@@ -848,7 +850,7 @@ export class Socket {
    * @private
    */
 
-  resetHeartbeat(){ if(this.conn.skipHeartbeat){ return }
+  resetHeartbeat(){ if(this.conn && this.conn.skipHeartbeat){ return }
     this.pendingHeartbeatRef = null
     clearInterval(this.heartbeatTimer)
     this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
@@ -989,15 +991,6 @@ export class Socket {
       }
     })
   }
-
-  /**
-   * @private
-   */
-  resetChannelTimers() {
-    this.channels.forEach(channel => {
-      channel.rejoinTimer.restart()
-    })
-  }
 }
 
 
@@ -1358,31 +1351,18 @@ class Timer {
 
   reset(){
     this.tries = 0
-    this.clearTimer()
-  }
-
-  restart(){
-    const processing = this.timer !== null
-    this.reset()
-    if (processing){
-      this.scheduleTimeout()
-    }
+    clearTimeout(this.timer)
   }
 
   /**
    * Cancels any previous scheduleTimeout and schedules callback
    */
   scheduleTimeout(){
-    this.clearTimer()
+    clearTimeout(this.timer)
 
     this.timer = setTimeout(() => {
       this.tries = this.tries + 1
       this.callback()
     }, this.timerCalc(this.tries + 1))
   }
-
-  clearTimer() {
-    clearTimeout(this.timer)
-    this.timer = null
-  }
 }
diff --git a/guides/channels.md b/guides/channels.md
index aaada208a..e0dfe06d4 100644
--- a/guides/channels.md
+++ b/guides/channels.md
@@ -54,7 +54,7 @@ Once the connection is established, each incoming message from a client is route
 If the channel server asks to broadcast a message, that message is sent to the local PubSub, which sends it out to any clients connected to the same server and subscribed to that topic.
 
 If there are other nodes in the cluster, the local PubSub also forwards the message to their PubSubs, which send it out to their own subscribers.
-Because only one message has to be sent per additional node, the performance cost of adding nodes in negligible, while each new node supports many more subscribers.
+Because only one message has to be sent per additional node, the performance cost of adding nodes is negligible, while each new node supports many more subscribers.
 
 The message flow looks something like this:
 
diff --git a/guides/contexts.md b/guides/contexts.md
index 232002763..38c800dc5 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -1021,15 +1021,14 @@ Open up your CMS context (`lib/hello/cms.ex`), and add this new function:
 ```elixir
 def inc_page_views(%Page{} = page) do
   {1, [%Page{views: views}]} =
-    Repo.update_all(
-      from(p in Page, where: p.id == ^page.id),
-      [inc: [views: 1]], returning: [:views])
+    from(p in Page, where: p.id == ^page.id, select: [:views])
+    |> Repo.update_all(inc: [views: 1])
 
   put_in(page.views, views)
 end
 ```
 
-We built a query for fetching the current page given its ID which we pass to `Repo.update_all`. Ecto's `Repo.update_all` allows us to perform batch updates against the database, and is perfect for atomically updating values, such as incrementing our views count. The result of the repo operation returns the number of updated records, along with the selected schema values specified by the `returning` option. When we receive the new page views, we use `put_in(page.views, views)` to place the new view count within the page.
+We built a query for fetching the current page given its ID which we pass to `Repo.update_all`. Ecto's `Repo.update_all` allows us to perform batch updates against the database, and is perfect for atomically updating values, such as incrementing our views count. The result of the repo operation returns the number of updated records, along with the selected schema values specified by the `select` option. When we receive the new page views, we use `put_in(page.views, views)` to place the new view count within the page.
 
 With our context function in place, let's make use of it in our CMS page controller. Update your `show` action in `lib/hello_web/controllers/cms/page_controller.ex` to call our new function:
 
diff --git a/guides/controllers.md b/guides/controllers.md
index 9d6c12277..653888225 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -2,7 +2,7 @@
 
 Phoenix controllers act as intermediary modules. Their functions - called actions - are invoked from the router in response to HTTP requests. The actions, in turn, gather all the necessary data and perform all the necessary steps before invoking the view layer to render a template or returning a JSON response.
 
-Phoenix controllers also build on the Plug package, and are themselves plugs. Controllers provide the functions to do almost anything we need to in an action. If we do find ourselves looking for something that Phoenix controllers don't provide; however, we might find what we're looking for in Plug itself. Please see the [Plug Guide](plug.html) or [Plug Documentation](https://hexdocs.pm/plug/) for more information.
+Phoenix controllers also build on the Plug package, and are themselves plugs. Controllers provide the functions to do almost anything we need to in an action. If we do find ourselves looking for something that Phoenix controllers don't provide, we might find what we're looking for in Plug itself. Please see the [Plug Guide](plug.html) or [Plug Documentation](https://hexdocs.pm/plug/) for more information.
 
 A newly generated Phoenix app will have a single controller, the `PageController`, which can be found at `lib/hello_web/controllers/page_controller.ex` and looks like this.
 
diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md
index 668f6ac46..aa6d417d7 100644
--- a/guides/deployment/deployment.md
+++ b/guides/deployment/deployment.md
@@ -65,7 +65,7 @@ This step is required only if you have static assets like images, JavaScript, st
 Compilation of static assets happens in two steps:
 
 ```console
-$ cd assets && webpack --mode production
+$ npm run deploy --prefix ./assets
 
 $ mix phx.digest
 
@@ -122,7 +122,7 @@ $ mix deps.get --only prod
 $ MIX_ENV=prod mix compile
 
 # Compile assets
-$ cd assets && webpack --mode production && cd ..
+$ npm run deploy --prefix ./assets
 
 $ mix phx.digest
 
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index d7319087a..5ef88cfac 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -85,13 +85,23 @@ Git remote heroku added
 
 > Note: the name of the Heroku application is the random string after "Creating" in the output above (mysterious-meadow-6277). This will be unique, so expect to see a different name from "mysterious-meadow-6277".
 
-The `--buildpack` option we are passing allows us to specify the [Elixir buildpack](https://github.com/HashNuke/heroku-buildpack-elixir) we want Heroku to use.
-A [buildpack](https://devcenter.heroku.com/articles/buildpacks) is a convenient way of packaging framework and/or runtime support. In our case it's installing Erlang, Elixir, fetching our application dependencies, and so on, before we run it.
+The `--buildpack` option we are passing allows us to specify the [Elixir buildpack](https://github.com/HashNuke/heroku-buildpack-elixir) we want Heroku to use. A [buildpack](https://devcenter.heroku.com/articles/buildpacks) is a convenient way of packaging framework and/or runtime support. In our case it's installing Erlang, Elixir, fetching our application dependencies, and so on, before we run it.
 
 The URL in the output is the URL to our application. If we open it in our browser now, we will get the default Heroku welcome page.
 
 > Note: if we hadn't initialized our Git repository before we ran the `heroku create` command, we wouldn't have our Heroku remote repository properly set up at this point. We can set that up manually by running: `heroku git:remote -a [our-app-name].`
 
+The buildpack uses a predefined Elixir and Erlang version but to avoid surprises when deploying, it is best to explicitly list the Elixir and Erlang version we want in production to be the same we are using during development or in your continuous integration servers. This can be done by creating a config file named `elixir_buildpack.config` in the root directory of your project with your target version of Elixir and Erlang:
+
+```
+# Elixir version
+elixir_version=1.8.1
+
+# Erlang version
+# available versions https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
+erlang_version=21.2.5
+```
+
 ## Adding the Phoenix Static Buildpack
 
 We need to compile static assets for a successful Phoenix deployment. The [Phoenix static buildpack](https://github.com/gjaldon/heroku-buildpack-phoenix-static) can take care of that for us, so let's add it now.
@@ -136,7 +146,7 @@ First, let's make sure our secret key is loaded from Heroku's environment variab
 config :hello, HelloWeb.Endpoint,
   url: [host: "example.com", port: 80],
   cache_static_manifest: "priv/static/cache_manifest.json",
-  secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
+  secret_key_base: System.get_env("SECRET_KEY_BASE") || raise("missing SECRET_KEY_BASE env var")
 ```
 
 Then, we'll add the production database configuration to `config/prod.exs`:
@@ -192,7 +202,7 @@ config :hello, HelloWeb.Endpoint,
   url: [scheme: "https", host: "mysterious-meadow-6277.herokuapp.com", port: 443],
   force_ssl: [rewrite_on: [:x_forwarded_proto]],
   cache_static_manifest: "priv/static/cache_manifest.json",
-  secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
+  secret_key_base: System.get_env("SECRET_KEY_BASE") || raise "missing SECRET_KEY_BASE env var"
 
 # Do not print debug messages in production
 config :logger, level: :info
@@ -266,8 +276,6 @@ Setting config vars and restarting mysterious-meadow-6277... done, v3
 SECRET_KEY_BASE: xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53
 ```
 
-If you need to make any of your config variables available at compile time, you will need to explicitly define which ones in a configuration file. Create a file `elixir_buildpack.config` in your application's root directory and add a line like: `config_vars_to_export=(MY_VAR)`. See [here](https://github.com/HashNuke/heroku-buildpack-elixir#specifying-config-vars-to-export-at-compile-time) for more information.
-
 ## Deploy Time!
 
 Our project is now ready to be deployed on Heroku.
@@ -276,6 +284,7 @@ Let's commit all our changes:
 
 ```
 $ git add config/prod.exs
+$ git add elixir_buildpack.config
 $ git add phoenix_static_buildpack.config
 $ git add compile
 $ git add Procfile
diff --git a/guides/ecto.md b/guides/ecto.md
index a28855fa3..daa2ebf40 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -7,8 +7,6 @@ Ecto currently has adapters for the following databases:
 * PostgreSQL
 * MySQL
 * MSSQL
-* SQLite3
-* MongoDB
 
 Newly generated Phoenix projects include Ecto with the PostgreSQL adapter by default (you can pass the `--no-ecto` flag to exclude this).
 
@@ -264,7 +262,7 @@ iex> changeset.errors
  bio: {"can't be blank", [validation: :required]}]
 ```
 
-What happens if we pass a key/value pair that is in neither defined in the schema nor required?
+What happens if we pass a key/value pair that is neither defined in the schema nor required?
 
 Inside our existing IEx shell, let's create a `params` map with valid values plus an extra `random_key: "random value"`.
 
diff --git a/guides/phoenix_mix_tasks.md b/guides/phoenix_mix_tasks.md
index 846b9189e..a656ac2dd 100644
--- a/guides/phoenix_mix_tasks.md
+++ b/guides/phoenix_mix_tasks.md
@@ -205,8 +205,8 @@ $ mix phx.gen.html Blog Post posts body:string word_count:integer
 * creating test/hello_web/controllers/post_controller_test.exs
 * creating lib/hello/blog/post.ex
 * creating priv/repo/migrations/20170906150129_create_posts.exs
-* creating lib/hello/blog/blog.ex
-* injecting lib/hello/blog/blog.ex
+* creating lib/hello/blog.ex
+* injecting lib/hello/blog.ex
 * creating test/hello/blog/blog_test.exs
 * injecting test/hello/blog/blog_test.exs
 ```
@@ -277,8 +277,8 @@ $ mix phx.gen.html Blog Post posts body:string word_count:integer --no-schema
 * creating lib/hello_web/templates/post/show.html.eex
 * creating lib/hello_web/views/post_view.ex
 * creating test/hello_web/controllers/post_controller_test.exs
-* creating lib/hello/blog/blog.ex
-* injecting lib/hello/blog/blog.ex
+* creating lib/hello/blog.ex
+* injecting lib/hello/blog.ex
 * creating test/hello/blog/blog_test.exs
 * injecting test/hello/blog/blog_test.exs
 ```
@@ -316,8 +316,8 @@ $ mix phx.gen.json Blog Post posts title:string content:string
 * creating lib/hello_web/controllers/fallback_controller.ex
 * creating lib/hello/blog/post.ex
 * creating priv/repo/migrations/20170906153323_create_posts.exs
-* creating lib/hello/blog/blog.ex
-* injecting lib/hello/blog/blog.ex
+* creating lib/hello/blog.ex
+* injecting lib/hello/blog.ex
 * creating test/hello/blog/blog_test.exs
 * injecting test/hello/blog/blog_test.exs
 ```
@@ -388,8 +388,8 @@ $ mix phx.gen.json Blog Post posts title:string content:string --no-schema
 * creating test/hello_web/controllers/post_controller_test.exs
 * creating lib/hello_web/views/changeset_view.ex
 * creating lib/hello_web/controllers/fallback_controller.ex
-* creating lib/hello/blog/blog.ex
-* injecting lib/hello/blog/blog.ex
+* creating lib/hello/blog.ex
+* injecting lib/hello/blog.ex
 * creating test/hello/blog/blog_test.exs
 * injecting test/hello/blog/blog_test.exs
 ```
@@ -408,9 +408,9 @@ Important: If we don't do this, our application won't compile, and we'll get an
 $ mix phx.server
 Compiling 18 files (.ex)
 
-== Compilation error in file lib/hello/blog/blog.ex ==
-** (CompileError) lib/hello/blog/blog.ex:65: Hello.Blog.Post.__struct__/0 is undefined, cannot expand struct Hello.Blog.Post
-    lib/hello/blog/blog.ex:65: (module)
+== Compilation error in file lib/hello/blog.ex ==
+** (CompileError) lib/hello/blog.ex:65: Hello.Blog.Post.__struct__/0 is undefined, cannot expand struct Hello.Blog.Post
+    lib/hello/blog.ex:65: (module)
     (stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
     (elixir) lib/kernel/parallel_compiler.ex:121: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1
 ```
@@ -425,8 +425,8 @@ The `mix phx.gen.context` task takes a number of arguments, the module name of t
 $ mix phx.gen.context Accounts User users name:string age:integer
 * creating lib/hello/accounts/user.ex
 * creating priv/repo/migrations/20170906161158_create_users.exs
-* creating lib/hello/accounts/accounts.ex
-* injecting lib/hello/accounts/accounts.ex
+* creating lib/hello/accounts.ex
+* injecting lib/hello/accounts.ex
 * creating test/hello/accounts/accounts_test.exs
 * injecting test/hello/accounts/accounts_test.exs
 ```
@@ -436,8 +436,8 @@ $ mix phx.gen.context Accounts User users name:string age:integer
 ```console
 * creating lib/hello/admin/accounts/user.ex
 * creating priv/repo/migrations/20170906161246_create_users.exs
-* creating lib/hello/admin/accounts/accounts.ex
-* injecting lib/hello/admin/accounts/accounts.ex
+* creating lib/hello/admin/accounts.ex
+* injecting lib/hello/admin/accounts.ex
 * creating test/hello/admin/accounts/accounts_test.exs
 * injecting test/hello/admin/accounts/accounts_test.exs
 ```
diff --git a/guides/presence.md b/guides/presence.md
index f3bb602f9..4569107fe 100644
--- a/guides/presence.md
+++ b/guides/presence.md
@@ -97,7 +97,7 @@ end
 
 Finally we can use the client-side Presence library included in `phoenix.js` to manage the state and presence diffs that come down the socket. It listens for the `"presence_state"` and `"presence_diff"` events and provides a simple callback for you to handle the events as they happen, with the `onSync` callback.
 
-The `onSync` callback allows you to easily react to presence state changes, which most often results in re-rendering an updated list of active users. You can use the `list` method is to format and return each individual presence based on the needs of your application.
+The `onSync` callback allows you to easily react to presence state changes, which most often results in re-rendering an updated list of active users. You can use the `list` method to format and return each individual presence based on the needs of your application.
 
 To iterate users, we use the `presences.list()` function which accepts a callback. The callback will be called for each presence item with 2 arguments, the presence id and a list of metas (one for each presence for that presence id). We use this to display the users and the number of devices they are online with.
 
diff --git a/guides/routing.md b/guides/routing.md
index 79edd7107..e4a62cbc7 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -32,7 +32,7 @@ defmodule HelloWeb.Router do
   # end
 end
 ```
-The name you gave your application will appear instead of `Hello` for both the router module and controller name.
+The name you gave your application will appear instead of `HelloWeb` for both the router module and controller name.
 
 The first line of this module, `use HelloWeb, :router`, simply makes Phoenix router functions available in our particular router.
 
@@ -115,7 +115,7 @@ user_path  PATCH   /users/:id       HelloWeb.UserController :update
 user_path  DELETE  /users/:id       HelloWeb.UserController :delete
 ```
 
-Of course, the name of your project will replace `Hello`.
+Of course, the name of your project will replace `HelloWeb`.
 
 This is the standard matrix of HTTP verbs, paths, and controller actions. Let's look at them individually, in a slightly different order.
 
diff --git a/guides/up_and_running.md b/guides/up_and_running.md
index 36c594d4d..550b53222 100644
--- a/guides/up_and_running.md
+++ b/guides/up_and_running.md
@@ -16,7 +16,7 @@ $ mix phx.new hello
 
 > A note about [webpack](https://webpack.js.org/) before we begin: Phoenix will use webpack for asset management by default. Webpack's dependencies are installed via the node package manager, not mix. Phoenix will prompt us to install them at the end of the `mix phx.new` task. If we say "no" at that point, and if we don't install those dependencies later with `npm install`, our application will raise errors when we try to start it, and our assets may not load properly. If we don't want to use webpack at all, we can simply pass `--no-webpack` to `mix phx.new`.
 
-> A note about [Ecto](https://hexdocs.pm/phoenix/ecto.html): Ecto allows our Phoenix application to communicate with a data store, such as PostgreSQL or MongoDB. If our application will not require this component we can skip this dependency by passing the `--no-ecto` flag to `mix phx.new`. This flag may also be combined with `--no-webpack` to create a skeleton application.
+> A note about [Ecto](https://hexdocs.pm/phoenix/ecto.html): Ecto allows our Phoenix application to communicate with a data store, such as PostgreSQL, MySQL, or MSSQL. If our application will not require this component we can skip this dependency by passing the `--no-ecto` flag to `mix phx.new`. This flag may also be combined with `--no-webpack` to create a skeleton application.
 
 > To learn more about `mix phx.new` you can read the [Mix Tasks Guide](phoenix_mix_tasks.html#phoenix-specific-mix-tasks).
 
diff --git a/guides/views.md b/guides/views.md
index 5f00d3041..787341c70 100644
--- a/guides/views.md
+++ b/guides/views.md
@@ -73,20 +73,20 @@ When we `use HelloWeb, :view`, we get other conveniences as well. Since `view/0`
 Let's open up the `lib/hello_web/templates/page/index.html.eex` and locate this stanza.
 
 ```html
-<div class="phx-hero">
-  <h2><%= gettext("Welcome to %{name}!", name: "Phoenix") %></h2>
-  <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
-</div>
+<section class="phx-hero">
+  <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
+  <p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
+</section>
 ```
 
 Then let's add a line with a link back to the same page. (The objective is to see how path helpers respond in a template, not to add any functionality.)
 
 ```html
-<div class="phx-hero">
-  <h2><%= gettext("Welcome to %{name}!", name: "Phoenix") %></h2>
-  <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
+<section class="phx-hero">
+  <h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
+  <p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
   <p><a href="<%= Routes.page_path(@conn, :index) %>">Link back to this page</a></p>
-</div>
+</section>
 ```
 
 Now we can reload the page and view source to see what we have.
diff --git a/installer/templates/phx_assets/phoenix.js b/installer/templates/phx_assets/phoenix.js
index ce4bdf108..d26df0cc2 100644
--- a/installer/templates/phx_assets/phoenix.js
+++ b/installer/templates/phx_assets/phoenix.js
@@ -1,1 +1,1 @@
-!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()}(this,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 b}),n.d(t,"Socket",function(){return R}),n.d(t,"LongPoll",function(){return T}),n.d(t,"Ajax",function(){return C}),n.d(t,"Presence",function(){return S});var u="undefined"!=typeof self?self:null,h="undefined"!=typeof window?window:null,l=u||h||void 0,f={connecting:0,open:1,closing:2,closed:3},p=1e4,d={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},v={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},y=[v.close,v.error,v.join,v.reply,v.leave],m={longpoll:"longpoll",websocket:"websocket"},g=function(e){if("function"==typeof e)return e;return function(){return e}},k=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}(),b=function(){function e(t,n,i){var o=this;s(this,e),this.state=d.closed,this.topic=t,this.params=g(n||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new k(this,v.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new w(function(){return o.rejoinUntilConnected()},this.socket.reconnectAfterMs),this.joinPush.receive("ok",function(){o.state=d.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=d.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=d.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 k(o,v.leave,g({}),o.timeout).send(),o.state=d.errored,o.joinPush.reset(),o.rejoinTimer.scheduleTimeout())}),this.on(v.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(v.close,e)}},{key:"onError",value:function(e){return this.on(v.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 k(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=d.leaving;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(v.close,"leave")},i=new k(this,v.leave,g({}),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 y.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=d.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===d.closed}},{key:"isErrored",value:function(){return this.state===d.errored}},{key:"isJoined",value:function(){return this.state===d.joined}},{key:"isJoining",value:function(){return this.state===d.joining}},{key:"isLeaving",value:function(){return this.state===d.leaving}}]),e}(),j={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]})}},R=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||p,this.transport=i.transport||l.WebSocket||T,this.defaultEncoder=j.encode,this.defaultDecoder=j.decode,this.transport!==T?(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=g(i.params||{}),this.endPoint="".concat(t,"/").concat(m.websocket),this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new w(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=C.appendParams(C.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=g(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(v.error)})}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case f.connecting:return"connecting";case f.open:return"open";case f.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 b(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}(),T=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=f.connecting,this.poll()}return c(e,[{key:"normalizeEndpoint",value:function(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+m.websocket),"$1/"+m.longpoll)}},{key:"endpointURL",value:function(){return C.appendParams(this.pollEndpoint,{token:this.token})}},{key:"closeAndRetry",value:function(){this.close(),this.readyState=f.connecting}},{key:"ontimeout",value:function(){this.onerror("timeout"),this.closeAndRetry()}},{key:"poll",value:function(){var e=this;this.readyState!==f.open&&this.readyState!==f.connecting||C.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=f.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;C.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=f.closed,this.onclose()}}]),e}(),C=function(){function e(){s(this,e)}return c(e,null,[{key:"request",value:function(e,t,n,i,o,r,s){if(l.XDomainRequest){var a=new XDomainRequest;this.xdomainRequest(a,e,t,i,o,r,s)}else{var c=l.XMLHttpRequest?new l.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}();C.states={complete:4};var S=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}(),w=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}()}])});
\ No newline at end of file
+!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()}(this,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 b}),n.d(t,"Socket",function(){return R}),n.d(t,"LongPoll",function(){return w}),n.d(t,"Ajax",function(){return C}),n.d(t,"Presence",function(){return S});var u="undefined"!=typeof self?self:null,h="undefined"!=typeof window?window:null,l=u||h||void 0,f={connecting:0,open:1,closing:2,closed:3},p=1e4,d={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},v={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},y=[v.close,v.error,v.join,v.reply,v.leave],g={longpoll:"longpoll",websocket:"websocket"},m=function(e){if("function"==typeof e)return e;return function(){return e}},k=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}(),b=function(){function e(t,n,i){var o=this;s(this,e),this.state=d.closed,this.topic=t,this.params=m(n||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new k(this,v.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new T(function(){return o.rejoinUntilConnected()},this.socket.reconnectAfterMs),this.joinPush.receive("ok",function(){o.state=d.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=d.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=d.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 k(o,v.leave,m({}),o.timeout).send(),o.state=d.errored,o.joinPush.reset(),o.rejoinTimer.scheduleTimeout())}),this.on(v.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 new Error("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(v.close,e)}},{key:"onError",value:function(e){return this.on(v.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 new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new k(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=d.leaving;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(v.close,"leave")},i=new k(this,v.leave,m({}),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 y.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=d.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 new Error("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===d.closed}},{key:"isErrored",value:function(){return this.state===d.errored}},{key:"isJoined",value:function(){return this.state===d.joined}},{key:"isJoining",value:function(){return this.state===d.joining}},{key:"isLeaving",value:function(){return this.state===d.leaving}}]),e}(),j={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]})}},R=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||p,this.transport=i.transport||l.WebSocket||w,this.defaultEncoder=j.encode,this.defaultDecoder=j.decode,this.transport!==w?(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=m(i.params||{}),this.endPoint="".concat(t,"/").concat(g.websocket),this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new T(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=C.appendParams(C.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=m(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.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(v.error)})}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case f.connecting:return"connecting";case f.open:return"open";case f.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 b(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)})}}]),e}(),w=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=f.connecting,this.poll()}return c(e,[{key:"normalizeEndpoint",value:function(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+g.websocket),"$1/"+g.longpoll)}},{key:"endpointURL",value:function(){return C.appendParams(this.pollEndpoint,{token:this.token})}},{key:"closeAndRetry",value:function(){this.close(),this.readyState=f.connecting}},{key:"ontimeout",value:function(){this.onerror("timeout"),this.closeAndRetry()}},{key:"poll",value:function(){var e=this;this.readyState!==f.open&&this.readyState!==f.connecting||C.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=f.open,e.onopen(),e.poll();break;case 0:case 500:e.onerror(),e.closeAndRetry();break;default:throw new Error("unhandled poll status ".concat(n))}})}},{key:"send",value:function(e){var t=this;C.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=f.closed,this.onclose()}}]),e}(),C=function(){function e(){s(this,e)}return c(e,null,[{key:"request",value:function(e,t,n,i,o,r,s){if(l.XDomainRequest){var a=new XDomainRequest;this.xdomainRequest(a,e,t,i,o,r,s)}else{var c=l.XMLHttpRequest?new l.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}();C.states={complete:4};var S=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}(),T=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,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout(function(){e.tries=e.tries+1,e.callback()},this.timerCalc(this.tries+1))}}]),e}()}])});
\ No newline at end of file
diff --git a/installer/templates/phx_assets/webpack/package.json b/installer/templates/phx_assets/webpack/package.json
index 7be6b8ac0..e5d0f5147 100644
--- a/installer/templates/phx_assets/webpack/package.json
+++ b/installer/templates/phx_assets/webpack/package.json
@@ -15,7 +15,7 @@
     "@babel/preset-env": "^7.0.0",
     "babel-loader": "^8.0.0",
     "copy-webpack-plugin": "^4.5.0",
-    "css-loader": "^0.28.10",
+    "css-loader": "^2.1.1",
     "mini-css-extract-plugin": "^0.4.0",
     "optimize-css-assets-webpack-plugin": "^4.0.0",
     "terser-webpack-plugin": "^1.1.0",
diff --git a/installer/templates/phx_assets/webpack/webpack.config.js b/installer/templates/phx_assets/webpack/webpack.config.js
index d63d8e799..40b8fa684 100644
--- a/installer/templates/phx_assets/webpack/webpack.config.js
+++ b/installer/templates/phx_assets/webpack/webpack.config.js
@@ -13,7 +13,7 @@ module.exports = (env, options) => ({
     ]
   },
   entry: {
-      './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
+    './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
   },
   output: {
     filename: 'app.js',
diff --git a/installer/templates/phx_single/config/prod.exs b/installer/templates/phx_single/config/prod.exs
index 375acea39..d767c2675 100644
--- a/installer/templates/phx_single/config/prod.exs
+++ b/installer/templates/phx_single/config/prod.exs
@@ -22,7 +22,7 @@ config :logger, level: :info
 # To get SSL working, you will need to add the `https` key
 # to the previous section and set your `:url` port to 443:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>,
+#     config :<%= web_app_name %>, <%= endpoint_module %>,
 #       ...
 #       url: [host: "example.com", port: 443],
 #       https: [
@@ -46,7 +46,7 @@ config :logger, level: :info
 # We also recommend setting `force_ssl` in your endpoint, ensuring
 # no data is ever sent via http, always redirecting to https:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>,
+#     config :<%= web_app_name %>, <%= endpoint_module %>,
 #       force_ssl: [hsts: true]
 #
 # Check `Plug.SSL` for all available options in `force_ssl`.
@@ -61,7 +61,7 @@ config :logger, level: :info
 # Alternatively, you can configure exactly which server to
 # start per endpoint:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>, server: true
+#     config :<%= web_app_name %>, <%= endpoint_module %>, server: true
 #
 # Note you can't rely on `System.get_env/1` when using releases.
 # See the releases documentation accordingly.
diff --git a/installer/templates/phx_umbrella/apps/app_name/mix.exs b/installer/templates/phx_umbrella/apps/app_name/mix.exs
index 1ae74e864..978f9049d 100644
--- a/installer/templates/phx_umbrella/apps/app_name/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/mix.exs
@@ -37,7 +37,8 @@ defmodule <%= app_module %>.MixProject do
   defp deps do
     [<%= if ecto do %>
       {:ecto_sql, "~> 3.0"},
-      {:<%= adapter_app %>, ">= 0.0.0"}
+      {:<%= adapter_app %>, ">= 0.0.0"},
+      {:jason, "~> 1.0"}
     <% end %>]
   end
 
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
index 2c32b6807..67688df68 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
@@ -23,7 +23,7 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
 # To get SSL working, you will need to add the `https` key
 # to the previous section and set your `:url` port to 443:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>,
+#     config :<%= web_app_name %>, <%= endpoint_module %>,
 #       ...
 #       url: [host: "example.com", port: 443],
 #       https: [
@@ -47,7 +47,7 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
 # We also recommend setting `force_ssl` in your endpoint, ensuring
 # no data is ever sent via http, always redirecting to https:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>,
+#     config :<%= web_app_name %>, <%= endpoint_module %>,
 #       force_ssl: [hsts: true]
 #
 # Check `Plug.SSL` for all available options in `force_ssl`.
@@ -62,7 +62,7 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
 # Alternatively, you can configure exactly which server to
 # start per endpoint:
 #
-#     config :<%= app_name %>, <%= endpoint_module %>, server: true
+#     config :<%= web_app_name %>, <%= endpoint_module %>, server: true
 #
 # Note you can't rely on `System.get_env/1` when using releases.
 # See the releases documentation accordingly.
diff --git a/installer/templates/phx_web/templates/page/index.html.eex b/installer/templates/phx_web/templates/page/index.html.eex
index 9953a4584..07a2ae6bd 100644
--- a/installer/templates/phx_web/templates/page/index.html.eex
+++ b/installer/templates/phx_web/templates/page/index.html.eex
@@ -1,6 +1,6 @@
 <section class="phx-hero">
   <h1><%%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
-  <p>A productive web framework that<br/>does not compromise speed and maintainability.</p>
+  <p>A productive web framework that<br/>does not compromise speed or maintainability.</p>
 </section>
 
 <section class="row">
diff --git a/lib/mix/phoenix.ex b/lib/mix/phoenix.ex
index 5a8464f9f..c65691402 100644
--- a/lib/mix/phoenix.ex
+++ b/lib/mix/phoenix.ex
@@ -91,7 +91,7 @@ defmodule Mix.Phoenix do
 
   """
   def inflect(singular) do
-    base       = Mix.Phoenix.base
+    base       = Mix.Phoenix.base()
     web_module = base |> web_module() |> inspect()
     scoped     = Phoenix.Naming.camelize(singular)
     path       = Phoenix.Naming.underscore(scoped)
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 41e4b72a8..71cac4431 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -30,7 +30,8 @@ defmodule Mix.Phoenix.Schema do
             web_path: nil,
             web_namespace: nil,
             context_app: nil,
-            route_helper: nil
+            route_helper: nil,
+            migration_module: nil
 
   @valid_types [
     :integer,
@@ -122,7 +123,8 @@ defmodule Mix.Phoenix.Schema do
       route_helper: route_helper(web_path, singular),
       sample_id: sample_id(opts),
       context_app: ctx_app,
-      generate?: generate?}
+      generate?: generate?,
+      migration_module: migration_module()}
   end
 
   @doc """
@@ -343,4 +345,11 @@ defmodule Mix.Phoenix.Schema do
     |> String.trim_leading("_")
     |> String.replace("/", "_")
   end
+
+  defp migration_module do
+    case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
+      migration_module when is_atom(migration_module) -> migration_module
+      other -> Mix.raise "Expected :migration_module to be a module, got: #{inspect(other)}"
+    end
+  end
 end
diff --git a/lib/mix/tasks/phx.ex b/lib/mix/tasks/phx.ex
index 2e6cef7d7..9ddf6a90d 100644
--- a/lib/mix/tasks/phx.ex
+++ b/lib/mix/tasks/phx.ex
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Phx do
     Application.ensure_all_started(:phoenix)
     Mix.shell.info "Phoenix v#{Application.spec(:phoenix, :vsn)}"
     Mix.shell.info "Productive. Reliable. Fast."
-    Mix.shell.info "A productive web framework that does not compromise speed and maintainability."
+    Mix.shell.info "A productive web framework that does not compromise speed or maintainability."
     Mix.shell.info "\nAvailable tasks:\n"
     Mix.Tasks.Help.run(["--search", "phx."])
   end
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 083bd90dc..8b9a1ea8c 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -22,7 +22,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 context module in `accounts.ex`, serving as the API boundary
     * a schema in `accounts/user.ex`, with a `users` table
 
   A migration file for the repository and test files for the context
@@ -208,6 +208,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
         raise_with_help "Expected the schema, #{inspect schema}, to be a valid module name"
       context == schema ->
         raise_with_help "The context and schema should have different names"
+      context == Mix.Phoenix.base() ->
+        raise_with_help "Cannot generate context #{context} because it has the same name as the application"
+      schema == Mix.Phoenix.base() ->
+        raise_with_help "Cannot generate schema #{schema} because it has the same name as the application"
       true ->
         args
     end
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 35398f17d..826b8585b 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
 
   Overall, this generator will add the following files to `lib/`:
 
-    * a context module in `lib/app/accounts/accounts.ex` for the accounts API
+    * a context module in `lib/app/accounts.ex` for the accounts API
     * a schema in `lib/app/accounts/user.ex`, with an `users` table
     * a view in `lib/app_web/views/user_view.ex`
     * a controller in `lib/app_web/controllers/user_controller.ex`
diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex
index 33245a448..b42e9606b 100644
--- a/lib/mix/tasks/phx.gen.json.ex
+++ b/lib/mix/tasks/phx.gen.json.ex
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do
 
   Overall, this generator will add the following files to `lib/`:
 
-    * a context module in `lib/app/accounts/accounts.ex` for the accounts API
+    * a context module in `lib/app/accounts.ex` for the accounts API
     * a schema in `lib/app/accounts/user.ex`, with an `users` table
     * a view in `lib/app_web/views/user_view.ex`
     * a controller in `lib/app_web/controllers/user_controller.ex`
diff --git a/lib/mix/tasks/phx.routes.ex b/lib/mix/tasks/phx.routes.ex
index 96b8ee7fc..7bc8f79f1 100644
--- a/lib/mix/tasks/phx.routes.ex
+++ b/lib/mix/tasks/phx.routes.ex
@@ -27,15 +27,27 @@ defmodule Mix.Tasks.Phx.Routes do
 
   @doc false
   def run(args, base \\ Mix.Phoenix.base()) do
-    Mix.Task.run "compile", args
+    Mix.Task.run("compile", args)
 
-    args
-    |> Enum.at(0)
-    |> router(base)
-    |> ConsoleFormatter.format()
+    {router_mod, opts} =
+      case OptionParser.parse(args, switches: [enpdoint: :string, router: :string]) do
+        {opts, [passed_router], _} -> {router(passed_router, base), opts}
+        {opts, [], _} -> {router(opts[:router], base), opts}
+      end
+
+
+    router_mod
+    |> ConsoleFormatter.format(endpoint(opts[:endpoint], base))
     |> Mix.shell.info()
   end
 
+  defp endpoint(nil, base) do
+    loaded(web_mod(base, "Endpoint"))
+  end
+  defp endpoint(module, base) do
+    loaded(Module.concat([module]))
+  end
+
   defp router(nil, base) do
     if Mix.Project.umbrella?() do
       Mix.raise """
@@ -55,11 +67,13 @@ defmodule Mix.Tasks.Phx.Routes do
     """
   end
   defp router(router_name, _base) do
-    arg_router = Module.concat("Elixir", router_name)
-    loaded(arg_router) || Mix.raise "the provided router, #{inspect arg_router}, does not exist"
+    arg_router = Module.concat([router_name])
+    loaded(arg_router) || Mix.raise "the provided router, #{inspect(arg_router)}, does not exist"
   end
 
-  defp loaded(module), do: Code.ensure_loaded?(module) && module
+  defp loaded(module) do
+    if Code.ensure_loaded?(module), do: module
+  end
 
   defp app_mod(base, name), do: Module.concat([base, name])
 
diff --git a/lib/phoenix/channel/server.ex b/lib/phoenix/channel/server.ex
index bc3542f5c..6082d2882 100644
--- a/lib/phoenix/channel/server.ex
+++ b/lib/phoenix/channel/server.ex
@@ -38,10 +38,23 @@ defmodule Phoenix.Channel.Server do
     args = [channel, {payload, from, socket}]
 
     case PoolSupervisor.start_child(socket.endpoint, socket.handler, from, args) do
-      {:ok, :undefined} ->
-        receive do: ({^ref, reply} -> {:error, reply})
       {:ok, pid} ->
-        receive do: ({^ref, reply} -> {:ok, reply, pid})
+        mon_ref = Process.monitor(pid)
+
+        receive do
+          {^ref, {:ok, reply}} ->
+            Process.demonitor(mon_ref, [:flush])
+            {:ok, reply, pid}
+
+          {^ref, {:error, reply}} ->
+            Process.demonitor(mon_ref, [:flush])
+            {:error, reply}
+
+          {:DOWN, ^mon_ref, _, _, reason} ->
+            Logger.error fn -> Exception.format_exit(reason) end
+            {:error, %{reason: "join crashed"}}
+        end
+
       {:error, reason} ->
         Logger.error fn -> Exception.format_exit(reason) end
         {:error, %{reason: "join crashed"}}
@@ -194,6 +207,23 @@ defmodule Phoenix.Channel.Server do
 
   @doc false
   def init({auth_payload, from, socket}) do
+    # TODO: Use handle_continue when we support Erlang/OTP 21+.
+    send(self(), {:join, __MODULE__})
+    {:ok, {auth_payload, from, socket}}
+  end
+
+  @doc false
+  def handle_call(:socket, _from, socket) do
+    {:reply, socket, socket}
+  end
+
+  @doc false
+  def handle_cast(:close, socket) do
+    {:stop, {:shutdown, :closed}, socket}
+  end
+
+  @doc false
+  def handle_info({:join, __MODULE__}, {auth_payload, from, socket}) do
     %{channel: channel, topic: topic, private: private} = socket
 
     socket = %{socket
@@ -202,13 +232,13 @@ defmodule Phoenix.Channel.Server do
 
     case channel_join(socket, channel, topic, auth_payload) do
       {:ok, socket} ->
-        init(socket, channel, topic, %{}, from)
+        init_info(socket, channel, topic, %{}, from)
       {:ok, reply, socket} ->
-        init(socket, channel, topic, reply, from)
+        init_info(socket, channel, topic, reply, from)
       {:error, reply} ->
         log_join socket, topic, fn -> "Replied #{topic} :error" end
-        GenServer.reply(from, reply)
-        :ignore
+        GenServer.reply(from, {:error, reply})
+        {:stop, :shutdown, socket}
       other ->
         raise """
         channel #{inspect socket.channel}.join/3 is expected to return one of:
@@ -222,40 +252,6 @@ defmodule Phoenix.Channel.Server do
     end
   end
 
-  defp channel_join(socket, channel, topic, auth_payload) do
-    instrument = %{params: auth_payload, socket: socket}
-
-    Phoenix.Endpoint.instrument socket, :phoenix_channel_join, instrument, fn ->
-      channel.join(topic, auth_payload, socket)
-    end
-  end
-
-  defp init(socket, channel, topic, reply, from) do
-    %{transport_pid: transport_pid, serializer: serializer, pubsub_server: pubsub_server} = socket
-    Process.monitor(transport_pid)
-
-    fastlane = {transport_pid, serializer, channel.__intercepts__()}
-    PubSub.subscribe(pubsub_server, topic, link: true, fastlane: fastlane)
-
-    log_join socket, topic, fn -> "Replied #{topic} :ok" end
-    GenServer.reply(from, reply)
-    {:ok, %{socket | joined: true}}
-  end
-
-  defp log_join(%{private: %{log_join: false}}, _topic, _func), do: :noop
-  defp log_join(%{private: %{log_join: level}}, _topic, func), do: Logger.log(level, func)
-
-  @doc false
-  def handle_call(:socket, _from, socket) do
-    {:reply, socket, socket}
-  end
-
-  @doc false
-  def handle_cast(:close, socket) do
-    {:stop, {:shutdown, :closed}, socket}
-  end
-
-  @doc false
   def handle_info(%Message{topic: topic, event: "phx_leave", ref: ref}, %{topic: topic} = socket) do
     handle_in({:stop, {:shutdown, :left}, :ok, put_in(socket.ref, ref)})
   end
@@ -318,6 +314,10 @@ defmodule Phoenix.Channel.Server do
     end
   end
 
+  def terminate(_reason, _socket) do
+    :ok
+  end
+
   @doc false
   def fastlane(subscribers, from, %Broadcast{event: event} = msg) do
     Enum.reduce(subscribers, %{}, fn
@@ -353,6 +353,31 @@ defmodule Phoenix.Channel.Server do
     end)
   end
 
+  ## Joins
+
+  defp channel_join(socket, channel, topic, auth_payload) do
+    instrument = %{params: auth_payload, socket: socket}
+
+    Phoenix.Endpoint.instrument socket, :phoenix_channel_join, instrument, fn ->
+      channel.join(topic, auth_payload, socket)
+    end
+  end
+
+  defp init_info(socket, channel, topic, reply, from) do
+    %{transport_pid: transport_pid, serializer: serializer, pubsub_server: pubsub_server} = socket
+    Process.monitor(transport_pid)
+
+    fastlane = {transport_pid, serializer, channel.__intercepts__()}
+    PubSub.subscribe(pubsub_server, topic, link: true, fastlane: fastlane)
+
+    log_join socket, topic, fn -> "Replied #{topic} :ok" end
+    GenServer.reply(from, {:ok, reply})
+    {:noreply, %{socket | joined: true}}
+  end
+
+  defp log_join(%{private: %{log_join: false}}, _topic, _func), do: :noop
+  defp log_join(%{private: %{log_join: level}}, _topic, func), do: Logger.log(level, func)
+
   ## Handle results
 
   defp handle_result({:stop, reason, socket}, _callback) do
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 90d71add9..3e6ea6d6c 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -806,7 +806,7 @@ defmodule Phoenix.Controller do
   end
 
   @doc """
-  Puts the URL or `%URI{}` to be used for route generation.
+  Puts the url string or `%URI{}` to be used for route generation.
 
   This function overrides the default URL generation pulled
   from the `%Plug.Conn{}`'s endpoint configuration.
@@ -824,8 +824,8 @@ defmodule Phoenix.Controller do
 
   Now when you call `Routes.some_route_url(conn, ...)`, it will use
   the router url set above. Keep in mind that, if you want to generate
-  routes to the current domain, it is preferred to use `Routes.some_route_path`
-  helpers, as those are always relative.
+  routes to the *current* domain, it is preferred to use
+  `Routes.some_route_path` helpers, as those are always relative.
   """
   def put_router_url(conn, %URI{} = uri) do
     put_private(conn, :phoenix_router_url, uri)
@@ -837,12 +837,22 @@ defmodule Phoenix.Controller do
   @doc """
   Puts the format in the connection.
 
+  This format is used when rendering a template as an atom.
+  For example, `render(conn, :foo)` will render `"foo.FORMAT"`
+  where the format is the one set here. The default format
+  is typically set from the negotiation done in `accepts/2`.
+
   See `get_format/1` for retrieval.
   """
   def put_format(conn, format), do: put_private(conn, :phoenix_format, format)
 
   @doc """
   Returns the request format, such as "json", "html".
+
+  This format is used when rendering a template as an atom.
+  For example, `render(conn, :foo)` will render `"foo.FORMAT"`
+  where the format is the one set here. The default format
+  is typically set from the negotiation done in `accepts/2`.
   """
   def get_format(conn) do
     conn.private[:phoenix_format] || conn.params["_format"]
@@ -1344,12 +1354,26 @@ defmodule Phoenix.Controller do
   end
 
   @doc """
-  Returns the current request path, with and without query params.
+  Returns the current request path with its default query parameters:
+
+      iex> current_path(conn)
+      "/users/123?existing=param"
+
+  See `current_path/2` to override the default parameters.
+  """
+  def current_path(%Plug.Conn{query_string: ""} = conn) do
+    conn.request_path
+  end
+
+  def current_path(%Plug.Conn{query_string: query_string} = conn) do
+    conn.request_path <> "?" <> query_string
+  end
+
+  @doc """
+  Returns the current path with the given query parameters.
 
-  By default, the connection's query params are included in
-  the generated path. Custom query params may be used instead
-  by providing a map of your own params. You may also retrieve
-  only the request path by passing an empty map of params.
+  You may also retrieve only the request path by passing an
+  empty map of params.
 
   ## Examples
 
@@ -1366,12 +1390,6 @@ defmodule Phoenix.Controller do
       "/users/123"
 
   """
-  def current_path(%Plug.Conn{query_string: ""} = conn) do
-    conn.request_path
-  end
-  def current_path(%Plug.Conn{query_string: query_string} = conn) do
-    conn.request_path <> "?" <> query_string
-  end
   def current_path(%Plug.Conn{} = conn, params) when params == %{} do
     conn.request_path
   end
@@ -1379,8 +1397,20 @@ defmodule Phoenix.Controller do
     conn.request_path <> "?" <> Plug.Conn.Query.encode(params)
   end
 
+  @doc """
+  Returns the current request url with its default query parameters:
+
+      iex> current_url(conn)
+      "https://www.example.com/users/123?existing=param"
+
+  See `current_url/2` to override the default parameters.
+  """
+  def current_url(%Plug.Conn{} = conn) do
+    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn)
+  end
+
   @doc ~S"""
-  Returns the current request URL, with and without query params.
+  Returns the current request URL with query params.
 
   The path will be retrieved from the currently requested path via
   `current_path/1`. The scheme, host and others will be received from
@@ -1420,11 +1450,8 @@ defmodule Phoenix.Controller do
       end
 
   However, if you want all generated URLs to always have a certain schema,
-  host, etc, you may invoke `put_router_url/2`.
+  host, etc, you may use `put_router_url/2`.
   """
-  def current_url(%Plug.Conn{} = conn) do
-    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn)
-  end
   def current_url(%Plug.Conn{} = conn, %{} = params) do
     Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn, params)
   end
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index af6b93e4e..c4030ce47 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -562,7 +562,7 @@ defmodule Phoenix.Endpoint do
 
   defp plug() do
     quote location: :keep do
-      use Plug.Builder
+      use Plug.Builder, init_mode: Phoenix.plug_init_mode()
       import Phoenix.Endpoint
 
       Module.register_attribute(__MODULE__, :phoenix_sockets, accumulate: true)
@@ -789,6 +789,7 @@ defmodule Phoenix.Endpoint do
 
   defp socket_path(path, config) do
     end_path_fragment = Keyword.fetch!(config, :path)
+
     {vars, path} =
       String.split(path <> "/" <> end_path_fragment, "/", trim: true)
       |> Enum.join("/")
@@ -834,9 +835,10 @@ defmodule Phoenix.Endpoint do
       of options. See "Shared configuration" and
       "Longpoll configuration" for the whole list
 
-    * `:shutdown` - the maximum shutdown time of each channel
-      when the endpoint is shutting down. Applies only to
-      channel-based sockets
+  If your socket is implemented using `Phoenix.Socket`,
+  you can also pass here all options accepted on
+  `use Phoenix.Socket`. An option given here will override
+  the value in `Phoenix.Socket`.
 
   ## Examples
 
@@ -911,7 +913,6 @@ defmodule Phoenix.Endpoint do
               connect_info: [:uri, signing_salt: "NZIguRPO"]
             ]
 
-
   ## Websocket configuration
 
   The following configuration applies only to `:websocket`.
diff --git a/lib/phoenix/endpoint/cowboy2_adapter.ex b/lib/phoenix/endpoint/cowboy2_adapter.ex
index e12d53d81..68cac1e4a 100644
--- a/lib/phoenix/endpoint/cowboy2_adapter.ex
+++ b/lib/phoenix/endpoint/cowboy2_adapter.ex
@@ -66,16 +66,16 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
   @doc false
   def info(scheme, endpoint, ref) do
     server = "cowboy #{Application.spec(:cowboy)[:vsn]}"
-    "Running #{inspect endpoint} with #{server} at #{uri(scheme, endpoint, ref)}"
+    "Running #{inspect endpoint} with #{server} at #{bound_address(scheme, ref)}"
   end
 
-  defp uri(scheme, endpoint, ref) do
+  defp bound_address(scheme, ref) do
     case :ranch.get_addr(ref) do
       {:local, unix_path} ->
-        %URI{host: URI.encode_www_form(unix_path), scheme: "#{scheme}+unix"}
+        "#{unix_path} (#{scheme}+unix)"
 
-      _ ->
-        endpoint.struct_url()
+      {addr, port} ->
+        "#{:inet.ntoa(addr)}:#{port} (#{scheme})"
     end
   end
 end
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index 9c074b3ba..f73a6d2f0 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -66,7 +66,7 @@ defmodule Phoenix.Endpoint.RenderErrors do
   end
 
   defp instrument_render_and_send(conn, kind, reason, stack, opts) do
-    level = Keyword.get(opts, :log_level, :info)
+    level = Keyword.get(opts, :log_level, :debug)
     status = status(kind, reason)
     conn = error_conn(conn, kind, reason)
     metadata = %{status: status, conn: conn, kind: kind, reason: reason, stacktrace: stack, log_level: level}
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index ab29af197..0cb20b856 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -14,7 +14,9 @@ defmodule Phoenix.Endpoint.Supervisor do
     case Supervisor.start_link(__MODULE__, {otp_app, mod}, name: mod) do
       {:ok, _} = ok ->
         warmup(mod)
+        log_access_info(otp_app, mod)
         ok
+
       {:error, _} = error ->
         error
     end
@@ -412,4 +414,10 @@ defmodule Phoenix.Endpoint.Supervisor do
       %{}
     end
   end
+
+  defp log_access_info(otp_app, endpoint) do
+    if Phoenix.Endpoint.server?(otp_app, endpoint) do
+      Logger.info("Access #{inspect(endpoint)} at #{endpoint.url()}")
+    end
+  end
 end
diff --git a/lib/phoenix/presence.ex b/lib/phoenix/presence.ex
index 6fa582a80..1a1635912 100644
--- a/lib/phoenix/presence.ex
+++ b/lib/phoenix/presence.ex
@@ -213,18 +213,11 @@ defmodule Phoenix.Presence do
   @callback fetch(topic, presences) :: presences
 
   @doc """
-  Returns presences for a channel.
+  Returns presences for a topic or a socket.
 
   Calls `list/2` with presence module.
   """
-  @callback list(socket :: Phoenix.Socket.t) :: presences
-
-  @doc """
-  Returns presences for a topic.
-
-  Calls `list/2` with presence module.
-  """
-  @callback list(topic) :: presences
+  @callback list(Phoenix.Socket.t | topic) :: presences
 
   @doc false
   @callback handle_diff(%{topic => {joins :: presences, leaves :: presences}}, state :: term) :: {:ok, state :: term}
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index af260cd73..fdecd0b93 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -323,6 +323,13 @@ defmodule Phoenix.Router do
     Helpers.define(env, routes_with_exprs)
     {matches, _} = Enum.map_reduce(routes_with_exprs, %{}, &build_match/2)
 
+    checks =
+      for {%{line: line}, %{dispatch: {plug, _}}} <- routes_with_exprs, into: %{} do
+        quote line: line do
+          {unquote(plug).init([]), true}
+        end
+      end
+
     # @anno is used here to avoid warnings if forwarding to root path
     match_404 =
       quote @anno do
@@ -335,6 +342,9 @@ defmodule Phoenix.Router do
       @doc false
       def __routes__,  do: unquote(Macro.escape(routes))
 
+      @doc false
+      def __checks__, do: unquote({:__block__, [], Map.keys(checks)})
+
       @doc false
       def __helpers__, do: __MODULE__.Helpers
 
@@ -361,7 +371,6 @@ defmodule Phoenix.Router do
       host: host
     } = exprs
 
-
     {pipe_name, pipe_definition, known_pipelines} =
       case known_pipelines do
         %{^pipe_through => name} ->
@@ -671,7 +680,9 @@ defmodule Phoenix.Router do
 
     * `:path` - a string containing the path scope
     * `:as` - a string or atom containing the named helper scope
-    * `:alias` - an alias (atom) containing the controller scope
+    * `:alias` - an alias (atom) containing the controller scope.
+      When set, this value may be overridden per route by passing `alias: false`
+      to route definitions, such as `get`, `post`, etc.
     * `:host` - a string containing the host scope, or prefix host scope,
       ie `"foo.bar.com"`, `"foo."`
     * `:private` - a map of private data to merge into the connection when a route matches
@@ -745,6 +756,22 @@ defmodule Phoenix.Router do
     end
   end
 
+  @doc """
+  Returns the full alias with the current scope's aliased prefix.
+
+  Useful for applying the same short-hand alias handling to
+  other values besides the second argument in route definitions.
+
+  ## Examples
+
+      scope "/", MyPrefix do
+        get "/", ProxyPlug, controller: scoped_alias(__MODULE__, MyController)
+      end
+  """
+  def scoped_alias(router_module, alias) do
+    Scope.expand_alias(router_module, alias)
+  end
+
   @doc """
   Forwards a request at the given path to a plug.
 
diff --git a/lib/phoenix/router/console_formatter.ex b/lib/phoenix/router/console_formatter.ex
index 9f9539413..fb68dd6f8 100644
--- a/lib/phoenix/router/console_formatter.ex
+++ b/lib/phoenix/router/console_formatter.ex
@@ -5,25 +5,83 @@ defmodule Phoenix.Router.ConsoleFormatter do
   @doc """
   Format the routes for printing.
   """
-  def format(router) do
-    routes = router.__routes__
-    column_widths = calculate_column_widths(routes)
-    Enum.map_join(routes, "", &format_route(&1, column_widths))
+  def format(router, endpoint \\ nil) do
+    routes = router.__routes__()
+    column_widths = calculate_column_widths(routes, endpoint)
+
+    routes
+    |> Enum.map_join("", &format_route(&1, column_widths))
+    |> Kernel.<>(format_endpoint(endpoint, column_widths))
   end
 
-  defp calculate_column_widths(routes) do
-    Enum.reduce routes, {0, 0, 0}, fn(route, acc) ->
-      %Route{verb: verb, path: path, helper: helper} = route
-      verb = verb_name(verb)
-      {verb_len, path_len, route_name_len} = acc
-      route_name = route_name(helper)
+  defp format_endpoint(nil, _), do: ""
+  defp format_endpoint(endpoint, widths) do
+    case endpoint.__sockets__() do
+      [] -> ""
+      sockets ->
+        Enum.map_join(sockets, "", fn socket ->
+          format_websocket(socket, widths) <>
+          format_longpoll(socket, widths)
+        end)
+      end
+  end
+
+  defp format_websocket({_path, Phoenix.LiveReloader.Socket, _opts}, _), do: ""
+  defp format_websocket({path, module, opts}, widths) do
+    if opts[:websocket] != false do
+      {verb_len, path_len, route_name_len} = widths
+
+      String.pad_leading("websocket", route_name_len) <> "  " <>
+      String.pad_trailing("WS", verb_len) <> "  " <>
+      String.pad_trailing(path <> "/websocket", path_len) <> "  " <>
+      inspect(module) <>
+      "\n"
+    else
+      ""
+    end
+  end
+
+  defp format_longpoll({_path, Phoenix.LiveReloader.Socket, _opts}, _), do: ""
+  defp format_longpoll({path, module, opts}, widths) do
+    if opts[:longpoll] != false do
+      for method <- ["GET", "POST"], into: "" do
+        {verb_len, path_len, route_name_len} = widths
 
-      {max(verb_len, String.length(verb)),
-       max(path_len, String.length(path)),
-       max(route_name_len, String.length(route_name))}
+        String.pad_leading("longpoll", route_name_len) <> "  " <>
+        String.pad_trailing(method, verb_len) <> "  " <>
+        String.pad_trailing(path <> "/longpoll", path_len) <> "  " <>
+        inspect(module) <>
+        "\n"
+      end
+    else
+      ""
     end
   end
 
+  defp calculate_column_widths(routes, endpoint) do
+    sockets = endpoint && endpoint.__sockets__() || []
+
+    widths =
+      Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
+        %Route{verb: verb, path: path, helper: helper} = route
+        verb = verb_name(verb)
+        {verb_len, path_len, route_name_len} = acc
+        route_name = route_name(helper)
+
+        {max(verb_len, String.length(verb)),
+        max(path_len, String.length(path)),
+        max(route_name_len, String.length(route_name))}
+      end)
+
+    Enum.reduce(sockets, widths, fn {path, _mod, _opts}, acc ->
+      {verb_len, path_len, route_name_len} = acc
+
+      {verb_len,
+       max(path_len, String.length(path <> "/websocket")),
+       max(route_name_len, String.length("websocket"))}
+    end)
+  end
+
   defp format_route(route, column_widths) do
     %Route{verb: verb, path: path, plug: plug,
            opts: opts, helper: helper} = route
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index c6a1704cc..ccdfb7466 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -3,7 +3,6 @@ defmodule Phoenix.Router.Helpers do
   @moduledoc false
 
   alias Phoenix.Router.Route
-  alias Phoenix.Socket
   alias Plug.Conn
 
   @anno (if :erlang.system_info(:otp_release) >= '19' do
@@ -23,7 +22,7 @@ defmodule Phoenix.Router.Helpers do
     end
   end
 
-  def url(_router, %Socket{endpoint: endpoint}) do
+  def url(_router, %_{endpoint: endpoint}) do
     endpoint.url()
   end
 
@@ -37,8 +36,8 @@ defmodule Phoenix.Router.Helpers do
 
   def url(router, other) do
     raise ArgumentError,
-      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{} or a Phoenix.Endpoint " <>
-      "when building url for #{inspect router}, got: #{inspect other}"
+      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+      "or a Phoenix.Endpoint when building url for #{inspect(router)}, got: #{inspect(other)}"
   end
 
   @doc """
@@ -55,7 +54,7 @@ defmodule Phoenix.Router.Helpers do
     (uri.path || "") <> path
   end
 
-  def path(_router, %Socket{endpoint: endpoint}, path) do
+  def path(_router, %_{endpoint: endpoint}, path) do
     endpoint.path(path)
   end
 
@@ -65,8 +64,8 @@ defmodule Phoenix.Router.Helpers do
 
   def path(router, other, _path) do
     raise ArgumentError,
-      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{} or a Phoenix.Endpoint " <>
-      "when building path for #{inspect router}, got: #{inspect other}"
+      "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+      "or a Phoenix.Endpoint when building path for #{inspect(router)}, got: #{inspect(other)}"
   end
 
   ## Helpers
@@ -208,7 +207,7 @@ defmodule Phoenix.Router.Helpers do
         private.phoenix_endpoint.static_path(path)
       end
 
-      def static_path(%Socket{endpoint: endpoint} = conn, path) do
+      def static_path(%_{endpoint: endpoint} = conn, path) do
         endpoint.static_path(path)
       end
 
@@ -223,7 +222,7 @@ defmodule Phoenix.Router.Helpers do
         static_url(private.phoenix_endpoint, path)
       end
 
-      def static_url(%Socket{endpoint: endpoint} = conn, path) do
+      def static_url(%_{endpoint: endpoint} = conn, path) do
         static_url(endpoint, path)
       end
 
diff --git a/lib/phoenix/router/scope.ex b/lib/phoenix/router/scope.ex
index 32ca4000f..9e8af044a 100644
--- a/lib/phoenix/router/scope.ex
+++ b/lib/phoenix/router/scope.ex
@@ -4,14 +4,16 @@ defmodule Phoenix.Router.Scope do
 
   @stack :phoenix_router_scopes
   @pipes :phoenix_pipeline_scopes
+  @top :phoenix_top_scopes
 
-  defstruct path: nil, alias: nil, as: nil, pipes: [], host: nil, private: %{}, assigns: %{}
+  defstruct path: [], alias: [], as: [], pipes: [], host: nil, private: %{}, assigns: %{}
 
   @doc """
   Initializes the scope.
   """
   def init(module) do
-    Module.put_attribute(module, @stack, [%Scope{}])
+    Module.put_attribute(module, @stack, [])
+    Module.put_attribute(module, @top, %Scope{})
     Module.put_attribute(module, @pipes, MapSet.new)
   end
 
@@ -23,9 +25,10 @@ defmodule Phoenix.Router.Scope do
     private = Keyword.get(opts, :private, %{})
     assigns = Keyword.get(opts, :assigns, %{})
     as      = Keyword.get(opts, :as, Phoenix.Naming.resource_name(plug, "Controller"))
+    alias?  = Keyword.get(opts, :alias, true)
 
     {path, host, alias, as, pipes, private, assigns} =
-      join(module, path, plug, as, private, assigns)
+      join(module, path, plug, alias?, as, private, assigns)
     Phoenix.Router.Route.build(line, kind, verb, path, host, alias, plug_opts, as, pipes, private, assigns)
   end
 
@@ -57,25 +60,15 @@ defmodule Phoenix.Router.Scope do
   """
   def pipe_through(module, new_pipes) do
     new_pipes = List.wrap(new_pipes)
-    stack_pipes =
-      module
-      |> get_stack()
-      |> Enum.flat_map(fn scope -> scope.pipes end)
-
-    update_stack(module, fn [scope | stack] ->
-      pipes = collect_pipes(new_pipes, stack_pipes, scope.pipes)
-      [put_in(scope.pipes, pipes) | stack]
-    end)
-  end
-  defp collect_pipes([] = _new_pipes, _stack_pipes, acc), do: acc
-  defp collect_pipes([pipe | new_pipes], stack_pipes, acc) do
-    if pipe in new_pipes or pipe in stack_pipes do
-      raise ArgumentError, """
-      duplicate pipe_through for #{inspect pipe}.
-      A plug may only be used once inside a scoped pipe_through
-      """
+    %{pipes: pipes} = top = get_top(module)
+
+    if pipe = Enum.find(new_pipes, & &1 in pipes) do
+      raise ArgumentError,
+            "duplicate pipe_through for #{inspect pipe}. " <>
+              "A plug may only be used once inside a scoped pipe_through"
     end
-    collect_pipes(new_pipes, stack_pipes, acc ++ [pipe])
+
+    put_top(module, %{top | pipes: pipes ++ new_pipes})
   end
 
   @doc """
@@ -86,36 +79,43 @@ defmodule Phoenix.Router.Scope do
   end
 
   def push(module, opts) when is_list(opts) do
-    path = with path when not is_nil(path) <- Keyword.get(opts, :path),
-                path <- validate_path(path),
-                do: String.split(path, "/", trim: true)
-
-    alias = Keyword.get(opts, :alias)
-    alias = alias && Atom.to_string(alias)
+    path =
+      if path = Keyword.get(opts, :path) do
+        path |> validate_path() |> String.split("/", trim: true)
+      else
+        []
+      end
+
+    alias = Keyword.get(opts, :alias) |> List.wrap() |> Enum.map(&Atom.to_string/1)
+    as = Keyword.get(opts, :as) |> List.wrap()
+    host = Keyword.get(opts, :host)
+    private = Keyword.get(opts, :private, %{})
+    assigns = Keyword.get(opts, :assigns, %{})
 
-    scope = %Scope{path: path,
-                   alias: alias,
-                   as: Keyword.get(opts, :as),
-                   host: Keyword.get(opts, :host),
-                   pipes: [],
-                   private: Keyword.get(opts, :private, %{}),
-                   assigns: Keyword.get(opts, :assigns, %{})}
+    top = get_top(module)
+    update_stack(module, fn stack -> [top | stack] end)
 
-    update_stack(module, fn stack -> [scope|stack] end)
+    put_top(module, %Scope{
+      path: top.path ++ path,
+      alias: top.alias ++ alias,
+      as: top.as ++ as,
+      host: host || top.host,
+      pipes: top.pipes,
+      private: Map.merge(top.private, private),
+      assigns: Map.merge(top.assigns, assigns)
+    })
   end
 
   @doc """
   Pops a scope from the module stack.
   """
   def pop(module) do
-    update_stack(module, fn [_|stack] -> stack end)
+    update_stack(module, fn [top | stack] ->
+      put_top(module, top)
+      stack
+    end)
   end
 
-  @doc """
-  Returns true if the module's definition is currently within a scope block.
-  """
-  def inside_scope?(module), do: length(get_stack(module)) > 1
-
   @doc """
   Add a forward to the router.
   """
@@ -132,70 +132,39 @@ defmodule Phoenix.Router.Scope do
     raise ArgumentError, "forward expects a module as the second argument, #{inspect plug} given"
   end
 
-  defp expand_alias(module, alias) do
-    if inside_scope?(module) do
-      module
-      |> get_stack()
-      |> join_alias(alias)
-    else
-      alias
-    end
-  end
-
-  defp join(module, path, alias, as, private, assigns) do
-    stack = get_stack(module)
-    {join_path(stack, path), find_host(stack), join_alias(stack, alias),
-     join_as(stack, as), join_pipe_through(stack), join_private(stack, private),
-     join_assigns(stack, assigns)}
-  end
-
-  defp join_path(stack, path) do
-    "/" <>
-      ([String.split(path, "/", trim: true) | extract(stack, :path)]
-       |> Enum.reverse()
-       |> Enum.concat()
-       |> Enum.join("/"))
-  end
-
-  defp join_alias(stack, alias) when is_atom(alias) do
-    [alias|extract(stack, :alias)]
-    |> Enum.reverse()
-    |> Module.concat()
-  end
-
-  defp join_as(_stack, nil), do: nil
-  defp join_as(stack, as) when is_atom(as) or is_binary(as) do
-    [as|extract(stack, :as)]
-    |> Enum.reverse()
-    |> Enum.join("_")
+  @doc """
+  Expands the alias in the current router scope.
+  """
+  def expand_alias(module, alias) do
+    join_alias(get_top(module), alias)
   end
 
-  defp join_private(stack, private) do
-    Enum.reduce stack, private, &Map.merge(&1.private, &2)
-  end
+  defp join(module, path, alias, alias?, as, private, assigns) do
+    top = get_top(module)
+    joined_alias =
+      if alias? do
+        join_alias(top, alias)
+      else
+        alias
+      end
 
-  defp join_assigns(stack, assigns) do
-    Enum.reduce stack, assigns, &Map.merge(&1.assigns, &2)
+    {join_path(top, path), top.host, joined_alias, join_as(top, as), top.pipes,
+     Map.merge(top.private, private), Map.merge(top.assigns, assigns)}
   end
 
-  defp join_pipe_through(stack) do
-    for scope <- Enum.reverse(stack),
-        item <- scope.pipes,
-        do: item
+  defp join_path(top, path) do
+    "/" <> Enum.join(top.path ++ String.split(path, "/", trim: true), "/")
   end
 
-  defp find_host(stack) do
-    Enum.find_value(stack, & &1.host)
+  defp join_alias(top, alias) when is_atom(alias) do
+    Module.concat(top.alias ++ [alias])
   end
 
-  defp extract(stack, attr) do
-    for scope <- stack,
-        item = Map.fetch!(scope, attr),
-        do: item
-  end
+  defp join_as(_top, nil), do: nil
+  defp join_as(top, as) when is_atom(as) or is_binary(as), do: Enum.join(top.as ++ [as], "_")
 
-  defp get_stack(module) do
-    get_attribute(module, @stack)
+  defp get_top(module) do
+    get_attribute(module, @top)
   end
 
   defp update_stack(module, fun) do
@@ -206,6 +175,11 @@ defmodule Phoenix.Router.Scope do
     update_attribute(module, @pipes, fun)
   end
 
+  defp put_top(module, value) do
+    Module.put_attribute(module, @top, value)
+    value
+  end
+
   defp get_attribute(module, attr) do
     Module.get_attribute(module, attr) ||
       raise "Phoenix router scope was not initialized"
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 01234dad2..b9be8c4ff 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -68,13 +68,20 @@ defmodule Phoenix.Socket do
     * `:transport_pid` - The pid of the socket's transport process
     * `:serializer` - The serializer for socket messages
 
-  ## Logging
+  ## Using options
 
-  Logging for socket connections is set via the `:log` option, for example:
+  On `use Phoenix.Socket`, the following options are accepted:
 
-      use Phoenix.Socket, log: :debug
+    * `:log` - the default level to log socket actions. Defaults
+      to `:info`. May be set to `false` to disable it
 
-  Defaults to the `:info` log level. Pass `false` to disable logging.
+    * `:shutdown` - the maximum shutdown time of each channel
+      when the endpoint is shutting down. Applies only to
+      channel-based sockets
+
+    * `:partitions` - each channel is spawned under a supervisor.
+      This option controls how many supervisors will be spawned
+      to handlle channels. Defaults to the number of cores.
 
   ## Garbage collection
 
@@ -131,9 +138,9 @@ defmodule Phoenix.Socket do
       {auth_payload, from, socket}
 
   A custom channel implementation MUST invoke
-  `GenServer.reply(from, reply_payload)` during its initialization
-  with a custom `reply_payload` that will be sent as a reply to the
-  client. Failing to do so will block the socket forever.
+  `GenServer.reply(from, {:ok | :error, reply_payload})` during its
+  initialization with a custom `reply_payload` that will be sent as
+  a reply to the client. Failing to do so will block the socket forever.
 
   A custom channel receives `Phoenix.Socket.Message` structs as regular
   messages from the transport. Replies to those messages and custom
@@ -250,7 +257,7 @@ defmodule Phoenix.Socket do
           channel_pid: pid,
           endpoint: atom,
           handler: atom,
-          id: nil,
+          id: String.t | nil,
           joined: boolean,
           ref: term,
           private: %{},
@@ -269,7 +276,7 @@ defmodule Phoenix.Socket do
       @behaviour Phoenix.Socket
       @before_compile Phoenix.Socket
       Module.register_attribute(__MODULE__, :phoenix_channels, accumulate: true)
-      @phoenix_log Keyword.get(unquote(opts), :log, :info)
+      @phoenix_socket_options unquote(opts)
 
       ## Callbacks
 
@@ -277,11 +284,11 @@ defmodule Phoenix.Socket do
 
       @doc false
       def child_spec(opts) do
-        Phoenix.Socket.__child_spec__(__MODULE__, opts)
+        Phoenix.Socket.__child_spec__(__MODULE__, opts, @phoenix_socket_options)
       end
 
       @doc false
-      def connect(map), do: Phoenix.Socket.__connect__(__MODULE__, map, @phoenix_log)
+      def connect(map), do: Phoenix.Socket.__connect__(__MODULE__, map, @phoenix_socket_options)
 
       @doc false
       def init(state), do: Phoenix.Socket.__init__(state)
@@ -400,11 +407,13 @@ defmodule Phoenix.Socket do
 
   ## CALLBACKS IMPLEMENTATION
 
-  def __child_spec__(handler, opts) do
+  def __child_spec__(handler, opts, socket_options) do
     import Supervisor.Spec
     endpoint = Keyword.fetch!(opts, :endpoint)
+
+    opts = Keyword.merge(socket_options, opts)
     shutdown = Keyword.get(opts, :shutdown, 5_000)
-    partitions = Keyword.get(opts, :partitions) || System.schedulers_online()
+    partitions = Keyword.get(opts, :partitions, System.schedulers_online())
 
     worker_opts = [shutdown: shutdown, restart: :temporary]
     worker = worker(Phoenix.Channel.Server, [], worker_opts)
@@ -412,7 +421,7 @@ defmodule Phoenix.Socket do
     supervisor(Phoenix.Socket.PoolSupervisor, [args], id: handler)
   end
 
-  def __connect__(user_socket, map, log) do
+  def __connect__(user_socket, map, socket_options) do
     %{
       endpoint: endpoint,
       options: options,
@@ -420,7 +429,10 @@ defmodule Phoenix.Socket do
       params: params,
       connect_info: connect_info
     } = map
+
     vsn = params["vsn"] || "1.0.0"
+    options = Keyword.merge(socket_options, options)
+    log = Keyword.get(options, :log, :info)
     meta = Map.merge(map, %{vsn: vsn, user_socket: user_socket, log: log})
 
     Phoenix.Endpoint.instrument(endpoint, :phoenix_socket_connect, meta, fn ->
diff --git a/lib/phoenix/template/eex_engine.ex b/lib/phoenix/template/eex_engine.ex
index 4c76eea30..43fbc373a 100644
--- a/lib/phoenix/template/eex_engine.ex
+++ b/lib/phoenix/template/eex_engine.ex
@@ -6,21 +6,22 @@ defmodule Phoenix.Template.EExEngine do
   @behaviour Phoenix.Template.Engine
 
   def compile(path, name) do
-    EEx.compile_file(path, engine: engine_for(name), line: 1, trim: true)
+    EEx.compile_file(path, [line: 1] ++ options_for(name))
   end
 
-  defp engine_for(name) do
+  defp options_for(name) do
     case Phoenix.Template.format_encoder(name) do
       Phoenix.HTML.Engine ->
         unless Code.ensure_loaded?(Phoenix.HTML.Engine) do
           raise "could not load Phoenix.HTML.Engine to use with .html.eex templates. " <>
-                "You can configure your own format encoder for HTML but we recommend " <>
-                "adding phoenix_html as a dependency as it provides XSS protection."
+                  "You can configure your own format encoder for HTML but we recommend " <>
+                  "adding phoenix_html as a dependency as it provides XSS protection."
         end
 
-        Phoenix.HTML.Engine
+        [engine: Phoenix.HTML.Engine, trim: true]
+
       _ ->
-        EEx.SmartEngine
+        [engine: EEx.SmartEngine]
     end
   end
 end
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index d10568238..9226e9029 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -233,6 +233,8 @@ defmodule Phoenix.Token do
     false
   end
 
+  defp expired?(_signed, max_age_secs) when max_age_secs <= 0, do: true
+
   defp expired?(signed, max_age_secs), do: (signed + trunc(max_age_secs * 1000)) < now_ms()
 
   defp now_ms, do: System.system_time(:millisecond)
diff --git a/mix.exs b/mix.exs
index fc43e63f6..f5f332bd7 100644
--- a/mix.exs
+++ b/mix.exs
@@ -22,7 +22,7 @@ defmodule Phoenix.MixProject do
       homepage_url: "http://www.phoenixframework.org",
       description: """
       Productive. Reliable. Fast. A productive web framework that
-      does not compromise speed and maintainability.
+      does not compromise speed or maintainability.
       """
     ]
   end
diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js
index ce4bdf108..d26df0cc2 100644
--- a/priv/static/phoenix.js
+++ b/priv/static/phoenix.js
@@ -1,1 +1,1 @@
-!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()}(this,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 b}),n.d(t,"Socket",function(){return R}),n.d(t,"LongPoll",function(){return T}),n.d(t,"Ajax",function(){return C}),n.d(t,"Presence",function(){return S});var u="undefined"!=typeof self?self:null,h="undefined"!=typeof window?window:null,l=u||h||void 0,f={connecting:0,open:1,closing:2,closed:3},p=1e4,d={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},v={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},y=[v.close,v.error,v.join,v.reply,v.leave],m={longpoll:"longpoll",websocket:"websocket"},g=function(e){if("function"==typeof e)return e;return function(){return e}},k=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}(),b=function(){function e(t,n,i){var o=this;s(this,e),this.state=d.closed,this.topic=t,this.params=g(n||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new k(this,v.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new w(function(){return o.rejoinUntilConnected()},this.socket.reconnectAfterMs),this.joinPush.receive("ok",function(){o.state=d.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=d.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=d.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 k(o,v.leave,g({}),o.timeout).send(),o.state=d.errored,o.joinPush.reset(),o.rejoinTimer.scheduleTimeout())}),this.on(v.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(v.close,e)}},{key:"onError",value:function(e){return this.on(v.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 k(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=d.leaving;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(v.close,"leave")},i=new k(this,v.leave,g({}),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 y.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=d.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===d.closed}},{key:"isErrored",value:function(){return this.state===d.errored}},{key:"isJoined",value:function(){return this.state===d.joined}},{key:"isJoining",value:function(){return this.state===d.joining}},{key:"isLeaving",value:function(){return this.state===d.leaving}}]),e}(),j={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]})}},R=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||p,this.transport=i.transport||l.WebSocket||T,this.defaultEncoder=j.encode,this.defaultDecoder=j.decode,this.transport!==T?(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=g(i.params||{}),this.endPoint="".concat(t,"/").concat(m.websocket),this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new w(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=C.appendParams(C.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=g(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(v.error)})}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case f.connecting:return"connecting";case f.open:return"open";case f.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 b(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}(),T=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=f.connecting,this.poll()}return c(e,[{key:"normalizeEndpoint",value:function(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+m.websocket),"$1/"+m.longpoll)}},{key:"endpointURL",value:function(){return C.appendParams(this.pollEndpoint,{token:this.token})}},{key:"closeAndRetry",value:function(){this.close(),this.readyState=f.connecting}},{key:"ontimeout",value:function(){this.onerror("timeout"),this.closeAndRetry()}},{key:"poll",value:function(){var e=this;this.readyState!==f.open&&this.readyState!==f.connecting||C.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=f.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;C.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=f.closed,this.onclose()}}]),e}(),C=function(){function e(){s(this,e)}return c(e,null,[{key:"request",value:function(e,t,n,i,o,r,s){if(l.XDomainRequest){var a=new XDomainRequest;this.xdomainRequest(a,e,t,i,o,r,s)}else{var c=l.XMLHttpRequest?new l.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}();C.states={complete:4};var S=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}(),w=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}()}])});
\ No newline at end of file
+!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()}(this,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 b}),n.d(t,"Socket",function(){return R}),n.d(t,"LongPoll",function(){return w}),n.d(t,"Ajax",function(){return C}),n.d(t,"Presence",function(){return S});var u="undefined"!=typeof self?self:null,h="undefined"!=typeof window?window:null,l=u||h||void 0,f={connecting:0,open:1,closing:2,closed:3},p=1e4,d={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},v={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},y=[v.close,v.error,v.join,v.reply,v.leave],g={longpoll:"longpoll",websocket:"websocket"},m=function(e){if("function"==typeof e)return e;return function(){return e}},k=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}(),b=function(){function e(t,n,i){var o=this;s(this,e),this.state=d.closed,this.topic=t,this.params=m(n||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new k(this,v.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new T(function(){return o.rejoinUntilConnected()},this.socket.reconnectAfterMs),this.joinPush.receive("ok",function(){o.state=d.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=d.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=d.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 k(o,v.leave,m({}),o.timeout).send(),o.state=d.errored,o.joinPush.reset(),o.rejoinTimer.scheduleTimeout())}),this.on(v.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 new Error("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(v.close,e)}},{key:"onError",value:function(e){return this.on(v.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 new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new k(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=d.leaving;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(v.close,"leave")},i=new k(this,v.leave,m({}),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 y.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=d.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 new Error("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===d.closed}},{key:"isErrored",value:function(){return this.state===d.errored}},{key:"isJoined",value:function(){return this.state===d.joined}},{key:"isJoining",value:function(){return this.state===d.joining}},{key:"isLeaving",value:function(){return this.state===d.leaving}}]),e}(),j={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]})}},R=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||p,this.transport=i.transport||l.WebSocket||w,this.defaultEncoder=j.encode,this.defaultDecoder=j.decode,this.transport!==w?(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=m(i.params||{}),this.endPoint="".concat(t,"/").concat(g.websocket),this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new T(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=C.appendParams(C.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=m(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.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(v.error)})}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case f.connecting:return"connecting";case f.open:return"open";case f.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 b(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)})}}]),e}(),w=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=f.connecting,this.poll()}return c(e,[{key:"normalizeEndpoint",value:function(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+g.websocket),"$1/"+g.longpoll)}},{key:"endpointURL",value:function(){return C.appendParams(this.pollEndpoint,{token:this.token})}},{key:"closeAndRetry",value:function(){this.close(),this.readyState=f.connecting}},{key:"ontimeout",value:function(){this.onerror("timeout"),this.closeAndRetry()}},{key:"poll",value:function(){var e=this;this.readyState!==f.open&&this.readyState!==f.connecting||C.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=f.open,e.onopen(),e.poll();break;case 0:case 500:e.onerror(),e.closeAndRetry();break;default:throw new Error("unhandled poll status ".concat(n))}})}},{key:"send",value:function(e){var t=this;C.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=f.closed,this.onclose()}}]),e}(),C=function(){function e(){s(this,e)}return c(e,null,[{key:"request",value:function(e,t,n,i,o,r,s){if(l.XDomainRequest){var a=new XDomainRequest;this.xdomainRequest(a,e,t,i,o,r,s)}else{var c=l.XMLHttpRequest?new l.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}();C.states={complete:4};var S=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}(),T=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,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout(function(){e.tries=e.tries+1,e.callback()},this.timerCalc(this.tries+1))}}]),e}()}])});
\ No newline at end of file
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index 5b4ee5264..2b1aa6e2e 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -1,5 +1,5 @@
 defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.table) %> do
-  use Ecto.Migration
+  use <%= inspect schema.migration_module %>
 
   def change do
     create table(:<%= schema.table %><%= if schema.binary_id do %>, primary_key: false<% end %>) do


Test patch:
diff --git a/assets/test/socket_test.js b/assets/test/socket_test.js
index fe38507b5..4790db775 100644
--- a/assets/test/socket_test.js
+++ b/assets/test/socket_test.js
@@ -605,25 +605,6 @@ describe("onConnOpen", () => {
     assert.ok(spy.calledOnce)
   })
 
-  it("resets all channel timers and schedules a timeout if the timer was in progress", () => {
-    const channel = socket.channel("topic", {})
-    const channel2 = socket.channel("topic2", {})
-
-    channel.rejoinTimer.tries = 1
-    channel2.rejoinTimer.tries = 2
-    channel2.rejoinTimer.scheduleTimeout()
-
-    assert.equal(channel.rejoinTimer.timer, null)
-    assert.notEqual(channel2.rejoinTimer.timer, null)
-
-    socket.onConnOpen()
-
-    assert.equal(channel.rejoinTimer.tries, 0)
-    assert.equal(channel2.rejoinTimer.tries, 0)
-    assert.equal(channel.rejoinTimer.timer, null)
-    assert.notEqual(channel2.rejoinTimer.timer, null)
-  })
-
   it("triggers onOpen callback", () => {
     const spy = sinon.spy()
 
diff --git a/guides/testing/testing.md b/guides/testing/testing.md
index 67e286d8c..f4b243ecb 100644
--- a/guides/testing/testing.md
+++ b/guides/testing/testing.md
@@ -443,8 +443,8 @@ $ mix phx.gen.html Users User users name:string email:string bio:string number_o
 * creating test/hello_web/controllers/user_controller_test.exs
 * creating lib/hello/users/user.ex
 * creating priv/repo/migrations/20180904210841_create_users.exs
-* creating lib/hello/users/users.ex
-* injecting lib/hello/users/users.ex
+* creating lib/hello/users.ex
+* injecting lib/hello/users.ex
 * creating test/hello/users/users_test.exs
 * injecting test/hello/users/users_test.exs
 
diff --git a/guides/testing/testing_channels.md b/guides/testing/testing_channels.md
index 17b947ef1..8acb11f95 100644
--- a/guides/testing/testing_channels.md
+++ b/guides/testing/testing_channels.md
@@ -168,6 +168,8 @@ Since the `handle_in/3` callback for the `"shout"` event just broadcasts the sam
 all subscribers in the `"room:lobby"` should receive the message. To check that, we do
 `assert_broadcast "shout", %{"hello" => "all"}`.
 
+**NOTE:** `assert_broadcast/3` tests that the message was broadcast in the PubSub system.
+  For testing if a client receives a message, use `assert_push/3`
 
 #### Testing an Asynchronous Push from the Server
 
diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md
index 02f1b5031..b70ec844d 100644
--- a/guides/testing/testing_controllers.md
+++ b/guides/testing/testing_controllers.md
@@ -35,8 +35,8 @@ $ mix phx.gen.context Accounts User users name:string email:string:unique passwo
 
 * creating lib/hello/accounts/user.ex
 * creating priv/repo/migrations/20170913155721_create_users.exs
-* creating lib/hello/accounts/accounts.ex
-* injecting lib/hello/accounts/accounts.ex
+* creating lib/hello/accounts.ex
+* injecting lib/hello/accounts.ex
 * creating test/hello/accounts/accounts_test.exs
 * injecting test/hello/accounts/accounts_test.exs
 
@@ -401,7 +401,7 @@ end
 
 We want a HTTP status code of 404 to notify the requester that this resource was not found, as well as an accompanying error message. Notice that we use [`text_response/2`](https://hexdocs.pm/phoenix/Phoenix.ConnTest.html#text_response/2) instead of [`json_response/2`](https://hexdocs.pm/phoenix/Phoenix.ConnTest.html#json_response/2) to assert that the status code is 404 and the response body matches the accompanying error message. You can run this test now to see what happens. You should see that an `Ecto.NoResultsError` is thrown, because there is no such user in the database.
 
-Our controller action needs to handle the error thrown by Ecto. We have two choices here. By default, this will be handled by the [phoenix_ecto](https://github.com/phoenixframework/phoenix_ecto) library, returning a 404. However if we want to show a custom error message, we can create a new `get_user/1` function that does not throw an Ecto error. For this example, we'll take the second path and implement a new `get_user/1` function in the file `lib/hello/accounts/accounts.ex`, just before the `get_user!/1` function:
+Our controller action needs to handle the error thrown by Ecto. We have two choices here. By default, this will be handled by the [phoenix_ecto](https://github.com/phoenixframework/phoenix_ecto) library, returning a 404. However if we want to show a custom error message, we can create a new `get_user/1` function that does not throw an Ecto error. For this example, we'll take the second path and implement a new `get_user/1` function in the file `lib/hello/accounts.ex`, just before the `get_user!/1` function:
 
 ```elixir
 @doc """
diff --git a/guides/testing/testing_schemas.md b/guides/testing/testing_schemas.md
index 71ebfbfc2..bc8cc871e 100644
--- a/guides/testing/testing_schemas.md
+++ b/guides/testing/testing_schemas.md
@@ -476,8 +476,8 @@ bio:string number_of_pets:integer
 * creating test/hello_web/controllers/user_controller_test.exs
 * creating lib/hello/accounts/user.ex
 * creating priv/repo/migrations/20180906212909_create_users.exs
-* creating lib/hello/accounts/accounts.ex
-* injecting lib/hello/accounts/accounts.ex
+* creating lib/hello/accounts.ex
+* injecting lib/hello/accounts.ex
 * creating test/hello/accounts/accounts_test.exs
 * injecting test/hello/accounts/accounts_test.exs
 
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 99dba6b6f..a77eea082 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -168,6 +168,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
       # app deps
       assert_file web_path(@app, "mix.exs"), fn file ->
         assert file =~ "{:phoenix_ecto,"
+        assert file =~ "{:jason,"
       end
 
       # Ecto
@@ -176,6 +177,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert file =~ "aliases: aliases()"
         assert file =~ "ecto.setup"
         assert file =~ "ecto.reset"
+        assert file =~ "{:jason,"
       end
 
       assert_file app_path(@app, "config/dev.exs"), config
diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex
index c572a714a..ffd002a58 100644
--- a/lib/phoenix/test/conn_test.ex
+++ b/lib/phoenix/test/conn_test.ex
@@ -10,9 +10,9 @@ defmodule Phoenix.ConnTest do
 
   `Phoenix.ConnTest` typically works against endpoints. That's the preferred way
   to test anything that your router dispatches to:
-  
+
       @endpoint MyAppWeb.Endpoint
-      
+
       test "says welcome on the home page" do
         conn = get(build_conn(), "/")
         assert conn.resp_body =~ "Welcome!"
@@ -55,7 +55,7 @@ defmodule Phoenix.ConnTest do
   and pass an atom representing the action to dispatch:
 
       @endpoint MyAppWeb.HomeController
-    
+
       test "says welcome on the home page" do
         conn = get(build_conn(), :index)
         assert conn.resp_body =~ "Welcome!"
@@ -68,12 +68,12 @@ defmodule Phoenix.ConnTest do
 
   Under other circumstances, you may be testing a view or another layer that
   requires a connection for processing. For such cases, a connection can be
-  created using the `conn/3` helper:
+  created using the `build_conn/3` helper:
 
       MyApp.UserView.render("hello.html", conn: build_conn(:get, "/"))
 
   While `build_conn/0` returns a connection with no request information to it,
-  `build_conn/2` returns a connection with the given request information already
+  `build_conn/3` returns a connection with the given request information already
   filled in.
 
   ## Recycling
@@ -370,7 +370,7 @@ defmodule Phoenix.ConnTest do
     if given == status do
       body
     else
-      raise "expected response with status #{given}, got: #{status}, with body:\n#{body}"
+      raise "expected response with status #{given}, got: #{status}, with body:\n#{inspect(body)}"
     end
   end
 
diff --git a/priv/templates/phx.gen.json/controller_test.exs b/priv/templates/phx.gen.json/controller_test.exs
index 671c4638c..bf2620814 100644
--- a/priv/templates/phx.gen.json/controller_test.exs
+++ b/priv/templates/phx.gen.json/controller_test.exs
@@ -37,7 +37,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
 
       assert %{
                "id" => id<%= for {key, val} <- schema.params.create do %>,
-               "<%= key %>" => <%= Phoenix.json_library().encode!(val) %><% end %>
+               "<%= key %>" => <%= val |> Phoenix.json_library().encode!() |> Phoenix.json_library().decode!() |> inspect() %><% end %>
              } = json_response(conn, 200)["data"]
     end
 
diff --git a/test/fixtures/templates/no_trim.text.eex b/test/fixtures/templates/no_trim.text.eex
new file mode 100644
index 000000000..6178391d9
--- /dev/null
+++ b/test/fixtures/templates/no_trim.text.eex
@@ -0,0 +1,3 @@
+<%= 123 %>
+<%= 456 %>
+<%= 789 %>
diff --git a/test/fixtures/templates/trim.html.eex b/test/fixtures/templates/trim.html.eex
new file mode 100644
index 000000000..6178391d9
--- /dev/null
+++ b/test/fixtures/templates/trim.html.eex
@@ -0,0 +1,3 @@
+<%= 123 %>
+<%= 456 %>
+<%= 789 %>
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index 8ca5a4d71..af3920e42 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -104,6 +104,14 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
         Gen.Context.run(~w(Blog Blog blogs))
       end
 
+      assert_raise Mix.Error, ~r/Cannot generate context Phoenix because it has the same name as the application/, fn ->
+        Gen.Context.run(~w(Phoenix Post blogs))
+      end
+
+      assert_raise Mix.Error, ~r/Cannot generate schema Phoenix because it has the same name as the application/, fn ->
+        Gen.Context.run(~w(Blog Phoenix blogs))
+      end
+
       assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
         Gen.Context.run(~w(Blog.Post posts))
       end
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index cfd6d3df4..51d274bcb 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -38,6 +38,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
     in_tmp_project config.test, fn ->
      Gen.Json.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
                      tags:array:text popular:boolean drafted_at:datetime
+                     params:map
                      published_at:utc_datetime
                      published_at_usec:utc_datetime_usec
                      deleted_at:naive_datetime
@@ -67,6 +68,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
                        "deleted_at" => "2010-04-17T14:00:00",
                        "deleted_at_usec" => "2010-04-17T14:00:00.000000",
                        "drafted_at" => "2010-04-17T14:00:00",
+                       "params" => %{},
                        "popular" => true,
                        "published_at" => "2010-04-17T14:00:00Z",
                        "published_at_usec" => "2010-04-17T14:00:00.000000Z",
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index b701702d1..a4cfc18f3 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -270,6 +270,25 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
     end
   end
 
+  test "generates migrations with a custom migration module", config do
+    in_tmp_project config.test, fn ->
+      try do
+        Application.put_env(:ecto_sql, :migration_module, MyCustomApp.MigrationModule)
+  
+        Gen.Schema.run(~w(Blog.Post posts))        
+  
+        assert [migration] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+  
+        assert_file migration, fn file ->
+          assert file =~ "use MyCustomApp.MigrationModule"
+          assert file =~ "create table(:posts) do"
+        end
+      after
+        Application.delete_env(:ecto_sql, :migration_module)
+      end
+    end
+  end
+
   test "generates schema without extra line break", config do
     in_tmp_project config.test, fn ->
       Gen.Schema.run(~w(Blog.Post posts title))
diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs
index 526a495e4..66c6bc76e 100644
--- a/test/phoenix/integration/websocket_channels_test.exs
+++ b/test/phoenix/integration/websocket_channels_test.exs
@@ -70,12 +70,13 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
     def init({payload, from, socket}) do
       case payload["action"] do
         "ok" ->
-          GenServer.reply(from, %{"action" => "ok"})
+          GenServer.reply(from, {:ok, %{"action" => "ok"}})
           {:ok, socket}
 
         "ignore" ->
-          GenServer.reply(from, %{"action" => "ignore"})
-          :ignore
+          GenServer.reply(from, {:error, %{"action" => "ignore"}})
+          send(self(), :stop)
+          {:ok, socket}
 
         "error" ->
           raise "oops"
@@ -86,6 +87,10 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
       send socket.transport_pid, {:socket_close, self(), :shutdown}
       {:stop, :shutdown, socket}
     end
+
+    def handle_info(:stop, socket) do
+      {:stop, :shutdown, socket}
+    end
   end
 
   defmodule UserSocketConnectInfo do
diff --git a/test/phoenix/router/console_formatter_test.exs b/test/phoenix/router/console_formatter_test.exs
index 919f0cc0e..15fe6170e 100644
--- a/test/phoenix/router/console_formatter_test.exs
+++ b/test/phoenix/router/console_formatter_test.exs
@@ -57,7 +57,38 @@ defmodule Phoenix.Router.ConsoleFormatterTest do
     """
   end
 
-  defp draw(router) do
-    ConsoleFormatter.format(router)
+  describe "endpoint sockets" do
+
+    def __sockets__, do: []
+
+    defmodule FormatterEndpoint do
+      use Phoenix.Endpoint, otp_app: :phoenix
+
+      socket "/socket", TestSocket, websocket: true
+    end
+
+    test "format with sockets" do
+      assert draw(RouterTestSingleRoutes, FormatterEndpoint) == """
+              page_path  GET     /                  Phoenix.PageController :index
+      upload_image_path  POST    /images            Phoenix.ImageController :upload
+      remove_image_path  DELETE  /images            Phoenix.ImageController :delete
+              websocket  WS      /socket/websocket  TestSocket
+               longpoll  GET     /socket/longpoll   TestSocket
+               longpoll  POST    /socket/longpoll   TestSocket
+      """
+    end
+
+
+    test "format without sockets" do
+      assert draw(RouterTestSingleRoutes, __MODULE__) == """
+              page_path  GET     /        Phoenix.PageController :index
+      upload_image_path  POST    /images  Phoenix.ImageController :upload
+      remove_image_path  DELETE  /images  Phoenix.ImageController :delete
+      """
+    end
+  end
+
+  defp draw(router, endpoint \\ nil) do
+    ConsoleFormatter.format(router, endpoint)
   end
 end
diff --git a/test/phoenix/router/pipeline_test.exs b/test/phoenix/router/pipeline_test.exs
index 1e0fa83a5..4268857ab 100644
--- a/test/phoenix/router/pipeline_test.exs
+++ b/test/phoenix/router/pipeline_test.exs
@@ -142,7 +142,8 @@ defmodule Phoenix.Router.PipelineTest do
         pipeline :browser do
         end
         scope "/" do
-          pipe_through [:browser, :auth, :browser]
+          pipe_through [:browser, :auth]
+          pipe_through [:browser]
         end
       end
     end
diff --git a/test/phoenix/router/scope_test.exs b/test/phoenix/router/scope_test.exs
index 8adb2974f..1a5d1826c 100644
--- a/test/phoenix/router/scope_test.exs
+++ b/test/phoenix/router/scope_test.exs
@@ -11,6 +11,10 @@ defmodule Phoenix.Router.ScopedRoutingTest do
     def edit(conn, _params), do: text(conn, "api v1 users edit")
     def foo_host(conn, _params), do: text(conn, "foo request from #{conn.host}")
     def baz_host(conn, _params), do: text(conn, "baz request from #{conn.host}")
+    def proxy(conn, _) do
+      {controller, action} = conn.private.proxy_to
+      controller.call(conn, controller.init(action))
+    end
   end
 
   defmodule Router do
@@ -42,6 +46,9 @@ defmodule Phoenix.Router.ScopedRoutingTest do
 
       scope "/v1", alias: V1 do
         resources "/users", UserController, only: [:delete], private: %{private_token: "baz"}
+        get "/noalias", Api.V1.UserController, :proxy,
+          private: %{proxy_to: {scoped_alias(__MODULE__, UserController), :show}},
+          alias: false
       end
     end
 
@@ -192,4 +199,10 @@ defmodule Phoenix.Router.ScopedRoutingTest do
       end
     end
   end
+
+  test "alias false with expanded scoped alias" do
+    conn = call(Router, :get, "/api/v1/noalias")
+    assert conn.status == 200
+    assert conn.resp_body == "api v1 users show"
+  end
 end
diff --git a/test/phoenix/template_test.exs b/test/phoenix/template_test.exs
index 39ea44ed7..d0e60b6f7 100644
--- a/test/phoenix/template_test.exs
+++ b/test/phoenix/template_test.exs
@@ -60,6 +60,11 @@ defmodule Phoenix.TemplateTest do
     end
   end
 
+  test "render eex templates only trim html" do
+    assert {:safe, ["123", "456" | _]} = View.render("trim.html", %{})
+    assert "123\n456\n789\n" = View.render("no_trim.text", %{})
+  end
+
   test "render eex templates sanitizes against xss by default" do
     assert View.render("show.html", %{message: ""}) ==
            {:safe, ["<div>Show! ", [], "</div>\n"]}
diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs
index d256db748..2269aaee0 100644
--- a/test/phoenix/test/conn_test.exs
+++ b/test/phoenix/test/conn_test.exs
@@ -264,9 +264,14 @@ defmodule Phoenix.Test.ConnTest do
     end
 
     assert_raise RuntimeError,
-                 "expected response with status 200, got: 404, with body:\noops", fn ->
+                 "expected response with status 200, got: 404, with body:\n\"oops\"", fn ->
       build_conn(:get, "/") |> resp(404, "oops") |> response(200)
     end
+
+    assert_raise RuntimeError,
+                 "expected response with status 200, got: 404, with body:\n<<192>>", fn ->
+      build_conn(:get, "/") |> resp(404, <<192>>) |> response(200)
+    end
   end
 
   test "html_response/2" do
@@ -313,7 +318,7 @@ defmodule Phoenix.Test.ConnTest do
       |> json_response(200)
     end
 
-    assert_raise RuntimeError, ~s(expected response with status 200, got: 400, with body:\n{"error": "oh oh"}), fn ->
+    assert_raise RuntimeError, ~s(expected response with status 200, got: 400, with body:\n) <> inspect(~s({"error": "oh oh"})), fn ->
       build_conn(:get, "/")
       |> put_resp_content_type("application/json")
       |> resp(400, ~s({"error": "oh oh"}))
diff --git a/test/phoenix/token_test.exs b/test/phoenix/token_test.exs
index e2b694598..d54754a3b 100644
--- a/test/phoenix/token_test.exs
+++ b/test/phoenix/token_test.exs
@@ -46,6 +46,7 @@ defmodule Phoenix.TokenTest do
     assert Token.verify(conn(), "id", token, max_age: -1000) == {:error, :expired}
     assert Token.verify(conn(), "id", token, max_age: 100) == {:ok, 1}
     assert Token.verify(conn(), "id", token, max_age: -100) == {:error, :expired}
+    assert Token.verify(conn(), "id", token, max_age: 0) == {:error, :expired}
 
     token = Token.sign(conn(), "id", 1)
     assert Token.verify(conn(), "id", token, max_age: 0.1) == {:ok, 1}