+
<.label for={@id}><%%= @label %>
.CoreComponents do
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
- "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
@errors == [] && "border-zinc-300 focus:border-zinc-400",
@errors != [] && "border-rose-400 focus:border-rose-400"
]}
@@ -405,7 +341,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
def error(assigns) do
~H"""
-
+
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
<%%= render_slot(@inner_block) %>
@@ -475,7 +411,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
<%%= col[:label] %>
- <%= if @gettext do %><%%= gettext("Actions") %><% else %>Actions<% end %>
+ <%= maybe_eex_gettext.("Actions", @gettext) %>
@@ -498,8 +434,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
+
-
.CoreComponents do
You can customize the size and colors of the icons by setting
width, height, and background color classes.
- Icons are extracted from your `assets/vendor/heroicons` directory and bundled
- within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
+ Icons are extracted from the `deps/heroicons` directory and bundled within
+ your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-x-mark-solid" />
- <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
+ <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 motion-safe:animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
@@ -598,8 +534,9 @@ defmodule <%= @web_namespace %>.CoreComponents do
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
+ time: 300,
transition:
- {"transition-all transform ease-out duration-300",
+ {"transition-all ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
@@ -610,36 +547,11 @@ defmodule <%= @web_namespace %>.CoreComponents do
to: selector,
time: 200,
transition:
- {"transition-all transform ease-in duration-200",
- "opacity-100 translate-y-0 sm:scale-100",
+ {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
- def show_modal(js \\ %JS{}, id) when is_binary(id) do
- js
- |> JS.show(to: "##{id}")
- |> JS.show(
- to: "##{id}-bg",
- transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
- )
- |> show("##{id}-container")
- |> JS.add_class("overflow-hidden", to: "body")
- |> JS.focus_first(to: "##{id}-content")
- end
-
- def hide_modal(js \\ %JS{}, id) do
- js
- |> JS.hide(
- to: "##{id}-bg",
- transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
- )
- |> hide("##{id}-container")
- |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
- |> JS.remove_class("overflow-hidden", to: "body")
- |> JS.pop_focus()
- end
-
@doc """
Translates an error message using gettext.
"""<%= if @gettext do %>
diff --git a/installer/templates/phx_web/components/layouts.ex b/installer/templates/phx_web/components/layouts.ex
index 65fb90797e..fdb2d4e0bb 100644
--- a/installer/templates/phx_web/components/layouts.ex
+++ b/installer/templates/phx_web/components/layouts.ex
@@ -1,4 +1,13 @@
defmodule <%= @web_namespace %>.Layouts do
+ @moduledoc """
+ This module holds different layouts used by your application.
+
+ See the `layouts` directory for all templates available.
+ The "root" layout is a skeleton rendered as part of the
+ application router. The "app" layout is set as the default
+ layout on both `use <%= @web_namespace %>, :controller` and
+ `use <%= @web_namespace %>, :live_view`.
+ """
use <%= @web_namespace %>, :html
embed_templates "layouts/*"
diff --git a/installer/templates/phx_web/components/layouts/app.html.heex b/installer/templates/phx_web/components/layouts/app.html.heex
index c50aa21d28..d164090735 100644
--- a/installer/templates/phx_web/components/layouts/app.html.heex
+++ b/installer/templates/phx_web/components/layouts/app.html.heex
@@ -26,7 +26,7 @@
- <.flash_group flash={@flash} />
<%%= @inner_content %>
+<.flash_group flash={@flash} />
diff --git a/installer/templates/phx_web/components/layouts/root.html.heex b/installer/templates/phx_web/components/layouts/root.html.heex
index b9926899eb..12f7f1d87e 100644
--- a/installer/templates/phx_web/components/layouts/root.html.heex
+++ b/installer/templates/phx_web/components/layouts/root.html.heex
@@ -4,14 +4,14 @@
- <.live_title suffix=" · Phoenix Framework">
- <%%= assigns[:page_title] || "<%= @app_module %>" %>
+ <.live_title default="<%= @app_module %>" suffix=" · Phoenix Framework">
+ <%%= assigns[:page_title] %>
-
+
<%%= @inner_content %>
diff --git a/installer/templates/phx_web/controllers/error_html.ex b/installer/templates/phx_web/controllers/error_html.ex
index 9d575e7da9..ae8de38417 100644
--- a/installer/templates/phx_web/controllers/error_html.ex
+++ b/installer/templates/phx_web/controllers/error_html.ex
@@ -1,4 +1,9 @@
defmodule <%= @web_namespace %>.ErrorHTML do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on HTML requests.
+
+ See config/config.exs.
+ """
use <%= @web_namespace %>, :html
# If you want to customize your error pages,
diff --git a/installer/templates/phx_web/controllers/error_json.ex b/installer/templates/phx_web/controllers/error_json.ex
index 2e852bc99d..60310936d8 100644
--- a/installer/templates/phx_web/controllers/error_json.ex
+++ b/installer/templates/phx_web/controllers/error_json.ex
@@ -1,4 +1,10 @@
defmodule <%= @web_namespace %>.ErrorJSON do
+ @moduledoc """
+ This module is invoked by your endpoint in case of errors on JSON requests.
+
+ See config/config.exs.
+ """
+
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
diff --git a/installer/templates/phx_web/controllers/page_html.ex b/installer/templates/phx_web/controllers/page_html.ex
index d7181e591b..99e281e635 100644
--- a/installer/templates/phx_web/controllers/page_html.ex
+++ b/installer/templates/phx_web/controllers/page_html.ex
@@ -1,4 +1,9 @@
defmodule <%= @web_namespace %>.PageHTML do
+ @moduledoc """
+ This module contains pages rendered by PageController.
+
+ See the `page_html` directory for all templates available.
+ """
use <%= @web_namespace %>, :html
embed_templates "page_html/*"
diff --git a/installer/templates/phx_web/controllers/page_html/home.html.heex b/installer/templates/phx_web/controllers/page_html/home.html.heex
index 3a5917f9f9..071a7104d3 100644
--- a/installer/templates/phx_web/controllers/page_html/home.html.heex
+++ b/installer/templates/phx_web/controllers/page_html/home.html.heex
@@ -53,7 +53,7 @@
v<%%= Application.spec(:phoenix, :vsn) %>
-
+
Peace of mind from prototype to production.
@@ -201,6 +201,21 @@
Join our Discord server
+
do
same_site: "Lax"
]
- <%= if !(@dashboard || @live) do %><%= "# " %><% end %>socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
+ <%= if !(@dashboard || @live) do %><%= "# " %><% end %>socket "/live", Phoenix.LiveView.Socket,
+ <%= if !(@dashboard || @live) do %><%= "# " %><% end %> websocket: [connect_info: [session: @session_options]],
+ <%= if !(@dashboard || @live) do %><%= "# " %><% end %> longpoll: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
- # You should set gzip to true if you are running phx.digest
- # when deploying your static files in production.
+ # When code reloading is disabled (e.g., in production),
+ # the `gzip` option is enabled to serve compressed
+ # static files generated by running `phx.digest`.
plug Plug.Static,
at: "/",
from: :<%= @web_app_name %>,
- gzip: false,
+ gzip: not code_reloading?,
only: <%= @web_namespace %>.static_paths()
# Code reloading can be explicitly enabled under the
diff --git a/installer/test/mix_helper.exs b/installer/test/mix_helper.exs
index 89e7d87455..1addd2ec1f 100644
--- a/installer/test/mix_helper.exs
+++ b/installer/test/mix_helper.exs
@@ -11,47 +11,53 @@ defmodule MixHelper do
end
defp random_string(len) do
- len |> :crypto.strong_rand_bytes() |> Base.encode64() |> binary_part(0, len)
+ len |> :crypto.strong_rand_bytes() |> Base.url_encode64() |> binary_part(0, len)
end
def in_tmp(which, function) do
- path = Path.join([tmp_path(), random_string(10), to_string(which)])
+ base = Path.join([tmp_path(), random_string(10)])
+ path = Path.join([base, to_string(which)])
try do
File.rm_rf!(path)
File.mkdir_p!(path)
File.cd!(path, function)
after
- File.rm_rf!(path)
+ File.rm_rf!(base)
end
end
def in_tmp_project(which, function) do
conf_before = Application.get_env(:phoenix, :generators) || []
- path = Path.join([tmp_path(), random_string(10), to_string(which)])
+ base = Path.join([tmp_path(), random_string(10)])
+ path = Path.join([base, to_string(which)])
try do
File.rm_rf!(path)
File.mkdir_p!(path)
+
File.cd!(path, fn ->
File.touch!("mix.exs")
+
File.write!(".formatter.exs", """
[
import_deps: [:phoenix, :ecto, :ecto_sql],
inputs: ["*.exs"]
]
""")
+
function.()
end)
after
- File.rm_rf!(path)
+ File.rm_rf!(base)
Application.put_env(:phoenix, :generators, conf_before)
end
end
def in_tmp_umbrella_project(which, function) do
conf_before = Application.get_env(:phoenix, :generators) || []
- path = Path.join([tmp_path(), random_string(10), to_string(which)])
+ base = Path.join([tmp_path(), random_string(10)])
+ path = Path.join([base, to_string(which)])
try do
apps_path = Path.join(path, "apps")
@@ -61,13 +67,15 @@ defmodule MixHelper do
File.mkdir_p!(apps_path)
File.mkdir_p!(config_path)
File.touch!(Path.join(path, "mix.exs"))
+
for file <- ~w(config.exs dev.exs test.exs prod.exs) do
File.write!(Path.join(config_path, file), "import Config\n")
end
+
File.cd!(apps_path, function)
after
Application.put_env(:phoenix, :generators, conf_before)
- File.rm_rf!(path)
+ File.rm_rf!(base)
end
end
@@ -76,7 +84,10 @@ defmodule MixHelper do
try do
capture_io(:stderr, fn ->
- Mix.Project.in_project(app, path, [], fun)
+ Mix.Project.in_project(app, path, [prune_code_paths: false], fn mod ->
+ fun.(mod)
+ Mix.Project.clear_deps_cache()
+ end)
end)
after
Mix.Project.push(name, file)
@@ -94,13 +105,17 @@ defmodule MixHelper do
def assert_file(file, match) do
cond do
is_list(match) ->
- assert_file file, &(Enum.each(match, fn(m) -> assert &1 =~ m end))
+ assert_file(file, &Enum.each(match, fn m -> assert &1 =~ m end))
+
is_binary(match) or is_struct(match, Regex) ->
- assert_file file, &(assert &1 =~ match)
+ assert_file(file, &assert(&1 =~ match))
+
is_function(match, 1) ->
assert_file(file)
match.(File.read!(file))
- true -> raise inspect({file, match})
+
+ true ->
+ raise inspect({file, match})
end
end
@@ -118,6 +133,7 @@ defmodule MixHelper do
def with_generator_env(app_name \\ :phoenix, new_env, fun) do
config_before = Application.fetch_env(app_name, :generators)
Application.put_env(app_name, :generators, new_env)
+
try do
fun.()
after
@@ -150,7 +166,8 @@ defmodule MixHelper do
def flush do
receive do
_ -> flush()
- after 0 -> :ok
+ after
+ 0 -> :ok
end
end
end
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index be2e4db816..d33502a688 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -54,7 +54,9 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file("phx_blog/config/config.exs", fn file ->
assert file =~ "ecto_repos: [PhxBlog.Repo]"
+ assert file =~ "generators: [timestamp_type: :utc_datetime]"
assert file =~ "config :phoenix, :json_library, Jason"
+ assert file =~ ~s[cd: Path.expand("../assets", __DIR__),]
refute file =~ "namespace: PhxBlog"
refute file =~ "config :phx_blog, :generators"
end)
@@ -108,6 +110,8 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
assert file =~ "defmodule PhxBlogWeb.CoreComponents"
+ assert file =~ ~S|aria-label={gettext("close")}|
+ assert file =~ ~S|<.flash kind={:info} title={gettext("Success!")} flash={@flash} />|
end)
assert_file("phx_blog/lib/phx_blog_web/components/layouts.ex", fn file ->
@@ -149,12 +153,11 @@ defmodule Mix.Tasks.Phx.NewTest do
# tailwind
assert_file("phx_blog/assets/css/app.css")
- assert_file("phx_blog/assets/tailwind.config.js")
- assert_file("phx_blog/assets/vendor/heroicons/LICENSE.md")
- assert_file("phx_blog/assets/vendor/heroicons/UPGRADE.md")
- assert_file("phx_blog/assets/vendor/heroicons/optimized/24/outline/cake.svg")
- assert_file("phx_blog/assets/vendor/heroicons/optimized/24/solid/cake.svg")
- assert_file("phx_blog/assets/vendor/heroicons/optimized/20/solid/cake.svg")
+
+ assert_file("phx_blog/assets/tailwind.config.js", fn file ->
+ assert file =~ "phx_blog_web.ex"
+ assert file =~ "phx_blog_web/**/*.*ex"
+ end)
refute File.exists?("phx_blog/priv/static/assets/app.css")
refute File.exists?("phx_blog/priv/static/assets/app.js")
@@ -198,7 +201,7 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file(
"phx_blog/config/test.exs",
- ~R/database: "phx_blog_test#\{System.get_env\("MIX_TEST_PARTITION"\)\}"/
+ ~r/database: "phx_blog_test#\{System.get_env\("MIX_TEST_PARTITION"\)\}"/
)
assert_file("phx_blog/lib/phx_blog/repo.ex", ~r"defmodule PhxBlog.Repo")
@@ -254,12 +257,8 @@ defmodule Mix.Tasks.Phx.NewTest do
# Mailer
assert_file("phx_blog/mix.exs", fn file ->
- assert file =~ "{:swoosh, \"~> 1.3\"}"
- assert file =~ "{:finch, \"~> 0.13\"}"
- end)
-
- assert_file("phx_blog/lib/phx_blog/application.ex", fn file ->
- assert file =~ "{Finch, name: PhxBlog.Finch}"
+ assert file =~ "{:swoosh, \"~> 1.16\"}"
+ assert file =~ "{:req, \"~> 0.5.4\"}"
end)
assert_file("phx_blog/lib/phx_blog/mailer.ex", fn file ->
@@ -282,7 +281,7 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file("phx_blog/config/prod.exs", fn file ->
assert file =~
- "config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: PhxBlog.Finch"
+ "config :swoosh, api_client: Swoosh.ApiClient.Req"
end)
# Install dependencies?
@@ -297,7 +296,11 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]}
# Gettext
- assert_file("phx_blog/lib/phx_blog_web/gettext.ex", ~r"defmodule PhxBlogWeb.Gettext")
+ assert_file("phx_blog/lib/phx_blog_web/gettext.ex", [
+ ~r"defmodule PhxBlogWeb.Gettext",
+ ~r"use Gettext\.Backend, otp_app: :phx_blog"
+ ])
+
assert File.exists?("phx_blog/priv/gettext/errors.pot")
assert File.exists?("phx_blog/priv/gettext/en/LC_MESSAGES/errors.po")
end)
@@ -370,7 +373,12 @@ defmodule Mix.Tasks.Phx.NewTest do
refute_file("phx_blog/priv/gettext/en/LC_MESSAGES/errors.po")
refute_file("phx_blog/priv/gettext/errors.pot")
assert_file("phx_blog/mix.exs", &refute(&1 =~ ~r":gettext"))
- assert_file("phx_blog/lib/phx_blog_web.ex", &refute(&1 =~ ~r"import AmsMockWeb.Gettext"))
+
+ assert_file(
+ "phx_blog/lib/phx_blog_web.ex",
+ &refute(&1 =~ ~r"use Gettext, backend: AmsMockWeb.Gettext")
+ )
+
assert_file("phx_blog/config/dev.exs", &refute(&1 =~ ~r"gettext"))
# No HTML
@@ -414,12 +422,8 @@ defmodule Mix.Tasks.Phx.NewTest do
# No mailer or emails
assert_file("phx_blog/mix.exs", fn file ->
- refute file =~ "{:swoosh, \"~> 1.3\"}"
- refute file =~ "{:finch, \"~> 0.13\"}"
- end)
-
- assert_file("phx_blog/lib/phx_blog/application.ex", fn file ->
- refute file =~ "{Finch, name: PhxBlog.Finch"
+ refute file =~ "{:swoosh"
+ refute file =~ "{:req"
end)
refute File.exists?("phx_blog/lib/phx_blog/mailer.ex")
@@ -509,6 +513,14 @@ defmodule Mix.Tasks.Phx.NewTest do
refute file =~ ~s|pipeline :browser|
assert file =~ ~s|pipe_through [:fetch_session, :protect_from_forgery]|
end)
+
+ assert_file("phx_blog/config/config.exs", fn file ->
+ refute file =~ ~s|config :phoenix_live_view|
+ end)
+
+ assert_file("phx_blog/config/test.exs", fn file ->
+ refute file =~ ~s|config :phoenix_live_view|
+ end)
end)
end
@@ -549,27 +561,21 @@ defmodule Mix.Tasks.Phx.NewTest do
end)
end
- test "new with binary_id" do
- in_tmp("new with binary_id", fn ->
- Mix.Tasks.Phx.New.run([@app_name, "--binary-id"])
- assert_file("phx_blog/config/config.exs", ~r/generators: \[binary_id: true\]/)
- end)
- end
-
- test "new with uppercase" do
- in_tmp("new with uppercase", fn ->
- Mix.Tasks.Phx.New.run(["phxBlog"])
+ test "new with --no-gettext" do
+ in_tmp("new with no_gettext", fn ->
+ Mix.Tasks.Phx.New.run([@app_name, "--no-gettext"])
- assert_file("phxBlog/README.md")
-
- assert_file("phxBlog/mix.exs", fn file ->
- assert file =~ "app: :phxBlog"
+ assert_file("phx_blog/lib/phx_blog_web/components/core_components.ex", fn file ->
+ assert file =~ ~S|aria-label="close"|
+ assert file =~ ~S|<.flash kind={:info} title="Success!" flash={@flash} />|
end)
+ end)
+ end
- assert_file("phxBlog/config/dev.exs", fn file ->
- assert file =~ ~r/config :phxBlog, PhxBlog.Repo,/
- assert file =~ "database: \"phxblog_dev\""
- end)
+ test "new with binary_id" do
+ in_tmp("new with binary_id", fn ->
+ Mix.Tasks.Phx.New.run([@app_name, "--binary-id"])
+ assert_file("phx_blog/config/config.exs", ~r/generators: \[.*binary_id: true\.*]/)
end)
end
@@ -598,7 +604,16 @@ defmodule Mix.Tasks.Phx.NewTest do
assert file =~ "deps_path: \"../../deps\""
assert file =~ "lockfile: \"../../mix.lock\""
end)
+
+ refute_file("phx_blog/config/config.exs")
+ end)
+
+ assert_file("config/config.exs", fn file ->
+ assert file =~ "PhxBlogWeb.Endpoint"
+ assert file =~ ~s[cd: Path.expand("../apps/phx_blog/assets", __DIR__),]
end)
+
+ assert_file("config/config.exs", "PhxBlogWeb.Endpoint")
end)
end
@@ -681,6 +696,15 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file("custom_path/config/runtime.exs", [~r/database: database_path/])
assert_file("custom_path/lib/custom_path/repo.ex", "Ecto.Adapters.SQLite3")
+ assert_file("custom_path/lib/custom_path/application.ex", fn file ->
+ assert file =~ "{Ecto.Migrator"
+ assert file =~ "repos: Application.fetch_env!(:custom_path, :ecto_repos)"
+ assert file =~ "skip: skip_migrations?()"
+
+ assert file =~ "defp skip_migrations?() do"
+ assert file =~ ~s/System.get_env("RELEASE_NAME") != nil/
+ end)
+
assert_file("custom_path/test/support/conn_case.ex", "DataCase.setup_sandbox(tags)")
assert_file(
@@ -751,6 +775,10 @@ defmodule Mix.Tasks.Phx.NewTest do
Mix.Tasks.Phx.New.run(["valid", "--app", "007invalid"])
end
+ assert_raise Mix.Error, ~r"Application name must start with a letter and ", fn ->
+ Mix.Tasks.Phx.New.run(["exInvalidAppName"])
+ end
+
assert_raise Mix.Error, ~r"Module name must be a valid Elixir alias", fn ->
Mix.Tasks.Phx.New.run(["valid", "--module", "not.valid"])
end
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 1ca8a943ff..31a95ed3a4 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -183,6 +183,11 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
assert_file(web_path(@app, ".gitignore"), ~r/\n$/)
assert_file(web_path(@app, "assets/css/app.css"))
+ assert_file(web_path(@app, "assets/tailwind.config.js"), fn file ->
+ assert file =~ "phx_umb_web.ex"
+ assert file =~ "phx_umb_web/**/*.*ex"
+ end)
+
assert_file(web_path(@app, "priv/static/favicon.ico"))
refute File.exists?(web_path(@app, "priv/static/assets/app.css"))
@@ -195,7 +200,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
assert file =~ "{:phoenix,"
assert file =~ "{:phoenix_live_view,"
assert file =~ "{:gettext,"
- assert file =~ "{:plug_cowboy,"
+ assert file =~ "{:bandit,"
end)
# app deps
@@ -272,12 +277,8 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
# Mailer
assert_file(app_path(@app, "mix.exs"), fn file ->
- assert file =~ "{:swoosh, \"~> 1.3\"}"
- assert file =~ "{:finch, \"~> 0.13\"}"
- end)
-
- assert_file(app_path(@app, "lib/#{@app}/application.ex"), fn file ->
- assert file =~ "{Finch, name: PhxUmb.Finch}"
+ assert file =~ "{:swoosh, \"~> 1.16\"}"
+ assert file =~ "{:req, \"~> 0.5.4\"}"
end)
assert_file(app_path(@app, "lib/#{@app}/mailer.ex"), fn file ->
@@ -299,7 +300,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
end)
assert_file(root_path(@app, "config/prod.exs"), fn file ->
- assert file =~ "config :swoosh, :api_client, PhxUmb.Finch"
+ assert file =~ "config :swoosh, :api_client, Swoosh.ApiClient.Req"
end)
# Install dependencies?
@@ -314,7 +315,11 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]}
# Gettext
- assert_file(web_path(@app, "lib/#{@app}_web/gettext.ex"), ~r"defmodule PhxUmbWeb.Gettext")
+ assert_file(web_path(@app, "lib/#{@app}_web/gettext.ex"), [
+ ~r"defmodule PhxUmbWeb.Gettext",
+ ~r"use Gettext\.Backend, otp_app: :phx_umb_web"
+ ])
+
assert File.exists?(web_path(@app, "priv/gettext/errors.pot"))
assert File.exists?(web_path(@app, "priv/gettext/en/LC_MESSAGES/errors.po"))
end)
@@ -408,12 +413,8 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
# Without mailer
assert_file(web_path(@app, "mix.exs"), fn file ->
- refute file =~ "{:swoosh, \"~> 1.3\"}"
- refute file =~ "{:finch, \"~> 0.13\"}"
- end)
-
- assert_file(app_path(@app, "lib/#{@app}/application.ex"), fn file ->
- refute file =~ "{Finch, name: PhxUmb.Finch}"
+ refute file =~ "{:swoosh"
+ refute file =~ "{:req"
end)
refute File.exists?(app_path(@app, "lib/#{@app}/mailer.ex"))
@@ -524,27 +525,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
end)
end
- test "new with uppercase" do
- in_tmp("new with uppercase", fn ->
- Mix.Tasks.Phx.New.run(["phxUmb", "--umbrella"])
-
- assert_file("phxUmb_umbrella/README.md")
-
- assert_file("phxUmb_umbrella/apps/phxUmb/mix.exs", fn file ->
- assert file =~ "app: :phxUmb"
- end)
-
- assert_file("phxUmb_umbrella/apps/phxUmb_web/mix.exs", fn file ->
- assert file =~ "app: :phxUmb_web"
- end)
-
- assert_file("phxUmb_umbrella/config/dev.exs", fn file ->
- assert file =~ ~r/config :phxUmb, PhxUmb.Repo,/
- assert file =~ "database: \"phxumb_dev\""
- end)
- end)
- end
-
test "new with path, app and module" do
in_tmp("new with path, app and module", fn ->
project_path = Path.join(File.cwd!(), "custom_path")
@@ -649,6 +629,15 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
assert_file(app_path(app, "mix.exs"), ":ecto_sqlite3")
assert_file(app_path(app, "lib/custom_path/repo.ex"), "Ecto.Adapters.SQLite3")
+ assert_file(app_path(app, "lib/custom_path/application.ex"), fn file ->
+ assert file =~ "{Ecto.Migrator"
+ assert file =~ "repos: Application.fetch_env!(:custom_path, :ecto_repos)"
+ assert file =~ "skip: skip_migrations?()"
+
+ assert file =~ "defp skip_migrations?() do"
+ assert file =~ ~s/System.get_env("RELEASE_NAME") != nil/
+ end)
+
assert_file(root_path(app, "config/dev.exs"), [~r/database: .*_dev.db/])
assert_file(root_path(app, "config/test.exs"), [~r/database: .*_test.db/])
assert_file(root_path(app, "config/runtime.exs"), [~r/database: database_path/])
@@ -695,14 +684,14 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
end)
end
- test "new with bandit web adapter" do
- in_tmp("new with bandit web adapter", fn ->
+ test "new with cowboy web adapter" do
+ in_tmp("new with cowboy web adapter", fn ->
app = "custom_path"
project_path = Path.join(File.cwd!(), app)
- Mix.Tasks.Phx.New.run([project_path, "--umbrella", "--adapter", "bandit"])
- assert_file(web_path(app, "mix.exs"), ":bandit")
+ Mix.Tasks.Phx.New.run([project_path, "--umbrella", "--adapter", "cowboy"])
+ assert_file(web_path(app, "mix.exs"), ":plug_cowboy")
- assert_file(root_path(app, "config/config.exs"), "adapter: Bandit.PhoenixAdapter")
+ assert_file(root_path(app, "config/config.exs"), "adapter: Phoenix.Endpoint.Cowboy2Adapter")
end)
end
@@ -715,6 +704,10 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
Mix.Tasks.Phx.New.run(["valid1", "--app", "007invalid", "--umbrella"])
end
+ assert_raise Mix.Error, ~r"Application name must start with a letter and ", fn ->
+ Mix.Tasks.Phx.New.run(["valid1", "--app", "exInvalidAppAnme", "--umbrella"])
+ end
+
assert_raise Mix.Error, ~r"Module name must be a valid Elixir alias", fn ->
Mix.Tasks.Phx.New.run(["valid2", "--module", "not.valid", "--umbrella"])
end
diff --git a/installer/test/phx_new_web_test.exs b/installer/test/phx_new_web_test.exs
index 2f13f4cda9..9911ce22c0 100644
--- a/installer/test/phx_new_web_test.exs
+++ b/installer/test/phx_new_web_test.exs
@@ -63,4 +63,15 @@ defmodule Mix.Tasks.Phx.New.WebTest do
assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]}
end
end
+
+ test "app_name is included in tailwind config" do
+ in_tmp_umbrella_project "new with defaults", fn ->
+ Mix.Tasks.Phx.New.Web.run(["testweb"])
+
+ assert_file "testweb/assets/tailwind.config.js", fn file ->
+ assert file =~ "testweb.ex"
+ assert file =~ "testweb/**/*.*ex"
+ end
+ end
+ end
end
diff --git a/integration_test/README.md b/integration_test/README.md
index 38e04b73f8..152a1f1db4 100644
--- a/integration_test/README.md
+++ b/integration_test/README.md
@@ -27,25 +27,7 @@ This allows all tests to be run with the following command
$ mix test --include database
-
-## Alternative ways to run the integration tests
-
-Phoenix uses Earthly as part of the CI process. It is possible to use earthly to test changes to integration tests locally. It is also possible to run the CI process locally, which is helpful when debugging CI failures.
-
-[Installation Instructions](https://docs.earthly.dev/installation)
-
-To run integration tests against all supported Elixir and OTP versions:
-
- $ earthly -P +all-integration-test
-
-
-To test a specific version:
-
- $ earthly -P --build-arg ELIXIR=1.11.2 --build-arg OTP=21.3.8.18 +integration-test
-
-To run the entire CI process locally, including unit and integration tests:
-
- $ earthly -P +all
+Or alternatively, with docker and docker compose installed, you can just run `./docker.sh`.
## How tests are written
diff --git a/integration_test/config/config.exs b/integration_test/config/config.exs
index 906700f8c4..aa9faf5f65 100644
--- a/integration_test/config/config.exs
+++ b/integration_test/config/config.exs
@@ -4,4 +4,6 @@ config :phoenix, :json_library, Jason
config :swoosh, api_client: false
-config :tailwind, :version, "3.3.2"
+config :tailwind, :version, "3.4.3"
+
+config :phoenix_live_view, enable_expensive_runtime_checks: true
\ No newline at end of file
diff --git a/integration_test/docker.sh b/integration_test/docker.sh
new file mode 100755
index 0000000000..c2a2965141
--- /dev/null
+++ b/integration_test/docker.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env sh -e
+
+# adapt with versions from .github/versions/ci.yml if necessary;
+# you can also override these with environment variables
+ELIXIR="${ELIXIR:-1.17.3}"
+ERLANG="${ERLANG:-27.1.2}"
+SUFFIX="${SUFFIX:-alpine-3.20.3}"
+
+# Get the directory of the script
+SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
+
+# Get the parent directory
+PARENT_DIR=$(dirname "$SCRIPT_DIR")
+
+# Check if docker-compose is available
+if command -v docker-compose &> /dev/null
+then
+ COMPOSE_CMD="docker-compose"
+elif docker compose version &> /dev/null
+then
+ COMPOSE_CMD="docker compose"
+else
+ echo "Error: Neither docker-compose nor the docker compose plugin is available."
+ exit 1
+fi
+
+# Start databases
+$COMPOSE_CMD up -d
+
+# Run test script in container
+docker run --rm --network=integration_test_default \
+ -w $PARENT_DIR -v $PARENT_DIR:$PARENT_DIR \
+ -it hexpm/elixir:$ELIXIR-erlang-$ERLANG-$SUFFIX sh integration_test/test.sh
+
+$COMPOSE_CMD down
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 623aa1ffdf..63cedf5317 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -10,7 +10,7 @@ defmodule Phoenix.Integration.MixProject do
[
app: :phoenix_integration,
version: "0.1.0",
- elixir: "~> 1.14",
+ elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
deps: deps()
@@ -33,29 +33,38 @@ defmodule Phoenix.Integration.MixProject do
[
{:phx_new, path: "../installer"},
{:phoenix, path: "..", override: true},
- {:phoenix_ecto, "~> 4.4"},
- {:esbuild, "~> 0.7", runtime: false},
+ {:phoenix_ecto, "~> 4.5"},
+ {:esbuild, "~> 0.8", runtime: false},
{:ecto_sql, "~> 3.10"},
{:postgrex, ">= 0.0.0"},
{:myxql, ">= 0.0.0"},
{:tds, ">= 0.0.0"},
{:ecto_sqlite3, ">= 0.0.0"},
- {:phoenix_html, "~> 3.3"},
- {:phoenix_live_view, "~> 0.19.0"},
+ {:phoenix_html, "~> 4.1"},
+ # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"},
+ {:phoenix_live_view, "~> 1.0.0-rc.0", override: true},
+ {:dns_cluster, "~> 0.1.1"},
{:floki, ">= 0.30.0"},
{:phoenix_live_reload, "~> 1.2"},
- {:phoenix_live_dashboard, "~> 0.8.0"},
- {:telemetry_metrics, "~> 0.6"},
+ {:phoenix_live_dashboard, "~> 0.8.3"},
+ {:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
- {:gettext, "~> 0.20"},
+ {:gettext, "~> 0.26"},
{:jason, "~> 1.2"},
- {:swoosh, "~> 1.3"},
- {:plug_cowboy, "~> 2.5"},
+ {:swoosh, "~> 1.16"},
+ {:bandit, "~> 1.0"},
{:bcrypt_elixir, "~> 3.0"},
- {:argon2_elixir, "~> 3.0"},
+ {:argon2_elixir, "~> 4.0"},
{:pbkdf2_elixir, "~> 2.0"},
{:tailwind, "~> 0.2"},
- {:finch, "~> 0.13"}
+ {:heroicons,
+ github: "tailwindlabs/heroicons",
+ tag: "v2.1.1",
+ sparse: "optimized",
+ app: false,
+ compile: false,
+ depth: 1},
+ {:req, "~> 0.5.4"}
]
end
end
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 838581c92f..6ce1c9a9ce 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -1,51 +1,51 @@
%{
- "argon2_elixir": {:hex, :argon2_elixir, "3.1.0", "4135e0a1b4ff800d42c85aa663e068efa3cb356297189b5b65caa992db8ec8cf", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "c08feae0ee0292165d1b945003363c7cd8523d002e0483c627dfca930291dd73"},
- "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
- "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
- "cc_precompiler": {:hex, :cc_precompiler, "0.1.7", "77de20ac77f0e53f20ca82c563520af0237c301a1ec3ab3bc598e8a96c7ee5d9", [:mix], [{:elixir_make, "~> 0.7.3", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2768b28bf3c2b4f788c995576b39b8cb5d47eb788526d93bd52206c1d8bf4b75"},
- "comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
- "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
- "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
- "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
- "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
- "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
- "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
- "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
- "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.10.3", "82ce316a8727f1daec397a9932b1a20130ea1ac33c3257b78eded1d3f45ae9b3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.9", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "b4fa32d09f5e5c05d3401ade3dd4416e3c7072d5117c150cb4adeea72760fb93"},
- "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
- "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"},
- "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
- "exqlite": {:hex, :exqlite, "0.13.14", "acd8b58c2245c6aa611262a887509c6aa862a05bfeb174faf348375bd9fc7edb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "e81cd9b811e70a43b8d2d4ee76d3ce57ff349890ec4182f8f5223ead38ac4996"},
- "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
- "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
- "floki": {:hex, :floki, "0.34.3", "5e2dcaec5d7c228ce5b1d3501502e308b2d79eb655e4191751a1fe491c37feac", [:mix], [], "hexpm", "9577440eea5b97924b4bf3c7ea55f7b8b6dce589f9b28b096cc294a8dc342341"},
- "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
- "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
- "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
- "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
- "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"},
- "myxql": {:hex, :myxql, "0.6.3", "3d77683a09f1227abb8b73d66b275262235c5cae68182f0cfa5897d72a03700e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "af9eb517ddaced5c5c28e8749015493757fd4413f2cfccea449c466d405d9f51"},
- "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
- "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
- "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.1.0", "ce2f75056d43281df044a6da902c933f73789dcea560e25c24c9ede80b8365cc", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "bf8aa304bd2b47ed74de6e5eb4c6b7dc766b936a0a86d643ada89657c715f525"},
- "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
- "phoenix_html": {:hex, :phoenix_html, "3.3.1", "4788757e804a30baac6b3fc9695bf5562465dd3f1da8eb8460ad5b404d9a2178", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "bed1906edd4906a15fd7b412b85b05e521e1f67c9a85418c55999277e553d0d3"},
- "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.0", "0b3158b5b198aa444473c91d23d79f52fb077e807ffad80dacf88ce078fa8df2", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "87785a54474fed91a67a1227a741097eb1a42c2e49d3c0d098b588af65cd410d"},
- "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
- "phoenix_live_view": {:hex, :phoenix_live_view, "0.19.4", "dd9ffe3ca0683bdef4f340bcdd2c35a6ee0d581a2696033fc25f52e742618bdc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fd2c666d227476d63af7b8c20e6e61d16f07eb49f924cf4198fca7668156f15b"},
+ "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"},
+ "bandit": {:hex, :bandit, "1.5.4", "8e56e7cfc06f3c57995be0d9bf4e45b972d8732f5c7e96ef8ec0735f52079527", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "04c2b38874769af67fe7f10034f606ad6dda1d8f80c4d7a0c616b347584d5aff"},
+ "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
+ "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
+ "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
+ "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
+ "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"},
+ "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
+ "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
+ "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"},
+ "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"},
+ "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
+ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
+ "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
+ "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
+ "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
+ "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
+ "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"},
+ "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
+ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
+ "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
+ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+ "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
+ "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
+ "myxql": {:hex, :myxql, "0.7.0", "3382f139b0b0da977a8fc33c8cded125e20df2e400f8d7b7e674fa62a7e077dd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "40e4b7ad4973c8b895e86a3de04ff7a79c2cf72b9f2bddef7717afb4ab36d8c0"},
+ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
+ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+ "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.2.0", "2ec4f7daae2bf74cb9e52df3554bbdcec8a38104a7f0ccaa4d45d5919e4c3f19", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "6c4af97f5cae925c56caded648520510ea583eebf1587e185b9f445762197aff"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"},
+ "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
+ "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"},
+ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
+ "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
- "phoenix_template": {:hex, :phoenix_template, "1.0.2", "a3dd349493d7c0b8f58da8175f805963a5b809ffc7d8c1b8dd46ba5b199ef58f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "ab78ebc964685b9eeba102344049eb32d69e582c497d5a0ae6f25909db00c67b"},
- "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
- "postgrex": {:hex, :postgrex, "0.17.1", "01c29fd1205940ee55f7addb8f1dc25618ca63a8817e56fac4f6846fc2cddcbe", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "14b057b488e73be2beee508fb1955d8db90d6485c6466428fe9ccf1d6692a555"},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "swoosh": {:hex, :swoosh, "1.11.3", "49caa2653205bfa0a567b5404afb5c39e932a9678d2e43cc78271670721397c8", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e6f3381f9e95d88effa31446177d9cb7c3f6d617c355ab806ddddceda35208d7"},
- "tailwind": {:hex, :tailwind, "0.2.1", "83d8eadbe71a8e8f67861fe7f8d51658ecfb258387123afe4d9dc194eddc36b0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "e8a13f6107c95f73e58ed1b4221744e1eb5a093cd1da244432067e19c8c9a277"},
- "tds": {:hex, :tds, "2.3.2", "0499fd3049f024e2918a0a5c4f2072992fb7204f8e0b53cd325a8d930460765b", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "d6a17f0b17a4381fc18eee133ba3d31ba07e80337c3340e480ea257319d544f0"},
- "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
- "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
- "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
- "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"},
- "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
+ "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
+ "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"},
+ "req": {:hex, :req, "0.5.4", "e375e4812adf83ffcf787871d7a124d873e983e3b77466e6608b973582f7f837", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a17998ffe2ef54f79bfdd782ef9f4cbf987d93851e89444cbc466a6a25eee494"},
+ "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"},
+ "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
+ "tds": {:hex, :tds, "2.3.5", "fedfb96d53206f01eac62ead859e47e1541a62e1553e9eb7a8801c7dca59eae8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "52e350f5dd5584bbcff9859e331be144d290b41bd4c749b936014a17660662f2"},
+ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
+ "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
+ "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
}
diff --git a/integration_test/test.sh b/integration_test/test.sh
new file mode 100755
index 0000000000..a2cdb03528
--- /dev/null
+++ b/integration_test/test.sh
@@ -0,0 +1,22 @@
+#!/bin/sh -e
+
+mix local.rebar --force
+mix local.hex --force
+
+# Install Dependencies
+apk add --no-progress --update git socat make gcc libc-dev
+
+# Set up local proxies
+socat TCP-LISTEN:5432,fork TCP-CONNECT:postgres:5432&
+socat TCP-LISTEN:3306,fork TCP-CONNECT:mysql:3306&
+socat TCP-LISTEN:1433,fork TCP-CONNECT:mssql:1433&
+
+# Run installer tests
+echo "Running installer tests"
+cd installer
+mix test
+
+echo "Running integration tests"
+cd ../integration_test
+mix deps.get
+mix test --include database
diff --git a/integration_test/test/code_generation/app_with_defaults_test.exs b/integration_test/test/code_generation/app_with_defaults_test.exs
index 9796a983ec..57d069f794 100644
--- a/integration_test/test/code_generation/app_with_defaults_test.exs
+++ b/integration_test/test/code_generation/app_with_defaults_test.exs
@@ -130,11 +130,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
@@ -158,11 +156,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithDefaultsTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
diff --git a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
index 7d6126dd5b..96ca203de2 100644
--- a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
+++ b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMSSQLAdapterTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
diff --git a/integration_test/test/code_generation/app_with_mysql_adapter_test.exs b/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
index f349bee77d..0886d421f9 100644
--- a/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
+++ b/integration_test/test/code_generation/app_with_mysql_adapter_test.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMySqlAdapterTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
diff --git a/integration_test/test/code_generation/app_with_no_options_test.exs b/integration_test/test/code_generation/app_with_no_options_test.exs
index b5386adccc..daf153814b 100644
--- a/integration_test/test/code_generation/app_with_no_options_test.exs
+++ b/integration_test/test/code_generation/app_with_no_options_test.exs
@@ -42,7 +42,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithNoOptionsTest do
end)
:inets.start()
- {:ok, response} = request_with_retries("http://localhost:4000")
+ {:ok, response} = request_with_retries("http://localhost:4000", 20)
assert response.status_code == 200
assert response.body =~ "PhxBlog"
@@ -68,7 +68,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithNoOptionsTest do
)
end
- defp request_with_retries(url, retries \\ 10)
+ defp request_with_retries(url, retries)
defp request_with_retries(_url, 0), do: {:error, :out_of_retries}
@@ -85,7 +85,7 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithNoOptionsTest do
}}
{:error, {:failed_connect, _}} ->
- Process.sleep(1_000)
+ Process.sleep(5_000)
request_with_retries(url, retries - 1)
{:error, reason} ->
diff --git a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs b/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
index d748d81bd7..4576a6cfc0 100644
--- a/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
+++ b/integration_test/test/code_generation/app_with_sqlite3_adapter.exs
@@ -69,11 +69,9 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithSQLite3AdapterTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
diff --git a/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs b/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
index ee7148db38..9dc71ba64d 100644
--- a/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
+++ b/integration_test/test/code_generation/umbrella_app_with_defaults_test.exs
@@ -136,11 +136,9 @@ defmodule Phoenix.Integration.CodeGeneration.UmbrellaAppWithDefaultsTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
@@ -165,11 +163,9 @@ defmodule Phoenix.Integration.CodeGeneration.UmbrellaAppWithDefaultsTest do
pipe_through [:browser]
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
""")
end)
diff --git a/lib/mix/phoenix.ex b/lib/mix/phoenix.ex
index 39d3b000a6..7c6b8e6d53 100644
--- a/lib/mix/phoenix.ex
+++ b/lib/mix/phoenix.ex
@@ -29,6 +29,12 @@ defmodule Mix.Phoenix do
def copy_from(apps, source_dir, binding, mapping) when is_list(mapping) do
roots = Enum.map(apps, &to_app_source(&1, source_dir))
+ binding =
+ Keyword.merge(binding,
+ maybe_heex_attr_gettext: &maybe_heex_attr_gettext/2,
+ maybe_eex_gettext: &maybe_eex_gettext/2
+ )
+
for {format, source_file_path, target} <- mapping do
source =
Enum.find_value(roots, fn root ->
@@ -359,4 +365,41 @@ defmodule Mix.Phoenix do
def prepend_newline(string) do
"\n" <> string
end
+
+ # In the context of a HEEx attribute value, transforms a given message into a
+ # dynamic `gettext` call or a fixed-value string attribute, depending on the
+ # `gettext?` parameter.
+ #
+ # ## Examples
+ #
+ # iex> ~s| |
+ # ~S| |
+ #
+ # iex> ~s| |
+ # ~S| |
+ defp maybe_heex_attr_gettext(message, gettext?) do
+ if gettext? do
+ ~s|{gettext(#{inspect(message)})}|
+ else
+ inspect(message)
+ end
+ end
+
+ # In the context of an EEx template, transforms a given message into a dynamic
+ # `gettext` call or the message as is, depending on the `gettext?` parameter.
+ #
+ # ## Examples
+ #
+ # iex> ~s|#{maybe_eex_gettext("Hello", true)} |
+ # ~S|<%= gettext("Hello") %> |
+ #
+ # iex> ~s|#{maybe_eex_gettext("Hello", false)} |
+ # ~S|Hello |
+ defp maybe_eex_gettext(message, gettext?) do
+ if gettext? do
+ ~s|<%= gettext(#{inspect(message)}) %>|
+ else
+ message
+ end
+ end
end
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 95584a0313..7fec805e31 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -5,6 +5,7 @@ defmodule Mix.Phoenix.Schema do
defstruct module: nil,
repo: nil,
+ repo_alias: nil,
table: nil,
collection: nil,
embedded?: false,
@@ -28,6 +29,7 @@ defmodule Mix.Phoenix.Schema do
migration_defaults: nil,
migration?: false,
params: %{},
+ optionals: [],
sample_id: nil,
web_path: nil,
web_namespace: nil,
@@ -36,9 +38,10 @@ defmodule Mix.Phoenix.Schema do
route_prefix: nil,
api_route_prefix: nil,
migration_module: nil,
- fixture_unique_functions: %{},
- fixture_params: %{},
- prefix: nil
+ fixture_unique_functions: [],
+ fixture_params: [],
+ prefix: nil,
+ timestamp_type: :naive_datetime
@valid_types [
:integer,
@@ -69,15 +72,16 @@ defmodule Mix.Phoenix.Schema do
end
def new(schema_name, schema_plural, cli_attrs, opts) do
- ctx_app = opts[:context_app] || Mix.Phoenix.context_app()
- otp_app = Mix.Phoenix.otp_app()
- opts = Keyword.merge(Application.get_env(otp_app, :generators, []), opts)
- base = Mix.Phoenix.context_base(ctx_app)
- basename = Phoenix.Naming.underscore(schema_name)
- module = Module.concat([base, schema_name])
- repo = opts[:repo] || Module.concat([base, "Repo"])
- file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
- table = opts[:table] || schema_plural
+ ctx_app = opts[:context_app] || Mix.Phoenix.context_app()
+ otp_app = Mix.Phoenix.otp_app()
+ opts = Keyword.merge(Application.get_env(otp_app, :generators, []), opts)
+ base = Mix.Phoenix.context_base(ctx_app)
+ basename = Phoenix.Naming.underscore(schema_name)
+ module = Module.concat([base, schema_name])
+ repo = opts[:repo] || Module.concat([base, "Repo"])
+ repo_alias = if String.ends_with?(Atom.to_string(repo), ".Repo"), do: "", else: ", as: Repo"
+ file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
+ table = opts[:table] || schema_plural
{cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
{assocs, attrs} = partition_attrs_and_assocs(module, attrs(cli_attrs))
types = types(attrs)
@@ -96,11 +100,15 @@ defmodule Mix.Phoenix.Schema do
collection = if schema_plural == singular, do: singular <> "_collection", else: schema_plural
string_attr = string_attr(types)
create_params = params(attrs, :create)
+
+ optionals = for {key, :map} <- types, do: key, into: []
+
default_params_key =
case Enum.at(create_params, 0) do
{key, _} -> key
nil -> :some_field
end
+
fixture_unique_functions = fixture_unique_functions(singular, uniques, attrs)
%Schema{
@@ -108,6 +116,7 @@ defmodule Mix.Phoenix.Schema do
migration?: Keyword.get(opts, :migration, true),
module: module,
repo: repo,
+ repo_alias: repo_alias,
table: table,
embedded?: embedded?,
alias: module |> Module.split() |> List.last() |> Module.concat(nil),
@@ -116,6 +125,7 @@ defmodule Mix.Phoenix.Schema do
plural: schema_plural,
singular: singular,
collection: collection,
+ optionals: optionals,
assocs: assocs,
types: types,
defaults: schema_defaults(attrs),
@@ -125,6 +135,7 @@ defmodule Mix.Phoenix.Schema do
human_singular: Phoenix.Naming.humanize(singular),
human_plural: Phoenix.Naming.humanize(schema_plural),
binary_id: opts[:binary_id],
+ timestamp_type: opts[:timestamp_type] || :naive_datetime,
migration_defaults: migration_defaults(attrs),
string_attr: string_attr,
params: %{
@@ -141,7 +152,7 @@ defmodule Mix.Phoenix.Schema do
context_app: ctx_app,
generate?: generate?,
migration_module: migration_module(),
- fixture_unique_functions: fixture_unique_functions,
+ fixture_unique_functions: Enum.sort(fixture_unique_functions),
fixture_params: fixture_params(attrs, fixture_unique_functions),
prefix: opts[:prefix]
}
@@ -158,11 +169,12 @@ defmodule Mix.Phoenix.Schema do
end
def extract_attr_flags(cli_attrs) do
- {attrs, uniques, redacts} = Enum.reduce(cli_attrs, {[], [], []}, fn attr, {attrs, uniques, redacts} ->
- [attr_name | rest] = String.split(attr, ":")
- attr_name = String.to_atom(attr_name)
- split_flags(Enum.reverse(rest), attr_name, attrs, uniques, redacts)
- end)
+ {attrs, uniques, redacts} =
+ Enum.reduce(cli_attrs, {[], [], []}, fn attr, {attrs, uniques, redacts} ->
+ [attr_name | rest] = String.split(attr, ":")
+ attr_name = String.to_atom(attr_name)
+ split_flags(Enum.reverse(rest), attr_name, attrs, uniques, redacts)
+ end)
{Enum.reverse(attrs), uniques, redacts}
end
@@ -174,7 +186,7 @@ defmodule Mix.Phoenix.Schema do
do: split_flags(rest, name, attrs, uniques, [name | redacts])
defp split_flags(rest, name, attrs, uniques, redacts),
- do: {[Enum.join([name | Enum.reverse(rest)], ":") | attrs ], uniques, redacts}
+ do: {[Enum.join([name | Enum.reverse(rest)], ":") | attrs], uniques, redacts}
@doc """
Parses the attrs as received by generators.
@@ -196,9 +208,9 @@ defmodule Mix.Phoenix.Schema do
end
@doc """
- Converts the given value to map format when it is a date, time, datetime or naive_datetime.
+ Converts the given value to map format when it's a date, time, datetime or naive_datetime.
- Since `form_component.html.heex` generated by the live generator uses selects for dates and/or
+ Since `form.html.heex` generated by the live generator uses selects for dates and/or
times, fixtures must use map format for those fields in order to submit the live form.
"""
def live_form_value(%Date{} = date), do: Calendar.strftime(date, "%Y-%m-%d")
@@ -216,7 +228,7 @@ defmodule Mix.Phoenix.Schema do
def live_form_value(value), do: value
@doc """
- Build an invalid value for `@invalid_attrs` which is nil by default.
+ Builds an invalid value for `@invalid_attrs` which is nil by default.
* In case the value is a list, this will return an empty array.
* In case the value is date, datetime, naive_datetime or time, this will return an invalid date.
@@ -247,8 +259,18 @@ defmodule Mix.Phoenix.Schema do
end)
end
- def type_and_opts_for_schema({:enum, opts}), do: ~s|Ecto.Enum, values: #{inspect Keyword.get(opts, :values)}|
- def type_and_opts_for_schema(other), do: inspect other
+ @doc """
+ Returns the required fields in the schema. Anything not in the `optionals` list
+ is considered required.
+ """
+ def required_fields(schema) do
+ Enum.reject(schema.attrs, fn {key, _} -> key in schema.optionals end)
+ end
+
+ def type_and_opts_for_schema({:enum, opts}),
+ do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
+
+ def type_and_opts_for_schema(other), do: inspect(other)
def maybe_redact_field(true), do: ", redact: true"
def maybe_redact_field(false), do: ""
@@ -258,14 +280,16 @@ defmodule Mix.Phoenix.Schema do
"""
def value(schema, field, value) do
schema.types
- |> Map.fetch!(field)
+ |> Keyword.fetch!(field)
|> inspect_value(value)
end
+
defp inspect_value(:decimal, value), do: "Decimal.new(\"#{value}\")"
defp inspect_value(_type, value), do: inspect(value)
defp list_to_attr([key]), do: {String.to_atom(key), :string}
defp list_to_attr([key, value]), do: {String.to_atom(key), String.to_atom(value)}
+
defp list_to_attr([key, comp, value]) do
{String.to_atom(key), {String.to_atom(comp), String.to_atom(value)}}
end
@@ -274,55 +298,103 @@ defmodule Mix.Phoenix.Schema do
defp type_to_default(key, t, :create) do
case t do
- {:array, type} -> build_array_values(type, :create)
- {:enum, values} -> build_enum_values(values, :create)
- :integer -> 42
- :float -> 120.5
- :decimal -> "120.5"
- :boolean -> true
- :map -> %{}
- :text -> "some #{key}"
- :date -> Date.add(Date.utc_today(), -1)
- :time -> ~T[14:00:00]
- :time_usec -> ~T[14:00:00.000000]
- :uuid -> "7488a646-e31f-11e4-aace-600308960662"
- :utc_datetime -> DateTime.add(build_utc_datetime(), -@one_day_in_seconds, :second, Calendar.UTCOnlyTimeZoneDatabase)
- :utc_datetime_usec -> DateTime.add(build_utc_datetime_usec(), -@one_day_in_seconds, :second, Calendar.UTCOnlyTimeZoneDatabase)
- :naive_datetime -> NaiveDateTime.add(build_utc_naive_datetime(), -@one_day_in_seconds)
- :naive_datetime_usec -> NaiveDateTime.add(build_utc_naive_datetime_usec(), -@one_day_in_seconds)
- _ -> "some #{key}"
+ {:array, type} ->
+ build_array_values(type, :create)
+
+ {:enum, values} ->
+ build_enum_values(values, :create)
+
+ :integer ->
+ 42
+
+ :float ->
+ 120.5
+
+ :decimal ->
+ "120.5"
+
+ :boolean ->
+ true
+
+ :map ->
+ %{}
+
+ :text ->
+ "some #{key}"
+
+ :date ->
+ Date.add(Date.utc_today(), -1)
+
+ :time ->
+ ~T[14:00:00]
+
+ :time_usec ->
+ ~T[14:00:00.000000]
+
+ :uuid ->
+ "7488a646-e31f-11e4-aace-600308960662"
+
+ :utc_datetime ->
+ DateTime.add(
+ build_utc_datetime(),
+ -@one_day_in_seconds,
+ :second,
+ Calendar.UTCOnlyTimeZoneDatabase
+ )
+
+ :utc_datetime_usec ->
+ DateTime.add(
+ build_utc_datetime_usec(),
+ -@one_day_in_seconds,
+ :second,
+ Calendar.UTCOnlyTimeZoneDatabase
+ )
+
+ :naive_datetime ->
+ NaiveDateTime.add(build_utc_naive_datetime(), -@one_day_in_seconds)
+
+ :naive_datetime_usec ->
+ NaiveDateTime.add(build_utc_naive_datetime_usec(), -@one_day_in_seconds)
+
+ _ ->
+ "some #{key}"
end
end
+
defp type_to_default(key, t, :update) do
case t do
- {:array, type} -> build_array_values(type, :update)
- {:enum, values} -> build_enum_values(values, :update)
- :integer -> 43
- :float -> 456.7
- :decimal -> "456.7"
- :boolean -> false
- :map -> %{}
- :text -> "some updated #{key}"
- :date -> Date.utc_today()
- :time -> ~T[15:01:01]
- :time_usec -> ~T[15:01:01.000000]
- :uuid -> "7488a646-e31f-11e4-aace-600308960668"
- :utc_datetime -> build_utc_datetime()
- :utc_datetime_usec -> build_utc_datetime_usec()
- :naive_datetime -> build_utc_naive_datetime()
- :naive_datetime_usec -> build_utc_naive_datetime_usec()
- _ -> "some updated #{key}"
+ {:array, type} -> build_array_values(type, :update)
+ {:enum, values} -> build_enum_values(values, :update)
+ :integer -> 43
+ :float -> 456.7
+ :decimal -> "456.7"
+ :boolean -> false
+ :map -> %{}
+ :text -> "some updated #{key}"
+ :date -> Date.utc_today()
+ :time -> ~T[15:01:01]
+ :time_usec -> ~T[15:01:01.000000]
+ :uuid -> "7488a646-e31f-11e4-aace-600308960668"
+ :utc_datetime -> build_utc_datetime()
+ :utc_datetime_usec -> build_utc_datetime_usec()
+ :naive_datetime -> build_utc_naive_datetime()
+ :naive_datetime_usec -> build_utc_naive_datetime_usec()
+ _ -> "some updated #{key}"
end
end
defp build_array_values(:string, :create),
- do: Enum.map([1,2], &("option#{&1}"))
+ do: Enum.map([1, 2], &"option#{&1}")
+
defp build_array_values(:integer, :create),
- do: [1,2]
+ do: [1, 2]
+
defp build_array_values(:string, :update),
do: ["option1"]
+
defp build_array_values(:integer, :update),
do: [1]
+
defp build_array_values(_, _),
do: []
@@ -353,22 +425,26 @@ defmodule Mix.Phoenix.Schema do
mix phx.gen.schema Comment comments body:text status:enum:published:unpublished
"""
- defp validate_attr!({name, :datetime}), do: validate_attr!({name, :naive_datetime})
+ defp validate_attr!({name, :datetime}), do: {name, :naive_datetime}
+
defp validate_attr!({name, :array}) do
- Mix.raise """
+ Mix.raise("""
Phoenix generators expect the type of the array to be given to #{name}:array.
For example:
mix phx.gen.schema Post posts settings:array:string
- """
+ """)
end
- defp validate_attr!({_name, :enum}), do: Mix.raise @enum_missing_value_error
+
+ defp validate_attr!({_name, :enum}), do: Mix.raise(@enum_missing_value_error)
defp validate_attr!({_name, type} = attr) when type in @valid_types, do: attr
- defp validate_attr!({_name, {:enum, _vals}} = attr), do: attr
defp validate_attr!({_name, {type, _}} = attr) when type in @valid_types, do: attr
+
defp validate_attr!({_, type}) do
- Mix.raise "Unknown type `#{inspect type}` given to generator. " <>
- "The supported types are: #{@valid_types |> Enum.sort() |> Enum.join(", ")}"
+ Mix.raise(
+ "Unknown type `#{inspect(type)}` given to generator. " <>
+ "The supported types are: #{@valid_types |> Enum.sort() |> Enum.join(", ")}"
+ )
end
defp partition_attrs_and_assocs(schema_module, attrs) do
@@ -376,14 +452,17 @@ defmodule Mix.Phoenix.Schema do
Enum.split_with(attrs, fn
{_, {:references, _}} ->
true
+
{key, :references} ->
- Mix.raise """
+ Mix.raise("""
Phoenix generators expect the table to be given to #{key}:references.
For example:
mix phx.gen.schema Comment comments body:text post_id:references:posts
- """
- _ -> false
+ """)
+
+ _ ->
+ false
end)
assocs =
@@ -399,8 +478,8 @@ defmodule Mix.Phoenix.Schema do
defp schema_defaults(attrs) do
Enum.into(attrs, %{}, fn
- {key, :boolean} -> {key, ", default: false"}
- {key, _} -> {key, ""}
+ {key, :boolean} -> {key, ", default: false"}
+ {key, _} -> {key, ""}
end)
end
@@ -412,7 +491,7 @@ defmodule Mix.Phoenix.Schema do
end
defp types(attrs) do
- Enum.into(attrs, %{}, fn
+ Keyword.new(attrs, fn
{key, {:enum, vals}} -> {key, {:enum, values: translate_enum_vals(vals)}}
{key, {root, val}} -> {key, {root, schema_type(val)}}
{key, val} -> {key, schema_type(val)}
@@ -428,9 +507,10 @@ defmodule Mix.Phoenix.Schema do
defp schema_type(:text), do: :string
defp schema_type(:uuid), do: Ecto.UUID
+
defp schema_type(val) do
if Code.ensure_loaded?(Ecto.Type) and not Ecto.Type.primitive?(val) do
- Mix.raise "Unknown type `#{val}` given to generator"
+ Mix.raise("Unknown type `#{val}` given to generator")
else
val
end
@@ -444,14 +524,14 @@ defmodule Mix.Phoenix.Schema do
|> Enum.uniq_by(fn {key, _} -> key end)
|> Enum.map(fn
{key, false} -> "create index(:#{table}, [:#{key}])"
- {key, true} -> "create unique_index(:#{table}, [:#{key}])"
+ {key, true} -> "create unique_index(:#{table}, [:#{key}])"
end)
end
defp migration_defaults(attrs) do
Enum.into(attrs, %{}, fn
- {key, :boolean} -> {key, ", default: false, null: false"}
- {key, _} -> {key, ""}
+ {key, :boolean} -> {key, ", default: false, null: false"}
+ {key, _} -> {key, ""}
end)
end
@@ -482,7 +562,7 @@ defmodule Mix.Phoenix.Schema do
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)}"
+ other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
end
end
@@ -521,17 +601,19 @@ defmodule Mix.Phoenix.Schema do
{function_def, true}
end
-
{attr, {function_name, function_def, needs_impl?}}
end)
end
defp fixture_params(attrs, fixture_unique_functions) do
- Map.new(attrs, fn {attr, type} ->
- case Map.fetch(fixture_unique_functions, attr) do
- {:ok, {function_name, _function_def, _needs_impl?}} ->
+ attrs
+ |> Enum.sort()
+ |> Enum.map(fn {attr, type} ->
+ case fixture_unique_functions do
+ %{^attr => {function_name, _function_def, _needs_impl?}} ->
{attr, "#{function_name}()"}
- :error ->
+
+ %{} ->
{attr, inspect(type_to_default(attr, type, :create))}
end
end)
diff --git a/lib/mix/tasks/phx.digest.clean.ex b/lib/mix/tasks/phx.digest.clean.ex
index 704b8ecf01..9f5347aac3 100644
--- a/lib/mix/tasks/phx.digest.clean.ex
+++ b/lib/mix/tasks/phx.digest.clean.ex
@@ -33,6 +33,8 @@ defmodule Mix.Tasks.Phx.Digest.Clean do
* `--all` - specifies that all compiled assets (including the manifest)
will be removed. Note this overrides the age and keep switches.
+
+ * `--no-compile` - do not run mix compile
"""
@switches [output: :string, age: :integer, keep: :integer, all: :boolean]
@@ -40,7 +42,10 @@ defmodule Mix.Tasks.Phx.Digest.Clean do
@doc false
def run(all_args) do
# Ensure all compressors are compiled.
- Mix.Task.run("compile", all_args)
+ if "--no-compile" not in all_args do
+ Mix.Task.run("compile", all_args)
+ end
+
{:ok, _} = Application.ensure_all_started(:phoenix)
{opts, _, _} = OptionParser.parse(all_args, switches: @switches, aliases: [o: :output])
diff --git a/lib/mix/tasks/phx.digest.ex b/lib/mix/tasks/phx.digest.ex
index 5523213f40..8f1ce97fc8 100644
--- a/lib/mix/tasks/phx.digest.ex
+++ b/lib/mix/tasks/phx.digest.ex
@@ -40,6 +40,15 @@ defmodule Mix.Tasks.Phx.Digest do
It is possible to digest the stylesheet asset references without the query
string "?vsn=d" with the option `--no-vsn`.
+
+ ## Options
+
+ * `-o, --output` - indicates the path to your compiled
+ assets directory. Defaults to `priv/static`
+
+ * `--no-vsn` - do not add version query string to assets
+
+ * `--no-compile` - do not run mix compile
"""
@default_opts [vsn: true]
@@ -48,7 +57,12 @@ defmodule Mix.Tasks.Phx.Digest do
@doc false
def run(all_args) do
# Ensure all compressors are compiled.
- Mix.Task.run "compile", all_args
+ if "--no-compile" not in all_args do
+ Mix.Task.run("compile", all_args)
+ end
+
+ Mix.Task.reenable("phx.digest")
+
{:ok, _} = Application.ensure_all_started(:phoenix)
{opts, args, _} = OptionParser.parse(all_args, switches: @switches, aliases: [o: :output])
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 3401f56702..04cb5d2348 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -116,7 +116,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
table: :string,
merge_with_existing_context: :boolean,
prefix: :string,
- live: :boolean
+ live: :boolean,
+ compile: :boolean
]
@doc false
@@ -130,18 +131,15 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
hashing_library = build_hashing_library!(opts)
context_args = OptionParser.to_argv(opts, switches: @switches) ++ parsed
-
{context, schema} = Gen.Context.build(context_args, __MODULE__)
context = put_live_option(context)
-
Gen.Context.prompt_for_code_injection(context)
- if Keyword.get(test_opts, :validate_dependencies?, true) do
+ if "--no-compile" not in args do
# Needed so we can get the ecto adapter and ensure other
# libraries are loaded.
Mix.Task.run("compile")
-
validate_required_dependencies!()
end
@@ -170,7 +168,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
live?: Keyword.fetch!(context.opts, :live)
]
- paths = generator_paths()
+ paths = Mix.Phoenix.generator_paths()
prompt_for_conflicts(context)
@@ -276,69 +274,91 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
case Keyword.fetch(context.opts, :live) do
{:ok, true} ->
live_files = [
- "registration_live.ex": [web_pre, "live", web_path, "#{singular}_registration_live.ex"],
+ "registration_live.ex": [
+ web_pre,
+ "live",
+ web_path,
+ "#{singular}_live",
+ "registration.ex"
+ ],
"registration_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_registration_live_test.exs"
+ "#{singular}_live",
+ "registration_test.exs"
],
- "login_live.ex": [web_pre, "live", web_path, "#{singular}_login_live.ex"],
+ "login_live.ex": [web_pre, "live", web_path, "#{singular}_live", "login.ex"],
"login_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_login_live_test.exs"
+ "#{singular}_live",
+ "login_test.exs"
],
"reset_password_live.ex": [
web_pre,
"live",
web_path,
- "#{singular}_reset_password_live.ex"
+ "#{singular}_live",
+ "reset_password.ex"
],
"reset_password_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_reset_password_live_test.exs"
+ "#{singular}_live",
+ "reset_password_test.exs"
],
"forgot_password_live.ex": [
web_pre,
"live",
web_path,
- "#{singular}_forgot_password_live.ex"
+ "#{singular}_live",
+ "forgot_password.ex"
],
"forgot_password_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_forgot_password_live_test.exs"
+ "#{singular}_live",
+ "forgot_password_test.exs"
],
- "settings_live.ex": [web_pre, "live", web_path, "#{singular}_settings_live.ex"],
+ "settings_live.ex": [web_pre, "live", web_path, "#{singular}_live", "settings.ex"],
"settings_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_settings_live_test.exs"
+ "#{singular}_live",
+ "settings_test.exs"
+ ],
+ "confirmation_live.ex": [
+ web_pre,
+ "live",
+ web_path,
+ "#{singular}_live",
+ "confirmation.ex"
],
- "confirmation_live.ex": [web_pre, "live", web_path, "#{singular}_confirmation_live.ex"],
"confirmation_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_confirmation_live_test.exs"
+ "#{singular}_live",
+ "confirmation_test.exs"
],
"confirmation_instructions_live.ex": [
web_pre,
"live",
web_path,
- "#{singular}_confirmation_instructions_live.ex"
+ "#{singular}_live",
+ "confirmation_instructions.ex"
],
"confirmation_instructions_live_test.exs": [
web_test_pre,
"live",
web_path,
- "#{singular}_confirmation_instructions_live_test.exs"
+ "#{singular}_live",
+ "confirmation_instructions_test.exs"
]
]
@@ -718,14 +738,6 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
defp web_path_prefix(%Schema{web_path: nil}), do: ""
defp web_path_prefix(%Schema{web_path: web_path}), do: "/" <> web_path
- # The paths to look for template files for generators.
- #
- # Defaults to checking the current app's `priv` directory,
- # and falls back to phx_gen_auth's `priv` directory.
- defp generator_paths do
- [".", :phoenix]
- end
-
defp inject_before_final_end(content_to_inject, file_path) do
with {:ok, file} <- read_file(file_path),
{:ok, new_file} <- Injector.inject_before_final_end(file, content_to_inject) do
diff --git a/lib/mix/tasks/phx.gen.auth/hashing_library.ex b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
index a922eabe40..2d37afc265 100644
--- a/lib/mix/tasks/phx.gen.auth/hashing_library.ex
+++ b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
@@ -3,6 +3,13 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
defstruct [:name, :module, :mix_dependency, :test_config]
+ @type t :: %__MODULE__{
+ name: atom(),
+ module: module(),
+ mix_dependency: binary(),
+ test_config: binary()
+ }
+
def build("bcrypt") do
lib = %__MODULE__{
name: :bcrypt,
@@ -33,7 +40,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
lib = %__MODULE__{
name: :argon2,
module: Argon2,
- mix_dependency: ~s|{:argon2_elixir, "~> 3.0"}|,
+ mix_dependency: ~s|{:argon2_elixir, "~> 4.0"}|,
test_config: """
config :argon2_elixir, t_cost: 1, m_cost: 8
"""
diff --git a/lib/mix/tasks/phx.gen.auth/injector.ex b/lib/mix/tasks/phx.gen.auth/injector.ex
index 90a2116c84..48ae16782c 100644
--- a/lib/mix/tasks/phx.gen.auth/injector.ex
+++ b/lib/mix/tasks/phx.gen.auth/injector.ex
@@ -161,7 +161,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
Menu code to inject into the application layout template.
"""
def app_layout_menu_code_to_inject(%Schema{} = schema, padding \\ 4, newline \\ "\n") do
- already_injected_str = "#{schema.route_prefix}/log_in"
+ already_injected_str = "#{schema.route_prefix}/log-in"
base_tailwind_classes = "text-[0.8125rem] leading-6 text-zinc-900"
link_tailwind_classes = "#{base_tailwind_classes} font-semibold hover:text-zinc-700"
@@ -182,7 +182,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
<.link
- href={~p"#{schema.route_prefix}/log_out"}
+ href={~p"#{schema.route_prefix}/log-out"}
method="delete"
class="#{link_tailwind_classes}"
>
@@ -200,7 +200,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
<.link
- href={~p"#{schema.route_prefix}/log_in"}
+ href={~p"#{schema.route_prefix}/log-in"}
class="#{link_tailwind_classes}"
>
Log in
diff --git a/lib/mix/tasks/phx.gen.cert.ex b/lib/mix/tasks/phx.gen.cert.ex
index 84b6d1e1b4..363b6d1516 100644
--- a/lib/mix/tasks/phx.gen.cert.ex
+++ b/lib/mix/tasks/phx.gen.cert.ex
@@ -49,7 +49,9 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
@doc false
def run(all_args) do
if Mix.Project.umbrella?() do
- Mix.raise("mix phx.gen.cert must be invoked from within your *_web application root directory")
+ Mix.raise(
+ "mix phx.gen.cert must be invoked from within your *_web application root directory"
+ )
end
{opts, args} =
@@ -233,20 +235,18 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
defp new_cert(public_key, common_name, hostnames) do
<> = :crypto.strong_rand_bytes(8)
- # Dates must be in 'YYMMDD' format
- {{year, month, day}, _} =
- :erlang.timestamp()
- |> :calendar.now_to_datetime()
-
- yy = year |> Integer.to_string() |> String.slice(2, 2)
- mm = month |> Integer.to_string() |> String.pad_leading(2, "0")
- dd = day |> Integer.to_string() |> String.pad_leading(2, "0")
-
- not_before = yy <> mm <> dd
+ today = Date.utc_today()
- yy2 = (year + 1) |> Integer.to_string() |> String.slice(2, 2)
+ not_before =
+ today
+ |> Date.to_iso8601(:basic)
+ |> String.slice(2, 6)
- not_after = yy2 <> mm <> dd
+ not_after =
+ today
+ |> Date.add(365)
+ |> Date.to_iso8601(:basic)
+ |> String.slice(2, 6)
otp_tbs_certificate(
version: :v3,
@@ -255,8 +255,8 @@ defmodule Mix.Tasks.Phx.Gen.Cert do
issuer: rdn(common_name),
validity:
validity(
- notBefore: {:utcTime, '#{not_before}000000Z'},
- notAfter: {:utcTime, '#{not_after}000000Z'}
+ notBefore: {:utcTime, ~c"#{not_before}000000Z"},
+ notAfter: {:utcTime, ~c"#{not_after}000000Z"}
),
subject: rdn(common_name),
subjectPublicKeyInfo:
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 934a08fa1a..a951720758 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -55,6 +55,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
config :your_app, :generators,
migration: true,
binary_id: false,
+ timestamp_type: :naive_datetime,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
You can override those options per invocation by providing corresponding
@@ -78,16 +79,27 @@ defmodule Mix.Tasks.Phx.Gen.Context do
alias Mix.Phoenix.{Context, Schema}
alias Mix.Tasks.Phx.Gen
- @switches [binary_id: :boolean, table: :string, web: :string,
- schema: :boolean, context: :boolean, context_app: :string,
- merge_with_existing_context: :boolean, prefix: :string, live: :boolean]
+ @switches [
+ binary_id: :boolean,
+ table: :string,
+ web: :string,
+ schema: :boolean,
+ context: :boolean,
+ context_app: :string,
+ merge_with_existing_context: :boolean,
+ prefix: :string,
+ live: :boolean,
+ compile: :boolean
+ ]
@default_opts [schema: true, context: true]
@doc false
def run(args) do
if Mix.Project.umbrella?() do
- Mix.raise "mix phx.gen.context must be invoked from within your *_web application root directory"
+ Mix.raise(
+ "mix phx.gen.context must be invoked from within your *_web application root directory"
+ )
end
{context, schema} = build(args)
@@ -120,6 +132,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
defp parse_opts(args) do
{opts, parsed, invalid} = OptionParser.parse(args, switches: @switches)
+
merged_opts =
@default_opts
|> Keyword.merge(opts)
@@ -127,7 +140,9 @@ defmodule Mix.Tasks.Phx.Gen.Context do
{merged_opts, parsed, invalid}
end
+
defp put_context_app(opts, nil), do: opts
+
defp put_context_app(opts, string) do
Keyword.put(opts, :context_app, String.to_atom(string))
end
@@ -154,7 +169,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
@doc false
def ensure_context_file_exists(%Context{file: file} = context, paths, binding) do
unless Context.pre_existing?(context) do
- Mix.Generator.create_file(file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context.ex", binding))
+ Mix.Generator.create_file(
+ file,
+ Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context.ex", binding)
+ )
end
end
@@ -162,7 +180,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
ensure_context_file_exists(context, paths, binding)
paths
- |> Mix.Phoenix.eval_from("priv/templates/phx.gen.context/#{schema_access_template(context)}", binding)
+ |> Mix.Phoenix.eval_from(
+ "priv/templates/phx.gen.context/#{schema_access_template(context)}",
+ binding
+ )
|> inject_eex_before_final_end(file, binding)
end
@@ -173,7 +194,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
@doc false
def ensure_test_file_exists(%Context{test_file: test_file} = context, paths, binding) do
unless Context.pre_existing_tests?(context) do
- Mix.Generator.create_file(test_file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context_test.exs", binding))
+ Mix.Generator.create_file(
+ test_file,
+ Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context_test.exs", binding)
+ )
end
end
@@ -186,13 +210,24 @@ defmodule Mix.Tasks.Phx.Gen.Context do
end
@doc false
- def ensure_test_fixtures_file_exists(%Context{test_fixtures_file: test_fixtures_file} = context, paths, binding) do
+ def ensure_test_fixtures_file_exists(
+ %Context{test_fixtures_file: test_fixtures_file} = context,
+ paths,
+ binding
+ ) do
unless Context.pre_existing_test_fixtures?(context) do
- Mix.Generator.create_file(test_fixtures_file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/fixtures_module.ex", binding))
+ Mix.Generator.create_file(
+ test_fixtures_file,
+ Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/fixtures_module.ex", binding)
+ )
end
end
- defp inject_test_fixture(%Context{test_fixtures_file: test_fixtures_file} = context, paths, binding) do
+ defp inject_test_fixture(
+ %Context{test_fixtures_file: test_fixtures_file} = context,
+ paths,
+ binding
+ ) do
ensure_test_fixtures_file_exists(context, paths, binding)
paths
@@ -214,20 +249,14 @@ defmodule Mix.Tasks.Phx.Gen.Context do
)
if Enum.any?(fixture_functions_needing_implementations) do
- Mix.shell.info(
- """
-
- Some of the generated database columns are unique. Please provide
- unique implementations for the following fixture function(s) in
- #{context.test_fixtures_file}:
-
- #{
- fixture_functions_needing_implementations
- |> Enum.map_join(&indent(&1, 2))
- |> String.trim_trailing()
- }
- """
- )
+ Mix.shell().info("""
+
+ Some of the generated database columns are unique. Please provide
+ unique implementations for the following fixture function(s) in
+ #{context.test_fixtures_file}:
+
+ #{fixture_functions_needing_implementations |> Enum.map_join(&indent(&1, 2)) |> String.trim_trailing()}
+ """)
end
end
@@ -237,11 +266,11 @@ defmodule Mix.Tasks.Phx.Gen.Context do
string
|> String.split("\n")
|> Enum.map_join(fn line ->
- if String.trim(line) == "" do
- "\n"
- else
- indent_string <> line <> "\n"
- end
+ if String.trim(line) == "" do
+ "\n"
+ else
+ indent_string <> line <> "\n"
+ end
end)
end
@@ -283,27 +312,38 @@ defmodule Mix.Tasks.Phx.Gen.Context do
defp validate_args!([context, schema, _plural | _] = args, help) do
cond do
not Context.valid?(context) ->
- help.raise_with_help "Expected the context, #{inspect context}, to be a valid module name"
+ help.raise_with_help(
+ "Expected the context, #{inspect(context)}, to be a valid module name"
+ )
+
not Schema.valid?(schema) ->
- help.raise_with_help "Expected the schema, #{inspect schema}, to be a valid module name"
+ help.raise_with_help("Expected the schema, #{inspect(schema)}, to be a valid module name")
+
context == schema ->
- help.raise_with_help "The context and schema should have different names"
+ help.raise_with_help("The context and schema should have different names")
+
context == Mix.Phoenix.base() ->
- help.raise_with_help "Cannot generate context #{context} because it has the same name as the application"
+ help.raise_with_help(
+ "Cannot generate context #{context} because it has the same name as the application"
+ )
+
schema == Mix.Phoenix.base() ->
- help.raise_with_help "Cannot generate schema #{schema} because it has the same name as the application"
+ help.raise_with_help(
+ "Cannot generate schema #{schema} because it has the same name as the application"
+ )
+
true ->
args
end
end
defp validate_args!(_, help) do
- help.raise_with_help "Invalid arguments"
+ help.raise_with_help("Invalid arguments")
end
@doc false
def raise_with_help(msg) do
- Mix.raise """
+ Mix.raise("""
#{msg}
mix phx.gen.html, phx.gen.json, phx.gen.live, and phx.gen.context
@@ -319,11 +359,12 @@ defmodule Mix.Tasks.Phx.Gen.Context do
The context serves as the API boundary for the given resource.
Multiple resources may belong to a context and a resource may be
split over distinct contexts (such as Accounts.User and Payments.User).
- """
+ """)
end
@doc false
def prompt_for_code_injection(%Context{generate?: false}), do: :ok
+
def prompt_for_code_injection(%Context{} = context) do
if Context.pre_existing?(context) && !merge_with_existing_context?(context) do
System.halt()
@@ -347,7 +388,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
* If they are not closely related, another context probably works better
- The fact two entities are related in the database does not mean they belong \
+ The fact that two entities are related in the database does not mean they belong \
to the same context.
If you are not sure, prefer creating a new context over adding to the existing one.
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 01262692b2..76fe2aa388 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -2,54 +2,64 @@ defmodule Mix.Tasks.Phx.Gen.Html do
@shortdoc "Generates context and controller for an HTML resource"
@moduledoc """
- Generates controller, HTML views, and context for an HTML resource.
+ Generates controller with view, templates, schema and context for an HTML resource.
mix phx.gen.html Accounts User users name:string age:integer
- The first argument is the context module followed by the schema module
- and its plural name (used as the schema table name).
+ The first argument, `Accounts`, is the resource's context.
+ A context is an Elixir module that serves as an API boundary for closely related resources.
- The context is an Elixir module that serves as an API boundary for
- the given resource. A context often holds many related resources.
- Therefore, if the context already exists, it will be augmented with
- functions for the given resource.
+ The second argument, `User`, is the resource's schema.
+ A schema is an Elixir module responsible for mapping database fields into an Elixir struct.
+ The `User` schema above specifies two fields with their respective colon-delimited data types:
+ `name:string` and `age:integer`. See `mix phx.gen.schema` for more information on attributes.
> Note: A resource may also be split
> over distinct contexts (such as `Accounts.User` and `Payments.User`).
- The schema is responsible for mapping the database fields into an
- Elixir struct. It is followed by an optional list of attributes,
- with their respective names and types. See `mix phx.gen.schema`
- for more information on attributes.
+ This generator adds the following files to `lib/`:
- Overall, this generator will add the following files to `lib/`:
+ * a controller in `lib/my_app_web/controllers/user_controller.ex`
+ * default CRUD HTML templates in `lib/my_app_web/controllers/user_html`
+ * an HTML view collocated with the controller in `lib/my_app_web/controllers/user_html.ex`
+ * a schema in `lib/my_app/accounts/user.ex`, with an `users` table
+ * a context module in `lib/my_app/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 controller in `lib/app_web/controllers/user_controller.ex`
- * an HTML view collocated with the controller in `lib/app_web/controllers/user_html.ex`
- * default CRUD templates in `lib/app_web/controllers/user_html`
+ Additionally, this generator creates the following files:
- ## The context app
+ * a migration for the schema in `priv/repo/migrations`
+ * a controller test module in `test/my_app/controllers/user_controller_test.exs`
+ * a context test module in `test/my_app/accounts_test.exs`
+ * a context test helper module in `test/support/fixtures/accounts_fixtures.ex`
- A migration file for the repository and test files for the context and
- controller features will also be generated.
+ If the context already exists, this generator injects functions for the given resource into
+ the context, context test, and context test helper modules.
- The location of the web files (controllers, HTML views, templates, etc) in an
- umbrella application will vary based on the `:context_app` config located
- in your applications `:generators` configuration. When set, the Phoenix
- generators will generate web files directly in your lib and test folders
- since the application is assumed to be isolated to web specific functionality.
- If `:context_app` is not set, the generators will place web related lib
- and test files in a `web/` directory since the application is assumed
- to be handling both web and domain specific functionality.
- Example configuration:
+ ## Umbrella app configuration
- config :my_app_web, :generators, context_app: :my_app
+ By default, Phoenix injects both web and domain specific functionality into the same
+ application. When using umbrella applications, those concerns are typically broken
+ into two separate apps, your context application - let's call it `my_app` - and its web
+ layer, which Phoenix assumes to be `my_app_web`.
+
+ You can teach Phoenix to use this style via the `:context_app` configuration option
+ in your `my_app_umbrella/config/config.exs`:
+
+ config :my_app_web,
+ ecto_repos: [Stuff.Repo],
+ generators: [context_app: :my_app]
Alternatively, the `--context-app` option may be supplied to the generator:
- mix phx.gen.html Sales User users --context-app warehouse
+ mix phx.gen.html Sales User users --context-app my_app
+
+ If you delete the `:context_app` configuration option, Phoenix will automatically put generated web files in
+ `my_app_umbrella/apps/my_app_web_web`.
+
+ If you change the value of `:context_app` to `:new_value`, `my_app_umbrella/apps/new_value_web`
+ must already exist or you will get the following error:
+
+ ** (Mix) no directory for context_app :new_value found in my_app_web's deps.
## Web namespace
@@ -172,7 +182,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
@doc false
def inputs(%Schema{} = schema) do
- Enum.map(schema.attrs, fn
+ schema.attrs
+ |> Enum.reject(fn {_key, type} -> type == :map end)
+ |> Enum.map(fn
{key, :integer} ->
~s(<.input field={f[#{inspect(key)}]} type="number" label="#{label(key)}" />)
@@ -186,7 +198,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do
~s(<.input field={f[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
{key, :text} ->
- ~s(<.input field={f[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+ ~s(<.input field={f[#{inspect(key)}]} type="textarea" label="#{label(key)}" />)
{key, :date} ->
~s(<.input field={f[#{inspect(key)}]} type="date" label="#{label(key)}" />)
@@ -246,6 +258,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
lines = input |> String.split("\n") |> Enum.reject(&(&1 == ""))
case lines do
+ [] ->
+ []
+
[line] ->
[columns, line]
diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex
index dbe7878457..08efb5f16c 100644
--- a/lib/mix/tasks/phx.gen.json.ex
+++ b/lib/mix/tasks/phx.gen.json.ex
@@ -171,7 +171,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do
else
Mix.shell().info("""
- Add the resource to your :api scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
+ Add the resource to the "#{Application.get_env(ctx_app, :generators)[:api_prefix] || "/api"}" scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
resources "/#{schema.plural}", #{inspect(schema.alias)}Controller, except: [:new, :edit]
""")
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index ea9a291350..f0b6aa28aa 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -19,9 +19,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
types. See `mix help phx.gen.schema` for more information on attributes.
When this command is run for the first time, a `Components` module will be
- created if it does not exist, along with the resource level LiveViews and
- components, including `UserLive.Index`, `UserLive.Show`, and
- `UserLive.FormComponent` modules for the new resource.
+ created if it does not exist, along with the resource level LiveViews,
+ including `UserLive.Index`, `UserLive.Show`, and `UserLive.Form` modules for
+ the new resource.
> Note: A resource may also be split
> over distinct contexts (such as `Accounts.User` and `Payments.User`).
@@ -32,8 +32,8 @@ defmodule Mix.Tasks.Phx.Gen.Live do
* a schema in `lib/app/accounts/user.ex`, with a `users` table
* a LiveView in `lib/app_web/live/user_live/show.ex`
* a LiveView in `lib/app_web/live/user_live/index.ex`
- * a LiveComponent in `lib/app_web/live/user_live/form_component.ex`
- * a helpers module in `lib/app_web/live/live_helpers.ex` with a modal
+ * a LiveView in `lib/app_web/live/user_live/form.ex`
+ * a components module in `lib/app_web/components/core_components.ex`
After file generation is complete, there will be output regarding required
updates to the `lib/app_web/router.ex` file.
@@ -41,11 +41,9 @@ defmodule Mix.Tasks.Phx.Gen.Live do
Add the live routes to your browser scope in lib/app_web/router.ex:
live "/users", UserLive.Index, :index
- live "/users/new", UserLive.Index, :new
- live "/users/:id/edit", UserLive.Index, :edit
-
+ live "/users/new", UserLive.Form, :new
live "/users/:id", UserLive.Show, :show
- live "/users/:id/show/edit", UserLive.Show, :edit
+ live "/users/:id/edit", UserLive.Form, :edit
## The context app
@@ -147,9 +145,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
[
{:eex, "show.ex", Path.join(web_live, "show.ex")},
{:eex, "index.ex", Path.join(web_live, "index.ex")},
- {:eex, "form_component.ex", Path.join(web_live, "form_component.ex")},
- {:eex, "index.html.heex", Path.join(web_live, "index.html.heex")},
- {:eex, "show.html.heex", Path.join(web_live, "show.html.heex")},
+ {:eex, "form.ex", Path.join(web_live, "form.ex")},
{:eex, "live_test.exs", Path.join(test_live, "#{schema.singular}_live_test.exs")},
{:new_eex, "core_components.ex",
Path.join([web_prefix, "components", "core_components.ex"])}
@@ -262,16 +258,17 @@ defmodule Mix.Tasks.Phx.Gen.Live do
defp live_route_instructions(schema) do
[
~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|,
- ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|,
- ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|,
+ ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Form, :new\n|,
~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|,
- ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit|
+ ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Form, :edit|
]
end
@doc false
def inputs(%Schema{} = schema) do
- Enum.map(schema.attrs, fn
+ schema.attrs
+ |> Enum.reject(fn {_key, type} -> type == :map end)
+ |> Enum.map(fn
{_, {:references, _}} ->
nil
@@ -288,7 +285,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
~s(<.input field={@form[#{inspect(key)}]} type="checkbox" label="#{label(key)}" />)
{key, :text} ->
- ~s(<.input field={@form[#{inspect(key)}]} type="text" label="#{label(key)}" />)
+ ~s(<.input field={@form[#{inspect(key)}]} type="textarea" label="#{label(key)}" />)
{key, :date} ->
~s(<.input field={@form[#{inspect(key)}]} type="date" label="#{label(key)}" />)
diff --git a/lib/mix/tasks/phx.gen.presence.ex b/lib/mix/tasks/phx.gen.presence.ex
index ad39cce505..fe60b634bc 100644
--- a/lib/mix/tasks/phx.gen.presence.ex
+++ b/lib/mix/tasks/phx.gen.presence.ex
@@ -21,7 +21,7 @@ defmodule Mix.Tasks.Phx.Gen.Presence do
end
def run([alias_name]) do
if Mix.Project.umbrella?() do
- Mix.raise "mix phx.gen.presence must be invoked from within your *_web application root directory"
+ Mix.raise "mix phx.gen.presence must be invoked from within your *_web application's root directory"
end
context_app = Mix.Phoenix.context_app()
otp_app = Mix.Phoenix.otp_app()
diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex
index d1b09db614..3f93149e9e 100644
--- a/lib/mix/tasks/phx.gen.release.ex
+++ b/lib/mix/tasks/phx.gen.release.ex
@@ -19,7 +19,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
running `mix release`.
To skip generating the migration-related files, use the `--no-ecto` flag. To
- force these migration-related files to be generated, the use `--ecto` flag.
+ force these migration-related files to be generated, use the `--ecto` flag.
## Docker
@@ -107,9 +107,9 @@ defmodule Mix.Tasks.Phx.Gen.Release do
_build/dev/rel/#{app}/bin/#{app}
""")
- if opts.ecto do
+ if opts.ecto and opts.socket_db_adaptor_installed do
post_install_instructions("config/runtime.exs", ~r/ECTO_IPV6/, """
- [warn] Conditional IPV6 support missing from runtime configuration.
+ [warn] Conditional IPv6 support missing from runtime configuration.
Add the following to your config/runtime.exs:
@@ -149,6 +149,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
|> OptionParser.parse!(strict: [ecto: :boolean, docker: :boolean])
|> elem(0)
|> Keyword.put_new_lazy(:ecto, &ecto_sql_installed?/0)
+ |> Keyword.put_new_lazy(:socket_db_adaptor_installed, &socket_db_adaptor_installed?/0)
|> Keyword.put_new(:docker, false)
|> Map.new()
end
@@ -188,9 +189,31 @@ defmodule Mix.Tasks.Phx.Gen.Release do
defp ecto_sql_installed?, do: Mix.Project.deps_paths() |> Map.has_key?(:ecto_sql)
- @debian "bullseye"
+ defp socket_db_adaptor_installed? do
+ Mix.Project.deps_paths(depth: 1)
+ |> Map.take([:tds, :myxql, :postgrex])
+ |> map_size() > 0
+ end
+
+ @debian "bookworm"
+ defp elixir_and_debian_vsn(elixir_vsn, otp_vsn) do
+ url =
+ "https://hub.docker.com/v2/namespaces/hexpm/repositories/elixir/tags?name=#{elixir_vsn}-erlang-#{otp_vsn}-debian-#{@debian}-"
+
+ fetch_body!(url)
+ |> Phoenix.json_library().decode!()
+ |> Map.fetch!("results")
+ |> Enum.find_value(:error, fn %{"name" => name} ->
+ if String.ends_with?(name, "-slim") do
+ elixir_vsn = name |> String.split("-") |> List.first()
+ %{"vsn" => vsn} = Regex.named_captures(~r/.*debian-#{@debian}-(?.*)-slim/, name)
+ {:ok, elixir_vsn, vsn}
+ end
+ end)
+ end
+
defp gen_docker(binding) do
- elixir_vsn =
+ wanted_elixir_vsn =
case Version.parse!(System.version()) do
%{major: major, minor: minor, pre: ["dev"]} -> "#{major}.#{minor - 1}.0"
_ -> System.version()
@@ -198,22 +221,27 @@ defmodule Mix.Tasks.Phx.Gen.Release do
otp_vsn = otp_vsn()
- url =
- "https://hub.docker.com/v2/namespaces/hexpm/repositories/elixir/tags?name=#{elixir_vsn}-erlang-#{otp_vsn}-debian-#{@debian}-"
+ vsns =
+ case elixir_and_debian_vsn(wanted_elixir_vsn, otp_vsn) do
+ {:ok, elixir_vsn, debian_vsn} ->
+ {:ok, elixir_vsn, debian_vsn}
- debian_vsn =
- fetch_body!(url)
- |> Phoenix.json_library().decode!()
- |> Map.fetch!("results")
- |> Enum.find_value(:error, fn %{"name" => name} ->
- if String.ends_with?(name, "-slim") do
- %{"vsn" => vsn} = Regex.named_captures(~r/.*debian-#{@debian}-(?.*)-slim/, name)
- {:ok, vsn}
- end
- end)
+ :error ->
+ case elixir_and_debian_vsn("", otp_vsn) do
+ {:ok, elixir_vsn, debian_vsn} ->
+ Logger.warning(
+ "Docker image for Elixir #{wanted_elixir_vsn} not found, defaulting to Elixir #{elixir_vsn}"
+ )
+
+ {:ok, elixir_vsn, debian_vsn}
+
+ :error ->
+ :error
+ end
+ end
- case debian_vsn do
- {:ok, debian_vsn} ->
+ case vsns do
+ {:ok, elixir_vsn, debian_vsn} ->
binding =
Keyword.merge(binding,
debian: @debian,
@@ -228,7 +256,11 @@ defmodule Mix.Tasks.Phx.Gen.Release do
])
:error ->
- raise "unable to fetch supported Docker image for Elixir #{elixir_vsn} and Erlang #{otp_vsn}"
+ raise """
+ unable to fetch supported Docker image for Elixir #{wanted_elixir_vsn} and Erlang #{otp_vsn}.
+ Please check https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=#{otp_vsn}\
+ for a suitable Elixir version
+ """
end
end
diff --git a/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex
index 6d28156a47..7d2da6e631 100644
--- a/lib/mix/tasks/phx.gen.schema.ex
+++ b/lib/mix/tasks/phx.gen.schema.ex
@@ -86,6 +86,21 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
Generated migration can use `binary_id` for schema's primary key
and its references with option `--binary-id`.
+ ## repo
+
+ Generated migration can use `repo` to set the migration repository
+ folder with option `--repo`:
+
+ $ mix phx.gen.schema Blog.Post posts --repo MyApp.Repo.Auth
+
+ ## migration_dir
+
+ Generated migrations can be added to a specific `--migration-dir` which sets
+ the migration folder path:
+
+ $ mix phx.gen.schema Blog.Post posts --migration-dir /path/to/directory
+
+
## prefix
By default migrations and schemas are generated without a prefix.
@@ -111,18 +126,27 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
config :your_app, :generators,
migration: true,
binary_id: false,
+ timestamp_type: :naive_datetime,
sample_binary_id: "11111111-1111-1111-1111-111111111111"
You can override those options per invocation by providing corresponding
switches, e.g. `--no-binary-id` to use normal ids despite the default
configuration or `--migration` to force generation of the migration.
+
+ ## UTC timestamps
+
+ By setting the `:timestamp_type` to `:utc_datetime`, the timestamps
+ will be created using the UTC timezone. This results in a `DateTime` struct
+ instead of a `NaiveDateTime`. This can also be set to `:utc_datetime_usec` for
+ microsecond precision.
+
"""
use Mix.Task
alias Mix.Phoenix.Schema
- @switches [migration: :boolean, binary_id: :boolean, table: :string,
- web: :string, context_app: :string, prefix: :string]
+ @switches [migration: :boolean, binary_id: :boolean, table: :string, web: :string,
+ context_app: :string, prefix: :string, repo: :string, migration_dir: :string]
@doc false
def run(args) do
@@ -155,10 +179,17 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
parent_opts
|> Keyword.merge(schema_opts)
|> put_context_app(schema_opts[:context_app])
+ |> maybe_update_repo_module()
- schema = Schema.new(schema_name, plural, attrs, opts)
+ Schema.new(schema_name, plural, attrs, opts)
+ end
- schema
+ defp maybe_update_repo_module(opts) do
+ if is_nil(opts[:repo]) do
+ opts
+ else
+ Keyword.update!(opts, :repo, &Module.concat([&1]))
+ end
end
defp put_context_app(opts, nil), do: opts
@@ -172,12 +203,26 @@ defmodule Mix.Tasks.Phx.Gen.Schema do
end
@doc false
- def copy_new_files(%Schema{context_app: ctx_app} = schema, paths, binding) do
+ def copy_new_files(%Schema{context_app: ctx_app, repo: repo, opts: opts} = schema, paths, binding) do
files = files_to_be_generated(schema)
Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.schema", binding, files)
if schema.migration? do
- migration_path = Mix.Phoenix.context_app_path(ctx_app, "priv/repo/migrations/#{timestamp()}_create_#{schema.table}.exs")
+ migration_dir =
+ cond do
+ migration_dir = opts[:migration_dir] ->
+ migration_dir
+
+ opts[:repo] ->
+ repo_name = repo |> Module.split() |> List.last() |> Macro.underscore()
+ Mix.Phoenix.context_app_path(ctx_app, "priv/#{repo_name}/migrations/")
+
+ true ->
+ Mix.Phoenix.context_app_path(ctx_app, "priv/repo/migrations/")
+ end
+
+ migration_path = Path.join(migration_dir, "#{timestamp()}_create_#{schema.table}.exs")
+
Mix.Phoenix.copy_from paths, "priv/templates/phx.gen.schema", binding, [
{:eex, "migration.exs", migration_path},
]
diff --git a/lib/mix/tasks/phx.gen.socket.ex b/lib/mix/tasks/phx.gen.socket.ex
index 6aaca6d995..16896bbec6 100644
--- a/lib/mix/tasks/phx.gen.socket.ex
+++ b/lib/mix/tasks/phx.gen.socket.ex
@@ -6,7 +6,7 @@ defmodule Mix.Tasks.Phx.Gen.Socket do
$ mix phx.gen.socket User
- Accepts the module name for the socket
+ Accepts the module name for the socket.
The generated files will contain:
@@ -18,9 +18,9 @@ defmodule Mix.Tasks.Phx.Gen.Socket do
For an umbrella application:
* a client in `apps/my_app_web/assets/js`
- * a socket in `apps/my_app_web/lib/app_name_web/channels`
+ * a socket in `apps/my_app_web/lib/my_app_web/channels`
- You can then generated channels with `mix phx.gen.channel`.
+ You can then generate channels with `mix phx.gen.channel`.
"""
use Mix.Task
@@ -28,7 +28,7 @@ defmodule Mix.Tasks.Phx.Gen.Socket do
def run(args) do
if Mix.Project.umbrella?() do
Mix.raise(
- "mix phx.gen.socket must be invoked from within your *_web application root directory"
+ "mix phx.gen.socket must be invoked from within your *_web application's root directory"
)
end
diff --git a/lib/mix/tasks/phx.routes.ex b/lib/mix/tasks/phx.routes.ex
index d12348c4a7..ab8717a6c3 100644
--- a/lib/mix/tasks/phx.routes.ex
+++ b/lib/mix/tasks/phx.routes.ex
@@ -63,7 +63,10 @@ defmodule Mix.Tasks.Phx.Routes do
@doc false
def run(args, base \\ Mix.Phoenix.base()) do
- Mix.Task.run("compile", args)
+ if "--no-compile" not in args do
+ Mix.Task.run("compile")
+ end
+
Mix.Task.reenable("phx.routes")
{opts, args, _} =
@@ -94,10 +97,9 @@ defmodule Mix.Tasks.Phx.Routes do
%{plug: plug, plug_opts: plug_opts} = meta
{module, func_name} =
- if log_mod = meta[:log_module] do
- {log_mod, meta[:log_function]}
- else
- {plug, plug_opts}
+ case meta[:mfa] do
+ {mod, fun, _} -> {mod, fun}
+ _ -> {plug, plug_opts}
end
Mix.shell().info("Module: #{inspect(module)}")
@@ -182,7 +184,7 @@ defmodule Mix.Tasks.Phx.Routes do
end)
case function_infos do
- {_, line, _, _, _} -> line
+ {_, anno, _, _, _} -> :erl_anno.line(anno)
nil -> nil
end
end
diff --git a/lib/mix/tasks/phx.server.ex b/lib/mix/tasks/phx.server.ex
index 048982adbc..f0d85b01f5 100644
--- a/lib/mix/tasks/phx.server.ex
+++ b/lib/mix/tasks/phx.server.ex
@@ -33,7 +33,7 @@ defmodule Mix.Tasks.Phx.Server do
@impl true
def run(args) do
Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
- Mix.Tasks.Run.run(open_args(args) ++ run_args())
+ Mix.Tasks.Run.run(run_args() ++ open_args(args))
end
defp iex_running? do
diff --git a/lib/phoenix/channel.ex b/lib/phoenix/channel.ex
index 48ac52850d..0f902dc6b1 100644
--- a/lib/phoenix/channel.ex
+++ b/lib/phoenix/channel.ex
@@ -32,6 +32,11 @@ defmodule Phoenix.Channel do
{:ok, socket}
end
+ The first argument is the topic, the second argument is a map payload given by
+ the client, and the third argument is an instance of `Phoenix.Socket`. The
+ `socket` to all channel callbacks, so check its module and documentation to
+ learn its fields and the different ways to interact with it.
+
## Authorization
Clients must join a channel to send and receive PubSub events on that channel.
@@ -69,8 +74,14 @@ defmodule Phoenix.Channel do
## Broadcasts
- Here's an example of receiving an incoming `"new_msg"` event from one client,
- and broadcasting the message to all topic subscribers for this socket.
+ You can broadcast events from anywhere in your application to a topic by
+ the `broadcast` function in the endpoint:
+
+ MyAppWeb.Endpoint.broadcast!("room:13", "new_message", %{content: "hello"})
+
+ It is also possible to broadcast directly from channels. Here's an example of
+ receiving an incoming `"new_msg"` event from one client, and broadcasting the
+ message to all topic subscribers for this socket.
def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
broadcast!(socket, "new_msg", %{uid: uid, body: body})
@@ -189,30 +200,6 @@ defmodule Phoenix.Channel do
{:noreply, socket}
end
- ## Broadcasting to an external topic
-
- In some cases, you will want to broadcast messages without the context of
- a `socket`. This could be for broadcasting from within your channel to an
- external topic, or broadcasting from elsewhere in your application like a
- controller or another process. Such can be done via your endpoint:
-
- # within channel
- def handle_in("new_msg", %{"uid" => uid, "body" => body}, socket) do
- ...
- broadcast_from!(socket, "new_msg", %{uid: uid, body: body})
- MyAppWeb.Endpoint.broadcast_from!(self(), "room:superadmin",
- "new_msg", %{uid: uid, body: body})
- {:noreply, socket}
- end
-
- # within controller
- def create(conn, params) do
- ...
- MyAppWeb.Endpoint.broadcast!("room:" <> rid, "new_msg", %{uid: uid, body: body})
- MyAppWeb.Endpoint.broadcast!("room:superadmin", "new_msg", %{uid: uid, body: body})
- redirect(conn, to: "/")
- end
-
## Terminate
On termination, the channel callback `terminate/2` will be invoked with
@@ -335,14 +322,16 @@ defmodule Phoenix.Channel do
## Logging
By default, channel `"join"` and `"handle_in"` events are logged, using
- the level `:info` and `:debug`, respectively. Logs can be customized per
- event type or disabled by setting the `:log_join` and `:log_handle_in`
- options when using `Phoenix.Channel`. For example, the following
- configuration logs join events as `:info`, but disables logging for
+ the level `:info` and `:debug`, respectively. You can change the level used
+ for each event, or disable logs, per event type by setting the `:log_join`
+ and `:log_handle_in` options when using `Phoenix.Channel`. For example, the
+ following configuration logs join events as `:info`, but disables logging for
incoming events:
use Phoenix.Channel, log_join: :info, log_handle_in: false
+ Note that changing an event type's level doesn't affect what is logged,
+ unless you set it to `false`, it affects the associated level.
"""
alias Phoenix.Socket
alias Phoenix.Channel.Server
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 931a7ccc54..433cc75476 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -31,9 +31,24 @@ defmodule Phoenix.CodeReloader do
This function is a no-op and returns `:ok` if Mix is not available.
+ The reloader should also be configured as a Mix listener in project's
+ mix.exs file (since Elixir v1.18):
+
+ def project do
+ [
+ ...,
+ listeners: [Phoenix.CodeReloader]
+ ]
+ end
+
+ This way the reloader can notice whenever the project is compiled
+ concurrently.
+
## Options
- * `:reloadable_args` - additional CLI args to pass to the compiler tasks
+ * `:reloadable_args` - additional CLI args to pass to the compiler tasks.
+ Defaults to `["--no-all-warnings"]` so only warnings related to the
+ files being compiled are printed
"""
@spec reload(module, keyword) :: :ok | {:error, binary()}
@@ -55,6 +70,10 @@ defmodule Phoenix.CodeReloader do
@spec sync :: :ok
defdelegate sync, to: Phoenix.CodeReloader.Server
+ @doc false
+ @spec child_spec(keyword) :: Supervisor.child_spec()
+ defdelegate child_spec(opts), to: Phoenix.CodeReloader.MixListener
+
## Plug
@behaviour Plug
@@ -64,22 +83,26 @@ defmodule Phoenix.CodeReloader do
primary: "#EB532D",
accent: "#a0b0c0",
text_color: "#304050",
- logo: "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K",
+ logo:
+ "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K",
monospace_font: "menlo, consolas, monospace"
}
@doc """
API used by Plug to start the code reloader.
"""
- def init(opts), do: Keyword.put_new(opts, :reloader, &Phoenix.CodeReloader.reload/1)
+ def init(opts) do
+ Keyword.put_new(opts, :reloader, &Phoenix.CodeReloader.reload/2)
+ end
@doc """
API used by Plug to invoke the code reloader on every request.
"""
def call(conn, opts) do
- case opts[:reloader].(conn.private.phoenix_endpoint) do
+ case opts[:reloader].(conn.private.phoenix_endpoint, opts) do
:ok ->
conn
+
{:error, output} ->
conn
|> put_resp_content_type("text/html")
@@ -270,7 +293,7 @@ defmodule Phoenix.CodeReloader do
defp format_output(output) do
output
- |> String.trim
- |> Plug.HTML.html_escape
+ |> String.trim()
+ |> Plug.HTML.html_escape()
end
end
diff --git a/lib/phoenix/code_reloader/mix_listener.ex b/lib/phoenix/code_reloader/mix_listener.ex
new file mode 100644
index 0000000000..f136314ee9
--- /dev/null
+++ b/lib/phoenix/code_reloader/mix_listener.ex
@@ -0,0 +1,72 @@
+defmodule Phoenix.CodeReloader.MixListener do
+ @moduledoc false
+
+ use GenServer
+
+ @name __MODULE__
+
+ @spec start_link(keyword) :: GenServer.on_start()
+ def start_link(_opts) do
+ GenServer.start_link(__MODULE__, {}, name: @name)
+ end
+
+ @spec started? :: boolean()
+ def started? do
+ Process.whereis(Phoenix.CodeReloader.MixListener) != nil
+ end
+
+ @doc """
+ Unloads all modules invalidated by external compilations.
+
+ Only reloads modules from the given apps.
+ """
+ @spec purge([atom()]) :: :ok
+ def purge(apps) do
+ GenServer.call(@name, {:purge, apps}, :infinity)
+ end
+
+ @impl true
+ def init({}) do
+ {:ok, %{to_purge: %{}}}
+ end
+
+ @impl true
+ def handle_call({:purge, apps}, _from, state) do
+ for app <- apps, modules = state.to_purge[app] do
+ purge_modules(modules)
+ end
+
+ {:reply, :ok, %{state | to_purge: %{}}}
+ end
+
+ @impl true
+ def handle_info({:modules_compiled, info}, state) do
+ if info.os_pid == System.pid() do
+ # Ignore compilations from ourselves, because the modules are
+ # already updated in memory
+ {:noreply, state}
+ else
+ %{changed: changed, removed: removed} = info.modules_diff
+
+ state =
+ update_in(state.to_purge[info.app], fn to_purge ->
+ to_purge = to_purge || MapSet.new()
+ to_purge = Enum.into(changed, to_purge)
+ Enum.into(removed, to_purge)
+ end)
+
+ {:noreply, state}
+ end
+ end
+
+ def handle_info(_message, state) do
+ {:noreply, state}
+ end
+
+ defp purge_modules(modules) do
+ for module <- modules do
+ :code.purge(module)
+ :code.delete(module)
+ end
+ end
+end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index e1146c4a18..16ff92bf19 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -48,7 +48,7 @@ defmodule Phoenix.CodeReloader.Server do
Mix.Project.build_structure()
else
Logger.warning(
- "Phoenix is unable to create symlinks. Phoenix' code reloader will run " <>
+ "Phoenix is unable to create symlinks. Phoenix's code reloader will run " <>
"considerably faster if symlinks are allowed." <> os_symlink(:os.type())
)
end
@@ -61,25 +61,41 @@ defmodule Phoenix.CodeReloader.Server do
def handle_call({:reload!, endpoint, opts}, from, state) do
compilers = endpoint.config(:reloadable_compilers)
apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
- args = Keyword.get(opts, :reloadable_args, [])
+ args = Keyword.get(opts, :reloadable_args, ["--no-all-warnings"])
- # We do a backup of the endpoint in case compilation fails.
- # If so we can bring it back to finish the request handling.
- backup = load_backup(endpoint)
froms = all_waiting([from], endpoint)
- {res, out} =
- proxy_io(fn ->
- try do
- mix_compile(Code.ensure_loaded(Mix.Task), compilers, apps, args, state.timestamp)
- catch
- :exit, {:shutdown, 1} ->
- :error
-
- kind, reason ->
- IO.puts(Exception.format(kind, reason, __STACKTRACE__))
- :error
- end
+ {backup, res, out} =
+ with_build_lock(fn ->
+ purge_fallback? =
+ if Phoenix.CodeReloader.MixListener.started?() do
+ Phoenix.CodeReloader.MixListener.purge(apps)
+ false
+ else
+ warn_missing_mix_listener()
+ true
+ end
+
+ # We do a backup of the endpoint in case compilation fails.
+ # If so we can bring it back to finish the request handling.
+ backup = load_backup(endpoint)
+
+ {res, out} =
+ proxy_io(fn ->
+ try do
+ task_loaded = Code.ensure_loaded(Mix.Task)
+ mix_compile(task_loaded, compilers, apps, args, state.timestamp, purge_fallback?)
+ catch
+ :exit, {:shutdown, 1} ->
+ :error
+
+ kind, reason ->
+ IO.puts(Exception.format(kind, reason, __STACKTRACE__))
+ :error
+ end
+ end)
+
+ {backup, res, out}
end)
reply =
@@ -175,7 +191,34 @@ defmodule Phoenix.CodeReloader.Server do
defp purge_protocols(_path), do: :ok
end
- defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, compile_args, timestamp) do
+ defp warn_missing_mix_listener do
+ listeners_supported? = Version.match?(System.version(), ">= 1.18.0-dev")
+
+ if listeners_supported? do
+ IO.warn("""
+ a Mix listener expected by Phoenix.CodeReloader is missing.
+
+ Please add the listener to your mix.exs configuration, like so:
+
+ def project do
+ [
+ ...,
+ listeners: [Phoenix.CodeReloader]
+ ]
+ end
+
+ """)
+ end
+ end
+
+ defp mix_compile(
+ {:module, Mix.Task},
+ compilers,
+ apps_to_reload,
+ compile_args,
+ timestamp,
+ purge_fallback?
+ ) do
config = Mix.Project.config()
path = Mix.Project.consolidation_path(config)
@@ -184,40 +227,77 @@ defmodule Phoenix.CodeReloader.Server do
purge_protocols(path)
end
- mix_compile_deps(Mix.Dep.cached(), apps_to_reload, compile_args, compilers, timestamp, path)
- mix_compile_project(config[:app], apps_to_reload, compile_args, compilers, timestamp, path)
+ mix_compile_deps(
+ Mix.Dep.cached(),
+ apps_to_reload,
+ compile_args,
+ compilers,
+ timestamp,
+ path,
+ purge_fallback?
+ )
+
+ mix_compile_project(
+ config[:app],
+ apps_to_reload,
+ compile_args,
+ compilers,
+ timestamp,
+ path,
+ purge_fallback?
+ )
if config[:consolidate_protocols] do
+ # If we are consolidating protocols, we need to purge all of its modules
+ # to ensure the consolidated versions are loaded. "mix compile" performs
+ # a similar task.
Code.prepend_path(path)
+ purge_modules(path)
end
:ok
end
- defp mix_compile({:error, _reason}, _, _, _, _) do
+ defp mix_compile({:error, _reason}, _, _, _, _, _) do
raise "the Code Reloader is enabled but Mix is not available. If you want to " <>
"use the Code Reloader in production or inside an escript, you must add " <>
":mix to your applications list. Otherwise, you must disable code reloading " <>
"in such environments"
end
- defp mix_compile_deps(deps, apps_to_reload, compile_args, compilers, timestamp, path) do
+ defp mix_compile_deps(
+ deps,
+ apps_to_reload,
+ compile_args,
+ compilers,
+ timestamp,
+ path,
+ purge_fallback?
+ ) do
for dep <- deps, dep.app in apps_to_reload do
Mix.Dep.in_dependency(dep, fn _ ->
- mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+ mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
end)
end
end
- defp mix_compile_project(nil, _, _, _, _, _), do: :ok
-
- defp mix_compile_project(app, apps_to_reload, compile_args, compilers, timestamp, path) do
+ defp mix_compile_project(nil, _, _, _, _, _, _), do: :ok
+
+ defp mix_compile_project(
+ app,
+ apps_to_reload,
+ compile_args,
+ compilers,
+ timestamp,
+ path,
+ purge_fallback?
+ ) do
if app in apps_to_reload do
- mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+ mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
end
end
- defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path) do
+ defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?) do
manifests = Mix.Tasks.Compile.Elixir.manifests()
configs = Mix.Project.config_files()
config = Mix.Project.config()
@@ -226,7 +306,8 @@ defmodule Phoenix.CodeReloader.Server do
[] ->
# If the manifests are more recent than the timestamp,
# someone updated this app behind the scenes, so purge all beams.
- if Mix.Utils.stale?(manifests, [timestamp]) do
+ # TODO: remove once we depend on Elixir 1.18
+ if purge_fallback? and Mix.Utils.stale?(manifests, [timestamp]) do
purge_modules(Path.join(Mix.Project.app_path(config), "ebin"))
end
@@ -256,14 +337,21 @@ defmodule Phoenix.CodeReloader.Server do
# We call build_structure mostly for Windows so new
# assets in priv are copied to the build directory.
Mix.Project.build_structure(config)
+
+ # TODO: The purge option may no longer be required from Elixir v1.18
args = ["--purge-consolidation-path-if-stale", consolidation_path | compile_args]
- result = run_compilers(compilers, args, [])
+
+ result =
+ with_logger_app(config, fn ->
+ run_compilers(compilers, args, [])
+ end)
cond do
result == :error ->
exit({:shutdown, 1})
result == :ok && config[:consolidate_protocols] ->
+ # TODO: Calling compile.protocols may no longer be required from Elixir v1.18
Mix.Task.reenable("compile.protocols")
Mix.Task.run("compile.protocols", [])
:ok
@@ -277,7 +365,14 @@ defmodule Phoenix.CodeReloader.Server do
defp purge_modules(path) do
with {:ok, beams} <- File.ls(path) do
- Enum.map(beams, &(&1 |> Path.rootname(".beam") |> String.to_atom() |> purge_module()))
+ for beam <- beams do
+ case :binary.split(beam, ".beam") do
+ [module, ""] -> module |> String.to_atom() |> purge_module()
+ _ -> :ok
+ end
+ end
+
+ :ok
end
end
@@ -320,4 +415,24 @@ defmodule Phoenix.CodeReloader.Server do
:noop
end
end
+
+ # TODO: remove once we depend on Elixir 1.17
+ defp with_logger_app(config, fun) do
+ app = Keyword.fetch!(config, :app)
+ logger_config_app = Application.get_env(:logger, :compile_time_application)
+
+ try do
+ Logger.configure(compile_time_application: app)
+ fun.()
+ after
+ Logger.configure(compile_time_application: logger_config_app)
+ end
+ end
+
+ # TODO: remove once we depend on Elixir 1.18
+ if Code.ensure_loaded?(Mix.Project) and function_exported?(Mix.Project, :with_build_lock, 1) do
+ defp with_build_lock(fun), do: Mix.Project.with_build_lock(fun)
+ else
+ defp with_build_lock(fun), do: fun.()
+ end
end
diff --git a/lib/phoenix/config.ex b/lib/phoenix/config.ex
index fc9ed22ed9..e319610593 100644
--- a/lib/phoenix/config.ex
+++ b/lib/phoenix/config.ex
@@ -20,8 +20,8 @@ defmodule Phoenix.Config do
@doc """
Puts a given key-value pair in config.
"""
- def put_new(module, key, value) do
- :ets.insert_new(module, {key, value})
+ def put(module, key, value) do
+ :ets.insert(module, {key, value})
end
@doc """
@@ -152,6 +152,7 @@ defmodule Phoenix.Config do
{:stop, :normal, :ok, {module, permanent}}
true ->
+ clear_cache(module)
{:reply, :ok, {module, permanent}}
end
end
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 259123215a..b13ac956dc 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -504,7 +504,7 @@ defmodule Phoenix.Controller do
end
end
- @invalid_local_url_chars ["\\", "/%", "/\t"]
+ @invalid_local_url_chars ["\\", "/%09", "/\t"]
defp validate_local_url("//" <> _ = to), do: raise_invalid_url(to)
defp validate_local_url("/" <> _ = to) do
@@ -1033,7 +1033,7 @@ defmodule Phoenix.Controller do
defp assigns_layout(conn, _assigns, format) do
case conn.private[:phoenix_layout] do
%{^format => bad_value, _: good_value} when good_value != false ->
- IO.warn """
+ IO.warn("""
conflicting layouts found. A layout has been set with format, such as:
put_layout(conn, #{format}: #{inspect(bad_value)})
@@ -1043,12 +1043,13 @@ defmodule Phoenix.Controller do
put_layout(conn, #{inspect(good_value)})
In this case, the layout without format will always win.
+ Passing the layout without a format is currently soft-deprecated.
If you use layouts with formats, make sure that they are
used everywhere. Also remember to configure your controller
to use layouts with formats:
use Phoenix.Controller, layouts: [#{format}: #{inspect(bad_value)}]
- """
+ """)
if format in layout_formats(conn), do: good_value, else: false
@@ -1093,7 +1094,7 @@ defmodule Phoenix.Controller do
conn
else
content_type = content_type <> "; charset=utf-8"
- %Plug.Conn{conn | resp_headers: [{"content-type", content_type} | resp_headers]}
+ %{conn | resp_headers: [{"content-type", content_type} | resp_headers]}
end
end
@@ -1205,7 +1206,7 @@ defmodule Phoenix.Controller do
Defaults to none
* `:offset` - the bytes to offset when reading. Defaults to `0`
* `:length` - the total bytes to read. Defaults to `:all`
- * `:encode` - encodes the filename using `URI.encode_www_form/1`.
+ * `:encode` - encodes the filename using `URI.encode/2`.
Defaults to `true`. When `false`, disables encoding. If you
disable encoding, you need to guarantee there are no special
characters in the filename, such as quotes, newlines, etc.
@@ -1258,16 +1259,22 @@ defmodule Phoenix.Controller do
disposition_type = get_disposition_type(Keyword.get(opts, :disposition, :attachment))
warn_if_ajax(conn)
+ disposition = ~s[#{disposition_type}; filename="#{encoded_filename}"]
+
+ disposition =
+ if encoded_filename != filename do
+ disposition <> "; filename*=utf-8''#{encoded_filename}"
+ else
+ disposition
+ end
+
conn
|> put_resp_content_type(content_type, opts[:charset])
- |> put_resp_header(
- "content-disposition",
- ~s[#{disposition_type}; filename="#{encoded_filename}"]
- )
+ |> put_resp_header("content-disposition", disposition)
end
defp encode_filename(filename, false), do: filename
- defp encode_filename(filename, true), do: URI.encode_www_form(filename)
+ defp encode_filename(filename, true), do: URI.encode(filename, &URI.char_unreserved?/1)
defp get_disposition_type(:attachment), do: "attachment"
defp get_disposition_type(:inline), do: "inline"
@@ -1316,7 +1323,7 @@ defmodule Phoenix.Controller do
"""
@spec scrub_params(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
- def scrub_params(conn, required_key) when is_binary(required_key) do
+ def scrub_params(%Plug.Conn{} = conn, required_key) when is_binary(required_key) do
param = Map.get(conn.params, required_key) |> scrub_param()
unless param do
@@ -1324,7 +1331,7 @@ defmodule Phoenix.Controller do
end
params = Map.put(conn.params, required_key, param)
- %Plug.Conn{conn | params: params}
+ %{conn | params: params}
end
defp scrub_param(%{__struct__: mod} = struct) when is_atom(mod) do
diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex
index 26a93210d7..69b1b4fa84 100644
--- a/lib/phoenix/digester.ex
+++ b/lib/phoenix/digester.ex
@@ -194,7 +194,7 @@ defmodule Phoenix.Digester do
compressors = Application.fetch_env!(:phoenix, :static_compressors)
Enum.each(compressors, fn compressor ->
- [file_extension | _] = compressor.file_extensions
+ [file_extension | _] = compressor.file_extensions()
compressed_digested_result =
compressor.compress_file(file.digested_filename, file.digested_content)
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5514569702..a923be9a84 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -62,18 +62,11 @@ defmodule Phoenix.Endpoint do
YourAppWeb.Endpoint.config(:port)
YourAppWeb.Endpoint.config(:some_config, :default_value)
- ### Dynamic configuration
-
- For dynamically configuring the endpoint, such as loading data
- from environment variables or configuration files, Phoenix invokes
- the `c:init/2` callback on the endpoint, passing the atom `:supervisor`
- as the first argument and the endpoint configuration as second.
-
- All of Phoenix configuration, except the Compile-time configuration
- below can be set dynamically from the `c:init/2` callback.
-
### Compile-time configuration
+ Compile-time configuration may be set on `config/dev.exs`, `config/prod.exs`
+ and so on, but has no effect on `config/runtime.exs`:
+
* `:code_reloader` - when `true`, enables code reloading functionality.
For the list of code reloader configuration options see
`Phoenix.CodeReloader.reload/1`. Keep in mind code reloading is
@@ -99,6 +92,12 @@ defmodule Phoenix.Endpoint do
### Runtime configuration
+ The configuration below may be set on `config/dev.exs`, `config/prod.exs`
+ and so on, as well as on `config/runtime.exs`. Typically, if you need to
+ configure them with system environment variables, you set them in
+ `config/runtime.exs`. These options may also be set when starting the
+ endpoint in your supervision tree, such as `{MyApp.Endpoint, options}`.
+
* `:adapter` - which webserver adapter to use for serving web requests.
See the "Adapter configuration" section below
@@ -182,6 +181,8 @@ defmodule Phoenix.Endpoint do
[another: {Mod, :fun, [arg1, arg2]}]
+ When `false`, watchers can be disabled.
+
* `:force_watchers` - when `true`, forces your watchers to start
even when the `:server` option is set to `false`.
@@ -192,7 +193,7 @@ defmodule Phoenix.Endpoint do
live_reload: [
url: "ws://localhost:4000",
patterns: [
- ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
+ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$",
~r"lib/app_web/(live|views)/.*(ex)$",
~r"lib/app_web/templates/.*(eex)$"
]
@@ -220,17 +221,28 @@ defmodule Phoenix.Endpoint do
### Adapter configuration
- Phoenix allows you to choose which webserver adapter to use. The default
- is `Phoenix.Endpoint.Cowboy2Adapter` which can be configured via the
- following top-level options.
+ Phoenix allows you to choose which webserver adapter to use. Newly generated
+ applications created via the `phx.new` Mix task use the
+ [`Bandit`](https://github.com/mtrudel/bandit) webserver via the
+ `Bandit.PhoenixAdapter` adapter. If not otherwise specified via the `adapter`
+ option Phoenix will fall back to the `Phoenix.Endpoint.Cowboy2Adapter` for
+ backwards compatibility with applications generated prior to Phoenix 1.7.8.
+
+ Both adapters can be configured in a similar manner using the following two
+ top-level options:
* `:http` - the configuration for the HTTP server. It accepts all options
- as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/). Defaults
- to `false`
+ as defined by either [`Bandit`](https://hexdocs.pm/bandit/Bandit.html#t:options/0)
+ or [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/) depending on your
+ choice of adapter. Defaults to `false`
* `:https` - the configuration for the HTTPS server. It accepts all options
- as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/). Defaults
- to `false`
+ as defined by either [`Bandit`](https://hexdocs.pm/bandit/Bandit.html#t:options/0)
+ or [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/) depending on your
+ choice of adapter. Defaults to `false`
+
+ In addition, the connection draining can be configured for the Cowboy webserver via the following
+ top-level option (this is not required for Bandit as it has connection draining built-in):
* `:drainer` - a drainer process waits for any on-going request to finish
during application shutdown. It accepts the `:shutdown` and
@@ -251,6 +263,9 @@ defmodule Phoenix.Endpoint do
* for handling paths and URLs: `c:struct_url/0`, `c:url/0`, `c:path/1`,
`c:static_url/0`,`c:static_path/1`, and `c:static_integrity/1`
+ * for gathering runtime information about the address and port the
+ endpoint is running on: `c:server_info/1`
+
* for broadcasting to channels: `c:broadcast/3`, `c:broadcast!/3`,
`c:broadcast_from/4`, `c:broadcast_from!/4`, `c:local_broadcast/3`,
and `c:local_broadcast_from/4`
@@ -261,8 +276,8 @@ defmodule Phoenix.Endpoint do
"""
- @type topic :: String.t
- @type event :: String.t
+ @type topic :: String.t()
+ @type event :: String.t()
@type msg :: map | {:binary, binary}
require Logger
@@ -275,7 +290,7 @@ defmodule Phoenix.Endpoint do
Starts endpoint's configuration cache and possibly the servers for
handling requests.
"""
- @callback start_link(keyword) :: Supervisor.on_start
+ @callback start_link(keyword) :: Supervisor.on_start()
@doc """
Access the endpoint configuration given by key.
@@ -287,60 +302,61 @@ defmodule Phoenix.Endpoint do
"""
@callback config_change(changed :: term, removed :: term) :: term
- @doc """
- Initialize the endpoint configuration.
-
- Invoked when the endpoint supervisor starts, allows dynamically
- configuring the endpoint from system environment or other runtime sources.
- """
- @callback init(:supervisor, config :: Keyword.t) :: {:ok, Keyword.t}
-
# Paths and URLs
@doc """
Generates the endpoint base URL, but as a `URI` struct.
"""
- @callback struct_url() :: URI.t
+ @callback struct_url() :: URI.t()
@doc """
Generates the endpoint base URL without any path information.
"""
- @callback url() :: String.t
+ @callback url() :: String.t()
@doc """
Generates the path information when routing to this endpoint.
"""
- @callback path(path :: String.t) :: String.t
+ @callback path(path :: String.t()) :: String.t()
@doc """
Generates the static URL without any path information.
"""
- @callback static_url() :: String.t
+ @callback static_url() :: String.t()
@doc """
Generates a route to a static file in `priv/static`.
"""
- @callback static_path(path :: String.t) :: String.t
+ @callback static_path(path :: String.t()) :: String.t()
@doc """
Generates an integrity hash to a static file in `priv/static`.
"""
- @callback static_integrity(path :: String.t) :: String.t | nil
+ @callback static_integrity(path :: String.t()) :: String.t() | nil
@doc """
Generates a two item tuple containing the `static_path` and `static_integrity`.
"""
- @callback static_lookup(path :: String.t) :: {String.t, String.t} | {String.t, nil}
+ @callback static_lookup(path :: String.t()) :: {String.t(), String.t()} | {String.t(), nil}
@doc """
Returns the script name from the :url configuration.
"""
- @callback script_name() :: [String.t]
+ @callback script_name() :: [String.t()]
@doc """
Returns the host from the :url configuration.
"""
- @callback host() :: String.t
+ @callback host() :: String.t()
+
+ # Server information
+
+ @doc """
+ Returns the address and port that the server is running on
+ """
+ @callback server_info(Plug.Conn.scheme()) ::
+ {:ok, {:inet.ip_address(), :inet.port_number()} | :inet.returned_non_ip_address()}
+ | {:error, term()}
# Channels
@@ -349,7 +365,7 @@ defmodule Phoenix.Endpoint do
See `Phoenix.PubSub.subscribe/3` for options.
"""
- @callback subscribe(topic, opts :: Keyword.t) :: :ok | {:error, term}
+ @callback subscribe(topic, opts :: Keyword.t()) :: :ok | {:error, term}
@doc """
Unsubscribes the caller from the given topic.
@@ -404,19 +420,12 @@ defmodule Phoenix.Endpoint do
defp config(opts) do
quote do
- @otp_app unquote(opts)[:otp_app] || raise "endpoint expects :otp_app to be given"
+ @otp_app unquote(opts)[:otp_app] || raise("endpoint expects :otp_app to be given")
var!(config) = Phoenix.Endpoint.Supervisor.config(@otp_app, __MODULE__)
var!(code_reloading?) = var!(config)[:code_reloader]
# Avoid unused variable warnings
_ = var!(code_reloading?)
-
- @doc false
- def init(_key, config) do
- {:ok, config}
- end
-
- defoverridable init: 2
end
end
@@ -478,7 +487,8 @@ defmodule Phoenix.Endpoint do
banner: {Phoenix.Endpoint.RenderErrors, :__debugger_banner__, []},
style: [
primary: "#EB532D",
- logo: "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
+ logo:
+ "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K"
]
end
@@ -579,17 +589,11 @@ defmodule Phoenix.Endpoint do
Generates a route to a static file in `priv/static`.
"""
def static_path(path) do
- {path, fragment} = path_and_fragment(path)
-
- persistent!().static_path <> elem(static_lookup(path), 0) <> fragment
- end
+ prefix = persistent!().static_path
- defp path_and_fragment(path_incl_fragment) do
- path_incl_fragment
- |> String.split("#", parts: 2)
- |> case do
- [path, fragment] -> {path, "#" <> fragment}
- [path | _] -> {path, ""}
+ case :binary.split(path, "#") do
+ [path, fragment] -> prefix <> elem(static_lookup(path), 0) <> "#" <> fragment
+ [path] -> prefix <> elem(static_lookup(path), 0)
end
end
@@ -604,9 +608,17 @@ defmodule Phoenix.Endpoint do
and the second item being the `static_integrity`.
"""
def static_lookup(path) do
- Phoenix.Config.cache(__MODULE__, {:__phoenix_static__, path},
- &Phoenix.Endpoint.Supervisor.static_lookup(&1, path))
+ Phoenix.Config.cache(
+ __MODULE__,
+ {:__phoenix_static__, path},
+ &Phoenix.Endpoint.Supervisor.static_lookup(&1, path)
+ )
end
+
+ @doc """
+ Returns the address and port that the server is running on
+ """
+ def server_info(scheme), do: config(:adapter).server_info(__MODULE__, scheme)
end
end
@@ -632,7 +644,7 @@ defmodule Phoenix.Endpoint do
end
quote do
- defoverridable [call: 2]
+ defoverridable call: 2
# Inline render errors so we set the endpoint before calling it.
def call(conn, opts) do
@@ -644,11 +656,25 @@ defmodule Phoenix.Endpoint do
rescue
e in Plug.Conn.WrapperError ->
%{conn: conn, kind: kind, reason: reason, stack: stack} = e
- Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, config(:render_errors))
+
+ Phoenix.Endpoint.RenderErrors.__catch__(
+ conn,
+ kind,
+ reason,
+ stack,
+ config(:render_errors)
+ )
catch
kind, reason ->
stack = __STACKTRACE__
- Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, config(:render_errors))
+
+ Phoenix.Endpoint.RenderErrors.__catch__(
+ conn,
+ kind,
+ reason,
+ stack,
+ config(:render_errors)
+ )
end
end
@@ -698,23 +724,23 @@ defmodule Phoenix.Endpoint do
|> Enum.join("/")
|> Plug.Router.Utils.build_path_match()
- conn_ast =
- if vars == [] do
- quote do
- conn
- end
- else
- params =
- for var <- vars,
- param = Atom.to_string(var),
- not match?("_" <> _, param),
- do: {param, Macro.var(var, nil)}
-
- quote do
- params = %{unquote_splicing(params)}
- %Plug.Conn{conn | path_params: params, params: params}
- end
+ conn_ast =
+ if vars == [] do
+ quote do
+ conn
end
+ else
+ params =
+ for var <- vars,
+ param = Atom.to_string(var),
+ not match?("_" <> _, param),
+ do: {param, Macro.var(var, nil)}
+
+ quote do
+ params = %{unquote_splicing(params)}
+ %Plug.Conn{conn | path_params: params, params: params}
+ end
+ end
{conn_ast, path}
end
@@ -722,7 +748,13 @@ defmodule Phoenix.Endpoint do
## API
@doc """
- Defines a websocket/longpoll mount-point for a socket.
+ Defines a websocket/longpoll mount-point for a `socket`.
+
+ It expects a `path`, a `socket` module, and a set of options.
+ The socket module is typically defined with `Phoenix.Socket`.
+
+ Both websocket and longpolling connections are supported out
+ of the box.
## Options
@@ -738,10 +770,32 @@ defmodule Phoenix.Endpoint do
and ["Longpoll configuration"](#socket/3-longpoll-configuration)
for the whole list
- If your socket is implemented using `Phoenix.Socket`,
- you can also pass to each transport above all options
- accepted on `use Phoenix.Socket`. An option given here
- will override the value in `use Phoenix.Socket`.
+ * `:drainer` - a keyword list or a custom MFA function returning a keyword list, for example:
+
+ {MyAppWeb.Socket, :drainer_configuration, []}
+
+ configuring how to drain sockets on application shutdown.
+ The goal is to notify all channels (and
+ LiveViews) clients to reconnect. The supported options are:
+
+ * `:batch_size` - How many clients to notify at once in a given batch.
+ Defaults to 10000.
+ * `:batch_interval` - The amount of time in milliseconds given for a
+ batch to terminate. Defaults to 2000ms.
+ * `:shutdown` - The maximum amount of time in milliseconds allowed
+ to drain all batches. Defaults to 30000ms.
+
+ For example, if you have 150k connections, the default values will
+ split them into 15 batches of 10k connections. Each batch takes
+ 2000ms before the next batch starts. In this case, we will do everything
+ right under the maximum shutdown time of 30000ms. Therefore, as
+ you increase the number of connections, remember to adjust the shutdown
+ accordingly. Finally, after the socket drainer runs, the lower level
+ HTTP/HTTPS connection drainer will still run, and apply to all connections.
+ Set it to `false` to disable draining.
+
+ You can also pass the options below on `use Phoenix.Socket`.
+ The values specified here override the value in `use Phoenix.Socket`.
## Examples
@@ -780,8 +834,9 @@ defmodule Phoenix.Endpoint do
If `true`, the header is checked against `:host` in `YourAppWeb.Endpoint.config(:url)[:host]`.
- If `false`, your app is vulnerable to Cross-Site WebSocket Hijacking (CSWSH)
- attacks. Only use in development, when the host is truly unknown or when
+ If `false` and you do not validate the session in your socket, your app
+ is vulnerable to Cross-Site WebSocket Hijacking (CSWSH) attacks.
+ Only use in development, when the host is truly unknown or when
serving clients that do not send the `origin` header, such as mobile apps.
You can also specify a list of explicitly allowed origins. Wildcards are
@@ -804,29 +859,16 @@ defmodule Phoenix.Endpoint do
The MFA is invoked with the request `%URI{}` as the first argument,
followed by arguments in the MFA list, and must return a boolean.
+ * `:check_csrf` - if the transport should perform CSRF check. To avoid
+ "Cross-Site WebSocket Hijacking", you must have at least one of
+ `check_origin` and `check_csrf` enabled. If you set both to `false`,
+ Phoenix will raise, but it is still possible to disable both by passing
+ a custom MFA to `check_origin`. In such cases, it is your responsibility
+ to ensure at least one of them is enabled. Defaults to `true`
+
* `:code_reloader` - enable or disable the code reloader. Defaults to your
endpoint configuration
- * `:drainer` - a keyword list configuring how to drain sockets
- on application shutdown. The goal is to notify all channels (and
- LiveViews) clients to reconnect. The supported options are:
-
- * `:batch_size` - How many clients to notify at once in a given batch.
- Defaults to 10000.
- * `:batch_interval` - The amount of time in milliseconds given for a
- batch to terminate. Defaults to 2000ms.
- * `:shutdown` - The maximum amount of time in milliseconds allowed
- to drain all batches. Defaults to 30000ms.
-
- For example, if you have 150k connections, the default values will
- split them into 15 batches of 10k connections. Each batch takes
- 2000ms before the next batch starts. In this case, we will do everything
- right under the maximum shutdown time of 30000ms. Therefore, as
- you increase the number of connections, remember to adjust the shutdown
- accordingly. Finally, after the socket drainer runs, the lower level
- HTTP/HTTPS connection drainer will still run, and apply to all connections.
- Set it to `false` to disable draining.
-
* `:connect_info` - a list of keys that represent data to be copied from
the transport to be made available in the user socket `connect/3` callback.
See the "Connect info" subsection for valid keys
@@ -839,9 +881,9 @@ defmodule Phoenix.Endpoint do
* `:trace_context_headers` - a list of all trace context headers. Supported
headers are defined by the [W3C Trace Context Specification](https://www.w3.org/TR/trace-context-1/).
- These headers are necessary for libraries such as [OpenTelemetry](https://opentelemetry.io/) to extract
- trace propagation information to know this request is part of a larger trace
- in progress.
+ These headers are necessary for libraries such as [OpenTelemetry](https://opentelemetry.io/)
+ to extract trace propagation information to know this request is part of a
+ larger trace in progress.
* `:x_headers` - all request headers that have an "x-" prefix
@@ -882,6 +924,22 @@ defmodule Phoenix.Endpoint do
]
```
+ > #### Where are my headers? {: .tip}
+ >
+ > Phoenix only gives you limited access to the connection headers for security
+ > reasons. WebSockets are cross-domain, which means that, when a user "John Doe"
+ > visits a malicious website, the malicious website can open up a WebSocket
+ > connection to your application, and the browser will gladly submit John Doe's
+ > authentication/cookie information. If you were to accept this information as is,
+ > the malicious website would have full control of a WebSocket connection to your
+ > application, authenticated on John Doe's behalf.
+ >
+ > To safe-guard your application, Phoenix limits and validates the connection
+ > information your socket can access. This means your application is safe from
+ > these attacks, but you can't access cookies and other headers in your socket.
+ > You may access the session stored in the connection via the `:connect_info`
+ > option, provided you also pass a csrf token when connecting over WebSocket.
+
## 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 84117a9c7a..96482b95aa 100644
--- a/lib/phoenix/endpoint/cowboy2_adapter.ex
+++ b/lib/phoenix/endpoint/cowboy2_adapter.ex
@@ -68,7 +68,7 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
{refs, child_specs} = Enum.unzip(refs_and_specs)
- if drainer = (refs != [] && Keyword.get(config, :drainer, [])) do
+ if drainer = refs != [] && Keyword.get(config, :drainer, []) do
child_specs ++ [{Plug.Cowboy.Drainer, Keyword.put_new(drainer, :refs, refs)}]
else
child_specs
@@ -80,7 +80,7 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
Application.ensure_all_started(:ssl)
end
- ref = Module.concat(endpoint, scheme |> Atom.to_string() |> String.upcase())
+ ref = make_ref(endpoint, scheme)
plug =
if code_reloader? do
@@ -118,7 +118,7 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
defp info(scheme, endpoint, ref) do
server = "cowboy #{Application.spec(:cowboy)[:vsn]}"
- "Running #{inspect endpoint} with #{server} at #{bound_address(scheme, ref)}"
+ "Running #{inspect(endpoint)} with #{server} at #{bound_address(scheme, ref)}"
end
defp bound_address(scheme, ref) do
@@ -137,4 +137,19 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
defp port_to_integer({:system, env_var}), do: port_to_integer(System.get_env(env_var))
defp port_to_integer(port) when is_binary(port), do: String.to_integer(port)
defp port_to_integer(port) when is_integer(port), do: port
+
+ def server_info(endpoint, scheme) do
+ address =
+ endpoint
+ |> make_ref(scheme)
+ |> :ranch.get_addr()
+
+ {:ok, address}
+ rescue
+ e -> {:error, Exception.message(e)}
+ end
+
+ defp make_ref(endpoint, scheme) do
+ Module.concat(endpoint, scheme |> Atom.to_string() |> String.upcase())
+ end
end
diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex
index b4241dd8e1..0e0d494bff 100644
--- a/lib/phoenix/endpoint/render_errors.ex
+++ b/lib/phoenix/endpoint/render_errors.ex
@@ -52,12 +52,12 @@ defmodule Phoenix.Endpoint.RenderErrors do
end
@doc false
- def __catch__(conn, kind, reason, stack, opts) do
+ def __catch__(%Plug.Conn{} = conn, kind, reason, stack, opts) do
conn =
receive do
@already_sent ->
send(self(), @already_sent)
- %Plug.Conn{conn | state: :sent}
+ %{conn | state: :sent}
after
0 ->
instrument_render_and_send(conn, kind, reason, stack, opts)
@@ -126,13 +126,13 @@ defmodule Phoenix.Endpoint.RenderErrors do
Controller.render(conn, template, assigns)
end
- defp maybe_fetch_query_params(conn) do
+ defp maybe_fetch_query_params(%Plug.Conn{} = conn) do
fetch_query_params(conn)
rescue
Plug.Conn.InvalidQueryError ->
case conn.params do
- %Plug.Conn.Unfetched{} -> %Plug.Conn{conn | query_params: %{}, params: %{}}
- params -> %Plug.Conn{conn | query_params: %{}, params: params}
+ %Plug.Conn.Unfetched{} -> %{conn | query_params: %{}, params: %{}}
+ params -> %{conn | query_params: %{}, params: params}
end
end
@@ -151,7 +151,7 @@ defmodule Phoenix.Endpoint.RenderErrors do
true ->
raise ArgumentError,
- "expected :render_errors to have :formats or :view/:accept, but got: #{inspect(opts)}"
+ "expected :render_errors to have :formats or :view/:accepts, but got: #{inspect(opts)}"
end
end
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index d1e66a113f..597f175d90 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -31,19 +31,25 @@ defmodule Phoenix.Endpoint.Supervisor do
env_conf = config(otp_app, mod, default_conf)
secret_conf =
- case mod.init(:supervisor, env_conf) do
- {:ok, init_conf} ->
- if is_nil(Application.get_env(otp_app, mod)) and init_conf == env_conf do
- Logger.warning(
- "no configuration found for otp_app #{inspect(otp_app)} and module #{inspect(mod)}"
- )
- end
+ cond do
+ Code.ensure_loaded?(mod) and function_exported?(mod, :init, 2) ->
+ IO.warn(
+ "#{inspect(mod)}.init/2 is deprecated, use config/runtime.exs instead " <>
+ "or pass additional options when starting the endpoint in your supervision tree"
+ )
+ {:ok, init_conf} = mod.init(:supervisor, env_conf)
init_conf
- other ->
- raise ArgumentError,
- "expected init/2 callback to return {:ok, config}, got: #{inspect(other)}"
+ is_nil(Application.get_env(otp_app, mod)) ->
+ Logger.warning(
+ "no configuration found for otp_app #{inspect(otp_app)} and module #{inspect(mod)}"
+ )
+
+ env_conf
+
+ true ->
+ env_conf
end
extra_conf = [
@@ -78,11 +84,10 @@ defmodule Phoenix.Endpoint.Supervisor do
children =
config_children(mod, secret_conf, default_conf) ++
pubsub_children(mod, conf) ++
- socket_children(mod, :child_spec) ++
+ socket_children(mod, conf, :child_spec) ++
server_children(mod, conf, server?) ++
- socket_children(mod, :drainer_spec) ++
+ socket_children(mod, conf, :drainer_spec) ++
watcher_children(mod, conf, server?)
-
Supervisor.init(children, strategy: :one_for_one)
end
@@ -112,8 +117,9 @@ defmodule Phoenix.Endpoint.Supervisor do
end
end
- defp socket_children(endpoint, fun) do
- for {_, socket, opts} <- Enum.uniq_by(endpoint.__sockets__, &elem(&1, 1)),
+ defp socket_children(endpoint, conf, fun) do
+ for {_, socket, opts} <- Enum.uniq_by(endpoint.__sockets__(), &elem(&1, 1)),
+ _ = check_origin_or_csrf_checked!(conf, opts),
spec = apply_or_ignore(socket, fun, [[endpoint: endpoint] ++ opts]),
spec != :ignore do
spec
@@ -129,6 +135,22 @@ defmodule Phoenix.Endpoint.Supervisor do
end
end
+ defp check_origin_or_csrf_checked!(endpoint_conf, socket_opts) do
+ check_origin = endpoint_conf[:check_origin]
+
+ for {transport, transport_opts} <- socket_opts, is_list(transport_opts) do
+ check_origin = Keyword.get(transport_opts, :check_origin, check_origin)
+
+ check_csrf = transport_opts[:check_csrf]
+
+ if check_origin == false and check_csrf == false do
+ raise ArgumentError,
+ "one of :check_origin and :check_csrf must be set to non-false value for " <>
+ "transport #{inspect(transport)}"
+ end
+ end
+ end
+
defp config_children(mod, conf, default_conf) do
args = {mod, conf, default_conf, name: Module.concat(mod, "Config")}
[{Phoenix.Config, args}]
@@ -137,7 +159,7 @@ defmodule Phoenix.Endpoint.Supervisor do
defp server_children(mod, config, server?) do
cond do
server? ->
- adapter = config[:adapter] || Phoenix.Endpoint.Cowboy2Adapter
+ adapter = config[:adapter]
adapter.child_specs(mod, config)
config[:http] || config[:https] ->
@@ -155,8 +177,10 @@ defmodule Phoenix.Endpoint.Supervisor do
end
defp watcher_children(_mod, conf, server?) do
+ watchers = conf[:watchers] || []
+
if server? || conf[:force_watchers] do
- Enum.map(conf[:watchers], &{Phoenix.Endpoint.Watcher, &1})
+ Enum.map(watchers, &{Phoenix.Endpoint.Watcher, &1})
else
[]
end
@@ -196,6 +220,11 @@ defmodule Phoenix.Endpoint.Supervisor do
render_errors: [view: render_errors(module), accepts: ~w(html), layout: false],
# Runtime config
+
+ # Even though Bandit is the default in apps generated via the installer,
+ # we continue to use Cowboy as the default if not explicitly specified for
+ # backwards compatibility. TODO: Change this to default to Bandit in 2.0
+ adapter: Phoenix.Endpoint.Cowboy2Adapter,
cache_static_manifest: nil,
check_origin: true,
http: false,
@@ -370,7 +399,7 @@ defmodule Phoenix.Endpoint.Supervisor do
end
defp warmup_static(endpoint, %{"latest" => latest, "digests" => digests}) do
- Phoenix.Config.put_new(endpoint, :cache_static_manifest_latest, latest)
+ Phoenix.Config.put(endpoint, :cache_static_manifest_latest, latest)
with_vsn? = !endpoint.config(:cache_manifest_skip_vsn)
Enum.each(latest, fn {key, _} ->
diff --git a/lib/phoenix/endpoint/sync_code_reload_plug.ex b/lib/phoenix/endpoint/sync_code_reload_plug.ex
index 6b0113e0e7..642cdf3dfc 100644
--- a/lib/phoenix/endpoint/sync_code_reload_plug.ex
+++ b/lib/phoenix/endpoint/sync_code_reload_plug.ex
@@ -1,11 +1,11 @@
defmodule Phoenix.Endpoint.SyncCodeReloadPlug do
@moduledoc ~S"""
Wraps an Endpoint, attempting to sync with Phoenix's code reloader if
- an exception is raising which indicates that we may be in the middle of a reload.
+ an exception is raised which indicates that we may be in the middle of a reload.
We detect this by looking at the raised exception and seeing if it indicates
that the endpoint is not defined. This indicates that the code reloader may be
- mid way through a compile, and that we should attempt to retry the request
+ midway through a compile, and that we should attempt to retry the request
after the compile has completed. This is also why this must be implemented in
a separate module (one that is not recompiled in a typical code reload cycle),
since otherwise it may be the case that the endpoint itself is not defined.
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index 9375573e15..6a19b6a532 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -230,7 +230,7 @@ defmodule Phoenix.Logger do
level ->
Logger.log(level, fn ->
%{status: status, state: state} = conn
- status = Integer.to_string(status)
+ status = status_to_string(status)
[connection_type(state), ?\s, status, " in ", duration(duration)]
end)
end
@@ -252,12 +252,16 @@ defmodule Phoenix.Logger do
?\s,
error_banner(kind, reason),
" to ",
- Integer.to_string(status),
+ status_to_string(status),
" response"
]
end)
end
+ defp status_to_string(status) do
+ status |> Plug.Conn.Status.code() |> Integer.to_string()
+ end
+
defp error_banner(:error, %type{}), do: inspect(type)
defp error_banner(_kind, reason), do: inspect(reason)
@@ -269,7 +273,6 @@ defmodule Phoenix.Logger do
def phoenix_router_dispatch_start(_, _, metadata, _) do
%{log: level, conn: conn, plug: plug} = metadata
level = log_level(level, conn)
- log_module = metadata[:log_module] || plug
Logger.log(level, fn ->
%{
@@ -277,10 +280,16 @@ defmodule Phoenix.Logger do
plug_opts: plug_opts
} = metadata
+ log_mfa =
+ case metadata[:mfa] do
+ {mod, fun, arity} -> mfa(mod, fun, arity)
+ _ when is_atom(plug_opts) -> mfa(plug, plug_opts, 2)
+ _ -> inspect(plug)
+ end
+
[
"Processing with ",
- inspect(log_module),
- maybe_action(plug_opts),
+ log_mfa,
?\n,
" Parameters: ",
params(conn.params),
@@ -291,8 +300,8 @@ defmodule Phoenix.Logger do
end)
end
- defp maybe_action(action) when is_atom(action), do: [?., Atom.to_string(action), ?/, ?2]
- defp maybe_action(_), do: []
+ defp mfa(mod, fun, arity),
+ do: [inspect(mod), ?., Atom.to_string(fun), ?/, arity + ?0]
defp params(%Plug.Conn.Unfetched{}), do: "[UNFETCHED]"
defp params(params), do: params |> filter_values() |> inspect()
diff --git a/lib/phoenix/param.ex b/lib/phoenix/param.ex
index 7ccd2b7674..34a5d70d51 100644
--- a/lib/phoenix/param.ex
+++ b/lib/phoenix/param.ex
@@ -1,25 +1,34 @@
defprotocol Phoenix.Param do
- @moduledoc """
+ @moduledoc ~S"""
A protocol that converts data structures into URL parameters.
- This protocol is used by URL helpers and other parts of the
+ This protocol is used by `Phoenix.VerifiedRoutes` and other parts of the
Phoenix stack. For example, when you write:
- user_path(conn, :edit, @user)
+ ~p"/user/#{@user}/edit"
Phoenix knows how to extract the `:id` from `@user` thanks
to this protocol.
+ (Deprecated URL helpers, e.g. `user_path(conn, :edit, @user)`, work the
+ same way.)
+
By default, Phoenix implements this protocol for integers, binaries, atoms,
and structs. For structs, a key `:id` is assumed, but you may provide a
specific implementation.
- Nil values cannot be converted to param.
+ The term `nil` cannot be converted to param.
## Custom parameters
In order to customize the parameter for any struct,
- one can simply implement this protocol.
+ one can simply implement this protocol. For example for a `Date` struct:
+
+ defimpl Phoenix.Param, for: Date do
+ def to_param(date) do
+ Date.to_string(date)
+ end
+ end
However, for convenience, this protocol can also be
derivable. For example:
@@ -50,7 +59,7 @@ defprotocol Phoenix.Param do
@fallback_to_any true
- @spec to_param(term) :: String.t
+ @spec to_param(term) :: String.t()
def to_param(term)
end
@@ -79,7 +88,7 @@ end
defimpl Phoenix.Param, for: Map do
def to_param(map) do
raise ArgumentError,
- "maps cannot be converted to_param. A struct was expected, got: #{inspect map}"
+ "maps cannot be converted to_param. A struct was expected, got: #{inspect(map)}"
end
end
@@ -88,16 +97,18 @@ defimpl Phoenix.Param, for: Any do
key = Keyword.get(options, :key, :id)
unless Map.has_key?(struct, key) do
- raise ArgumentError, "cannot derive Phoenix.Param for struct #{inspect module} " <>
- "because it does not have key #{inspect key}. Please pass " <>
- "the :key option when deriving"
+ raise ArgumentError,
+ "cannot derive Phoenix.Param for struct #{inspect(module)} " <>
+ "because it does not have key #{inspect(key)}. Please pass " <>
+ "the :key option when deriving"
end
quote do
defimpl Phoenix.Param, for: unquote(module) do
def to_param(%{unquote(key) => nil}) do
- raise ArgumentError, "cannot convert #{inspect unquote(module)} to param, " <>
- "key #{inspect unquote(key)} contains a nil value"
+ raise ArgumentError,
+ "cannot convert #{inspect(unquote(module))} to param, " <>
+ "key #{inspect(unquote(key))} contains a nil value"
end
def to_param(%{unquote(key) => key}) when is_integer(key), do: Integer.to_string(key)
@@ -110,15 +121,16 @@ defimpl Phoenix.Param, for: Any do
def to_param(%{id: nil}) do
raise ArgumentError, "cannot convert struct to param, key :id contains a nil value"
end
+
def to_param(%{id: id}) when is_integer(id), do: Integer.to_string(id)
def to_param(%{id: id}) when is_binary(id), do: id
def to_param(%{id: id}), do: Phoenix.Param.to_param(id)
def to_param(map) when is_map(map) do
raise ArgumentError,
- "structs expect an :id key when converting to_param or a custom implementation " <>
- "of the Phoenix.Param protocol (read Phoenix.Param docs for more information), " <>
- "got: #{inspect map}"
+ "structs expect an :id key when converting to_param or a custom implementation " <>
+ "of the Phoenix.Param protocol (read Phoenix.Param docs for more information), " <>
+ "got: #{inspect(map)}"
end
def to_param(data) do
diff --git a/lib/phoenix/presence.ex b/lib/phoenix/presence.ex
index 87983cfa1f..ad81c106a8 100644
--- a/lib/phoenix/presence.ex
+++ b/lib/phoenix/presence.ex
@@ -284,7 +284,7 @@ defmodule Phoenix.Presence do
a `:phx_ref_prev` key will be present containing the previous
`:phx_ref` value.
"""
- @callback list(Phoenix.Socket.t() | topic) :: presences
+ @callback list(socket_or_topic :: Phoenix.Socket.t() | topic) :: presences
@doc """
Returns the map of presence metadata for a socket/topic-key pair.
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index bcc589db76..ad4683ddf4 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -6,12 +6,15 @@ defmodule Phoenix.Router do
defexception plug_status: 404, message: "no route found", conn: nil, router: nil
def exception(opts) do
- conn = Keyword.fetch!(opts, :conn)
+ conn = Keyword.fetch!(opts, :conn)
router = Keyword.fetch!(opts, :router)
- path = "/" <> Enum.join(conn.path_info, "/")
+ path = "/" <> Enum.join(conn.path_info, "/")
- %NoRouteError{message: "no route found for #{conn.method} #{path} (#{inspect router})",
- conn: conn, router: router}
+ %NoRouteError{
+ message: "no route found for #{conn.method} #{path} (#{inspect(router)})",
+ conn: conn,
+ router: router
+ }
end
end
@@ -100,6 +103,30 @@ defmodule Phoenix.Router do
GET /pages/hey/there/world
%{"page" => "y", "rest" => ["there" "world"]} = params
+ > #### Why the macros? {: .info}
+ >
+ > Phoenix does its best to keep the usage of macros low. You may have noticed,
+ > however, that the `Phoenix.Router` relies heavily on macros. Why is that?
+ >
+ > We use `get`, `post`, `put`, and `delete` to define your routes. We use macros
+ > for two purposes:
+ >
+ > * They define the routing engine, used on every request, to choose which
+ > controller to dispatch the request to. Thanks to macros, Phoenix compiles
+ > all of your routes to a single case-statement with pattern matching rules,
+ > which is heavily optimized by the Erlang VM
+ >
+ > * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`.
+ > As we will soon learn, verified routes allows to us to reference any route
+ > as if it is a plain looking string, except it is verified by the compiler
+ > to be valid (making it much harder to ship broken links, forms, mails, etc
+ > to production)
+ >
+ > In other words, the router relies on macros to build applications that are
+ > faster and safer. Also remember that macros in Elixir are compile-time only,
+ > which gives plenty of stability after the code is compiled. Phoenix also provides
+ > introspection for all defined routes via `mix phx.routes`.
+
## Generating routes
For generating routes inside your application, see the `Phoenix.VerifiedRoutes`
@@ -107,9 +134,9 @@ defmodule Phoenix.Router do
generate route paths and URLs with compile-time verification.
Phoenix also supports generating function helpers, which was the default
- mechanism in Phoenix v1.6 and earlier. we will explore it next.
+ mechanism in Phoenix v1.6 and earlier. We will explore it next.
- ### Helpers
+ ### Helpers (deprecated)
Phoenix generates a module `Helpers` inside your router by default, which contains
named helpers to help developers generate and keep their routes up to date.
@@ -183,15 +210,15 @@ defmodule Phoenix.Router do
Scopes allow us to scope on any path or even on the helper name:
- scope "/v1", MyAppWeb, host: "api." do
+ scope "/api/v1", MyAppWeb, as: :api_v1 do
get "/pages/:id", PageController, :show
end
For example, the route above will match on the path `"/api/v1/pages/1"`
- and the named route will be `api_v1_page_path`, as expected from the
- values given to `scope/2` option.
+ and the named helper will be `api_v1_page_path`, as expected from the
+ values given to `scope/4` option.
- Like all paths you can define dynamic segments that will be applied as
+ Like all paths, you can define dynamic segments that will be applied as
parameters in the controller:
scope "/api/:version", MyAppWeb do
@@ -277,7 +304,9 @@ defmodule Phoenix.Router do
Perhaps more importantly, it is also very common to define pipelines specific
to authentication and authorization. For example, you might have a pipeline
that requires all users are authenticated. Another pipeline may enforce only
- admin users can access certain routes.
+ admin users can access certain routes. Since routes are matched top to bottom,
+ it is recommended to place the authenticated/authorized routes before the
+ less restricted routes to ensure they are matched first.
Once your pipelines are defined, you reuse the pipelines in the desired
scopes, grouping your routes around their pipelines. For example, imagine
@@ -294,17 +323,17 @@ defmodule Phoenix.Router do
end
scope "/" do
- pipe_through [:browser]
+ pipe_through [:browser, :auth]
- get "/posts", PostController, :index
- get "/posts/:id", PostController, :show
+ get "/posts/new", PostController, :new
+ post "/posts", PostController, :create
end
scope "/" do
- pipe_through [:browser, :auth]
+ pipe_through [:browser]
- get "/posts/new", PostController, :new
- post "/posts", PostController, :create
+ get "/posts", PostController, :index
+ get "/posts/:id", PostController, :show
end
Note in the above how the routes are split across different scopes.
@@ -365,30 +394,52 @@ defmodule Phoenix.Router do
opts = resource.route
if resource.singleton do
- Enum.each resource.actions, fn
- :show -> get path, ctrl, :show, opts
- :new -> get path <> "/new", ctrl, :new, opts
- :edit -> get path <> "/edit", ctrl, :edit, opts
- :create -> post path, ctrl, :create, opts
- :delete -> delete path, ctrl, :delete, opts
- :update ->
+ Enum.each(resource.actions, fn
+ :show ->
+ get path, ctrl, :show, opts
+
+ :new ->
+ get path <> "/new", ctrl, :new, opts
+
+ :edit ->
+ get path <> "/edit", ctrl, :edit, opts
+
+ :create ->
+ post path, ctrl, :create, opts
+
+ :delete ->
+ delete path, ctrl, :delete, opts
+
+ :update ->
patch path, ctrl, :update, opts
- put path, ctrl, :update, Keyword.put(opts, :as, nil)
- end
+ put path, ctrl, :update, Keyword.put(opts, :as, nil)
+ end)
else
param = resource.param
- Enum.each resource.actions, fn
- :index -> get path, ctrl, :index, opts
- :show -> get path <> "/:" <> param, ctrl, :show, opts
- :new -> get path <> "/new", ctrl, :new, opts
- :edit -> get path <> "/:" <> param <> "/edit", ctrl, :edit, opts
- :create -> post path, ctrl, :create, opts
- :delete -> delete path <> "/:" <> param, ctrl, :delete, opts
- :update ->
+ Enum.each(resource.actions, fn
+ :index ->
+ get path, ctrl, :index, opts
+
+ :show ->
+ get path <> "/:" <> param, ctrl, :show, opts
+
+ :new ->
+ get path <> "/new", ctrl, :new, opts
+
+ :edit ->
+ get path <> "/:" <> param <> "/edit", ctrl, :edit, opts
+
+ :create ->
+ post path, ctrl, :create, opts
+
+ :delete ->
+ delete path <> "/:" <> param, ctrl, :delete, opts
+
+ :update ->
patch path <> "/:" <> param, ctrl, :update, opts
- put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)
- end
+ put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)
+ end)
end
end
end
@@ -397,7 +448,10 @@ defmodule Phoenix.Router do
@doc false
def __call__(
%{private: %{phoenix_router: router, phoenix_bypass: {router, pipes}}} = conn,
- metadata, prepare, pipeline, _
+ metadata,
+ prepare,
+ pipeline,
+ _
) do
conn = prepare.(conn, metadata)
@@ -470,13 +524,13 @@ defmodule Phoenix.Router do
def call(conn, _opts) do
%{method: method, path_info: path_info, host: host} = conn = prepare(conn)
+ # TODO: Remove try/catch on Elixir v1.13 as decode no longer raises
decoded =
- # TODO: Remove try/catch on Elixir v1.13 as decode no longer raises
try do
Enum.map(path_info, &URI.decode/1)
rescue
ArgumentError ->
- raise MalformedURIError, "malformed URI path: #{inspect conn.request_path}"
+ raise MalformedURIError, "malformed URI path: #{inspect(conn.request_path)}"
end
case __match_route__(decoded, method, host) do
@@ -488,7 +542,7 @@ defmodule Phoenix.Router do
end
end
- defoverridable [init: 1, call: 2]
+ defoverridable init: 1, call: 2
end
end
@@ -547,9 +601,12 @@ defmodule Phoenix.Router do
checks =
routes
- |> Enum.uniq_by(&{&1.line, &1.plug})
- |> Enum.map(fn %{line: line, plug: plug} ->
- quote line: line, do: _ = &unquote(plug).init/1
+ |> Enum.map(fn %{line: line, metadata: metadata, plug: plug} ->
+ {line, Map.get(metadata, :mfa, {plug, :init, 1})}
+ end)
+ |> Enum.uniq()
+ |> Enum.map(fn {line, {module, function, arity}} ->
+ quote line: line, do: _ = &(unquote(module).unquote(function) / unquote(arity))
end)
keys = [:verb, :path, :plug, :plug_opts, :helper, :metadata]
@@ -614,9 +671,9 @@ defmodule Phoenix.Router do
quote line: route.line do
def __match_route__(unquote(path), unquote(verb_match), unquote(host)) do
{unquote(build_metadata(route, path_params)),
- fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} -> unquote(prepare) end,
- &unquote(Macro.var(pipe_name, __MODULE__))/1,
- unquote(dispatch)}
+ fn var!(conn, :conn), %{path_params: var!(path_params, :conn)} ->
+ unquote(prepare)
+ end, &(unquote(Macro.var(pipe_name, __MODULE__)) / 1), unquote(dispatch)}
end
end
end
@@ -667,7 +724,7 @@ defmodule Phoenix.Router do
end
defp build_pipes(name, pipe_through) do
- plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
+ plugs = pipe_through |> Enum.reverse() |> Enum.map(&{&1, [], true})
opts = [init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug]
{conn, body} = Plug.Builder.compile(__ENV__, plugs, opts)
@@ -679,25 +736,29 @@ defmodule Phoenix.Router do
@doc """
Generates a route match based on an arbitrary HTTP method.
- Useful for defining routes not included in the builtin macros.
+ Useful for defining routes not included in the built-in macros.
The catch-all verb, `:*`, may also be used to match all HTTP methods.
## Options
- * `:as` - configures the named helper. If false, does not generate
+ * `:as` - configures the named helper. If `nil`, does not generate
a helper. Has no effect when using verified routes exclusively
* `:alias` - configure if the scope alias should be applied to the route.
Defaults to true, disables scoping if false.
- * `:log` - the level to log the route dispatching under,
- may be set to false. Defaults to `:debug`
+ * `:log` - the level to log the route dispatching under, may be set to false. Defaults to
+ `:debug`. Route dispatching contains information about how the route is handled (which controller
+ action is called, what parameters are available and which pipelines are used) and is separate from
+ the plug level logging. To alter the plug log level, please see
+ https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-dynamic-log-level.
* `:private` - a map of private data to merge into the connection
when a route matches
* `:assigns` - a map of data to merge into the connection when a route matches
* `:metadata` - a map of metadata used by the telemetry events and returned by
- `route_info/4`
+ `route_info/4`. The `:mfa` field is used by telemetry to print logs and by the
+ router to emit compile time checks. Custom fields may be added.
* `:warn_on_verify` - the boolean for whether matches to this route trigger
- an unmatched route warning for `Phoenix.VerifiedRoutes`. Useful to ignore
+ an unmatched route warning for `Phoenix.VerifiedRoutes`. It is useful to ignore
an otherwise catch-all route definition from being matched when verifying routes.
Defaults `false`.
@@ -728,15 +789,15 @@ defmodule Phoenix.Router do
defp add_route(kind, verb, path, plug, plug_opts, options) do
quote do
@phoenix_routes Scope.route(
- __ENV__.line,
- __ENV__.module,
- unquote(kind),
- unquote(verb),
- unquote(path),
- unquote(plug),
- unquote(plug_opts),
- unquote(options)
- )
+ __ENV__.line,
+ __ENV__.module,
+ unquote(kind),
+ unquote(verb),
+ unquote(path),
+ unquote(plug),
+ unquote(plug_opts),
+ unquote(options)
+ )
end
end
@@ -781,8 +842,9 @@ defmodule Phoenix.Router do
compiler =
quote unquote: false do
Scope.pipeline(__MODULE__, plug)
- {conn, body} = Plug.Builder.compile(__ENV__, @phoenix_pipeline,
- init_mode: Phoenix.plug_init_mode())
+
+ {conn, body} =
+ Plug.Builder.compile(__ENV__, @phoenix_pipeline, init_mode: Phoenix.plug_init_mode())
def unquote(plug)(unquote(conn), _) do
try do
@@ -795,6 +857,7 @@ defmodule Phoenix.Router do
Plug.Conn.WrapperError.reraise(unquote(conn), :error, reason, __STACKTRACE__)
end
end
+
@phoenix_pipeline nil
end
@@ -818,7 +881,7 @@ defmodule Phoenix.Router do
quote do
if pipeline = @phoenix_pipeline do
- @phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
+ @phoenix_pipeline [{unquote(plug), unquote(opts), true} | pipeline]
else
raise "cannot define plug at the router level, plug must be defined inside a pipeline"
end
@@ -907,7 +970,7 @@ defmodule Phoenix.Router do
and as the prefix for the parameter in nested resources. The default value
is automatically derived from the controller name, i.e. `UserController` will
have name `"user"`
- * `:as` - configures the named helper. If false, does not generate
+ * `:as` - configures the named helper. If `nil`, does not generate
a helper. Has no effect when using verified routes exclusively
* `:singleton` - defines routes for a singleton resource that is looked up by
the client without referencing an ID. Read below for more information
@@ -956,32 +1019,32 @@ defmodule Phoenix.Router do
"""
defmacro resources(path, controller, opts, do: nested_context) do
- add_resources path, controller, opts, do: nested_context
+ add_resources(path, controller, opts, do: nested_context)
end
@doc """
See `resources/4`.
"""
defmacro resources(path, controller, do: nested_context) do
- add_resources path, controller, [], do: nested_context
+ add_resources(path, controller, [], do: nested_context)
end
defmacro resources(path, controller, opts) do
- add_resources path, controller, opts, do: nil
+ add_resources(path, controller, opts, do: nil)
end
@doc """
See `resources/4`.
"""
defmacro resources(path, controller) do
- add_resources path, controller, [], do: nil
+ add_resources(path, controller, [], do: nil)
end
defp add_resources(path, controller, options, do: context) do
scope =
if context do
quote do
- scope resource.member, do: unquote(context)
+ scope(resource.member, do: unquote(context))
end
end
@@ -1019,8 +1082,11 @@ defmodule Phoenix.Router do
ie `"foo.bar.com"`, `"foo."`
* `:private` - a map of private data to merge into the connection when a route matches
* `:assigns` - a map of data to merge into the connection when a route matches
- * `:log` - the level to log the route dispatching under,
- may be set to false. Defaults to `:debug`
+ * `:log` - the level to log the route dispatching under, may be set to false. Defaults to
+ `:debug`. Route dispatching contains information about how the route is handled (which controller
+ action is called, what parameters are available and which pipelines are used) and is separate from
+ the plug level logging. To alter the plug log level, please see
+ https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-dynamic-log-level.
"""
defmacro scope(options, do: context) do
@@ -1090,11 +1156,12 @@ defmodule Phoenix.Router do
defmacro scope(path, alias, options, do: context) do
alias = expand_alias(alias, __CALLER__)
- options = quote do
- unquote(options)
- |> Keyword.put(:path, unquote(path))
- |> Keyword.put(:alias, unquote(alias))
- end
+ options =
+ quote do
+ unquote(options)
+ |> Keyword.put(:path, unquote(path))
+ |> Keyword.put(:alias, unquote(alias))
+ end
do_scope(options, context)
end
@@ -1102,6 +1169,7 @@ defmodule Phoenix.Router do
defp do_scope(options, context) do
quote do
Scope.push(__MODULE__, unquote(options))
+
try do
unquote(context)
after
diff --git a/lib/phoenix/router/console_formatter.ex b/lib/phoenix/router/console_formatter.ex
index 017219e1bd..58f852818c 100644
--- a/lib/phoenix/router/console_formatter.ex
+++ b/lib/phoenix/router/console_formatter.ex
@@ -19,45 +19,57 @@ defmodule Phoenix.Router.ConsoleFormatter do
end
defp format_endpoint(nil, _router, _), do: ""
+
defp format_endpoint(endpoint, router, widths) do
case endpoint.__sockets__() do
- [] -> ""
+ [] ->
+ ""
+
sockets ->
Enum.map_join(sockets, "", fn socket ->
format_websocket(socket, router, widths) <>
- format_longpoll(socket, router, widths)
+ format_longpoll(socket, router, widths)
end)
- end
+ end
end
defp format_websocket({_path, Phoenix.LiveReloader.Socket, _opts}, _router, _), do: ""
+
defp format_websocket({path, module, opts}, router, widths) do
if opts[:websocket] != false do
prefix = if router.__helpers__(), do: "websocket", else: ""
{verb_len, path_len, route_name_len} = widths
- String.pad_leading(prefix, route_name_len) <> " " <>
- String.pad_trailing(@socket_verb, verb_len) <> " " <>
- String.pad_trailing(path <> "/websocket", path_len) <> " " <>
- inspect(module) <>
- "\n"
+ String.pad_leading(prefix, route_name_len) <>
+ " " <>
+ String.pad_trailing(@socket_verb, verb_len) <>
+ " " <>
+ String.pad_trailing(path <> "/websocket", path_len) <>
+ " " <>
+ inspect(module) <>
+ "\n"
else
""
end
end
defp format_longpoll({_path, Phoenix.LiveReloader.Socket, _opts}, _router, _), do: ""
+
defp format_longpoll({path, module, opts}, router, widths) do
if opts[:longpoll] != false do
prefix = if router.__helpers__(), do: "longpoll", else: ""
+
for method <- @longpoll_verbs, into: "" do
{verb_len, path_len, route_name_len} = widths
- String.pad_leading(prefix, route_name_len) <> " " <>
- String.pad_trailing(method, verb_len) <> " " <>
- String.pad_trailing(path <> "/longpoll", path_len) <> " " <>
- inspect(module) <>
- "\n"
+ String.pad_leading(prefix, route_name_len) <>
+ " " <>
+ String.pad_trailing(method, verb_len) <>
+ " " <>
+ String.pad_trailing(path <> "/longpoll", path_len) <>
+ " " <>
+ inspect(module) <>
+ "\n"
end
else
""
@@ -65,7 +77,7 @@ defmodule Phoenix.Router.ConsoleFormatter do
end
defp calculate_column_widths(router, routes, endpoint) do
- sockets = endpoint && endpoint.__sockets__() || []
+ sockets = (endpoint && endpoint.__sockets__()) || []
widths =
Enum.reduce(routes, {0, 0, 0}, fn route, acc ->
@@ -73,36 +85,54 @@ defmodule Phoenix.Router.ConsoleFormatter do
verb = verb_name(verb)
{verb_len, path_len, route_name_len} = acc
route_name = route_name(router, helper)
- {max(verb_len, String.length(verb)),
- max(path_len, String.length(path)),
- max(route_name_len, String.length(route_name))}
+
+ {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
prefix = if router.__helpers__(), do: "websocket", else: ""
- verb_length = socket_verbs(opts) |> Enum.map(&String.length/1) |> Enum.max(&>=/2, fn -> 0 end)
- {max(verb_len, verb_length),
- max(path_len, String.length(path <> "/websocket")),
- max(route_name_len, String.length(prefix))}
+ verb_length =
+ socket_verbs(opts) |> Enum.map(&String.length/1) |> Enum.max(&>=/2, fn -> 0 end)
+
+ {max(verb_len, verb_length), max(path_len, String.length(path <> "/websocket")),
+ max(route_name_len, String.length(prefix))}
end)
end
defp format_route(route, router, column_widths) do
- %{verb: verb, path: path, plug: plug, metadata: metadata, plug_opts: plug_opts, helper: helper} = route
+ %{
+ verb: verb,
+ path: path,
+ plug: plug,
+ metadata: metadata,
+ plug_opts: plug_opts,
+ helper: helper
+ } = route
+
verb = verb_name(verb)
route_name = route_name(router, helper)
{verb_len, path_len, route_name_len} = column_widths
- log_module = metadata[:log_module] || plug
- String.pad_leading(route_name, route_name_len) <> " " <>
- String.pad_trailing(verb, verb_len) <> " " <>
- String.pad_trailing(path, path_len) <> " " <>
- "#{inspect(log_module)} #{inspect(plug_opts)}\n"
+ log_module =
+ case metadata[:mfa] do
+ {mod, _fun, _arity} -> mod
+ _ -> plug
+ end
+
+ String.pad_leading(route_name, route_name_len) <>
+ " " <>
+ String.pad_trailing(verb, verb_len) <>
+ " " <>
+ String.pad_trailing(path, path_len) <>
+ " " <>
+ "#{inspect(log_module)} #{inspect(plug_opts)}\n"
end
- defp route_name(_router, nil), do: ""
+ defp route_name(_router, nil), do: ""
+
defp route_name(router, name) do
if router.__helpers__() do
name <> "_path"
diff --git a/lib/phoenix/router/scope.ex b/lib/phoenix/router/scope.ex
index d0737ad676..4aa87919b3 100644
--- a/lib/phoenix/router/scope.ex
+++ b/lib/phoenix/router/scope.ex
@@ -37,7 +37,7 @@ defmodule Phoenix.Router.Scope do
path = validate_path(path)
private = Keyword.get(opts, :private, %{})
assigns = Keyword.get(opts, :assigns, %{})
- as = Keyword.get(opts, :as, Phoenix.Naming.resource_name(plug, "Controller"))
+ as = Keyword.get_lazy(opts, :as, fn -> Phoenix.Naming.resource_name(plug, "Controller") end)
alias? = Keyword.get(opts, :alias, true)
trailing_slash? = Keyword.get(opts, :trailing_slash, top.trailing_slash?) == true
warn_on_verify? = Keyword.get(opts, :warn_on_verify, false)
@@ -259,7 +259,10 @@ defmodule Phoenix.Router.Scope do
end
defp join_alias(top, alias) when is_atom(alias) do
- Module.concat(top.alias ++ [alias])
+ case Atom.to_string(alias) do
+ <> when head in ?a..?z -> alias
+ alias -> Module.concat(top.alias ++ [alias])
+ end
end
defp join_as(_top, nil), do: nil
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 468c0c0436..7963ef31e8 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -220,8 +220,15 @@ defmodule Phoenix.Socket do
See `Phoenix.Token` documentation for examples in
performing token verification on connect.
"""
- @callback connect(params :: map, Socket.t) :: {:ok, Socket.t} | {:error, term} | :error
- @callback connect(params :: map, Socket.t, connect_info :: map) :: {:ok, Socket.t} | {:error, term} | :error
+ @callback connect(params :: map, Socket.t(), connect_info :: map) ::
+ {:ok, Socket.t()} | {:error, term} | :error
+
+ @doc """
+ Shortcut version of `connect/3` which does not receive `connect_info`.
+
+ Provided for backwards compatibility.
+ """
+ @callback connect(params :: map, Socket.t()) :: {:ok, Socket.t()} | {:error, term} | :error
@doc ~S"""
Identifies the socket connection.
@@ -237,7 +244,7 @@ defmodule Phoenix.Socket do
Returning `nil` makes this socket anonymous.
"""
- @callback id(Socket.t) :: String.t | nil
+ @callback id(Socket.t()) :: String.t() | nil
@optional_callbacks connect: 2, connect: 3
@@ -270,15 +277,15 @@ defmodule Phoenix.Socket do
channel_pid: pid,
endpoint: atom,
handler: atom,
- id: String.t | nil,
+ id: String.t() | nil,
joined: boolean,
ref: term,
private: map,
pubsub_server: atom,
serializer: atom,
- topic: String.t,
+ topic: String.t(),
transport: atom,
- transport_pid: pid,
+ transport_pid: pid
}
defmacro __using__(opts) do
@@ -325,24 +332,32 @@ defmodule Phoenix.Socket do
## USER API
@doc """
- Adds key-value pairs to socket assigns.
+ Adds a `key`/`value` pair to `socket` assigns.
- A single key-value pair may be passed, a keyword list or map
- of assigns may be provided to be merged into existing socket
- assigns.
+ See also `assign/2` to add multiple key/value pairs.
## Examples
iex> assign(socket, :name, "Elixir")
- iex> assign(socket, name: "Elixir", logo: "💧")
"""
def assign(%Socket{} = socket, key, value) do
assign(socket, [{key, value}])
end
- def assign(%Socket{} = socket, attrs)
- when is_map(attrs) or is_list(attrs) do
- %{socket | assigns: Map.merge(socket.assigns, Map.new(attrs))}
+ @doc """
+ Adds key/value pairs to socket assigns.
+
+ A keyword list or a map of assigns must be given as argument to be merged into existing assigns.
+
+ ## Examples
+
+ iex> assign(socket, name: "Elixir", logo: "💧")
+ iex> assign(socket, %{name: "Elixir"})
+
+ """
+ def assign(%Socket{} = socket, keyword_or_map)
+ when is_map(keyword_or_map) or is_list(keyword_or_map) do
+ %{socket | assigns: Map.merge(socket.assigns, Map.new(keyword_or_map))}
end
@doc """
@@ -420,7 +435,7 @@ defmodule Phoenix.Socket do
case String.split(topic_pattern, "*") do
[prefix, ""] -> quote do: < _rest>>
[bare_topic] -> bare_topic
- _ -> raise ArgumentError, "channels using splat patterns must end with *"
+ _ -> raise ArgumentError, "channels using splat patterns must end with *"
end
end
@@ -445,7 +460,14 @@ defmodule Phoenix.Socket do
opts = Keyword.merge(socket_options, opts)
if drainer = Keyword.get(opts, :drainer, []) do
- {Phoenix.Socket.PoolDrainer, {endpoint, handler, drainer}}
+ drainer =
+ case drainer do
+ {module, function, arguments} ->
+ apply(module, function, arguments)
+ _ ->
+ drainer
+ end
+ {Phoenix.Socket.PoolDrainer, {endpoint, handler, drainer}}
else
:ignore
end
@@ -495,7 +517,7 @@ defmodule Phoenix.Socket do
defp result({:error, _}), do: :error
def __init__({state, %{id: id, endpoint: endpoint} = socket}) do
- _ = id && endpoint.subscribe(id, link: true)
+ _ = id && endpoint.subscribe(id)
{:ok, {state, %{socket | transport_pid: self()}}}
end
@@ -555,13 +577,16 @@ defmodule Phoenix.Socket do
{:ok, serializer}
:error ->
- Logger.warning "The client's requested transport version \"#{vsn}\" " <>
- "does not match server's version requirements of #{inspect serializers}"
+ Logger.warning(
+ "The client's requested transport version \"#{vsn}\" " <>
+ "does not match server's version requirements of #{inspect(serializers)}"
+ )
+
:error
end
:error ->
- Logger.warning "Client sent invalid transport version \"#{vsn}\""
+ Logger.warning("Client sent invalid transport version \"#{vsn}\"")
:error
end
end
@@ -599,8 +624,11 @@ defmodule Phoenix.Socket do
{:ok, {state, %{socket | id: id}}}
invalid ->
- Logger.warning "#{inspect handler}.id/1 returned invalid identifier " <>
- "#{inspect invalid}. Expected nil or a string."
+ Logger.warning(
+ "#{inspect(handler)}.id/1 returned invalid identifier " <>
+ "#{inspect(invalid)}. Expected nil or a string."
+ )
+
:error
end
@@ -611,9 +639,14 @@ defmodule Phoenix.Socket do
err
invalid ->
- connect_arity = if function_exported?(handler, :connect, 3), do: "connect/3", else: "connect/2"
- Logger.error "#{inspect handler}. #{connect_arity} returned invalid value #{inspect invalid}. " <>
- "Expected {:ok, socket}, {:error, reason} or :error"
+ connect_arity =
+ if function_exported?(handler, :connect, 3), do: "connect/3", else: "connect/2"
+
+ Logger.error(
+ "#{inspect(handler)}. #{connect_arity} returned invalid value #{inspect(invalid)}. " <>
+ "Expected {:ok, socket}, {:error, reason} or :error"
+ )
+
:error
end
end
@@ -629,22 +662,41 @@ defmodule Phoenix.Socket do
{:reply, :ok, encode_reply(socket, reply), {state, socket}}
end
- defp handle_in(nil, %{event: "phx_join", topic: topic, ref: ref, join_ref: join_ref} = message, state, socket) do
+ defp handle_in(
+ nil,
+ %{event: "phx_join", topic: topic, ref: ref, join_ref: join_ref} = message,
+ state,
+ socket
+ ) do
case socket.handler.__channel__(topic) do
{channel, opts} ->
case Phoenix.Channel.Server.join(socket, channel, message, opts) do
{:ok, reply, pid} ->
- reply = %Reply{join_ref: join_ref, ref: ref, topic: topic, status: :ok, payload: reply}
+ reply = %Reply{
+ join_ref: join_ref,
+ ref: ref,
+ topic: topic,
+ status: :ok,
+ payload: reply
+ }
+
state = put_channel(state, pid, topic, join_ref)
{:reply, :ok, encode_reply(socket, reply), {state, socket}}
{:error, reply} ->
- reply = %Reply{join_ref: join_ref, ref: ref, topic: topic, status: :error, payload: reply}
+ reply = %Reply{
+ join_ref: join_ref,
+ ref: ref,
+ topic: topic,
+ status: :error,
+ payload: reply
+ }
+
{:reply, :error, encode_reply(socket, reply), {state, socket}}
end
_ ->
- Logger.warning "Ignoring unmatched topic \"#{topic}\" in #{inspect(socket.handler)}"
+ Logger.warning("Ignoring unmatched topic \"#{topic}\" in #{inspect(socket.handler)}")
{:reply, :error, encode_ignore(socket, message), {state, socket}}
end
end
@@ -657,7 +709,7 @@ defmodule Phoenix.Socket do
if status != :leaving do
Logger.debug(fn ->
"Duplicate channel join for topic \"#{topic}\" in #{inspect(socket.handler)}. " <>
- "Closing existing channel for new join."
+ "Closing existing channel for new join."
end)
end
end
@@ -688,7 +740,12 @@ defmodule Phoenix.Socket do
{:ok, {state, socket}}
end
- defp handle_in(nil, %{event: "phx_leave", ref: ref, topic: topic, join_ref: join_ref}, state, socket) do
+ defp handle_in(
+ nil,
+ %{event: "phx_leave", ref: ref, topic: topic, join_ref: join_ref},
+ state,
+ socket
+ ) do
reply = %Reply{
ref: ref,
join_ref: join_ref,
@@ -711,8 +768,8 @@ defmodule Phoenix.Socket do
monitor_ref = Process.monitor(pid)
%{
- state |
- channels: Map.put(channels, topic, {pid, monitor_ref, :joined}),
+ state
+ | channels: Map.put(channels, topic, {pid, monitor_ref, :joined}),
channels_inverse: Map.put(channels_inverse, pid, {topic, join_ref})
}
end
@@ -722,8 +779,8 @@ defmodule Phoenix.Socket do
Process.demonitor(monitor_ref, [:flush])
%{
- state |
- channels: Map.delete(channels, topic),
+ state
+ | channels: Map.delete(channels, topic),
channels_inverse: Map.delete(channels_inverse, pid)
}
end
@@ -744,7 +801,14 @@ defmodule Phoenix.Socket do
end
defp encode_close(socket, topic, join_ref) do
- message = %Message{join_ref: join_ref, ref: join_ref, topic: topic, event: "phx_close", payload: %{}}
+ message = %Message{
+ join_ref: join_ref,
+ ref: join_ref,
+ topic: topic,
+ event: "phx_close",
+ payload: %{}
+ }
+
encode_reply(socket, message)
end
diff --git a/lib/phoenix/socket/pool_supervisor.ex b/lib/phoenix/socket/pool_supervisor.ex
index 936c0affee..a3297867fc 100644
--- a/lib/phoenix/socket/pool_supervisor.ex
+++ b/lib/phoenix/socket/pool_supervisor.ex
@@ -45,7 +45,7 @@ defmodule Phoenix.Socket.PoolSupervisor do
@impl true
def init({endpoint, name, partitions}) do
- # TODO: Use persisent term on Elixir v1.12+
+ # TODO: Use persistent term on Elixir v1.12+
ref = :ets.new(name, [:public, read_concurrency: true])
:ets.insert(ref, {:partitions, partitions})
Phoenix.Config.permanent(endpoint, {:socket, name}, ref)
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index d2e8595bb3..c9ab5dd956 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -136,10 +136,12 @@ defmodule Phoenix.Socket.Transport do
Connects to the socket.
The transport passes a map of metadata and the socket
- returns `{:ok, state}`. `{:error, reason}` or `:error`.
- The state must be stored by the transport and returned
- in all future operations. `{:error, reason}` can only
- be used with websockets.
+ returns `{:ok, state}`, `{:error, reason}` or `:error`.
+ The state must be stored by the transport and returned
+ in all future operations. When `{:error, reason}` is
+ returned, some transports - such as WebSockets - allow
+ customizing the response based on `reason` via a custom
+ `:error_handler`.
This function is used for authorization purposes and it
may be invoked outside of the process that effectively
@@ -197,7 +199,7 @@ defmodule Phoenix.Socket.Transport do
* `{:reply, status, reply, state}` - continues the socket with reply
* `{:stop, reason, state}` - stops the socket
- Control frames only supported when using websockets.
+ Control frames are only supported when using websockets.
The `options` contains an `opcode` key, this will be either `:ping` or
`:pong`.
@@ -292,9 +294,7 @@ defmodule Phoenix.Socket.Transport do
"""
def code_reload(conn, endpoint, opts) do
if Keyword.get(opts, :code_reloader, endpoint.config(:code_reloader)) do
- # If the WebSocket reconnects, then often the page has already been reloaded
- # and we don't want to print warnings twice, so we disable all warnings.
- Phoenix.CodeReloader.reload(endpoint, reloadable_args: ~w(--no-all-warnings))
+ Phoenix.CodeReloader.reload(endpoint)
end
conn
@@ -458,8 +458,9 @@ defmodule Phoenix.Socket.Transport do
* `:user_agent` - the value of the "user-agent" request header
+ The CSRF check can be disabled by setting the `:check_csrf` option to `false`.
"""
- def connect_info(conn, endpoint, keys) do
+ def connect_info(conn, endpoint, keys, opts \\ []) do
for key <- keys, into: %{} do
case key do
:peer_data ->
@@ -478,7 +479,7 @@ defmodule Phoenix.Socket.Transport do
{:user_agent, fetch_user_agent(conn)}
{:session, session} ->
- {:session, connect_session(conn, endpoint, session)}
+ {:session, connect_session(conn, endpoint, session, opts)}
{key, val} ->
{key, val}
@@ -486,26 +487,24 @@ defmodule Phoenix.Socket.Transport do
end
end
- defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}) do
+ defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}, opts) do
conn = Plug.Conn.fetch_cookies(conn)
+ check_csrf = Keyword.get(opts, :check_csrf, true)
- with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
- cookie when is_binary(cookie) <- conn.cookies[key],
+ with cookie when is_binary(cookie) <- conn.cookies[key],
conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)),
{_, session} <- store.get(conn, cookie, init),
- csrf_state when is_binary(csrf_state) <-
- Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]),
- true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do
+ true <- not check_csrf or csrf_token_valid?(conn, session, csrf_token_key) do
session
else
_ -> nil
end
end
- defp connect_session(conn, endpoint, {:mfa, {module, function, args}}) do
+ defp connect_session(conn, endpoint, {:mfa, {module, function, args}}, opts) do
case apply(module, function, args) do
session_config when is_list(session_config) ->
- connect_session(conn, endpoint, init_session(session_config))
+ connect_session(conn, endpoint, init_session(session_config), opts)
other ->
raise ArgumentError,
@@ -542,6 +541,14 @@ defmodule Phoenix.Socket.Transport do
end
end
+ defp csrf_token_valid?(conn, session, csrf_token_key) do
+ with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
+ csrf_state when is_binary(csrf_state) <-
+ Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]) do
+ Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token)
+ end
+ end
+
defp check_origin_config(handler, endpoint, opts) do
Phoenix.Config.cache(endpoint, {:check_origin, handler}, fn _ ->
check_origin =
diff --git a/lib/phoenix/test/channel_test.ex b/lib/phoenix/test/channel_test.ex
index a8337b69b1..4ca2ccc997 100644
--- a/lib/phoenix/test/channel_test.ex
+++ b/lib/phoenix/test/channel_test.ex
@@ -692,6 +692,8 @@ defmodule Phoenix.ChannelTest do
do: struct
def __stringify__(%{} = params),
do: Enum.into(params, %{}, &stringify_kv/1)
+ def __stringify__(params) when is_list(params),
+ do: Enum.map(params, &__stringify__/1)
def __stringify__(other),
do: other
diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex
index 470538e714..9def147398 100644
--- a/lib/phoenix/test/conn_test.ex
+++ b/lib/phoenix/test/conn_test.ex
@@ -476,6 +476,7 @@ defmodule Phoenix.ConnTest do
def recycle(conn, headers \\ ~w(accept accept-language authorization)) do
build_conn()
|> Map.put(:host, conn.host)
+ |> Map.put(:remote_ip, conn.remote_ip)
|> Plug.Test.recycle_cookies(conn)
|> Plug.Test.put_peer_data(Plug.Conn.get_peer_data(conn))
|> copy_headers(conn.req_headers, headers)
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index 932ef6c58f..ba01eca287 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -1,12 +1,18 @@
defmodule Phoenix.Token do
@moduledoc """
- Tokens provide a way to generate and verify bearer
- tokens for use in Channels or API authentication.
-
- The data stored in the token is signed to prevent tampering
- but not encrypted. This means it is safe to store identification
- information (such as user IDs) but should not be used to store
- confidential information (such as credit card numbers).
+ Conveniences to sign/encrypt data inside tokens
+ for use in Channels, API authentication, and more.
+
+ The data stored in the token is signed to prevent tampering, and is
+ optionally encrypted. This means that, so long as the
+ key (see below) remains secret, you can be assured that the data
+ stored in the token has not been tampered with by a third party.
+ However, unless the token is encrypted, it is not safe to use this
+ token to store private information, such as a user's sensitive
+ identification data, as it can be trivially decoded. If the
+ token is encrypted, its contents will be kept secret from the
+ client, but it is still a best practice to encode as little secret
+ information as possible, to minimize the impact of key leakage.
## Example
@@ -24,7 +30,8 @@ defmodule Phoenix.Token do
`endpoint`. We guarantee the token will only be valid for one day
by setting a max age (recommended).
- The first argument to both `sign/4` and `verify/4` can be one of:
+ The first argument to `sign/4`, `verify/4`, `encrypt/4`, and
+ `decrypt/4` can be one of:
* the module name of a Phoenix endpoint (shown above) - where
the secret key base is extracted from the endpoint
@@ -37,10 +44,10 @@ defmodule Phoenix.Token do
to provide adequate entropy
The second argument is a [cryptographic salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
- which must be the same in both calls to `sign/4` and `verify/4`.
- For instance, it may be called "user auth" and treated as namespace
- when generating a token that will be used to authenticate users on
- channels or on your APIs.
+ which must be the same in both calls to `sign/4` and `verify/4`, or
+ both calls to `encrypt/4` and `decrypt/4`. For instance, it may be
+ called "user auth" and treated as namespace when generating a token
+ that will be used to authenticate users on channels or on your APIs.
The third argument can be any term (string, int, list, etc.)
that you wish to codify into the token. Upon valid verification,
@@ -92,7 +99,11 @@ defmodule Phoenix.Token do
require Logger
- @type context :: Plug.Conn.t() | %{required(:endpoint) => atom, optional(atom()) => any()} | atom | binary
+ @type context ::
+ Plug.Conn.t()
+ | %{required(:endpoint) => atom, optional(atom()) => any()}
+ | atom
+ | binary
@type shared_opt ::
{:key_iterations, pos_integer}
@@ -114,9 +125,9 @@ defmodule Phoenix.Token do
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
when generating the encryption and signing keys. Defaults to `:sha256`
* `:signed_at` - set the timestamp of the token in seconds.
- Defaults to `System.system_time(:second)`
+ Defaults to `System.os_time(:millisecond)`
* `:max_age` - the default maximum age of the token. Defaults to
- 86400 seconds (1 day) and it may be overridden on verify/4.
+ 86400 seconds (1 day) and it may be overridden on `verify/4`.
"""
@spec sign(context, binary, term, [shared_opt | max_age_opt | signed_at_opt]) :: binary
@@ -127,7 +138,9 @@ defmodule Phoenix.Token do
end
@doc """
- Encodes, encrypts, and signs data into a token you can send to clients.
+ Encodes, encrypts, and signs data into a token you can send to
+ clients. Its usage is identical to that of `sign/4`, but the data
+ is extracted using `decrypt/4`, rather than `verify/4`.
## Options
@@ -138,9 +151,9 @@ defmodule Phoenix.Token do
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
when generating the encryption and signing keys. Defaults to `:sha256`
* `:signed_at` - set the timestamp of the token in seconds.
- Defaults to `System.system_time(:second)`
+ Defaults to `System.os_time(:millisecond)`
* `:max_age` - the default maximum age of the token. Defaults to
- 86400 seconds (1 day) and it may be overridden on verify/4.
+ 86400 seconds (1 day) and it may be overridden on `decrypt/4`.
"""
@spec encrypt(context, binary, term, [shared_opt | max_age_opt | signed_at_opt]) :: binary
@@ -200,9 +213,8 @@ defmodule Phoenix.Token do
* `:key_digest` - option passed to `Plug.Crypto.KeyGenerator`
when generating the encryption and signing keys. Defaults to `:sha256`
* `:max_age` - verifies the token only if it has been generated
- "max age" ago in seconds. A reasonable value is 1 day (86400
- seconds)
-
+ "max age" ago in seconds. Defaults to the max age signed in the
+ token by `sign/4`.
"""
@spec verify(context, binary, binary, [shared_opt | max_age_opt]) ::
{:ok, term} | {:error, :expired | :invalid | :missing}
@@ -215,6 +227,8 @@ defmodule Phoenix.Token do
@doc """
Decrypts the original data from the token and verifies its integrity.
+ Its usage is identical to `verify/4` but for encrypted tokens.
+
## Options
* `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
@@ -225,7 +239,7 @@ defmodule Phoenix.Token do
when generating the encryption and signing keys. Defaults to `:sha256`
* `:max_age` - verifies the token only if it has been generated
"max age" ago in seconds. Defaults to the max age signed in the
- token (86400)
+ token by `encrypt/4`.
"""
@spec decrypt(context, binary, binary, [shared_opt | max_age_opt]) :: term()
def decrypt(context, secret, token, opts \\ []) when is_binary(secret) do
diff --git a/lib/phoenix/transports/long_poll.ex b/lib/phoenix/transports/long_poll.ex
index a2dc056ca0..3a9915bb4e 100644
--- a/lib/phoenix/transports/long_poll.ex
+++ b/lib/phoenix/transports/long_poll.ex
@@ -4,6 +4,7 @@ defmodule Phoenix.Transports.LongPoll do
# 10MB
@max_base64_size 10_000_000
+ @connect_info_opts [:check_csrf]
import Plug.Conn
alias Phoenix.Socket.{V1, V2, Transport}
@@ -49,9 +50,9 @@ defmodule Phoenix.Transports.LongPoll do
# Starts a new session or listen to a message if one already exists.
defp dispatch(%{method: "GET"} = conn, endpoint, handler, opts) do
- case resume_session(conn.params, endpoint, opts) do
- {:ok, server_ref} ->
- listen(conn, server_ref, endpoint, opts)
+ case resume_session(conn, conn.params, endpoint, opts) do
+ {:ok, new_conn, server_ref} ->
+ listen(new_conn, server_ref, endpoint, opts)
:error ->
new_session(conn, endpoint, handler, opts)
@@ -60,9 +61,9 @@ defmodule Phoenix.Transports.LongPoll do
# Publish the message.
defp dispatch(%{method: "POST"} = conn, endpoint, _, opts) do
- case resume_session(conn.params, endpoint, opts) do
- {:ok, server_ref} ->
- publish(conn, server_ref, endpoint, opts)
+ case resume_session(conn, conn.params, endpoint, opts) do
+ {:ok, new_conn, server_ref} ->
+ publish(new_conn, server_ref, endpoint, opts)
:error ->
conn |> put_status(:gone) |> status_json()
@@ -136,7 +137,10 @@ defmodule Phoenix.Transports.LongPoll do
(System.system_time(:millisecond) |> Integer.to_string())
keys = Keyword.get(opts, :connect_info, [])
- connect_info = Transport.connect_info(conn, endpoint, keys)
+
+ connect_info =
+ Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts))
+
arg = {endpoint, handler, opts, conn.params, priv_topic, connect_info}
spec = {Phoenix.Transports.LongPoll.Server, arg}
@@ -153,7 +157,8 @@ defmodule Phoenix.Transports.LongPoll do
defp listen(conn, server_ref, endpoint, opts) do
ref = make_ref()
- broadcast_from!(endpoint, server_ref, {:flush, client_ref(server_ref), ref})
+ client_ref = client_ref(server_ref)
+ broadcast_from!(endpoint, server_ref, {:flush, client_ref, ref})
{status, messages} =
receive do
@@ -161,15 +166,18 @@ defmodule Phoenix.Transports.LongPoll do
{:ok, messages}
{:now_available, ^ref} ->
- broadcast_from!(endpoint, server_ref, {:flush, client_ref(server_ref), ref})
+ broadcast_from!(endpoint, server_ref, {:flush, client_ref, ref})
receive do
{:messages, messages, ^ref} -> {:ok, messages}
after
- opts[:window_ms] -> {:no_content, []}
+ opts[:window_ms] ->
+ broadcast_from!(endpoint, server_ref, {:expired, client_ref, ref})
+ {:no_content, []}
end
after
opts[:window_ms] ->
+ broadcast_from!(endpoint, server_ref, {:expired, client_ref, ref})
{:no_content, []}
end
@@ -180,17 +188,23 @@ defmodule Phoenix.Transports.LongPoll do
# Retrieves the serialized `Phoenix.LongPoll.Server` pid
# by publishing a message in the encrypted private topic.
- defp resume_session(%{"token" => token}, endpoint, opts) do
+ defp resume_session(%Plug.Conn{} = conn, %{"token" => token}, endpoint, opts) do
case verify_token(endpoint, token, opts) do
{:ok, {:v1, id, pid, priv_topic}} ->
server_ref = server_ref(endpoint.config(:endpoint_id), id, pid, priv_topic)
+ new_conn =
+ Plug.Conn.register_before_send(conn, fn conn ->
+ unsubscribe(endpoint, server_ref)
+ conn
+ end)
+
ref = make_ref()
:ok = subscribe(endpoint, server_ref)
broadcast_from!(endpoint, server_ref, {:subscribe, client_ref(server_ref), ref})
receive do
- {:subscribe, ^ref} -> {:ok, server_ref}
+ {:subscribe, ^ref} -> {:ok, new_conn, server_ref}
after
opts[:pubsub_timeout_ms] -> :error
end
@@ -200,7 +214,7 @@ defmodule Phoenix.Transports.LongPoll do
end
end
- defp resume_session(_params, _endpoint, _opts), do: :error
+ defp resume_session(%Plug.Conn{}, _params, _endpoint, _opts), do: :error
## Helpers
@@ -216,11 +230,17 @@ defmodule Phoenix.Transports.LongPoll do
defp client_ref(pid) when is_pid(pid), do: self()
defp subscribe(endpoint, topic) when is_binary(topic),
- do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic, link: true)
+ do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic)
defp subscribe(_endpoint, pid) when is_pid(pid),
do: :ok
+ defp unsubscribe(endpoint, topic) when is_binary(topic),
+ do: Phoenix.PubSub.unsubscribe(endpoint.config(:pubsub_server), topic)
+
+ defp unsubscribe(_endpoint, pid) when is_pid(pid),
+ do: :ok
+
defp broadcast_from!(endpoint, topic, msg) when is_binary(topic),
do: Phoenix.PubSub.broadcast_from!(endpoint.config(:pubsub_server), self(), topic, msg)
diff --git a/lib/phoenix/transports/long_poll_server.ex b/lib/phoenix/transports/long_poll_server.ex
index fd0fb87402..e3a5f23d8d 100644
--- a/lib/phoenix/transports/long_poll_server.ex
+++ b/lib/phoenix/transports/long_poll_server.ex
@@ -16,6 +16,7 @@ defmodule Phoenix.Transports.LongPoll.Server do
params: params,
connect_info: connect_info
}
+
window_ms = Keyword.fetch!(options, :window_ms)
case handler.connect(config) do
@@ -32,7 +33,7 @@ defmodule Phoenix.Transports.LongPoll.Server do
client_ref: nil
}
- :ok = PubSub.subscribe(state.pubsub_server, priv_topic, link: true)
+ :ok = PubSub.subscribe(state.pubsub_server, priv_topic)
schedule_inactive_shutdown(state.window_ms)
{:ok, state}
@@ -75,12 +76,23 @@ defmodule Phoenix.Transports.LongPoll.Server do
case state.buffer do
[] ->
{:noreply, %{state | client_ref: {client_ref, ref}, last_client_poll: now_ms()}}
+
buffer ->
broadcast_from!(state, client_ref, {:messages, Enum.reverse(buffer), ref})
{:noreply, %{state | client_ref: nil, last_client_poll: now_ms(), buffer: []}}
end
end
+ def handle_info({:expired, client_ref, ref}, state) do
+ case state.client_ref do
+ {^client_ref, ^ref} ->
+ {:noreply, %{state | client_ref: nil}}
+
+ _ ->
+ {:noreply, state}
+ end
+ end
+
def handle_info(:shutdown_if_inactive, state) do
if now_ms() - state.last_client_poll > state.window_ms do
{:stop, {:shutdown, :inactive}, state}
@@ -116,12 +128,16 @@ defmodule Phoenix.Transports.LongPoll.Server do
defp broadcast_from!(state, client_ref, msg) when is_binary(client_ref),
do: PubSub.broadcast_from!(state.pubsub_server, self(), client_ref, msg)
+
defp broadcast_from!(_state, client_ref, msg) when is_pid(client_ref),
do: send(client_ref, msg)
defp publish_reply(state, reply) when is_map(reply) do
- IO.warn "Returning a map from the LongPolling serializer is deprecated. " <>
- "Please return JSON encoded data instead (see Phoenix.Socket.Serializer)"
+ IO.warn(
+ "Returning a map from the LongPolling serializer is deprecated. " <>
+ "Please return JSON encoded data instead (see Phoenix.Socket.Serializer)"
+ )
+
publish_reply(state, Phoenix.json_library().encode_to_iodata!(reply))
end
diff --git a/lib/phoenix/transports/websocket.ex b/lib/phoenix/transports/websocket.ex
index 16be35bb54..dfc7bd2508 100644
--- a/lib/phoenix/transports/websocket.ex
+++ b/lib/phoenix/transports/websocket.ex
@@ -15,6 +15,8 @@ defmodule Phoenix.Transports.WebSocket do
#
@behaviour Plug
+ @connect_info_opts [:check_csrf]
+
import Plug.Conn
alias Phoenix.Socket.{V1, V2, Transport}
@@ -45,7 +47,9 @@ defmodule Phoenix.Transports.WebSocket do
%{params: params} = conn ->
keys = Keyword.get(opts, :connect_info, [])
- connect_info = Transport.connect_info(conn, endpoint, keys)
+
+ connect_info =
+ Transport.connect_info(conn, endpoint, keys, Keyword.take(opts, @connect_info_opts))
config = %{
endpoint: endpoint,
@@ -57,9 +61,13 @@ defmodule Phoenix.Transports.WebSocket do
case handler.connect(config) do
{:ok, arg} ->
- conn
- |> WebSockAdapter.upgrade(handler, arg, opts)
- |> halt()
+ try do
+ conn
+ |> WebSockAdapter.upgrade(handler, arg, opts)
+ |> halt()
+ rescue
+ e in WebSockAdapter.UpgradeError -> send_resp(conn, 400, e.message)
+ end
:error ->
send_resp(conn, 403, "")
diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex
index 089b491148..a932743b84 100644
--- a/lib/phoenix/verified_routes.ex
+++ b/lib/phoenix/verified_routes.ex
@@ -6,7 +6,7 @@ defmodule Phoenix.VerifiedRoutes do
application to be compile-time verified against your Phoenix router(s).
For example, the following path and URL usages:
- <.link href={~p"/sessions/new"} method="post">Sign in
+ <.link href={~p"/sessions/new"} method="post">Log in
redirect(to: url(~p"/posts/#{post}"))
@@ -59,7 +59,7 @@ defmodule Phoenix.VerifiedRoutes do
The majority of path and URL generation needs your application will be met
with `~p` and `url/1`, where all information necessary to construct the path
- or URL is provided the by the compile-time information stored in the Endpoint
+ or URL is provided by the compile-time information stored in the Endpoint
and Router passed to `use Phoenix.VerifiedRoutes`.
That said, there are some circumstances where `path/2`, `path/3`, `url/2`, and `url/3`
@@ -106,7 +106,7 @@ defmodule Phoenix.VerifiedRoutes do
defstruct router: nil,
route: nil,
inspected_route: nil,
- stacktrace: nil,
+ warn_location: nil,
test_path: nil
defmacro __using__(opts) do
@@ -166,7 +166,7 @@ defmodule Phoenix.VerifiedRoutes do
unless match_route?(route.router, route.test_path) do
IO.warn(
"no route path for #{inspect(route.router)} matches #{route.inspected_route}",
- route.stacktrace
+ route.warn_location
)
end
end)
@@ -194,9 +194,9 @@ defmodule Phoenix.VerifiedRoutes do
redirect(to: ~p"/users/#{@user}")
~H"""
- <.link to={~p"/users?page=#{@page}"}>profile
+ <.link href={~p"/users?page=#{@page}"}>profile
- <.link to={~p"/users?#{@params}"}>profile
+ <.link href={~p"/users?#{@params}"}>profile
"""
'''
defmacro sigil_p({:<<>>, _meta, _segments} = route, extra) do
@@ -253,10 +253,10 @@ defmodule Phoenix.VerifiedRoutes do
@doc ~S'''
Generates the router path with route verification.
- See `sigil_p/1` for more information.
+ See `sigil_p/2` for more information.
Warns when the provided path does not match against the router specified
- in `use Phoenix.VerifiedRoutes` or the `@router` module attribute.
+ in the router argument.
## Examples
@@ -267,33 +267,54 @@ defmodule Phoenix.VerifiedRoutes do
redirect(to: path(conn, MyAppWeb.Router, ~p"/users/#{@user}"))
~H"""
- <.link to={path(@uri, MyAppWeb.Router, "/users?page=#{@page}")}>profile
- <.link to={path(@uri, MyAppWeb.Router, "/users?#{@params}")}>profile
+ <.link href={path(@uri, MyAppWeb.Router, "/users?page=#{@page}")}>profile
+ <.link href={path(@uri, MyAppWeb.Router, "/users?#{@params}")}>profile
"""
'''
defmacro path(
conn_or_socket_or_endpoint_or_uri,
router,
- {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = og_ast
+ {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = sigil_p
) do
validate_sigil_p!(extra)
route
- |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+ |> build_route(sigil_p, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
|> inject_path(__CALLER__)
end
defmacro path(_endpoint, _router, other), do: raise_invalid_route(other)
+ @doc ~S'''
+ Generates the router path with route verification.
+
+ See `sigil_p/2` for more information.
+
+ Warns when the provided path does not match against the router specified
+ in `use Phoenix.VerifiedRoutes` or the `@router` module attribute.
+
+ ## Examples
+
+ import Phoenix.VerifiedRoutes
+
+ redirect(to: path(conn, ~p"/users/top"))
+
+ redirect(to: path(conn, ~p"/users/#{@user}"))
+
+ ~H"""
+ <.link href={path(@uri, "/users?page=#{@page}")}>profile
+ <.link href={path(@uri, "/users?#{@params}")}>profile
+ """
+ '''
defmacro path(
conn_or_socket_or_endpoint_or_uri,
- {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = og_ast
+ {:sigil_p, _, [{:<<>>, _meta, _segments} = route, extra]} = sigil_p
) do
validate_sigil_p!(extra)
router = attr!(__CALLER__, :router)
route
- |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+ |> build_route(sigil_p, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
|> inject_path(__CALLER__)
end
@@ -316,7 +337,7 @@ defmodule Phoenix.VerifiedRoutes do
redirect(to: url(conn, ~p"/users/#{@user}"))
~H"""
- <.link to={url(@uri, "/users?#{[page: @page]}")}>profile
+ <.link href={url(@uri, "/users?#{[page: @page]}")}>profile
"""
The router may also be provided in cases where you want to verify routes for a
@@ -340,12 +361,12 @@ defmodule Phoenix.VerifiedRoutes do
Forwarded paths in your main application router will be verified as usual,
such as `~p"/admin/users"`.
'''
- defmacro url({:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast) do
+ defmacro url({:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = sigil_p) do
endpoint = attr!(__CALLER__, :endpoint)
router = attr!(__CALLER__, :router)
route
- |> build_route(og_ast, __CALLER__, endpoint, router)
+ |> build_route(sigil_p, __CALLER__, endpoint, router)
|> inject_url(__CALLER__)
end
@@ -358,12 +379,12 @@ defmodule Phoenix.VerifiedRoutes do
"""
defmacro url(
conn_or_socket_or_endpoint_or_uri,
- {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast
+ {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = sigil_p
) do
router = attr!(__CALLER__, :router)
route
- |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+ |> build_route(sigil_p, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
|> inject_url(__CALLER__)
end
@@ -377,12 +398,12 @@ defmodule Phoenix.VerifiedRoutes do
defmacro url(
conn_or_socket_or_endpoint_or_uri,
router,
- {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = og_ast
+ {:sigil_p, _, [{:<<>>, _meta, _segments} = route, _]} = sigil_p
) do
router = Macro.expand(router, __CALLER__)
route
- |> build_route(og_ast, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
+ |> build_route(sigil_p, __CALLER__, conn_or_socket_or_endpoint_or_uri, router)
|> inject_url(__CALLER__)
end
@@ -390,7 +411,22 @@ defmodule Phoenix.VerifiedRoutes do
@doc """
Generates url to a static asset given its file path.
+
+ See `c:Phoenix.Endpoint.static_url/0` and `c:Phoenix.Endpoint.static_path/1` for more information.
+
+ ## Examples
+
+ iex> static_url(conn, "/assets/app.js")
+ "https://example.com/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
+
+ iex> static_url(socket, "/assets/app.js")
+ "https://example.com/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
+
+ iex> static_url(AppWeb.Endpoint, "/assets/app.js")
+ "https://example.com/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
"""
+ def static_url(conn_or_socket_or_endpoint, path)
+
def static_url(%Plug.Conn{private: private}, path) do
case private do
%{phoenix_static_url: static_url} -> concat_url(static_url, path)
@@ -408,7 +444,7 @@ defmodule Phoenix.VerifiedRoutes do
def static_url(other, path) do
raise ArgumentError,
- "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
+ "expected a %Plug.Conn{}, a %Phoenix.Socket{}, a struct with an :endpoint key, " <>
"or a Phoenix.Endpoint when building static url for #{path}, got: #{inspect(other)}"
end
@@ -423,31 +459,31 @@ defmodule Phoenix.VerifiedRoutes do
iex> unverified_url(conn, "/posts", page: 1)
"https://example.com/posts?page=1"
"""
- def unverified_url(ctx, path) when is_binary(path) do
- unverified_url(ctx, path, %{})
+ def unverified_url(conn_or_socket_or_endpoint_or_uri, path, params \\ %{})
+ when (is_map(params) or is_list(params)) and is_binary(path) do
+ guarded_unverified_url(conn_or_socket_or_endpoint_or_uri, path, params)
end
- def unverified_url(%Plug.Conn{private: private}, path, params)
- when is_map(params) or is_list(params) do
+ defp guarded_unverified_url(%Plug.Conn{private: private}, path, params) do
case private do
%{phoenix_router_url: url} when is_binary(url) -> concat_url(url, path, params)
%{phoenix_endpoint: endpoint} -> concat_url(endpoint.url(), path, params)
end
end
- def unverified_url(%_{endpoint: endpoint}, path, params) do
+ defp guarded_unverified_url(%_{endpoint: endpoint}, path, params) do
concat_url(endpoint.url(), path, params)
end
- def unverified_url(%URI{} = uri, path, params) do
+ defp guarded_unverified_url(%URI{} = uri, path, params) do
append_params(URI.to_string(%{uri | path: path}), params)
end
- def unverified_url(endpoint, path, params) when is_atom(endpoint) do
+ defp guarded_unverified_url(endpoint, path, params) when is_atom(endpoint) do
concat_url(endpoint.url(), path, params)
end
- def unverified_url(other, path, _params) do
+ defp guarded_unverified_url(other, path, _params) do
raise ArgumentError,
"expected a %Plug.Conn{}, a %Phoenix.Socket{}, a %URI{}, a struct with an :endpoint key, " <>
"or a Phoenix.Endpoint when building url at #{path}, got: #{inspect(other)}"
@@ -461,7 +497,25 @@ defmodule Phoenix.VerifiedRoutes do
@doc """
Generates path to a static asset given its file path.
+
+ See `c:Phoenix.Endpoint.static_path/1` for more information.
+
+ ## Examples
+
+ iex> static_path(conn, "/assets/app.js")
+ "/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
+
+ iex> static_path(socket, "/assets/app.js")
+ "/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
+
+ iex> static_path(AppWeb.Endpoint, "/assets/app.js")
+ "/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
+
+ iex> static_path(%URI{path: "/subresource"}, "/assets/app.js")
+ "/subresource/assets/app-813dfe33b5c7f8388bccaaa38eec8382.js"
"""
+ def static_path(conn_or_socket_or_endpoint_or_uri, path)
+
def static_path(%Plug.Conn{private: private}, path) do
case private do
%{phoenix_static_url: _} -> path
@@ -492,7 +546,7 @@ defmodule Phoenix.VerifiedRoutes do
iex> unverified_path(conn, AppWeb.Router, "/posts", page: 1)
"/posts?page=1"
"""
- def unverified_path(ctx, router, path, params \\ %{})
+ def unverified_path(conn_or_socket_or_endpoint_or_uri, router, path, params \\ %{})
def unverified_path(%Plug.Conn{} = conn, router, path, params) do
conn
@@ -642,7 +696,22 @@ defmodule Phoenix.VerifiedRoutes do
@doc """
Generates an integrity hash to a static asset given its file path.
+
+ See `c:Phoenix.Endpoint.static_integrity/1` for more information.
+
+ ## Examples
+
+ iex> static_integrity(conn, "/assets/app.js")
+ "813dfe33b5c7f8388bccaaa38eec8382"
+
+ iex> static_integrity(socket, "/assets/app.js")
+ "813dfe33b5c7f8388bccaaa38eec8382"
+
+ iex> static_integrity(AppWeb.Endpoint, "/assets/app.js")
+ "813dfe33b5c7f8388bccaaa38eec8382"
"""
+ def static_integrity(conn_or_socket_or_endpoint, path)
+
def static_integrity(%Plug.Conn{private: %{phoenix_endpoint: endpoint}}, path) do
static_integrity(endpoint, path)
end
@@ -700,7 +769,7 @@ defmodule Phoenix.VerifiedRoutes do
end
end
- defp build_route(route_ast, og_ast, env, endpoint_ctx, router) do
+ defp build_route(route_ast, sigil_p, env, endpoint_ctx, router) do
statics = Module.get_attribute(env.module, :phoenix_verified_statics, [])
router =
@@ -716,19 +785,30 @@ defmodule Phoenix.VerifiedRoutes do
"""
end
- {static?, test_path, path_ast, static_ast} =
+ {static?, meta, test_path, path_ast, static_ast} =
rewrite_path(route_ast, endpoint_ctx, router, statics)
route = %__MODULE__{
router: router,
- stacktrace: Macro.Env.stacktrace(env),
- inspected_route: Macro.to_string(og_ast),
+ warn_location: warn_location(meta, env),
+ inspected_route: Macro.to_string(sigil_p),
test_path: test_path
}
{route, static?, endpoint_ctx, route_ast, path_ast, static_ast}
end
+ if @after_verify_supported do
+ defp warn_location(meta, %{line: line, file: file, function: function, module: module}) do
+ column = if column = meta[:column], do: column + 2
+ [line: line, function: function, module: module, file: file, column: column]
+ end
+ else
+ defp warn_location(_meta, env) do
+ Macro.Env.stacktrace(env)
+ end
+ end
+
defp rewrite_path(route, endpoint, router, statics) do
{:<<>>, meta, segments} = route
{path_rewrite, query_rewrite} = verify_segment(segments, route)
@@ -759,7 +839,7 @@ defmodule Phoenix.VerifiedRoutes do
unquote(__MODULE__).static_path(unquote_splicing([endpoint, rewrite_route]))
end
- {static?, test_path, path_ast, static_ast}
+ {static?, meta, test_path, path_ast, static_ast}
end
defp attr!(%{function: nil}, _) do
diff --git a/mix.exs b/mix.exs
index 6b486a7cbb..c32c49d035 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,7 +8,7 @@ defmodule Phoenix.MixProject do
end
end
- @version "1.7.7"
+ @version "1.7.14"
@scm_url "https://github.com/phoenixframework/phoenix"
# If the elixir requirement is updated, we need to make the installer
@@ -71,39 +71,40 @@ defmodule Phoenix.MixProject do
defp deps do
[
{:plug, "~> 1.14"},
- {:plug_crypto, "~> 1.2"},
+ {:plug_crypto, "~> 1.2 or ~> 2.0"},
{:telemetry, "~> 0.4 or ~> 1.0"},
{:phoenix_pubsub, "~> 2.1"},
- # TODO drop phoenix_view as an optional dependency in Phoenix v2.0
- {:phoenix_view, "~> 2.0", optional: true},
{:phoenix_template, "~> 1.0"},
{:websock_adapter, "~> 0.5.3"},
+ # TODO drop phoenix_view as an optional dependency in Phoenix v2.0
+ {:phoenix_view, "~> 2.0", optional: true},
# TODO drop castore when we require OTP 25+
{:castore, ">= 0.0.0"},
# Optional deps
- {:plug_cowboy, "~> 2.6", optional: true},
+ {:plug_cowboy, "~> 2.7", optional: true},
{:jason, "~> 1.0", optional: true},
# Docs dependencies (some for cross references)
{:ex_doc, "~> 0.24", only: :docs},
{:ecto, "~> 3.0", only: :docs},
{:ecto_sql, "~> 3.10", only: :docs},
- {:gettext, "~> 0.20", only: :docs},
+ {:gettext, "~> 0.26", only: :docs},
{:telemetry_poller, "~> 1.0", only: :docs},
- {:telemetry_metrics, "~> 0.6", only: :docs},
+ {:telemetry_metrics, "~> 1.0", only: :docs},
{:makeup_eex, ">= 0.1.1", only: :docs},
{:makeup_elixir, "~> 0.16", only: :docs},
+ {:makeup_diff, "~> 0.1", only: :docs},
# Test dependencies
- {:phoenix_html, "~> 3.3", only: [:docs, :test]},
- {:phx_new, path: "./installer", only: :test},
+ {:phoenix_html, "~> 4.0", only: [:docs, :test]},
+ {:phx_new, path: "./installer", only: [:docs, :test]},
{:mint, "~> 1.4", only: :test},
{:mint_web_socket, "~> 1.0.0", only: :test},
# Dev dependencies
- {:esbuild, "~> 0.7", only: :dev}
+ {:esbuild, "~> 0.8", only: :dev}
]
end
@@ -155,6 +156,7 @@ defmodule Phoenix.MixProject do
"guides/telemetry.md",
"guides/asset_management.md",
"guides/authentication/mix_phx_gen_auth.md",
+ "guides/authentication/api_authentication.md",
"guides/real_time/channels.md",
"guides/real_time/presence.md",
"guides/testing/testing.md",
@@ -170,6 +172,7 @@ defmodule Phoenix.MixProject do
"guides/howto/file_uploads.md",
"guides/howto/using_ssl.md",
"guides/howto/writing_a_channels_client.md",
+ "guides/cheatsheets/router.cheatmd",
"CHANGELOG.md"
]
end
@@ -182,6 +185,7 @@ defmodule Phoenix.MixProject do
"Real-time": ~r/guides\/real_time\/.?/,
Testing: ~r/guides\/testing\/.?/,
Deployment: ~r/guides\/deployment\/.?/,
+ Cheatsheets: ~r/guides\/cheatsheets\/.?/,
"How-to's": ~r/guides\/howto\/.?/
]
end
diff --git a/mix.lock b/mix.lock
index a6bbdfd66d..b503567403 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,39 +1,40 @@
%{
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
- "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ecto": {:hex, :ecto, "3.10.1", "c6757101880e90acc6125b095853176a02da8f1afe056f91f1f90b80c9389822", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2ac4255f1601bdf7ac74c0ed971102c6829dc158719b94bd30041bbad77f87a"},
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
- "esbuild": {:hex, :esbuild, "0.7.0", "ce3afb13cd2c5fd63e13c0e2d0e0831487a97a7696cfa563707342bb825d122a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "4ae9f4f237c5ebcb001390b8ada65a12fb2bb04f3fe3d1f1692b7a06fbfe8752"},
- "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
- "gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
+ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
+ "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
+ "expo": {:hex, :expo, "1.0.0", "647639267e088717232f4d4451526e7a9de31a3402af7fcbda09b27e9a10395a", [:mix], [], "hexpm", "18d2093d344d97678e8a331ca0391e85d29816f9664a25653fd7e6166827827c"},
+ "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
"jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
- "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
+ "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
+ "makeup_diff": {:hex, :makeup_diff, "0.1.0", "5be352b6aa6f07fa6a236e3efd7ba689a03f28fb5d35b7a0fa0a1e4a64f6d8bb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "186bad5bb433a8afeb16b01423950e440072284a4103034ca899180343b9b4ac"},
"makeup_eex": {:hex, :makeup_eex, "0.1.1", "89352d5da318d97ae27bbcc87201f274504d2b71ede58ca366af6a5fbed9508d", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.16", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_html, "~> 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d111a0994eaaab09ef1a4b3b313ef806513bb4652152c26c0d7ca2be8402a964"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
- "makeup_html": {:hex, :makeup_html, "0.1.0", "b0228fda985e311d8f0d25bed58f8280826633a38d7448cabdd723e116165bcf", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0ca44e7dcb8d933e010740324470dd8ec947243b51304bd34b8165ef3281edc2"},
+ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
+ "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
+ "makeup_html": {:hex, :makeup_html, "0.1.1", "c3d4abd39d5f7e925faca72ada6e9cc5c6f5fa7cd5bc0158315832656cf14d7f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "44f2a61bc5243645dd7fafeaa6cc28793cd22f3c76b861e066168f9a5b2c26a4"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mint": {:hex, :mint, "1.4.1", "49b3b6ea35a9a38836d2ad745251b01ca9ec062f7cb66f546bf22e6699137126", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "cd261766e61011a9079cccf8fa9d826e7a397c24fbedf0e11b49312bea629b58"},
"mint_web_socket": {:hex, :mint_web_socket, "1.0.0", "b33e534a938ec10736cef2b00cd485f6abd70aef68b9194f4d92fe2f7b8bba06", [:mix], [{:mint, "~> 1.4 and >= 1.4.1", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "3d4fd81190fe60f16fef5ade89e008463d72e6a608a7f6af9041cd8b47458e30"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
- "phoenix_html": {:hex, :phoenix_html, "3.3.0", "bf451c71ebdaac8d2f40d3b703435e819ccfbb9ff243140ca3bd10c155f134cc", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "272c5c1533499f0132309936c619186480bafcc2246588f99a69ce85095556ef"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
+ "phoenix_html": {:hex, :phoenix_html, "4.0.0", "4857ec2edaccd0934a923c2b0ba526c44a173c86b847e8db725172e9e51d11d6", [:mix], [], "hexpm", "cee794a052f243291d92fa3ccabcb4c29bb8d236f655fb03bcbdc3a8214b8d13"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
- "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
- "phoenix_view": {:hex, :phoenix_view, "2.0.0", "e676c3058cdfd878faece9cc791fe2f7c810877fdf002db46ee8c01403b4b801", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "6511dd875191aa737092d1e9a74c73858e10253f4111fec8ba9c1a215bfccc77"},
- "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
- "plug_cowboy": {:hex, :plug_cowboy, "2.6.1", "9a3bbfceeb65eff5f39dab529e5cd79137ac36e913c02067dba3963a26efe9b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "de36e1a21f451a18b790f37765db198075c25875c64834bcc82d90b309eb6613"},
- "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
+ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+ "phoenix_view": {:hex, :phoenix_view, "2.0.3", "4d32c4817fce933693741deeb99ef1392619f942633dde834a5163124813aad3", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "cd34049af41be2c627df99cd4eaa71fc52a328c0c3d8e7d4aa28f880c30e7f64"},
+ "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
+ "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
+ "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
- "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"},
+ "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
"telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"},
- "websock": {:hex, :websock, "0.5.2", "b3c08511d8d79ed2c2f589ff430bd1fe799bb389686dafce86d28801783d8351", [:mix], [], "hexpm", "925f5de22fca6813dfa980fb62fd542ec43a2d1a1f83d2caec907483fe66ff05"},
- "websock_adapter": {:hex, :websock_adapter, "0.5.3", "4908718e42e4a548fc20e00e70848620a92f11f7a6add8cf0886c4232267498d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "cbe5b814c1f86b6ea002b52dd99f345aeecf1a1a6964e209d208fb404d930d3d"},
+ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+ "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"},
}
diff --git a/package.json b/package.json
index ab40e6a5d1..2e1bdbc13c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "phoenix",
- "version": "1.7.7",
+ "version": "1.7.14",
"description": "The official JavaScript client for the Phoenix web framework.",
"license": "MIT",
"module": "./priv/static/phoenix.mjs",
diff --git a/priv/static/favicon.ico b/priv/static/favicon.ico
index 73de524aaa..7f372bfc21 100644
Binary files a/priv/static/favicon.ico and b/priv/static/favicon.ico differ
diff --git a/priv/static/phoenix.cjs.js b/priv/static/phoenix.cjs.js
index 81e30570ee..a4134516b8 100644
--- a/priv/static/phoenix.cjs.js
+++ b/priv/static/phoenix.cjs.js
@@ -83,11 +83,18 @@ var Push = class {
this.recHooks = [];
this.sent = false;
}
+ /**
+ *
+ * @param {number} timeout
+ */
resend(timeout) {
this.timeout = timeout;
this.reset();
this.send();
}
+ /**
+ *
+ */
send() {
if (this.hasReceived("timeout")) {
return;
@@ -102,6 +109,11 @@ var Push = class {
join_ref: this.channel.joinRef()
});
}
+ /**
+ *
+ * @param {*} status
+ * @param {*} callback
+ */
receive(status, callback) {
if (this.hasReceived(status)) {
callback(this.receivedResp.response);
@@ -109,6 +121,9 @@ var Push = class {
this.recHooks.push({ status, callback });
return this;
}
+ /**
+ * @private
+ */
reset() {
this.cancelRefEvent();
this.ref = null;
@@ -116,19 +131,31 @@ var Push = class {
this.receivedResp = null;
this.sent = false;
}
+ /**
+ * @private
+ */
matchReceive({ status, response, _ref }) {
this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
}
+ /**
+ * @private
+ */
cancelRefEvent() {
if (!this.refEvent) {
return;
}
this.channel.off(this.refEvent);
}
+ /**
+ * @private
+ */
cancelTimeout() {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
+ /**
+ * @private
+ */
startTimeout() {
if (this.timeoutTimer) {
this.cancelTimeout();
@@ -145,9 +172,15 @@ var Push = class {
this.trigger("timeout", {});
}, this.timeout);
}
+ /**
+ * @private
+ */
hasReceived(status) {
return this.receivedResp && this.receivedResp.status === status;
}
+ /**
+ * @private
+ */
trigger(status, response) {
this.channel.trigger(this.refEvent, { status, response });
}
@@ -165,6 +198,9 @@ var Timer = class {
this.tries = 0;
clearTimeout(this.timer);
}
+ /**
+ * Cancels any previous scheduleTimeout and schedules callback
+ */
scheduleTimeout() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
@@ -194,12 +230,14 @@ var Channel = class {
}
}, this.socket.rejoinAfterMs);
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()));
- this.stateChangeRefs.push(this.socket.onOpen(() => {
- this.rejoinTimer.reset();
- if (this.isErrored()) {
- this.rejoin();
- }
- }));
+ this.stateChangeRefs.push(
+ this.socket.onOpen(() => {
+ this.rejoinTimer.reset();
+ if (this.isErrored()) {
+ this.rejoin();
+ }
+ })
+ );
this.joinPush.receive("ok", () => {
this.state = CHANNEL_STATES.joined;
this.rejoinTimer.reset();
@@ -245,6 +283,11 @@ var Channel = class {
this.trigger(this.replyEventName(ref), payload);
});
}
+ /**
+ * Join the channel
+ * @param {integer} timeout
+ * @returns {Push}
+ */
join(timeout = this.timeout) {
if (this.joinedOnce) {
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");
@@ -255,25 +298,87 @@ var Channel = class {
return this.joinPush;
}
}
+ /**
+ * Hook into channel close
+ * @param {Function} callback
+ */
onClose(callback) {
this.on(CHANNEL_EVENTS.close, callback);
}
+ /**
+ * Hook into channel errors
+ * @param {Function} callback
+ */
onError(callback) {
return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason));
}
+ /**
+ * Subscribes on channel events
+ *
+ * Subscription returns a ref counter, which can be used later to
+ * unsubscribe the exact event listener
+ *
+ * @example
+ * const ref1 = channel.on("event", do_stuff)
+ * const ref2 = channel.on("event", do_other_stuff)
+ * channel.off("event", ref1)
+ * // Since unsubscription, do_stuff won't fire,
+ * // while do_other_stuff will keep firing on the "event"
+ *
+ * @param {string} event
+ * @param {Function} callback
+ * @returns {integer} ref
+ */
on(event, callback) {
let ref = this.bindingRef++;
this.bindings.push({ event, ref, callback });
return ref;
}
+ /**
+ * Unsubscribes off of channel events
+ *
+ * Use the ref returned from a channel.on() to unsubscribe one
+ * handler, or pass nothing for the ref to unsubscribe all
+ * handlers for the given event.
+ *
+ * @example
+ * // Unsubscribe the do_stuff handler
+ * const ref1 = channel.on("event", do_stuff)
+ * channel.off("event", ref1)
+ *
+ * // Unsubscribe all handlers from event
+ * channel.off("event")
+ *
+ * @param {string} event
+ * @param {integer} ref
+ */
off(event, ref) {
this.bindings = this.bindings.filter((bind) => {
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref));
});
}
+ /**
+ * @private
+ */
canPush() {
return this.socket.isConnected() && this.isJoined();
}
+ /**
+ * Sends a message `event` to phoenix with the payload `payload`.
+ * Phoenix receives this in the `handle_in(event, payload, socket)`
+ * function. if phoenix replies or it times out (default 10000ms),
+ * then optionally the reply can be received.
+ *
+ * @example
+ * channel.push("event")
+ * .receive("ok", payload => console.log("phoenix replied:", payload))
+ * .receive("error", err => console.log("phoenix errored", err))
+ * .receive("timeout", () => console.log("timed out pushing"))
+ * @param {string} event
+ * @param {Object} payload
+ * @param {number} [timeout]
+ * @returns {Push}
+ */
push(event, payload, timeout = this.timeout) {
payload = payload || {};
if (!this.joinedOnce) {
@@ -290,6 +395,22 @@ var Channel = class {
}
return pushEvent;
}
+ /** Leaves the channel
+ *
+ * Unsubscribes from server events, and
+ * instructs channel to terminate on server
+ *
+ * Triggers onClose() hooks
+ *
+ * To receive leave acknowledgements, use the `receive`
+ * hook to bind to the server ack, ie:
+ *
+ * @example
+ * channel.leave().receive("ok", () => alert("left!") )
+ *
+ * @param {integer} timeout
+ * @returns {Push}
+ */
leave(timeout = this.timeout) {
this.rejoinTimer.reset();
this.joinPush.cancelTimeout();
@@ -307,9 +428,24 @@ var Channel = class {
}
return leavePush;
}
+ /**
+ * Overridable message hook
+ *
+ * Receives all events for specialized message handling
+ * before dispatching to the channel callbacks.
+ *
+ * Must return the payload, modified or unmodified
+ * @param {string} event
+ * @param {Object} payload
+ * @param {integer} ref
+ * @returns {Object}
+ */
onMessage(_event, payload, _ref) {
return payload;
}
+ /**
+ * @private
+ */
isMember(topic, event, payload, joinRef) {
if (this.topic !== topic) {
return false;
@@ -322,9 +458,15 @@ var Channel = class {
return true;
}
}
+ /**
+ * @private
+ */
joinRef() {
return this.joinPush.ref;
}
+ /**
+ * @private
+ */
rejoin(timeout = this.timeout) {
if (this.isLeaving()) {
return;
@@ -333,6 +475,9 @@ var Channel = class {
this.state = CHANNEL_STATES.joining;
this.joinPush.resend(timeout);
}
+ /**
+ * @private
+ */
trigger(event, payload, ref, joinRef) {
let handledPayload = this.onMessage(event, payload, ref, joinRef);
if (payload && !handledPayload) {
@@ -344,21 +489,39 @@ var Channel = class {
bind.callback(handledPayload, ref, joinRef || this.joinRef());
}
}
+ /**
+ * @private
+ */
replyEventName(ref) {
return `chan_reply_${ref}`;
}
+ /**
+ * @private
+ */
isClosed() {
return this.state === CHANNEL_STATES.closed;
}
+ /**
+ * @private
+ */
isErrored() {
return this.state === CHANNEL_STATES.errored;
}
+ /**
+ * @private
+ */
isJoined() {
return this.state === CHANNEL_STATES.joined;
}
+ /**
+ * @private
+ */
isJoining() {
return this.state === CHANNEL_STATES.joining;
}
+ /**
+ * @private
+ */
isLeaving() {
return this.state === CHANNEL_STATES.leaving;
}
@@ -473,7 +636,7 @@ var LongPoll = class {
};
this.pollEndpoint = this.normalizeEndpoint(endPoint);
this.readyState = SOCKET_STATES.connecting;
- this.poll();
+ setTimeout(() => this.poll(), 0);
}
normalizeEndpoint(endPoint) {
return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
@@ -529,6 +692,9 @@ var LongPoll = class {
}
});
}
+ // we collect all pushes within the current event loop by
+ // setTimeout 0, which optimizes back-to-back procedural
+ // pushes against an empty buffer
send(body) {
if (typeof body !== "string") {
body = arrayBufferToBase64(body);
@@ -640,6 +806,15 @@ var Presence = class {
inPendingSyncState() {
return !this.joinRef || this.joinRef !== this.channel.joinRef();
}
+ // lower-level public static API
+ /**
+ * Used to sync the list of presences on the server
+ * with the client's state. An optional `onJoin` and `onLeave` callback can
+ * be provided to react to changes in the client's local presences across
+ * disconnects and reconnects with the server.
+ *
+ * @returns {Presence}
+ */
static syncState(currentState, newState, onJoin, onLeave) {
let state = this.clone(currentState);
let joins = {};
@@ -670,6 +845,15 @@ var Presence = class {
});
return this.syncDiff(state, { joins, leaves }, onJoin, onLeave);
}
+ /**
+ *
+ * Used to sync a diff of presence join and leave
+ * events from the server, as they happen. Like `syncState`, `syncDiff`
+ * accepts optional `onJoin` and `onLeave` callbacks to react to a user
+ * joining or leaving from a device.
+ *
+ * @returns {Presence}
+ */
static syncDiff(state, diff, onJoin, onLeave) {
let { joins, leaves } = this.clone(diff);
if (!onJoin) {
@@ -706,6 +890,14 @@ var Presence = class {
});
return state;
}
+ /**
+ * Returns the array of presences, with selected metadata.
+ *
+ * @param {Object} presences
+ * @param {Function} chooser
+ *
+ * @returns {Presence}
+ */
static list(presences, chooser) {
if (!chooser) {
chooser = function(key, pres) {
@@ -716,6 +908,7 @@ var Presence = class {
return chooser(key, presence);
});
}
+ // private
static map(obj, func) {
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
}
@@ -745,6 +938,7 @@ var serializer_default = {
return callback({ join_ref, ref, topic, event, payload });
}
},
+ // private
binaryEncode(message) {
let { join_ref, ref, event, topic, payload } = message;
let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length;
@@ -832,6 +1026,10 @@ var Socket = class {
this.ref = 0;
this.timeout = opts.timeout || DEFAULT_TIMEOUT;
this.transport = opts.transport || global.WebSocket || LongPoll;
+ this.primaryPassedHealthCheck = false;
+ this.longPollFallbackMs = opts.longPollFallbackMs;
+ this.fallbackTimer = null;
+ this.sessionStore = opts.sessionStorage || global && global.sessionStorage;
this.establishedConnections = 0;
this.defaultEncoder = serializer_default.encode.bind(serializer_default);
this.defaultDecoder = serializer_default.decode.bind(serializer_default);
@@ -876,6 +1074,11 @@ var Socket = class {
}
};
this.logger = opts.logger || null;
+ if (!this.logger && opts.debug) {
+ this.logger = (kind, msg, data) => {
+ console.log(`${kind}: ${msg}`, data);
+ };
+ }
this.longpollerTimeout = opts.longpollerTimeout || 2e4;
this.params = closure(opts.params || {});
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
@@ -887,25 +1090,47 @@ var Socket = class {
this.teardown(() => this.connect());
}, this.reconnectAfterMs);
}
+ /**
+ * Returns the LongPoll transport reference
+ */
getLongPollTransport() {
return LongPoll;
}
+ /**
+ * Disconnects and replaces the active transport
+ *
+ * @param {Function} newTransport - The new transport class to instantiate
+ *
+ */
replaceTransport(newTransport) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
- this.sendBuffer = [];
if (this.conn) {
this.conn.close();
this.conn = null;
}
this.transport = newTransport;
}
+ /**
+ * Returns the socket protocol
+ *
+ * @returns {string}
+ */
protocol() {
return location.protocol.match(/^https/) ? "wss" : "ws";
}
+ /**
+ * The fully qualified socket url
+ *
+ * @returns {string}
+ */
endPointURL() {
- let uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params()), { vsn: this.vsn });
+ let uri = Ajax.appendParams(
+ Ajax.appendParams(this.endPoint, this.params()),
+ { vsn: this.vsn }
+ );
if (uri.charAt(0) !== "/") {
return uri;
}
@@ -914,12 +1139,29 @@ var Socket = class {
}
return `${this.protocol()}://${location.host}${uri}`;
}
+ /**
+ * Disconnects the socket
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
+ *
+ * @param {Function} callback - Optional callback which is called after socket is disconnected.
+ * @param {integer} code - A status code for disconnection (Optional).
+ * @param {string} reason - A textual description of the reason to disconnect. (Optional)
+ */
disconnect(callback, code, reason) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
this.teardown(callback, code, reason);
}
+ /**
+ *
+ * @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) {
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
@@ -928,42 +1170,75 @@ var Socket = class {
if (this.conn) {
return;
}
- this.connectClock++;
- this.closeWasClean = false;
- this.conn = new this.transport(this.endPointURL());
- this.conn.binaryType = this.binaryType;
- this.conn.timeout = this.longpollerTimeout;
- this.conn.onopen = () => this.onConnOpen();
- this.conn.onerror = (error) => this.onConnError(error);
- this.conn.onmessage = (event) => this.onConnMessage(event);
- this.conn.onclose = (event) => this.onConnClose(event);
+ if (this.longPollFallbackMs && this.transport !== LongPoll) {
+ this.connectWithFallback(LongPoll, this.longPollFallbackMs);
+ } else {
+ this.transportConnect();
+ }
}
+ /**
+ * Logs the message. Override `this.logger` for specialized logging. noops by default
+ * @param {string} kind
+ * @param {string} msg
+ * @param {Object} data
+ */
log(kind, msg, data) {
- this.logger(kind, msg, data);
+ this.logger && this.logger(kind, msg, data);
}
+ /**
+ * Returns true if a logger has been set on this socket.
+ */
hasLogger() {
return this.logger !== null;
}
+ /**
+ * Registers callbacks for connection open events
+ *
+ * @example socket.onOpen(function(){ console.info("the socket was opened") })
+ *
+ * @param {Function} callback
+ */
onOpen(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.open.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection close events
+ * @param {Function} callback
+ */
onClose(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.close.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection error events
+ *
+ * @example socket.onError(function(error){ alert("An error occurred") })
+ *
+ * @param {Function} callback
+ */
onError(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.error.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection message events
+ * @param {Function} callback
+ */
onMessage(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.message.push([ref, callback]);
return ref;
}
+ /**
+ * Pings the server and invokes the callback with the RTT in milliseconds
+ * @param {Function} callback
+ *
+ * Returns true if the ping was pushed or false if unable to be pushed.
+ */
ping(callback) {
if (!this.isConnected()) {
return false;
@@ -979,13 +1254,74 @@ var Socket = class {
});
return true;
}
+ /**
+ * @private
+ */
+ transportConnect() {
+ this.connectClock++;
+ this.closeWasClean = false;
+ this.conn = new this.transport(this.endPointURL());
+ this.conn.binaryType = this.binaryType;
+ this.conn.timeout = this.longpollerTimeout;
+ this.conn.onopen = () => this.onConnOpen();
+ this.conn.onerror = (error) => this.onConnError(error);
+ this.conn.onmessage = (event) => this.onConnMessage(event);
+ this.conn.onclose = (event) => this.onConnClose(event);
+ }
+ getSession(key) {
+ return this.sessionStore && this.sessionStore.getItem(key);
+ }
+ storeSession(key, val) {
+ this.sessionStore && this.sessionStore.setItem(key, val);
+ }
+ connectWithFallback(fallbackTransport, fallbackThreshold = 2500) {
+ clearTimeout(this.fallbackTimer);
+ let established = false;
+ let primaryTransport = true;
+ let openRef, errorRef;
+ let fallback = (reason) => {
+ this.log("transport", `falling back to ${fallbackTransport.name}...`, reason);
+ this.off([openRef, errorRef]);
+ primaryTransport = false;
+ this.replaceTransport(fallbackTransport);
+ this.transportConnect();
+ };
+ if (this.getSession(`phx:fallback:${fallbackTransport.name}`)) {
+ return fallback("memorized");
+ }
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ errorRef = this.onError((reason) => {
+ this.log("transport", "error", reason);
+ if (primaryTransport && !established) {
+ clearTimeout(this.fallbackTimer);
+ fallback(reason);
+ }
+ });
+ this.onOpen(() => {
+ established = true;
+ if (!primaryTransport) {
+ if (!this.primaryPassedHealthCheck) {
+ this.storeSession(`phx:fallback:${fallbackTransport.name}`, "true");
+ }
+ return this.log("transport", `established ${fallbackTransport.name} fallback`);
+ }
+ clearTimeout(this.fallbackTimer);
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ this.ping((rtt) => {
+ this.log("transport", "connected to primary after", rtt);
+ this.primaryPassedHealthCheck = true;
+ clearTimeout(this.fallbackTimer);
+ });
+ });
+ this.transportConnect();
+ }
clearHeartbeats() {
clearTimeout(this.heartbeatTimer);
clearTimeout(this.heartbeatTimeoutTimer);
}
onConnOpen() {
if (this.hasLogger())
- this.log("transport", `connected to ${this.endPointURL()}`);
+ this.log("transport", `${this.transport.name} connected to ${this.endPointURL()}`);
this.closeWasClean = false;
this.establishedConnections++;
this.flushSendBuffer();
@@ -993,6 +1329,9 @@ var Socket = class {
this.resetHeartbeat();
this.stateChangeCallbacks.open.forEach(([, callback]) => callback());
}
+ /**
+ * @private
+ */
heartbeatTimeout() {
if (this.pendingHeartbeatRef) {
this.pendingHeartbeatRef = null;
@@ -1069,6 +1408,9 @@ var Socket = class {
}
this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event));
}
+ /**
+ * @private
+ */
onConnError(error) {
if (this.hasLogger())
this.log("transport", error);
@@ -1081,6 +1423,9 @@ var Socket = class {
this.triggerChanError();
}
}
+ /**
+ * @private
+ */
triggerChanError() {
this.channels.forEach((channel) => {
if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) {
@@ -1088,6 +1433,9 @@ var Socket = class {
}
});
}
+ /**
+ * @returns {string}
+ */
connectionState() {
switch (this.conn && this.conn.readyState) {
case SOCKET_STATES.connecting:
@@ -1100,13 +1448,27 @@ var Socket = class {
return "closed";
}
}
+ /**
+ * @returns {boolean}
+ */
isConnected() {
return this.connectionState() === "open";
}
+ /**
+ * @private
+ *
+ * @param {Channel}
+ */
remove(channel) {
this.off(channel.stateChangeRefs);
- this.channels = this.channels.filter((c) => c.joinRef() !== channel.joinRef());
- }
+ this.channels = this.channels.filter((c) => c !== channel);
+ }
+ /**
+ * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
+ *
+ * @param {refs} - list of refs returned by calls to
+ * `onOpen`, `onClose`, `onError,` and `onMessage`
+ */
off(refs) {
for (let key in this.stateChangeCallbacks) {
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
@@ -1114,11 +1476,21 @@ var Socket = class {
});
}
}
+ /**
+ * Initiates a new channel for the given topic
+ *
+ * @param {string} topic
+ * @param {Object} chanParams - Parameters for the channel
+ * @returns {Channel}
+ */
channel(topic, chanParams = {}) {
let chan = new Channel(topic, chanParams, this);
this.channels.push(chan);
return chan;
}
+ /**
+ * @param {Object} data
+ */
push(data) {
if (this.hasLogger()) {
let { topic, event, payload, ref, join_ref } = data;
@@ -1130,6 +1502,10 @@ var Socket = class {
this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
}
}
+ /**
+ * Return the next message ref, accounting for overflows
+ * @returns {string}
+ */
makeRef() {
let newRef = this.ref + 1;
if (newRef === this.ref) {
diff --git a/priv/static/phoenix.cjs.js.map b/priv/static/phoenix.cjs.js.map
index 933c13f91c..30ec76025d 100644
--- a/priv/static/phoenix.cjs.js.map
+++ b/priv/static/phoenix.cjs.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../assets/js/phoenix/index.js", "../../assets/js/phoenix/utils.js", "../../assets/js/phoenix/constants.js", "../../assets/js/phoenix/push.js", "../../assets/js/phoenix/timer.js", "../../assets/js/phoenix/channel.js", "../../assets/js/phoenix/ajax.js", "../../assets/js/phoenix/longpoll.js", "../../assets/js/phoenix/presence.js", "../../assets/js/phoenix/serializer.js", "../../assets/js/phoenix/socket.js"],
- "sourcesContent": ["/**\n * Phoenix Channels JavaScript client\n *\n * ## Socket Connection\n *\n * A single connection is established to the server and\n * channels are multiplexed over the connection.\n * Connect to the server using the `Socket` class:\n *\n * ```javascript\n * let socket = new Socket(\"/socket\", {params: {userToken: \"123\"}})\n * socket.connect()\n * ```\n *\n * The `Socket` constructor takes the mount point of the socket,\n * the authentication params, as well as options that can be found in\n * the Socket docs, such as configuring the `LongPoll` transport, and\n * heartbeat.\n *\n * ## Channels\n *\n * Channels are isolated, concurrent processes on the server that\n * subscribe to topics and broker events between the client and server.\n * To join a channel, you must provide the topic, and channel params for\n * authorization. Here's an example chat room example where `\"new_msg\"`\n * events are listened for, messages are pushed to the server, and\n * the channel is joined with ok/error/timeout matches:\n *\n * ```javascript\n * let channel = socket.channel(\"room:123\", {token: roomToken})\n * channel.on(\"new_msg\", msg => console.log(\"Got message\", msg) )\n * $input.onEnter( e => {\n * channel.push(\"new_msg\", {body: e.target.val}, 10000)\n * .receive(\"ok\", (msg) => console.log(\"created message\", msg) )\n * .receive(\"error\", (reasons) => console.log(\"create failed\", reasons) )\n * .receive(\"timeout\", () => console.log(\"Networking issue...\") )\n * })\n *\n * channel.join()\n * .receive(\"ok\", ({messages}) => console.log(\"catching up\", messages) )\n * .receive(\"error\", ({reason}) => console.log(\"failed join\", reason) )\n * .receive(\"timeout\", () => console.log(\"Networking issue. Still waiting...\"))\n *```\n *\n * ## Joining\n *\n * Creating a channel with `socket.channel(topic, params)`, binds the params to\n * `channel.params`, which are sent up on `channel.join()`.\n * Subsequent rejoins will send up the modified params for\n * updating authorization params, or passing up last_message_id information.\n * Successful joins receive an \"ok\" status, while unsuccessful joins\n * receive \"error\".\n *\n * With the default serializers and WebSocket transport, JSON text frames are\n * used for pushing a JSON object literal. If an `ArrayBuffer` instance is provided,\n * binary encoding will be used and the message will be sent with the binary\n * opcode.\n *\n * *Note*: binary messages are only supported on the WebSocket transport.\n *\n * ## Duplicate Join Subscriptions\n *\n * While the client may join any number of topics on any number of channels,\n * the client may only hold a single subscription for each unique topic at any\n * given time. When attempting to create a duplicate subscription,\n * the server will close the existing channel, log a warning, and\n * spawn a new channel for the topic. The client will have their\n * `channel.onClose` callbacks fired for the existing channel, and the new\n * channel join will have its receive hooks processed as normal.\n *\n * ## Pushing Messages\n *\n * From the previous example, we can see that pushing messages to the server\n * can be done with `channel.push(eventName, payload)` and we can optionally\n * receive responses from the push. Additionally, we can use\n * `receive(\"timeout\", callback)` to abort waiting for our other `receive` hooks\n * and take action after some period of waiting. The default timeout is 10000ms.\n *\n *\n * ## Socket Hooks\n *\n * Lifecycle events of the multiplexed connection can be hooked into via\n * `socket.onError()` and `socket.onClose()` events, ie:\n *\n * ```javascript\n * socket.onError( () => console.log(\"there was an error with the connection!\") )\n * socket.onClose( () => console.log(\"the connection dropped\") )\n * ```\n *\n *\n * ## Channel Hooks\n *\n * For each joined channel, you can bind to `onError` and `onClose` events\n * to monitor the channel lifecycle, ie:\n *\n * ```javascript\n * channel.onError( () => console.log(\"there was an error!\") )\n * channel.onClose( () => console.log(\"the channel has gone away gracefully\") )\n * ```\n *\n * ### onError hooks\n *\n * `onError` hooks are invoked if the socket connection drops, or the channel\n * crashes on the server. In either case, a channel rejoin is attempted\n * automatically in an exponential backoff manner.\n *\n * ### onClose hooks\n *\n * `onClose` hooks are invoked only in two cases. 1) the channel explicitly\n * closed on the server, or 2). The client explicitly closed, by calling\n * `channel.leave()`\n *\n *\n * ## Presence\n *\n * The `Presence` object provides features for syncing presence information\n * from the server with the client and handling presences joining and leaving.\n *\n * ### Syncing state from the server\n *\n * To sync presence state from the server, first instantiate an object and\n * pass your channel in to track lifecycle events:\n *\n * ```javascript\n * let channel = socket.channel(\"some:topic\")\n * let presence = new Presence(channel)\n * ```\n *\n * Next, use the `presence.onSync` callback to react to state changes\n * from the server. For example, to render the list of users every time\n * the list changes, you could write:\n *\n * ```javascript\n * presence.onSync(() => {\n * myRenderUsersFunction(presence.list())\n * })\n * ```\n *\n * ### Listing Presences\n *\n * `presence.list` is used to return a list of presence information\n * based on the local state of metadata. By default, all presence\n * metadata is returned, but a `listBy` function can be supplied to\n * allow the client to select which metadata to use for a given presence.\n * For example, you may have a user online from different devices with\n * a metadata status of \"online\", but they have set themselves to \"away\"\n * on another device. In this case, the app may choose to use the \"away\"\n * status for what appears on the UI. The example below defines a `listBy`\n * function which prioritizes the first metadata which was registered for\n * each user. This could be the first tab they opened, or the first device\n * they came online from:\n *\n * ```javascript\n * let listBy = (id, {metas: [first, ...rest]}) => {\n * first.count = rest.length + 1 // count of this user's presences\n * first.id = id\n * return first\n * }\n * let onlineUsers = presence.list(listBy)\n * ```\n *\n * ### Handling individual presence join and leave events\n *\n * The `presence.onJoin` and `presence.onLeave` callbacks can be used to\n * react to individual presences joining and leaving the app. For example:\n *\n * ```javascript\n * let presence = new Presence(channel)\n *\n * // detect if user has joined for the 1st time or from another tab/device\n * presence.onJoin((id, current, newPres) => {\n * if(!current){\n * console.log(\"user has entered for the first time\", newPres)\n * } else {\n * console.log(\"user additional presence\", newPres)\n * }\n * })\n *\n * // detect if user has left from all tabs/devices, or is still present\n * presence.onLeave((id, current, leftPres) => {\n * if(current.metas.length === 0){\n * console.log(\"user has left from all devices\", leftPres)\n * } else {\n * console.log(\"user left from a device\", leftPres)\n * }\n * })\n * // receive presence data from server\n * presence.onSync(() => {\n * displayUsers(presence.list())\n * })\n * ```\n * @module phoenix\n */\n\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Presence from \"./presence\"\nimport Serializer from \"./serializer\"\nimport Socket from \"./socket\"\n\nexport {\n Channel,\n LongPoll,\n Presence,\n Serializer,\n Socket\n}\n", "// wraps value in closure or returns closure\nexport let closure = (value) => {\n if(typeof value === \"function\"){\n return value\n } else {\n let closure = function (){ return value }\n return closure\n }\n}\n", "export const globalSelf = typeof self !== \"undefined\" ? self : null\nexport const phxWindow = typeof window !== \"undefined\" ? window : null\nexport const global = globalSelf || phxWindow || global\nexport const DEFAULT_VSN = \"2.0.0\"\nexport const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}\nexport const DEFAULT_TIMEOUT = 10000\nexport const WS_CLOSE_NORMAL = 1000\nexport const CHANNEL_STATES = {\n closed: \"closed\",\n errored: \"errored\",\n joined: \"joined\",\n joining: \"joining\",\n leaving: \"leaving\",\n}\nexport const CHANNEL_EVENTS = {\n close: \"phx_close\",\n error: \"phx_error\",\n join: \"phx_join\",\n reply: \"phx_reply\",\n leave: \"phx_leave\"\n}\n\nexport const TRANSPORTS = {\n longpoll: \"longpoll\",\n websocket: \"websocket\"\n}\nexport const XHR_STATES = {\n complete: 4\n}\n", "/**\n * Initializes the Push\n * @param {Channel} channel - The Channel\n * @param {string} event - The event, for example `\"phx_join\"`\n * @param {Object} payload - The payload, for example `{user_id: 123}`\n * @param {number} timeout - The push timeout in milliseconds\n */\nexport default class Push {\n constructor(channel, event, payload, timeout){\n this.channel = channel\n this.event = event\n this.payload = payload || function (){ return {} }\n this.receivedResp = null\n this.timeout = timeout\n this.timeoutTimer = null\n this.recHooks = []\n this.sent = false\n }\n\n /**\n *\n * @param {number} timeout\n */\n resend(timeout){\n this.timeout = timeout\n this.reset()\n this.send()\n }\n\n /**\n *\n */\n send(){\n if(this.hasReceived(\"timeout\")){ return }\n this.startTimeout()\n this.sent = true\n this.channel.socket.push({\n topic: this.channel.topic,\n event: this.event,\n payload: this.payload(),\n ref: this.ref,\n join_ref: this.channel.joinRef()\n })\n }\n\n /**\n *\n * @param {*} status\n * @param {*} callback\n */\n receive(status, callback){\n if(this.hasReceived(status)){\n callback(this.receivedResp.response)\n }\n\n this.recHooks.push({status, callback})\n return this\n }\n\n /**\n * @private\n */\n reset(){\n this.cancelRefEvent()\n this.ref = null\n this.refEvent = null\n this.receivedResp = null\n this.sent = false\n }\n\n /**\n * @private\n */\n matchReceive({status, response, _ref}){\n this.recHooks.filter(h => h.status === status)\n .forEach(h => h.callback(response))\n }\n\n /**\n * @private\n */\n cancelRefEvent(){\n if(!this.refEvent){ return }\n this.channel.off(this.refEvent)\n }\n\n /**\n * @private\n */\n cancelTimeout(){\n clearTimeout(this.timeoutTimer)\n this.timeoutTimer = null\n }\n\n /**\n * @private\n */\n startTimeout(){\n if(this.timeoutTimer){ this.cancelTimeout() }\n this.ref = this.channel.socket.makeRef()\n this.refEvent = this.channel.replyEventName(this.ref)\n\n this.channel.on(this.refEvent, payload => {\n this.cancelRefEvent()\n this.cancelTimeout()\n this.receivedResp = payload\n this.matchReceive(payload)\n })\n\n this.timeoutTimer = setTimeout(() => {\n this.trigger(\"timeout\", {})\n }, this.timeout)\n }\n\n /**\n * @private\n */\n hasReceived(status){\n return this.receivedResp && this.receivedResp.status === status\n }\n\n /**\n * @private\n */\n trigger(status, response){\n this.channel.trigger(this.refEvent, {status, response})\n }\n}\n", "/**\n *\n * Creates a timer that accepts a `timerCalc` function to perform\n * calculated timeout retries, such as exponential backoff.\n *\n * @example\n * let reconnectTimer = new Timer(() => this.connect(), function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * })\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * reconnectTimer.scheduleTimeout() // fires after 5000\n * reconnectTimer.reset()\n * reconnectTimer.scheduleTimeout() // fires after 1000\n *\n * @param {Function} callback\n * @param {Function} timerCalc\n */\nexport default class Timer {\n constructor(callback, timerCalc){\n this.callback = callback\n this.timerCalc = timerCalc\n this.timer = null\n this.tries = 0\n }\n\n reset(){\n this.tries = 0\n clearTimeout(this.timer)\n }\n\n /**\n * Cancels any previous scheduleTimeout and schedules callback\n */\n scheduleTimeout(){\n clearTimeout(this.timer)\n\n this.timer = setTimeout(() => {\n this.tries = this.tries + 1\n this.callback()\n }, this.timerCalc(this.tries + 1))\n }\n}\n", "import {closure} from \"./utils\"\nimport {\n CHANNEL_EVENTS,\n CHANNEL_STATES,\n} from \"./constants\"\n\nimport Push from \"./push\"\nimport Timer from \"./timer\"\n\n/**\n *\n * @param {string} topic\n * @param {(Object|function)} params\n * @param {Socket} socket\n */\nexport default class Channel {\n constructor(topic, params, socket){\n this.state = CHANNEL_STATES.closed\n this.topic = topic\n this.params = closure(params || {})\n this.socket = socket\n this.bindings = []\n this.bindingRef = 0\n this.timeout = this.socket.timeout\n this.joinedOnce = false\n this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)\n this.pushBuffer = []\n this.stateChangeRefs = []\n\n this.rejoinTimer = new Timer(() => {\n if(this.socket.isConnected()){ this.rejoin() }\n }, this.socket.rejoinAfterMs)\n this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))\n this.stateChangeRefs.push(this.socket.onOpen(() => {\n this.rejoinTimer.reset()\n if(this.isErrored()){ this.rejoin() }\n })\n )\n this.joinPush.receive(\"ok\", () => {\n this.state = CHANNEL_STATES.joined\n this.rejoinTimer.reset()\n this.pushBuffer.forEach(pushEvent => pushEvent.send())\n this.pushBuffer = []\n })\n this.joinPush.receive(\"error\", () => {\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.onClose(() => {\n this.rejoinTimer.reset()\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `close ${this.topic} ${this.joinRef()}`)\n this.state = CHANNEL_STATES.closed\n this.socket.remove(this)\n })\n this.onError(reason => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `error ${this.topic}`, reason)\n if(this.isJoining()){ this.joinPush.reset() }\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.joinPush.receive(\"timeout\", () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)\n leavePush.send()\n this.state = CHANNEL_STATES.errored\n this.joinPush.reset()\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.on(CHANNEL_EVENTS.reply, (payload, ref) => {\n this.trigger(this.replyEventName(ref), payload)\n })\n }\n\n /**\n * Join the channel\n * @param {integer} timeout\n * @returns {Push}\n */\n join(timeout = this.timeout){\n if(this.joinedOnce){\n throw new Error(\"tried to join multiple times. 'join' can only be called a single time per channel instance\")\n } else {\n this.timeout = timeout\n this.joinedOnce = true\n this.rejoin()\n return this.joinPush\n }\n }\n\n /**\n * Hook into channel close\n * @param {Function} callback\n */\n onClose(callback){\n this.on(CHANNEL_EVENTS.close, callback)\n }\n\n /**\n * Hook into channel errors\n * @param {Function} callback\n */\n onError(callback){\n return this.on(CHANNEL_EVENTS.error, reason => callback(reason))\n }\n\n /**\n * Subscribes on channel events\n *\n * Subscription returns a ref counter, which can be used later to\n * unsubscribe the exact event listener\n *\n * @example\n * const ref1 = channel.on(\"event\", do_stuff)\n * const ref2 = channel.on(\"event\", do_other_stuff)\n * channel.off(\"event\", ref1)\n * // Since unsubscription, do_stuff won't fire,\n * // while do_other_stuff will keep firing on the \"event\"\n *\n * @param {string} event\n * @param {Function} callback\n * @returns {integer} ref\n */\n on(event, callback){\n let ref = this.bindingRef++\n this.bindings.push({event, ref, callback})\n return ref\n }\n\n /**\n * Unsubscribes off of channel events\n *\n * Use the ref returned from a channel.on() to unsubscribe one\n * handler, or pass nothing for the ref to unsubscribe all\n * handlers for the given event.\n *\n * @example\n * // Unsubscribe the do_stuff handler\n * const ref1 = channel.on(\"event\", do_stuff)\n * channel.off(\"event\", ref1)\n *\n * // Unsubscribe all handlers from event\n * channel.off(\"event\")\n *\n * @param {string} event\n * @param {integer} ref\n */\n off(event, ref){\n this.bindings = this.bindings.filter((bind) => {\n return !(bind.event === event && (typeof ref === \"undefined\" || ref === bind.ref))\n })\n }\n\n /**\n * @private\n */\n canPush(){ return this.socket.isConnected() && this.isJoined() }\n\n /**\n * Sends a message `event` to phoenix with the payload `payload`.\n * Phoenix receives this in the `handle_in(event, payload, socket)`\n * function. if phoenix replies or it times out (default 10000ms),\n * then optionally the reply can be received.\n *\n * @example\n * channel.push(\"event\")\n * .receive(\"ok\", payload => console.log(\"phoenix replied:\", payload))\n * .receive(\"error\", err => console.log(\"phoenix errored\", err))\n * .receive(\"timeout\", () => console.log(\"timed out pushing\"))\n * @param {string} event\n * @param {Object} payload\n * @param {number} [timeout]\n * @returns {Push}\n */\n push(event, payload, timeout = this.timeout){\n payload = payload || {}\n if(!this.joinedOnce){\n throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)\n }\n let pushEvent = new Push(this, event, function (){ return payload }, timeout)\n if(this.canPush()){\n pushEvent.send()\n } else {\n pushEvent.startTimeout()\n this.pushBuffer.push(pushEvent)\n }\n\n return pushEvent\n }\n\n /** Leaves the channel\n *\n * Unsubscribes from server events, and\n * instructs channel to terminate on server\n *\n * Triggers onClose() hooks\n *\n * To receive leave acknowledgements, use the `receive`\n * hook to bind to the server ack, ie:\n *\n * @example\n * channel.leave().receive(\"ok\", () => alert(\"left!\") )\n *\n * @param {integer} timeout\n * @returns {Push}\n */\n leave(timeout = this.timeout){\n this.rejoinTimer.reset()\n this.joinPush.cancelTimeout()\n\n this.state = CHANNEL_STATES.leaving\n let onClose = () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `leave ${this.topic}`)\n this.trigger(CHANNEL_EVENTS.close, \"leave\")\n }\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)\n leavePush.receive(\"ok\", () => onClose())\n .receive(\"timeout\", () => onClose())\n leavePush.send()\n if(!this.canPush()){ leavePush.trigger(\"ok\", {}) }\n\n return leavePush\n }\n\n /**\n * Overridable message hook\n *\n * Receives all events for specialized message handling\n * before dispatching to the channel callbacks.\n *\n * Must return the payload, modified or unmodified\n * @param {string} event\n * @param {Object} payload\n * @param {integer} ref\n * @returns {Object}\n */\n onMessage(_event, payload, _ref){ return payload }\n\n /**\n * @private\n */\n isMember(topic, event, payload, joinRef){\n if(this.topic !== topic){ return false }\n\n if(joinRef && joinRef !== this.joinRef()){\n if(this.socket.hasLogger()) this.socket.log(\"channel\", \"dropping outdated message\", {topic, event, payload, joinRef})\n return false\n } else {\n return true\n }\n }\n\n /**\n * @private\n */\n joinRef(){ return this.joinPush.ref }\n\n /**\n * @private\n */\n rejoin(timeout = this.timeout){\n if(this.isLeaving()){ return }\n this.socket.leaveOpenTopic(this.topic)\n this.state = CHANNEL_STATES.joining\n this.joinPush.resend(timeout)\n }\n\n /**\n * @private\n */\n trigger(event, payload, ref, joinRef){\n let handledPayload = this.onMessage(event, payload, ref, joinRef)\n if(payload && !handledPayload){ throw new Error(\"channel onMessage callbacks must return the payload, modified or unmodified\") }\n\n let eventBindings = this.bindings.filter(bind => bind.event === event)\n\n for(let i = 0; i < eventBindings.length; i++){\n let bind = eventBindings[i]\n bind.callback(handledPayload, ref, joinRef || this.joinRef())\n }\n }\n\n /**\n * @private\n */\n replyEventName(ref){ return `chan_reply_${ref}` }\n\n /**\n * @private\n */\n isClosed(){ return this.state === CHANNEL_STATES.closed }\n\n /**\n * @private\n */\n isErrored(){ return this.state === CHANNEL_STATES.errored }\n\n /**\n * @private\n */\n isJoined(){ return this.state === CHANNEL_STATES.joined }\n\n /**\n * @private\n */\n isJoining(){ return this.state === CHANNEL_STATES.joining }\n\n /**\n * @private\n */\n isLeaving(){ return this.state === CHANNEL_STATES.leaving }\n}\n", "import {\n global,\n XHR_STATES\n} from \"./constants\"\n\nexport default class Ajax {\n\n static request(method, endPoint, accept, body, timeout, ontimeout, callback){\n if(global.XDomainRequest){\n let req = new global.XDomainRequest() // IE8, IE9\n return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)\n } else {\n let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari\n return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)\n }\n }\n\n static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){\n req.timeout = timeout\n req.open(method, endPoint)\n req.onload = () => {\n let response = this.parseJSON(req.responseText)\n callback && callback(response)\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n // Work around bug in IE9 that requires an attached onprogress handler\n req.onprogress = () => { }\n\n req.send(body)\n return req\n }\n\n static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){\n req.open(method, endPoint, true)\n req.timeout = timeout\n req.setRequestHeader(\"Content-Type\", accept)\n req.onerror = () => callback && callback(null)\n req.onreadystatechange = () => {\n if(req.readyState === XHR_STATES.complete && callback){\n let response = this.parseJSON(req.responseText)\n callback(response)\n }\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n req.send(body)\n return req\n }\n\n static parseJSON(resp){\n if(!resp || resp === \"\"){ return null }\n\n try {\n return JSON.parse(resp)\n } catch (e){\n console && console.log(\"failed to parse JSON response\", resp)\n return null\n }\n }\n\n static serialize(obj, parentKey){\n let queryStr = []\n for(var key in obj){\n if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }\n let paramKey = parentKey ? `${parentKey}[${key}]` : key\n let paramVal = obj[key]\n if(typeof paramVal === \"object\"){\n queryStr.push(this.serialize(paramVal, paramKey))\n } else {\n queryStr.push(encodeURIComponent(paramKey) + \"=\" + encodeURIComponent(paramVal))\n }\n }\n return queryStr.join(\"&\")\n }\n\n static appendParams(url, params){\n if(Object.keys(params).length === 0){ return url }\n\n let prefix = url.match(/\\?/) ? \"&\" : \"?\"\n return `${url}${prefix}${this.serialize(params)}`\n }\n}\n", "import {\n SOCKET_STATES,\n TRANSPORTS\n} from \"./constants\"\n\nimport Ajax from \"./ajax\"\n\nlet arrayBufferToBase64 = (buffer) => {\n let binary = \"\"\n let bytes = new Uint8Array(buffer)\n let len = bytes.byteLength\n for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }\n return btoa(binary)\n}\n\nexport default class LongPoll {\n\n constructor(endPoint){\n this.endPoint = null\n this.token = null\n this.skipHeartbeat = true\n this.reqs = new Set()\n this.awaitingBatchAck = false\n this.currentBatch = null\n this.currentBatchTimer = null\n this.batchBuffer = []\n this.onopen = function (){ } // noop\n this.onerror = function (){ } // noop\n this.onmessage = function (){ } // noop\n this.onclose = function (){ } // noop\n this.pollEndpoint = this.normalizeEndpoint(endPoint)\n this.readyState = SOCKET_STATES.connecting\n this.poll()\n }\n\n normalizeEndpoint(endPoint){\n return (endPoint\n .replace(\"ws://\", \"http://\")\n .replace(\"wss://\", \"https://\")\n .replace(new RegExp(\"(.*)\\/\" + TRANSPORTS.websocket), \"$1/\" + TRANSPORTS.longpoll))\n }\n\n endpointURL(){\n return Ajax.appendParams(this.pollEndpoint, {token: this.token})\n }\n\n closeAndRetry(code, reason, wasClean){\n this.close(code, reason, wasClean)\n this.readyState = SOCKET_STATES.connecting\n }\n\n ontimeout(){\n this.onerror(\"timeout\")\n this.closeAndRetry(1005, \"timeout\", false)\n }\n\n isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }\n\n poll(){\n this.ajax(\"GET\", \"application/json\", null, () => this.ontimeout(), resp => {\n if(resp){\n var {status, token, messages} = resp\n this.token = token\n } else {\n status = 0\n }\n\n switch(status){\n case 200:\n messages.forEach(msg => {\n // Tasks are what things like event handlers, setTimeout callbacks,\n // promise resolves and more are run within.\n // In modern browsers, there are two different kinds of tasks,\n // microtasks and macrotasks.\n // Microtasks are mainly used for Promises, while macrotasks are\n // used for everything else.\n // Microtasks always have priority over macrotasks. If the JS engine\n // is looking for a task to run, it will always try to empty the\n // microtask queue before attempting to run anything from the\n // macrotask queue.\n //\n // For the WebSocket transport, messages always arrive in their own\n // event. This means that if any promises are resolved from within,\n // their callbacks will always finish execution by the time the\n // next message event handler is run.\n //\n // In order to emulate this behaviour, we need to make sure each\n // onmessage handler is run within its own macrotask.\n setTimeout(() => this.onmessage({data: msg}), 0)\n })\n this.poll()\n break\n case 204:\n this.poll()\n break\n case 410:\n this.readyState = SOCKET_STATES.open\n this.onopen({})\n this.poll()\n break\n case 403:\n this.onerror(403)\n this.close(1008, \"forbidden\", false)\n break\n case 0:\n case 500:\n this.onerror(500)\n this.closeAndRetry(1011, \"internal server error\", 500)\n break\n default: throw new Error(`unhandled poll status ${status}`)\n }\n })\n }\n\n // we collect all pushes within the current event loop by\n // setTimeout 0, which optimizes back-to-back procedural\n // pushes against an empty buffer\n\n send(body){\n if(typeof(body) !== \"string\"){ body = arrayBufferToBase64(body) }\n if(this.currentBatch){\n this.currentBatch.push(body)\n } else if(this.awaitingBatchAck){\n this.batchBuffer.push(body)\n } else {\n this.currentBatch = [body]\n this.currentBatchTimer = setTimeout(() => {\n this.batchSend(this.currentBatch)\n this.currentBatch = null\n }, 0)\n }\n }\n\n batchSend(messages){\n this.awaitingBatchAck = true\n this.ajax(\"POST\", \"application/x-ndjson\", messages.join(\"\\n\"), () => this.onerror(\"timeout\"), resp => {\n this.awaitingBatchAck = false\n if(!resp || resp.status !== 200){\n this.onerror(resp && resp.status)\n this.closeAndRetry(1011, \"internal server error\", false)\n } else if(this.batchBuffer.length > 0){\n this.batchSend(this.batchBuffer)\n this.batchBuffer = []\n }\n })\n }\n\n close(code, reason, wasClean){\n for(let req of this.reqs){ req.abort() }\n this.readyState = SOCKET_STATES.closed\n let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})\n this.batchBuffer = []\n clearTimeout(this.currentBatchTimer)\n this.currentBatchTimer = null\n if(typeof(CloseEvent) !== \"undefined\"){\n this.onclose(new CloseEvent(\"close\", opts))\n } else {\n this.onclose(opts)\n }\n }\n\n ajax(method, contentType, body, onCallerTimeout, callback){\n let req\n let ontimeout = () => {\n this.reqs.delete(req)\n onCallerTimeout()\n }\n req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {\n this.reqs.delete(req)\n if(this.isActive()){ callback(resp) }\n })\n this.reqs.add(req)\n }\n}\n", "/**\n * Initializes the Presence\n * @param {Channel} channel - The Channel\n * @param {Object} opts - The options,\n * for example `{events: {state: \"state\", diff: \"diff\"}}`\n */\nexport default class Presence {\n\n constructor(channel, opts = {}){\n let events = opts.events || {state: \"presence_state\", diff: \"presence_diff\"}\n this.state = {}\n this.pendingDiffs = []\n this.channel = channel\n this.joinRef = null\n this.caller = {\n onJoin: function (){ },\n onLeave: function (){ },\n onSync: function (){ }\n }\n\n this.channel.on(events.state, newState => {\n let {onJoin, onLeave, onSync} = this.caller\n\n this.joinRef = this.channel.joinRef()\n this.state = Presence.syncState(this.state, newState, onJoin, onLeave)\n\n this.pendingDiffs.forEach(diff => {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n })\n this.pendingDiffs = []\n onSync()\n })\n\n this.channel.on(events.diff, diff => {\n let {onJoin, onLeave, onSync} = this.caller\n\n if(this.inPendingSyncState()){\n this.pendingDiffs.push(diff)\n } else {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n onSync()\n }\n })\n }\n\n onJoin(callback){ this.caller.onJoin = callback }\n\n onLeave(callback){ this.caller.onLeave = callback }\n\n onSync(callback){ this.caller.onSync = callback }\n\n list(by){ return Presence.list(this.state, by) }\n\n inPendingSyncState(){\n return !this.joinRef || (this.joinRef !== this.channel.joinRef())\n }\n\n // lower-level public static API\n\n /**\n * Used to sync the list of presences on the server\n * with the client's state. An optional `onJoin` and `onLeave` callback can\n * be provided to react to changes in the client's local presences across\n * disconnects and reconnects with the server.\n *\n * @returns {Presence}\n */\n static syncState(currentState, newState, onJoin, onLeave){\n let state = this.clone(currentState)\n let joins = {}\n let leaves = {}\n\n this.map(state, (key, presence) => {\n if(!newState[key]){\n leaves[key] = presence\n }\n })\n this.map(newState, (key, newPresence) => {\n let currentPresence = state[key]\n if(currentPresence){\n let newRefs = newPresence.metas.map(m => m.phx_ref)\n let curRefs = currentPresence.metas.map(m => m.phx_ref)\n let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)\n let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)\n if(joinedMetas.length > 0){\n joins[key] = newPresence\n joins[key].metas = joinedMetas\n }\n if(leftMetas.length > 0){\n leaves[key] = this.clone(currentPresence)\n leaves[key].metas = leftMetas\n }\n } else {\n joins[key] = newPresence\n }\n })\n return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)\n }\n\n /**\n *\n * Used to sync a diff of presence join and leave\n * events from the server, as they happen. Like `syncState`, `syncDiff`\n * accepts optional `onJoin` and `onLeave` callbacks to react to a user\n * joining or leaving from a device.\n *\n * @returns {Presence}\n */\n static syncDiff(state, diff, onJoin, onLeave){\n let {joins, leaves} = this.clone(diff)\n if(!onJoin){ onJoin = function (){ } }\n if(!onLeave){ onLeave = function (){ } }\n\n this.map(joins, (key, newPresence) => {\n let currentPresence = state[key]\n state[key] = this.clone(newPresence)\n if(currentPresence){\n let joinedRefs = state[key].metas.map(m => m.phx_ref)\n let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)\n state[key].metas.unshift(...curMetas)\n }\n onJoin(key, currentPresence, newPresence)\n })\n this.map(leaves, (key, leftPresence) => {\n let currentPresence = state[key]\n if(!currentPresence){ return }\n let refsToRemove = leftPresence.metas.map(m => m.phx_ref)\n currentPresence.metas = currentPresence.metas.filter(p => {\n return refsToRemove.indexOf(p.phx_ref) < 0\n })\n onLeave(key, currentPresence, leftPresence)\n if(currentPresence.metas.length === 0){\n delete state[key]\n }\n })\n return state\n }\n\n /**\n * Returns the array of presences, with selected metadata.\n *\n * @param {Object} presences\n * @param {Function} chooser\n *\n * @returns {Presence}\n */\n static list(presences, chooser){\n if(!chooser){ chooser = function (key, pres){ return pres } }\n\n return this.map(presences, (key, presence) => {\n return chooser(key, presence)\n })\n }\n\n // private\n\n static map(obj, func){\n return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))\n }\n\n static clone(obj){ return JSON.parse(JSON.stringify(obj)) }\n}\n", "/* The default serializer for encoding and decoding messages */\nimport {\n CHANNEL_EVENTS\n} from \"./constants\"\n\nexport default {\n HEADER_LENGTH: 1,\n META_LENGTH: 4,\n KINDS: {push: 0, reply: 1, broadcast: 2},\n\n encode(msg, callback){\n if(msg.payload.constructor === ArrayBuffer){\n return callback(this.binaryEncode(msg))\n } else {\n let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]\n return callback(JSON.stringify(payload))\n }\n },\n\n decode(rawPayload, callback){\n if(rawPayload.constructor === ArrayBuffer){\n return callback(this.binaryDecode(rawPayload))\n } else {\n let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)\n return callback({join_ref, ref, topic, event, payload})\n }\n },\n\n // private\n\n binaryEncode(message){\n let {join_ref, ref, event, topic, payload} = message\n let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length\n let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)\n let view = new DataView(header)\n let offset = 0\n\n view.setUint8(offset++, this.KINDS.push) // kind\n view.setUint8(offset++, join_ref.length)\n view.setUint8(offset++, ref.length)\n view.setUint8(offset++, topic.length)\n view.setUint8(offset++, event.length)\n Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0)))\n\n var combined = new Uint8Array(header.byteLength + payload.byteLength)\n combined.set(new Uint8Array(header), 0)\n combined.set(new Uint8Array(payload), header.byteLength)\n\n return combined.buffer\n },\n\n binaryDecode(buffer){\n let view = new DataView(buffer)\n let kind = view.getUint8(0)\n let decoder = new TextDecoder()\n switch(kind){\n case this.KINDS.push: return this.decodePush(buffer, view, decoder)\n case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)\n case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)\n }\n },\n\n decodePush(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let topicSize = view.getUint8(2)\n let eventSize = view.getUint8(3)\n let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}\n },\n\n decodeReply(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let refSize = view.getUint8(2)\n let topicSize = view.getUint8(3)\n let eventSize = view.getUint8(4)\n let offset = this.HEADER_LENGTH + this.META_LENGTH\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let ref = decoder.decode(buffer.slice(offset, offset + refSize))\n offset = offset + refSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n let payload = {status: event, response: data}\n return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}\n },\n\n decodeBroadcast(buffer, view, decoder){\n let topicSize = view.getUint8(1)\n let eventSize = view.getUint8(2)\n let offset = this.HEADER_LENGTH + 2\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n\n return {join_ref: null, ref: null, topic: topic, event: event, payload: data}\n }\n}\n", "import {\n global,\n phxWindow,\n CHANNEL_EVENTS,\n DEFAULT_TIMEOUT,\n DEFAULT_VSN,\n SOCKET_STATES,\n TRANSPORTS,\n WS_CLOSE_NORMAL\n} from \"./constants\"\n\nimport {\n closure\n} from \"./utils\"\n\nimport Ajax from \"./ajax\"\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Serializer from \"./serializer\"\nimport Timer from \"./timer\"\n\n/** Initializes the Socket *\n *\n * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)\n *\n * @param {string} endPoint - The string WebSocket endpoint, ie, `\"ws://example.com/socket\"`,\n * `\"wss://example.com\"`\n * `\"/socket\"` (inherited host & protocol)\n * @param {Object} [opts] - Optional configuration\n * @param {Function} [opts.transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.\n *\n * Defaults to WebSocket with automatic LongPoll fallback.\n * @param {Function} [opts.encode] - The function to encode outgoing messages.\n *\n * Defaults to JSON encoder.\n *\n * @param {Function} [opts.decode] - The function to decode incoming messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.parse(payload))\n * ```\n *\n * @param {number} [opts.timeout] - The default timeout in milliseconds to trigger push timeouts.\n *\n * Defaults `DEFAULT_TIMEOUT`\n * @param {number} [opts.heartbeatIntervalMs] - The millisec interval to send a heartbeat message\n * @param {number} [opts.reconnectAfterMs] - The optional function that returns the millisec\n * socket reconnect interval.\n *\n * Defaults to stepped backoff of:\n *\n * ```javascript\n * function(tries){\n * return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n * }\n * ````\n *\n * @param {number} [opts.rejoinAfterMs] - The optional function that returns the millisec\n * rejoin interval for individual channels.\n *\n * ```javascript\n * function(tries){\n * return [1000, 2000, 5000][tries - 1] || 10000\n * }\n * ````\n *\n * @param {Function} [opts.logger] - The optional function for specialized logging, ie:\n *\n * ```javascript\n * function(kind, msg, data) {\n * console.log(`${kind}: ${msg}`, data)\n * }\n * ```\n *\n * @param {number} [opts.longpollerTimeout] - The maximum timeout of a long poll AJAX request.\n *\n * Defaults to 20s (double the server long poll timer).\n *\n * @param {(Object|function)} [opts.params] - The optional params to pass when connecting\n * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.\n *\n * Defaults to \"arraybuffer\"\n *\n * @param {vsn} [opts.vsn] - The serializer's protocol version to send on connect.\n *\n * Defaults to DEFAULT_VSN.\n*/\nexport default class Socket {\n constructor(endPoint, opts = {}){\n this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}\n this.channels = []\n this.sendBuffer = []\n this.ref = 0\n this.timeout = opts.timeout || DEFAULT_TIMEOUT\n this.transport = opts.transport || global.WebSocket || LongPoll\n this.establishedConnections = 0\n this.defaultEncoder = Serializer.encode.bind(Serializer)\n this.defaultDecoder = Serializer.decode.bind(Serializer)\n this.closeWasClean = false\n this.binaryType = opts.binaryType || \"arraybuffer\"\n this.connectClock = 1\n if(this.transport !== LongPoll){\n this.encode = opts.encode || this.defaultEncoder\n this.decode = opts.decode || this.defaultDecoder\n } else {\n this.encode = this.defaultEncoder\n this.decode = this.defaultDecoder\n }\n let awaitingConnectionOnPageShow = null\n if(phxWindow && phxWindow.addEventListener){\n phxWindow.addEventListener(\"pagehide\", _e => {\n if(this.conn){\n this.disconnect()\n awaitingConnectionOnPageShow = this.connectClock\n }\n })\n phxWindow.addEventListener(\"pageshow\", _e => {\n if(awaitingConnectionOnPageShow === this.connectClock){\n awaitingConnectionOnPageShow = null\n this.connect()\n }\n })\n }\n this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000\n this.rejoinAfterMs = (tries) => {\n if(opts.rejoinAfterMs){\n return opts.rejoinAfterMs(tries)\n } else {\n return [1000, 2000, 5000][tries - 1] || 10000\n }\n }\n this.reconnectAfterMs = (tries) => {\n if(opts.reconnectAfterMs){\n return opts.reconnectAfterMs(tries)\n } else {\n return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n }\n }\n this.logger = opts.logger || null\n this.longpollerTimeout = opts.longpollerTimeout || 20000\n this.params = closure(opts.params || {})\n this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`\n this.vsn = opts.vsn || DEFAULT_VSN\n this.heartbeatTimeoutTimer = null\n this.heartbeatTimer = null\n this.pendingHeartbeatRef = null\n this.reconnectTimer = new Timer(() => {\n this.teardown(() => this.connect())\n }, this.reconnectAfterMs)\n }\n\n /**\n * Returns the LongPoll transport reference\n */\n getLongPollTransport(){ return LongPoll }\n\n /**\n * Disconnects and replaces the active transport\n *\n * @param {Function} newTransport - The new transport class to instantiate\n *\n */\n replaceTransport(newTransport){\n this.connectClock++\n this.closeWasClean = true\n this.reconnectTimer.reset()\n this.sendBuffer = []\n if(this.conn){\n this.conn.close()\n this.conn = null\n }\n this.transport = newTransport\n }\n\n /**\n * Returns the socket protocol\n *\n * @returns {string}\n */\n protocol(){ return location.protocol.match(/^https/) ? \"wss\" : \"ws\" }\n\n /**\n * The fully qualified socket url\n *\n * @returns {string}\n */\n endPointURL(){\n let uri = Ajax.appendParams(\n Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})\n if(uri.charAt(0) !== \"/\"){ return uri }\n if(uri.charAt(1) === \"/\"){ return `${this.protocol()}:${uri}` }\n\n return `${this.protocol()}://${location.host}${uri}`\n }\n\n /**\n * Disconnects the socket\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.\n *\n * @param {Function} callback - Optional callback which is called after socket is disconnected.\n * @param {integer} code - A status code for disconnection (Optional).\n * @param {string} reason - A textual description of the reason to disconnect. (Optional)\n */\n disconnect(callback, code, reason){\n this.connectClock++\n this.closeWasClean = true\n this.reconnectTimer.reset()\n this.teardown(callback, code, reason)\n }\n\n /**\n *\n * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`\n *\n * Passing params to connect is deprecated; pass them in the Socket constructor instead:\n * `new Socket(\"/socket\", {params: {user_id: userToken}})`.\n */\n connect(params){\n if(params){\n console && console.log(\"passing params to connect is deprecated. Instead pass :params to the Socket constructor\")\n this.params = closure(params)\n }\n if(this.conn){ return }\n\n this.connectClock++\n this.closeWasClean = false\n this.conn = new this.transport(this.endPointURL())\n this.conn.binaryType = this.binaryType\n this.conn.timeout = this.longpollerTimeout\n this.conn.onopen = () => this.onConnOpen()\n this.conn.onerror = error => this.onConnError(error)\n this.conn.onmessage = event => this.onConnMessage(event)\n this.conn.onclose = event => this.onConnClose(event)\n }\n\n /**\n * Logs the message. Override `this.logger` for specialized logging. noops by default\n * @param {string} kind\n * @param {string} msg\n * @param {Object} data\n */\n log(kind, msg, data){ this.logger(kind, msg, data) }\n\n /**\n * Returns true if a logger has been set on this socket.\n */\n hasLogger(){ return this.logger !== null }\n\n /**\n * Registers callbacks for connection open events\n *\n * @example socket.onOpen(function(){ console.info(\"the socket was opened\") })\n *\n * @param {Function} callback\n */\n onOpen(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.open.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection close events\n * @param {Function} callback\n */\n onClose(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.close.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection error events\n *\n * @example socket.onError(function(error){ alert(\"An error occurred\") })\n *\n * @param {Function} callback\n */\n onError(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.error.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection message events\n * @param {Function} callback\n */\n onMessage(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.message.push([ref, callback])\n return ref\n }\n\n /**\n * Pings the server and invokes the callback with the RTT in milliseconds\n * @param {Function} callback\n *\n * Returns true if the ping was pushed or false if unable to be pushed.\n */\n ping(callback){\n if(!this.isConnected()){ return false }\n let ref = this.makeRef()\n let startTime = Date.now()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: ref})\n let onMsgRef = this.onMessage(msg => {\n if(msg.ref === ref){\n this.off([onMsgRef])\n callback(Date.now() - startTime)\n }\n })\n return true\n }\n\n /**\n * @private\n */\n\n clearHeartbeats(){\n clearTimeout(this.heartbeatTimer)\n clearTimeout(this.heartbeatTimeoutTimer)\n }\n\n onConnOpen(){\n if(this.hasLogger()) this.log(\"transport\", `connected to ${this.endPointURL()}`)\n this.closeWasClean = false\n this.establishedConnections++\n this.flushSendBuffer()\n this.reconnectTimer.reset()\n this.resetHeartbeat()\n this.stateChangeCallbacks.open.forEach(([, callback]) => callback())\n }\n\n /**\n * @private\n */\n\n heartbeatTimeout(){\n if(this.pendingHeartbeatRef){\n this.pendingHeartbeatRef = null\n if(this.hasLogger()){ this.log(\"transport\", \"heartbeat timeout. Attempting to re-establish connection\") }\n this.triggerChanError()\n this.closeWasClean = false\n this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, \"heartbeat timeout\")\n }\n }\n\n resetHeartbeat(){\n if(this.conn && this.conn.skipHeartbeat){ return }\n this.pendingHeartbeatRef = null\n this.clearHeartbeats()\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n teardown(callback, code, reason){\n if(!this.conn){\n return callback && callback()\n }\n\n this.waitForBufferDone(() => {\n if(this.conn){\n if(code){ this.conn.close(code, reason || \"\") } else { this.conn.close() }\n }\n\n this.waitForSocketClosed(() => {\n if(this.conn){\n this.conn.onopen = function (){ } // noop\n this.conn.onerror = function (){ } // noop\n this.conn.onmessage = function (){ } // noop\n this.conn.onclose = function (){ } // noop\n this.conn = null\n }\n\n callback && callback()\n })\n })\n }\n\n waitForBufferDone(callback, tries = 1){\n if(tries === 5 || !this.conn || !this.conn.bufferedAmount){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForBufferDone(callback, tries + 1)\n }, 150 * tries)\n }\n\n waitForSocketClosed(callback, tries = 1){\n if(tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForSocketClosed(callback, tries + 1)\n }, 150 * tries)\n }\n\n onConnClose(event){\n let closeCode = event && event.code\n if(this.hasLogger()) this.log(\"transport\", \"close\", event)\n this.triggerChanError()\n this.clearHeartbeats()\n if(!this.closeWasClean && closeCode !== 1000){\n this.reconnectTimer.scheduleTimeout()\n }\n this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event))\n }\n\n /**\n * @private\n */\n onConnError(error){\n if(this.hasLogger()) this.log(\"transport\", error)\n let transportBefore = this.transport\n let establishedBefore = this.establishedConnections\n this.stateChangeCallbacks.error.forEach(([, callback]) => {\n callback(error, transportBefore, establishedBefore)\n })\n if(transportBefore === this.transport || establishedBefore > 0){\n this.triggerChanError()\n }\n }\n\n /**\n * @private\n */\n triggerChanError(){\n this.channels.forEach(channel => {\n if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){\n channel.trigger(CHANNEL_EVENTS.error)\n }\n })\n }\n\n /**\n * @returns {string}\n */\n connectionState(){\n switch(this.conn && this.conn.readyState){\n case SOCKET_STATES.connecting: return \"connecting\"\n case SOCKET_STATES.open: return \"open\"\n case SOCKET_STATES.closing: return \"closing\"\n default: return \"closed\"\n }\n }\n\n /**\n * @returns {boolean}\n */\n isConnected(){ return this.connectionState() === \"open\" }\n\n /**\n * @private\n *\n * @param {Channel}\n */\n remove(channel){\n this.off(channel.stateChangeRefs)\n this.channels = this.channels.filter(c => c.joinRef() !== channel.joinRef())\n }\n\n /**\n * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.\n *\n * @param {refs} - list of refs returned by calls to\n * `onOpen`, `onClose`, `onError,` and `onMessage`\n */\n off(refs){\n for(let key in this.stateChangeCallbacks){\n this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {\n return refs.indexOf(ref) === -1\n })\n }\n }\n\n /**\n * Initiates a new channel for the given topic\n *\n * @param {string} topic\n * @param {Object} chanParams - Parameters for the channel\n * @returns {Channel}\n */\n channel(topic, chanParams = {}){\n let chan = new Channel(topic, chanParams, this)\n this.channels.push(chan)\n return chan\n }\n\n /**\n * @param {Object} data\n */\n push(data){\n if(this.hasLogger()){\n let {topic, event, payload, ref, join_ref} = data\n this.log(\"push\", `${topic} ${event} (${join_ref}, ${ref})`, payload)\n }\n\n if(this.isConnected()){\n this.encode(data, result => this.conn.send(result))\n } else {\n this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))\n }\n }\n\n /**\n * Return the next message ref, accounting for overflows\n * @returns {string}\n */\n makeRef(){\n let newRef = this.ref + 1\n if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }\n\n return this.ref.toString()\n }\n\n sendHeartbeat(){\n if(this.pendingHeartbeatRef && !this.isConnected()){ return }\n this.pendingHeartbeatRef = this.makeRef()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: this.pendingHeartbeatRef})\n this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)\n }\n\n flushSendBuffer(){\n if(this.isConnected() && this.sendBuffer.length > 0){\n this.sendBuffer.forEach(callback => callback())\n this.sendBuffer = []\n }\n }\n\n onConnMessage(rawMessage){\n this.decode(rawMessage.data, msg => {\n let {topic, event, payload, ref, join_ref} = msg\n if(ref && ref === this.pendingHeartbeatRef){\n this.clearHeartbeats()\n this.pendingHeartbeatRef = null\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n if(this.hasLogger()) this.log(\"receive\", `${payload.status || \"\"} ${topic} ${event} ${ref && \"(\" + ref + \")\" || \"\"}`, payload)\n\n for(let i = 0; i < this.channels.length; i++){\n const channel = this.channels[i]\n if(!channel.isMember(topic, event, payload, join_ref)){ continue }\n channel.trigger(event, payload, ref, join_ref)\n }\n\n for(let i = 0; i < this.stateChangeCallbacks.message.length; i++){\n let [, callback] = this.stateChangeCallbacks.message[i]\n callback(msg)\n }\n })\n }\n\n leaveOpenTopic(topic){\n let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))\n if(dupChannel){\n if(this.hasLogger()) this.log(\"transport\", `leaving duplicate topic \"${topic}\"`)\n dupChannel.leave()\n }\n }\n}\n"],
- "mappings": "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",
- "names": []
+ "sourcesContent": ["/**\n * Phoenix Channels JavaScript client\n *\n * ## Socket Connection\n *\n * A single connection is established to the server and\n * channels are multiplexed over the connection.\n * Connect to the server using the `Socket` class:\n *\n * ```javascript\n * let socket = new Socket(\"/socket\", {params: {userToken: \"123\"}})\n * socket.connect()\n * ```\n *\n * The `Socket` constructor takes the mount point of the socket,\n * the authentication params, as well as options that can be found in\n * the Socket docs, such as configuring the `LongPoll` transport, and\n * heartbeat.\n *\n * ## Channels\n *\n * Channels are isolated, concurrent processes on the server that\n * subscribe to topics and broker events between the client and server.\n * To join a channel, you must provide the topic, and channel params for\n * authorization. Here's an example chat room example where `\"new_msg\"`\n * events are listened for, messages are pushed to the server, and\n * the channel is joined with ok/error/timeout matches:\n *\n * ```javascript\n * let channel = socket.channel(\"room:123\", {token: roomToken})\n * channel.on(\"new_msg\", msg => console.log(\"Got message\", msg) )\n * $input.onEnter( e => {\n * channel.push(\"new_msg\", {body: e.target.val}, 10000)\n * .receive(\"ok\", (msg) => console.log(\"created message\", msg) )\n * .receive(\"error\", (reasons) => console.log(\"create failed\", reasons) )\n * .receive(\"timeout\", () => console.log(\"Networking issue...\") )\n * })\n *\n * channel.join()\n * .receive(\"ok\", ({messages}) => console.log(\"catching up\", messages) )\n * .receive(\"error\", ({reason}) => console.log(\"failed join\", reason) )\n * .receive(\"timeout\", () => console.log(\"Networking issue. Still waiting...\"))\n *```\n *\n * ## Joining\n *\n * Creating a channel with `socket.channel(topic, params)`, binds the params to\n * `channel.params`, which are sent up on `channel.join()`.\n * Subsequent rejoins will send up the modified params for\n * updating authorization params, or passing up last_message_id information.\n * Successful joins receive an \"ok\" status, while unsuccessful joins\n * receive \"error\".\n *\n * With the default serializers and WebSocket transport, JSON text frames are\n * used for pushing a JSON object literal. If an `ArrayBuffer` instance is provided,\n * binary encoding will be used and the message will be sent with the binary\n * opcode.\n *\n * *Note*: binary messages are only supported on the WebSocket transport.\n *\n * ## Duplicate Join Subscriptions\n *\n * While the client may join any number of topics on any number of channels,\n * the client may only hold a single subscription for each unique topic at any\n * given time. When attempting to create a duplicate subscription,\n * the server will close the existing channel, log a warning, and\n * spawn a new channel for the topic. The client will have their\n * `channel.onClose` callbacks fired for the existing channel, and the new\n * channel join will have its receive hooks processed as normal.\n *\n * ## Pushing Messages\n *\n * From the previous example, we can see that pushing messages to the server\n * can be done with `channel.push(eventName, payload)` and we can optionally\n * receive responses from the push. Additionally, we can use\n * `receive(\"timeout\", callback)` to abort waiting for our other `receive` hooks\n * and take action after some period of waiting. The default timeout is 10000ms.\n *\n *\n * ## Socket Hooks\n *\n * Lifecycle events of the multiplexed connection can be hooked into via\n * `socket.onError()` and `socket.onClose()` events, ie:\n *\n * ```javascript\n * socket.onError( () => console.log(\"there was an error with the connection!\") )\n * socket.onClose( () => console.log(\"the connection dropped\") )\n * ```\n *\n *\n * ## Channel Hooks\n *\n * For each joined channel, you can bind to `onError` and `onClose` events\n * to monitor the channel lifecycle, ie:\n *\n * ```javascript\n * channel.onError( () => console.log(\"there was an error!\") )\n * channel.onClose( () => console.log(\"the channel has gone away gracefully\") )\n * ```\n *\n * ### onError hooks\n *\n * `onError` hooks are invoked if the socket connection drops, or the channel\n * crashes on the server. In either case, a channel rejoin is attempted\n * automatically in an exponential backoff manner.\n *\n * ### onClose hooks\n *\n * `onClose` hooks are invoked only in two cases. 1) the channel explicitly\n * closed on the server, or 2). The client explicitly closed, by calling\n * `channel.leave()`\n *\n *\n * ## Presence\n *\n * The `Presence` object provides features for syncing presence information\n * from the server with the client and handling presences joining and leaving.\n *\n * ### Syncing state from the server\n *\n * To sync presence state from the server, first instantiate an object and\n * pass your channel in to track lifecycle events:\n *\n * ```javascript\n * let channel = socket.channel(\"some:topic\")\n * let presence = new Presence(channel)\n * ```\n *\n * Next, use the `presence.onSync` callback to react to state changes\n * from the server. For example, to render the list of users every time\n * the list changes, you could write:\n *\n * ```javascript\n * presence.onSync(() => {\n * myRenderUsersFunction(presence.list())\n * })\n * ```\n *\n * ### Listing Presences\n *\n * `presence.list` is used to return a list of presence information\n * based on the local state of metadata. By default, all presence\n * metadata is returned, but a `listBy` function can be supplied to\n * allow the client to select which metadata to use for a given presence.\n * For example, you may have a user online from different devices with\n * a metadata status of \"online\", but they have set themselves to \"away\"\n * on another device. In this case, the app may choose to use the \"away\"\n * status for what appears on the UI. The example below defines a `listBy`\n * function which prioritizes the first metadata which was registered for\n * each user. This could be the first tab they opened, or the first device\n * they came online from:\n *\n * ```javascript\n * let listBy = (id, {metas: [first, ...rest]}) => {\n * first.count = rest.length + 1 // count of this user's presences\n * first.id = id\n * return first\n * }\n * let onlineUsers = presence.list(listBy)\n * ```\n *\n * ### Handling individual presence join and leave events\n *\n * The `presence.onJoin` and `presence.onLeave` callbacks can be used to\n * react to individual presences joining and leaving the app. For example:\n *\n * ```javascript\n * let presence = new Presence(channel)\n *\n * // detect if user has joined for the 1st time or from another tab/device\n * presence.onJoin((id, current, newPres) => {\n * if(!current){\n * console.log(\"user has entered for the first time\", newPres)\n * } else {\n * console.log(\"user additional presence\", newPres)\n * }\n * })\n *\n * // detect if user has left from all tabs/devices, or is still present\n * presence.onLeave((id, current, leftPres) => {\n * if(current.metas.length === 0){\n * console.log(\"user has left from all devices\", leftPres)\n * } else {\n * console.log(\"user left from a device\", leftPres)\n * }\n * })\n * // receive presence data from server\n * presence.onSync(() => {\n * displayUsers(presence.list())\n * })\n * ```\n * @module phoenix\n */\n\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Presence from \"./presence\"\nimport Serializer from \"./serializer\"\nimport Socket from \"./socket\"\n\nexport {\n Channel,\n LongPoll,\n Presence,\n Serializer,\n Socket\n}\n", "// wraps value in closure or returns closure\nexport let closure = (value) => {\n if(typeof value === \"function\"){\n return value\n } else {\n let closure = function (){ return value }\n return closure\n }\n}\n", "export const globalSelf = typeof self !== \"undefined\" ? self : null\nexport const phxWindow = typeof window !== \"undefined\" ? window : null\nexport const global = globalSelf || phxWindow || global\nexport const DEFAULT_VSN = \"2.0.0\"\nexport const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}\nexport const DEFAULT_TIMEOUT = 10000\nexport const WS_CLOSE_NORMAL = 1000\nexport const CHANNEL_STATES = {\n closed: \"closed\",\n errored: \"errored\",\n joined: \"joined\",\n joining: \"joining\",\n leaving: \"leaving\",\n}\nexport const CHANNEL_EVENTS = {\n close: \"phx_close\",\n error: \"phx_error\",\n join: \"phx_join\",\n reply: \"phx_reply\",\n leave: \"phx_leave\"\n}\n\nexport const TRANSPORTS = {\n longpoll: \"longpoll\",\n websocket: \"websocket\"\n}\nexport const XHR_STATES = {\n complete: 4\n}\n", "/**\n * Initializes the Push\n * @param {Channel} channel - The Channel\n * @param {string} event - The event, for example `\"phx_join\"`\n * @param {Object} payload - The payload, for example `{user_id: 123}`\n * @param {number} timeout - The push timeout in milliseconds\n */\nexport default class Push {\n constructor(channel, event, payload, timeout){\n this.channel = channel\n this.event = event\n this.payload = payload || function (){ return {} }\n this.receivedResp = null\n this.timeout = timeout\n this.timeoutTimer = null\n this.recHooks = []\n this.sent = false\n }\n\n /**\n *\n * @param {number} timeout\n */\n resend(timeout){\n this.timeout = timeout\n this.reset()\n this.send()\n }\n\n /**\n *\n */\n send(){\n if(this.hasReceived(\"timeout\")){ return }\n this.startTimeout()\n this.sent = true\n this.channel.socket.push({\n topic: this.channel.topic,\n event: this.event,\n payload: this.payload(),\n ref: this.ref,\n join_ref: this.channel.joinRef()\n })\n }\n\n /**\n *\n * @param {*} status\n * @param {*} callback\n */\n receive(status, callback){\n if(this.hasReceived(status)){\n callback(this.receivedResp.response)\n }\n\n this.recHooks.push({status, callback})\n return this\n }\n\n /**\n * @private\n */\n reset(){\n this.cancelRefEvent()\n this.ref = null\n this.refEvent = null\n this.receivedResp = null\n this.sent = false\n }\n\n /**\n * @private\n */\n matchReceive({status, response, _ref}){\n this.recHooks.filter(h => h.status === status)\n .forEach(h => h.callback(response))\n }\n\n /**\n * @private\n */\n cancelRefEvent(){\n if(!this.refEvent){ return }\n this.channel.off(this.refEvent)\n }\n\n /**\n * @private\n */\n cancelTimeout(){\n clearTimeout(this.timeoutTimer)\n this.timeoutTimer = null\n }\n\n /**\n * @private\n */\n startTimeout(){\n if(this.timeoutTimer){ this.cancelTimeout() }\n this.ref = this.channel.socket.makeRef()\n this.refEvent = this.channel.replyEventName(this.ref)\n\n this.channel.on(this.refEvent, payload => {\n this.cancelRefEvent()\n this.cancelTimeout()\n this.receivedResp = payload\n this.matchReceive(payload)\n })\n\n this.timeoutTimer = setTimeout(() => {\n this.trigger(\"timeout\", {})\n }, this.timeout)\n }\n\n /**\n * @private\n */\n hasReceived(status){\n return this.receivedResp && this.receivedResp.status === status\n }\n\n /**\n * @private\n */\n trigger(status, response){\n this.channel.trigger(this.refEvent, {status, response})\n }\n}\n", "/**\n *\n * Creates a timer that accepts a `timerCalc` function to perform\n * calculated timeout retries, such as exponential backoff.\n *\n * @example\n * let reconnectTimer = new Timer(() => this.connect(), function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * })\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * reconnectTimer.scheduleTimeout() // fires after 5000\n * reconnectTimer.reset()\n * reconnectTimer.scheduleTimeout() // fires after 1000\n *\n * @param {Function} callback\n * @param {Function} timerCalc\n */\nexport default class Timer {\n constructor(callback, timerCalc){\n this.callback = callback\n this.timerCalc = timerCalc\n this.timer = null\n this.tries = 0\n }\n\n reset(){\n this.tries = 0\n clearTimeout(this.timer)\n }\n\n /**\n * Cancels any previous scheduleTimeout and schedules callback\n */\n scheduleTimeout(){\n clearTimeout(this.timer)\n\n this.timer = setTimeout(() => {\n this.tries = this.tries + 1\n this.callback()\n }, this.timerCalc(this.tries + 1))\n }\n}\n", "import {closure} from \"./utils\"\nimport {\n CHANNEL_EVENTS,\n CHANNEL_STATES,\n} from \"./constants\"\n\nimport Push from \"./push\"\nimport Timer from \"./timer\"\n\n/**\n *\n * @param {string} topic\n * @param {(Object|function)} params\n * @param {Socket} socket\n */\nexport default class Channel {\n constructor(topic, params, socket){\n this.state = CHANNEL_STATES.closed\n this.topic = topic\n this.params = closure(params || {})\n this.socket = socket\n this.bindings = []\n this.bindingRef = 0\n this.timeout = this.socket.timeout\n this.joinedOnce = false\n this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)\n this.pushBuffer = []\n this.stateChangeRefs = []\n\n this.rejoinTimer = new Timer(() => {\n if(this.socket.isConnected()){ this.rejoin() }\n }, this.socket.rejoinAfterMs)\n this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))\n this.stateChangeRefs.push(this.socket.onOpen(() => {\n this.rejoinTimer.reset()\n if(this.isErrored()){ this.rejoin() }\n })\n )\n this.joinPush.receive(\"ok\", () => {\n this.state = CHANNEL_STATES.joined\n this.rejoinTimer.reset()\n this.pushBuffer.forEach(pushEvent => pushEvent.send())\n this.pushBuffer = []\n })\n this.joinPush.receive(\"error\", () => {\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.onClose(() => {\n this.rejoinTimer.reset()\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `close ${this.topic} ${this.joinRef()}`)\n this.state = CHANNEL_STATES.closed\n this.socket.remove(this)\n })\n this.onError(reason => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `error ${this.topic}`, reason)\n if(this.isJoining()){ this.joinPush.reset() }\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.joinPush.receive(\"timeout\", () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)\n leavePush.send()\n this.state = CHANNEL_STATES.errored\n this.joinPush.reset()\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.on(CHANNEL_EVENTS.reply, (payload, ref) => {\n this.trigger(this.replyEventName(ref), payload)\n })\n }\n\n /**\n * Join the channel\n * @param {integer} timeout\n * @returns {Push}\n */\n join(timeout = this.timeout){\n if(this.joinedOnce){\n throw new Error(\"tried to join multiple times. 'join' can only be called a single time per channel instance\")\n } else {\n this.timeout = timeout\n this.joinedOnce = true\n this.rejoin()\n return this.joinPush\n }\n }\n\n /**\n * Hook into channel close\n * @param {Function} callback\n */\n onClose(callback){\n this.on(CHANNEL_EVENTS.close, callback)\n }\n\n /**\n * Hook into channel errors\n * @param {Function} callback\n */\n onError(callback){\n return this.on(CHANNEL_EVENTS.error, reason => callback(reason))\n }\n\n /**\n * Subscribes on channel events\n *\n * Subscription returns a ref counter, which can be used later to\n * unsubscribe the exact event listener\n *\n * @example\n * const ref1 = channel.on(\"event\", do_stuff)\n * const ref2 = channel.on(\"event\", do_other_stuff)\n * channel.off(\"event\", ref1)\n * // Since unsubscription, do_stuff won't fire,\n * // while do_other_stuff will keep firing on the \"event\"\n *\n * @param {string} event\n * @param {Function} callback\n * @returns {integer} ref\n */\n on(event, callback){\n let ref = this.bindingRef++\n this.bindings.push({event, ref, callback})\n return ref\n }\n\n /**\n * Unsubscribes off of channel events\n *\n * Use the ref returned from a channel.on() to unsubscribe one\n * handler, or pass nothing for the ref to unsubscribe all\n * handlers for the given event.\n *\n * @example\n * // Unsubscribe the do_stuff handler\n * const ref1 = channel.on(\"event\", do_stuff)\n * channel.off(\"event\", ref1)\n *\n * // Unsubscribe all handlers from event\n * channel.off(\"event\")\n *\n * @param {string} event\n * @param {integer} ref\n */\n off(event, ref){\n this.bindings = this.bindings.filter((bind) => {\n return !(bind.event === event && (typeof ref === \"undefined\" || ref === bind.ref))\n })\n }\n\n /**\n * @private\n */\n canPush(){ return this.socket.isConnected() && this.isJoined() }\n\n /**\n * Sends a message `event` to phoenix with the payload `payload`.\n * Phoenix receives this in the `handle_in(event, payload, socket)`\n * function. if phoenix replies or it times out (default 10000ms),\n * then optionally the reply can be received.\n *\n * @example\n * channel.push(\"event\")\n * .receive(\"ok\", payload => console.log(\"phoenix replied:\", payload))\n * .receive(\"error\", err => console.log(\"phoenix errored\", err))\n * .receive(\"timeout\", () => console.log(\"timed out pushing\"))\n * @param {string} event\n * @param {Object} payload\n * @param {number} [timeout]\n * @returns {Push}\n */\n push(event, payload, timeout = this.timeout){\n payload = payload || {}\n if(!this.joinedOnce){\n throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)\n }\n let pushEvent = new Push(this, event, function (){ return payload }, timeout)\n if(this.canPush()){\n pushEvent.send()\n } else {\n pushEvent.startTimeout()\n this.pushBuffer.push(pushEvent)\n }\n\n return pushEvent\n }\n\n /** Leaves the channel\n *\n * Unsubscribes from server events, and\n * instructs channel to terminate on server\n *\n * Triggers onClose() hooks\n *\n * To receive leave acknowledgements, use the `receive`\n * hook to bind to the server ack, ie:\n *\n * @example\n * channel.leave().receive(\"ok\", () => alert(\"left!\") )\n *\n * @param {integer} timeout\n * @returns {Push}\n */\n leave(timeout = this.timeout){\n this.rejoinTimer.reset()\n this.joinPush.cancelTimeout()\n\n this.state = CHANNEL_STATES.leaving\n let onClose = () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `leave ${this.topic}`)\n this.trigger(CHANNEL_EVENTS.close, \"leave\")\n }\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)\n leavePush.receive(\"ok\", () => onClose())\n .receive(\"timeout\", () => onClose())\n leavePush.send()\n if(!this.canPush()){ leavePush.trigger(\"ok\", {}) }\n\n return leavePush\n }\n\n /**\n * Overridable message hook\n *\n * Receives all events for specialized message handling\n * before dispatching to the channel callbacks.\n *\n * Must return the payload, modified or unmodified\n * @param {string} event\n * @param {Object} payload\n * @param {integer} ref\n * @returns {Object}\n */\n onMessage(_event, payload, _ref){ return payload }\n\n /**\n * @private\n */\n isMember(topic, event, payload, joinRef){\n if(this.topic !== topic){ return false }\n\n if(joinRef && joinRef !== this.joinRef()){\n if(this.socket.hasLogger()) this.socket.log(\"channel\", \"dropping outdated message\", {topic, event, payload, joinRef})\n return false\n } else {\n return true\n }\n }\n\n /**\n * @private\n */\n joinRef(){ return this.joinPush.ref }\n\n /**\n * @private\n */\n rejoin(timeout = this.timeout){\n if(this.isLeaving()){ return }\n this.socket.leaveOpenTopic(this.topic)\n this.state = CHANNEL_STATES.joining\n this.joinPush.resend(timeout)\n }\n\n /**\n * @private\n */\n trigger(event, payload, ref, joinRef){\n let handledPayload = this.onMessage(event, payload, ref, joinRef)\n if(payload && !handledPayload){ throw new Error(\"channel onMessage callbacks must return the payload, modified or unmodified\") }\n\n let eventBindings = this.bindings.filter(bind => bind.event === event)\n\n for(let i = 0; i < eventBindings.length; i++){\n let bind = eventBindings[i]\n bind.callback(handledPayload, ref, joinRef || this.joinRef())\n }\n }\n\n /**\n * @private\n */\n replyEventName(ref){ return `chan_reply_${ref}` }\n\n /**\n * @private\n */\n isClosed(){ return this.state === CHANNEL_STATES.closed }\n\n /**\n * @private\n */\n isErrored(){ return this.state === CHANNEL_STATES.errored }\n\n /**\n * @private\n */\n isJoined(){ return this.state === CHANNEL_STATES.joined }\n\n /**\n * @private\n */\n isJoining(){ return this.state === CHANNEL_STATES.joining }\n\n /**\n * @private\n */\n isLeaving(){ return this.state === CHANNEL_STATES.leaving }\n}\n", "import {\n global,\n XHR_STATES\n} from \"./constants\"\n\nexport default class Ajax {\n\n static request(method, endPoint, accept, body, timeout, ontimeout, callback){\n if(global.XDomainRequest){\n let req = new global.XDomainRequest() // IE8, IE9\n return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)\n } else {\n let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari\n return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)\n }\n }\n\n static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){\n req.timeout = timeout\n req.open(method, endPoint)\n req.onload = () => {\n let response = this.parseJSON(req.responseText)\n callback && callback(response)\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n // Work around bug in IE9 that requires an attached onprogress handler\n req.onprogress = () => { }\n\n req.send(body)\n return req\n }\n\n static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){\n req.open(method, endPoint, true)\n req.timeout = timeout\n req.setRequestHeader(\"Content-Type\", accept)\n req.onerror = () => callback && callback(null)\n req.onreadystatechange = () => {\n if(req.readyState === XHR_STATES.complete && callback){\n let response = this.parseJSON(req.responseText)\n callback(response)\n }\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n req.send(body)\n return req\n }\n\n static parseJSON(resp){\n if(!resp || resp === \"\"){ return null }\n\n try {\n return JSON.parse(resp)\n } catch (e){\n console && console.log(\"failed to parse JSON response\", resp)\n return null\n }\n }\n\n static serialize(obj, parentKey){\n let queryStr = []\n for(var key in obj){\n if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }\n let paramKey = parentKey ? `${parentKey}[${key}]` : key\n let paramVal = obj[key]\n if(typeof paramVal === \"object\"){\n queryStr.push(this.serialize(paramVal, paramKey))\n } else {\n queryStr.push(encodeURIComponent(paramKey) + \"=\" + encodeURIComponent(paramVal))\n }\n }\n return queryStr.join(\"&\")\n }\n\n static appendParams(url, params){\n if(Object.keys(params).length === 0){ return url }\n\n let prefix = url.match(/\\?/) ? \"&\" : \"?\"\n return `${url}${prefix}${this.serialize(params)}`\n }\n}\n", "import {\n SOCKET_STATES,\n TRANSPORTS\n} from \"./constants\"\n\nimport Ajax from \"./ajax\"\n\nlet arrayBufferToBase64 = (buffer) => {\n let binary = \"\"\n let bytes = new Uint8Array(buffer)\n let len = bytes.byteLength\n for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }\n return btoa(binary)\n}\n\nexport default class LongPoll {\n\n constructor(endPoint){\n this.endPoint = null\n this.token = null\n this.skipHeartbeat = true\n this.reqs = new Set()\n this.awaitingBatchAck = false\n this.currentBatch = null\n this.currentBatchTimer = null\n this.batchBuffer = []\n this.onopen = function (){ } // noop\n this.onerror = function (){ } // noop\n this.onmessage = function (){ } // noop\n this.onclose = function (){ } // noop\n this.pollEndpoint = this.normalizeEndpoint(endPoint)\n this.readyState = SOCKET_STATES.connecting\n // we must wait for the caller to finish setting up our callbacks and timeout properties\n setTimeout(() => this.poll(), 0)\n }\n\n normalizeEndpoint(endPoint){\n return (endPoint\n .replace(\"ws://\", \"http://\")\n .replace(\"wss://\", \"https://\")\n .replace(new RegExp(\"(.*)\\/\" + TRANSPORTS.websocket), \"$1/\" + TRANSPORTS.longpoll))\n }\n\n endpointURL(){\n return Ajax.appendParams(this.pollEndpoint, {token: this.token})\n }\n\n closeAndRetry(code, reason, wasClean){\n this.close(code, reason, wasClean)\n this.readyState = SOCKET_STATES.connecting\n }\n\n ontimeout(){\n this.onerror(\"timeout\")\n this.closeAndRetry(1005, \"timeout\", false)\n }\n\n isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }\n\n poll(){\n this.ajax(\"GET\", \"application/json\", null, () => this.ontimeout(), resp => {\n if(resp){\n var {status, token, messages} = resp\n this.token = token\n } else {\n status = 0\n }\n\n switch(status){\n case 200:\n messages.forEach(msg => {\n // Tasks are what things like event handlers, setTimeout callbacks,\n // promise resolves and more are run within.\n // In modern browsers, there are two different kinds of tasks,\n // microtasks and macrotasks.\n // Microtasks are mainly used for Promises, while macrotasks are\n // used for everything else.\n // Microtasks always have priority over macrotasks. If the JS engine\n // is looking for a task to run, it will always try to empty the\n // microtask queue before attempting to run anything from the\n // macrotask queue.\n //\n // For the WebSocket transport, messages always arrive in their own\n // event. This means that if any promises are resolved from within,\n // their callbacks will always finish execution by the time the\n // next message event handler is run.\n //\n // In order to emulate this behaviour, we need to make sure each\n // onmessage handler is run within its own macrotask.\n setTimeout(() => this.onmessage({data: msg}), 0)\n })\n this.poll()\n break\n case 204:\n this.poll()\n break\n case 410:\n this.readyState = SOCKET_STATES.open\n this.onopen({})\n this.poll()\n break\n case 403:\n this.onerror(403)\n this.close(1008, \"forbidden\", false)\n break\n case 0:\n case 500:\n this.onerror(500)\n this.closeAndRetry(1011, \"internal server error\", 500)\n break\n default: throw new Error(`unhandled poll status ${status}`)\n }\n })\n }\n\n // we collect all pushes within the current event loop by\n // setTimeout 0, which optimizes back-to-back procedural\n // pushes against an empty buffer\n\n send(body){\n if(typeof(body) !== \"string\"){ body = arrayBufferToBase64(body) }\n if(this.currentBatch){\n this.currentBatch.push(body)\n } else if(this.awaitingBatchAck){\n this.batchBuffer.push(body)\n } else {\n this.currentBatch = [body]\n this.currentBatchTimer = setTimeout(() => {\n this.batchSend(this.currentBatch)\n this.currentBatch = null\n }, 0)\n }\n }\n\n batchSend(messages){\n this.awaitingBatchAck = true\n this.ajax(\"POST\", \"application/x-ndjson\", messages.join(\"\\n\"), () => this.onerror(\"timeout\"), resp => {\n this.awaitingBatchAck = false\n if(!resp || resp.status !== 200){\n this.onerror(resp && resp.status)\n this.closeAndRetry(1011, \"internal server error\", false)\n } else if(this.batchBuffer.length > 0){\n this.batchSend(this.batchBuffer)\n this.batchBuffer = []\n }\n })\n }\n\n close(code, reason, wasClean){\n for(let req of this.reqs){ req.abort() }\n this.readyState = SOCKET_STATES.closed\n let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})\n this.batchBuffer = []\n clearTimeout(this.currentBatchTimer)\n this.currentBatchTimer = null\n if(typeof(CloseEvent) !== \"undefined\"){\n this.onclose(new CloseEvent(\"close\", opts))\n } else {\n this.onclose(opts)\n }\n }\n\n ajax(method, contentType, body, onCallerTimeout, callback){\n let req\n let ontimeout = () => {\n this.reqs.delete(req)\n onCallerTimeout()\n }\n req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {\n this.reqs.delete(req)\n if(this.isActive()){ callback(resp) }\n })\n this.reqs.add(req)\n }\n}\n", "/**\n * Initializes the Presence\n * @param {Channel} channel - The Channel\n * @param {Object} opts - The options,\n * for example `{events: {state: \"state\", diff: \"diff\"}}`\n */\nexport default class Presence {\n\n constructor(channel, opts = {}){\n let events = opts.events || {state: \"presence_state\", diff: \"presence_diff\"}\n this.state = {}\n this.pendingDiffs = []\n this.channel = channel\n this.joinRef = null\n this.caller = {\n onJoin: function (){ },\n onLeave: function (){ },\n onSync: function (){ }\n }\n\n this.channel.on(events.state, newState => {\n let {onJoin, onLeave, onSync} = this.caller\n\n this.joinRef = this.channel.joinRef()\n this.state = Presence.syncState(this.state, newState, onJoin, onLeave)\n\n this.pendingDiffs.forEach(diff => {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n })\n this.pendingDiffs = []\n onSync()\n })\n\n this.channel.on(events.diff, diff => {\n let {onJoin, onLeave, onSync} = this.caller\n\n if(this.inPendingSyncState()){\n this.pendingDiffs.push(diff)\n } else {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n onSync()\n }\n })\n }\n\n onJoin(callback){ this.caller.onJoin = callback }\n\n onLeave(callback){ this.caller.onLeave = callback }\n\n onSync(callback){ this.caller.onSync = callback }\n\n list(by){ return Presence.list(this.state, by) }\n\n inPendingSyncState(){\n return !this.joinRef || (this.joinRef !== this.channel.joinRef())\n }\n\n // lower-level public static API\n\n /**\n * Used to sync the list of presences on the server\n * with the client's state. An optional `onJoin` and `onLeave` callback can\n * be provided to react to changes in the client's local presences across\n * disconnects and reconnects with the server.\n *\n * @returns {Presence}\n */\n static syncState(currentState, newState, onJoin, onLeave){\n let state = this.clone(currentState)\n let joins = {}\n let leaves = {}\n\n this.map(state, (key, presence) => {\n if(!newState[key]){\n leaves[key] = presence\n }\n })\n this.map(newState, (key, newPresence) => {\n let currentPresence = state[key]\n if(currentPresence){\n let newRefs = newPresence.metas.map(m => m.phx_ref)\n let curRefs = currentPresence.metas.map(m => m.phx_ref)\n let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)\n let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)\n if(joinedMetas.length > 0){\n joins[key] = newPresence\n joins[key].metas = joinedMetas\n }\n if(leftMetas.length > 0){\n leaves[key] = this.clone(currentPresence)\n leaves[key].metas = leftMetas\n }\n } else {\n joins[key] = newPresence\n }\n })\n return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)\n }\n\n /**\n *\n * Used to sync a diff of presence join and leave\n * events from the server, as they happen. Like `syncState`, `syncDiff`\n * accepts optional `onJoin` and `onLeave` callbacks to react to a user\n * joining or leaving from a device.\n *\n * @returns {Presence}\n */\n static syncDiff(state, diff, onJoin, onLeave){\n let {joins, leaves} = this.clone(diff)\n if(!onJoin){ onJoin = function (){ } }\n if(!onLeave){ onLeave = function (){ } }\n\n this.map(joins, (key, newPresence) => {\n let currentPresence = state[key]\n state[key] = this.clone(newPresence)\n if(currentPresence){\n let joinedRefs = state[key].metas.map(m => m.phx_ref)\n let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)\n state[key].metas.unshift(...curMetas)\n }\n onJoin(key, currentPresence, newPresence)\n })\n this.map(leaves, (key, leftPresence) => {\n let currentPresence = state[key]\n if(!currentPresence){ return }\n let refsToRemove = leftPresence.metas.map(m => m.phx_ref)\n currentPresence.metas = currentPresence.metas.filter(p => {\n return refsToRemove.indexOf(p.phx_ref) < 0\n })\n onLeave(key, currentPresence, leftPresence)\n if(currentPresence.metas.length === 0){\n delete state[key]\n }\n })\n return state\n }\n\n /**\n * Returns the array of presences, with selected metadata.\n *\n * @param {Object} presences\n * @param {Function} chooser\n *\n * @returns {Presence}\n */\n static list(presences, chooser){\n if(!chooser){ chooser = function (key, pres){ return pres } }\n\n return this.map(presences, (key, presence) => {\n return chooser(key, presence)\n })\n }\n\n // private\n\n static map(obj, func){\n return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))\n }\n\n static clone(obj){ return JSON.parse(JSON.stringify(obj)) }\n}\n", "/* The default serializer for encoding and decoding messages */\nimport {\n CHANNEL_EVENTS\n} from \"./constants\"\n\nexport default {\n HEADER_LENGTH: 1,\n META_LENGTH: 4,\n KINDS: {push: 0, reply: 1, broadcast: 2},\n\n encode(msg, callback){\n if(msg.payload.constructor === ArrayBuffer){\n return callback(this.binaryEncode(msg))\n } else {\n let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]\n return callback(JSON.stringify(payload))\n }\n },\n\n decode(rawPayload, callback){\n if(rawPayload.constructor === ArrayBuffer){\n return callback(this.binaryDecode(rawPayload))\n } else {\n let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)\n return callback({join_ref, ref, topic, event, payload})\n }\n },\n\n // private\n\n binaryEncode(message){\n let {join_ref, ref, event, topic, payload} = message\n let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length\n let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)\n let view = new DataView(header)\n let offset = 0\n\n view.setUint8(offset++, this.KINDS.push) // kind\n view.setUint8(offset++, join_ref.length)\n view.setUint8(offset++, ref.length)\n view.setUint8(offset++, topic.length)\n view.setUint8(offset++, event.length)\n Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0)))\n\n var combined = new Uint8Array(header.byteLength + payload.byteLength)\n combined.set(new Uint8Array(header), 0)\n combined.set(new Uint8Array(payload), header.byteLength)\n\n return combined.buffer\n },\n\n binaryDecode(buffer){\n let view = new DataView(buffer)\n let kind = view.getUint8(0)\n let decoder = new TextDecoder()\n switch(kind){\n case this.KINDS.push: return this.decodePush(buffer, view, decoder)\n case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)\n case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)\n }\n },\n\n decodePush(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let topicSize = view.getUint8(2)\n let eventSize = view.getUint8(3)\n let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}\n },\n\n decodeReply(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let refSize = view.getUint8(2)\n let topicSize = view.getUint8(3)\n let eventSize = view.getUint8(4)\n let offset = this.HEADER_LENGTH + this.META_LENGTH\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let ref = decoder.decode(buffer.slice(offset, offset + refSize))\n offset = offset + refSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n let payload = {status: event, response: data}\n return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}\n },\n\n decodeBroadcast(buffer, view, decoder){\n let topicSize = view.getUint8(1)\n let eventSize = view.getUint8(2)\n let offset = this.HEADER_LENGTH + 2\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n\n return {join_ref: null, ref: null, topic: topic, event: event, payload: data}\n }\n}\n", "import {\n global,\n phxWindow,\n CHANNEL_EVENTS,\n DEFAULT_TIMEOUT,\n DEFAULT_VSN,\n SOCKET_STATES,\n TRANSPORTS,\n WS_CLOSE_NORMAL\n} from \"./constants\"\n\nimport {\n closure\n} from \"./utils\"\n\nimport Ajax from \"./ajax\"\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Serializer from \"./serializer\"\nimport Timer from \"./timer\"\n\n/** Initializes the Socket *\n *\n * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)\n *\n * @param {string} endPoint - The string WebSocket endpoint, ie, `\"ws://example.com/socket\"`,\n * `\"wss://example.com\"`\n * `\"/socket\"` (inherited host & protocol)\n * @param {Object} [opts] - Optional configuration\n * @param {Function} [opts.transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.\n *\n * Defaults to WebSocket with automatic LongPoll fallback if WebSocket is not defined.\n * To fallback to LongPoll when WebSocket attempts fail, use `longPollFallbackMs: 2500`.\n *\n * @param {Function} [opts.longPollFallbackMs] - The millisecond time to attempt the primary transport\n * before falling back to the LongPoll transport. Disabled by default.\n *\n * @param {Function} [opts.debug] - When true, enables debug logging. Default false.\n *\n * @param {Function} [opts.encode] - The function to encode outgoing messages.\n *\n * Defaults to JSON encoder.\n *\n * @param {Function} [opts.decode] - The function to decode incoming messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.parse(payload))\n * ```\n *\n * @param {number} [opts.timeout] - The default timeout in milliseconds to trigger push timeouts.\n *\n * Defaults `DEFAULT_TIMEOUT`\n * @param {number} [opts.heartbeatIntervalMs] - The millisec interval to send a heartbeat message\n * @param {number} [opts.reconnectAfterMs] - The optional function that returns the millisec\n * socket reconnect interval.\n *\n * Defaults to stepped backoff of:\n *\n * ```javascript\n * function(tries){\n * return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n * }\n * ````\n *\n * @param {number} [opts.rejoinAfterMs] - The optional function that returns the millisec\n * rejoin interval for individual channels.\n *\n * ```javascript\n * function(tries){\n * return [1000, 2000, 5000][tries - 1] || 10000\n * }\n * ````\n *\n * @param {Function} [opts.logger] - The optional function for specialized logging, ie:\n *\n * ```javascript\n * function(kind, msg, data) {\n * console.log(`${kind}: ${msg}`, data)\n * }\n * ```\n *\n * @param {number} [opts.longpollerTimeout] - The maximum timeout of a long poll AJAX request.\n *\n * Defaults to 20s (double the server long poll timer).\n *\n * @param {(Object|function)} [opts.params] - The optional params to pass when connecting\n * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.\n *\n * Defaults to \"arraybuffer\"\n *\n * @param {vsn} [opts.vsn] - The serializer's protocol version to send on connect.\n *\n * Defaults to DEFAULT_VSN.\n *\n * @param {Object} [opts.sessionStorage] - An optional Storage compatible object\n * Phoenix uses sessionStorage for longpoll fallback history. Overriding the store is\n * useful when Phoenix won't have access to `sessionStorage`. For example, This could\n * happen if a site loads a cross-domain channel in an iframe. Example usage:\n *\n * class InMemoryStorage {\n * constructor() { this.storage = {} }\n * getItem(keyName) { return this.storage[keyName] || null }\n * removeItem(keyName) { delete this.storage[keyName] }\n * setItem(keyName, keyValue) { this.storage[keyName] = keyValue }\n * }\n *\n*/\nexport default class Socket {\n constructor(endPoint, opts = {}){\n this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}\n this.channels = []\n this.sendBuffer = []\n this.ref = 0\n this.timeout = opts.timeout || DEFAULT_TIMEOUT\n this.transport = opts.transport || global.WebSocket || LongPoll\n this.primaryPassedHealthCheck = false\n this.longPollFallbackMs = opts.longPollFallbackMs\n this.fallbackTimer = null\n this.sessionStore = opts.sessionStorage || (global && global.sessionStorage)\n this.establishedConnections = 0\n this.defaultEncoder = Serializer.encode.bind(Serializer)\n this.defaultDecoder = Serializer.decode.bind(Serializer)\n this.closeWasClean = false\n this.binaryType = opts.binaryType || \"arraybuffer\"\n this.connectClock = 1\n if(this.transport !== LongPoll){\n this.encode = opts.encode || this.defaultEncoder\n this.decode = opts.decode || this.defaultDecoder\n } else {\n this.encode = this.defaultEncoder\n this.decode = this.defaultDecoder\n }\n let awaitingConnectionOnPageShow = null\n if(phxWindow && phxWindow.addEventListener){\n phxWindow.addEventListener(\"pagehide\", _e => {\n if(this.conn){\n this.disconnect()\n awaitingConnectionOnPageShow = this.connectClock\n }\n })\n phxWindow.addEventListener(\"pageshow\", _e => {\n if(awaitingConnectionOnPageShow === this.connectClock){\n awaitingConnectionOnPageShow = null\n this.connect()\n }\n })\n }\n this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000\n this.rejoinAfterMs = (tries) => {\n if(opts.rejoinAfterMs){\n return opts.rejoinAfterMs(tries)\n } else {\n return [1000, 2000, 5000][tries - 1] || 10000\n }\n }\n this.reconnectAfterMs = (tries) => {\n if(opts.reconnectAfterMs){\n return opts.reconnectAfterMs(tries)\n } else {\n return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n }\n }\n this.logger = opts.logger || null\n if(!this.logger && opts.debug){\n this.logger = (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }\n }\n this.longpollerTimeout = opts.longpollerTimeout || 20000\n this.params = closure(opts.params || {})\n this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`\n this.vsn = opts.vsn || DEFAULT_VSN\n this.heartbeatTimeoutTimer = null\n this.heartbeatTimer = null\n this.pendingHeartbeatRef = null\n this.reconnectTimer = new Timer(() => {\n this.teardown(() => this.connect())\n }, this.reconnectAfterMs)\n }\n\n /**\n * Returns the LongPoll transport reference\n */\n getLongPollTransport(){ return LongPoll }\n\n /**\n * Disconnects and replaces the active transport\n *\n * @param {Function} newTransport - The new transport class to instantiate\n *\n */\n replaceTransport(newTransport){\n this.connectClock++\n this.closeWasClean = true\n clearTimeout(this.fallbackTimer)\n this.reconnectTimer.reset()\n if(this.conn){\n this.conn.close()\n this.conn = null\n }\n this.transport = newTransport\n }\n\n /**\n * Returns the socket protocol\n *\n * @returns {string}\n */\n protocol(){ return location.protocol.match(/^https/) ? \"wss\" : \"ws\" }\n\n /**\n * The fully qualified socket url\n *\n * @returns {string}\n */\n endPointURL(){\n let uri = Ajax.appendParams(\n Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})\n if(uri.charAt(0) !== \"/\"){ return uri }\n if(uri.charAt(1) === \"/\"){ return `${this.protocol()}:${uri}` }\n\n return `${this.protocol()}://${location.host}${uri}`\n }\n\n /**\n * Disconnects the socket\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.\n *\n * @param {Function} callback - Optional callback which is called after socket is disconnected.\n * @param {integer} code - A status code for disconnection (Optional).\n * @param {string} reason - A textual description of the reason to disconnect. (Optional)\n */\n disconnect(callback, code, reason){\n this.connectClock++\n this.closeWasClean = true\n clearTimeout(this.fallbackTimer)\n this.reconnectTimer.reset()\n this.teardown(callback, code, reason)\n }\n\n /**\n *\n * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`\n *\n * Passing params to connect is deprecated; pass them in the Socket constructor instead:\n * `new Socket(\"/socket\", {params: {user_id: userToken}})`.\n */\n connect(params){\n if(params){\n console && console.log(\"passing params to connect is deprecated. Instead pass :params to the Socket constructor\")\n this.params = closure(params)\n }\n if(this.conn){ return }\n if(this.longPollFallbackMs && this.transport !== LongPoll){\n this.connectWithFallback(LongPoll, this.longPollFallbackMs)\n } else {\n this.transportConnect()\n }\n }\n\n /**\n * Logs the message. Override `this.logger` for specialized logging. noops by default\n * @param {string} kind\n * @param {string} msg\n * @param {Object} data\n */\n log(kind, msg, data){ this.logger && this.logger(kind, msg, data) }\n\n /**\n * Returns true if a logger has been set on this socket.\n */\n hasLogger(){ return this.logger !== null }\n\n /**\n * Registers callbacks for connection open events\n *\n * @example socket.onOpen(function(){ console.info(\"the socket was opened\") })\n *\n * @param {Function} callback\n */\n onOpen(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.open.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection close events\n * @param {Function} callback\n */\n onClose(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.close.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection error events\n *\n * @example socket.onError(function(error){ alert(\"An error occurred\") })\n *\n * @param {Function} callback\n */\n onError(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.error.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection message events\n * @param {Function} callback\n */\n onMessage(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.message.push([ref, callback])\n return ref\n }\n\n /**\n * Pings the server and invokes the callback with the RTT in milliseconds\n * @param {Function} callback\n *\n * Returns true if the ping was pushed or false if unable to be pushed.\n */\n ping(callback){\n if(!this.isConnected()){ return false }\n let ref = this.makeRef()\n let startTime = Date.now()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: ref})\n let onMsgRef = this.onMessage(msg => {\n if(msg.ref === ref){\n this.off([onMsgRef])\n callback(Date.now() - startTime)\n }\n })\n return true\n }\n\n /**\n * @private\n */\n\n transportConnect(){\n this.connectClock++\n this.closeWasClean = false\n this.conn = new this.transport(this.endPointURL())\n this.conn.binaryType = this.binaryType\n this.conn.timeout = this.longpollerTimeout\n this.conn.onopen = () => this.onConnOpen()\n this.conn.onerror = error => this.onConnError(error)\n this.conn.onmessage = event => this.onConnMessage(event)\n this.conn.onclose = event => this.onConnClose(event)\n }\n\n getSession(key){ return this.sessionStore && this.sessionStore.getItem(key) }\n\n storeSession(key, val){ this.sessionStore && this.sessionStore.setItem(key, val) }\n\n connectWithFallback(fallbackTransport, fallbackThreshold = 2500){\n clearTimeout(this.fallbackTimer)\n let established = false\n let primaryTransport = true\n let openRef, errorRef\n let fallback = (reason) => {\n this.log(\"transport\", `falling back to ${fallbackTransport.name}...`, reason)\n this.off([openRef, errorRef])\n primaryTransport = false\n this.replaceTransport(fallbackTransport)\n this.transportConnect()\n }\n if(this.getSession(`phx:fallback:${fallbackTransport.name}`)){ return fallback(\"memorized\") }\n\n this.fallbackTimer = setTimeout(fallback, fallbackThreshold)\n\n errorRef = this.onError(reason => {\n this.log(\"transport\", \"error\", reason)\n if(primaryTransport && !established){\n clearTimeout(this.fallbackTimer)\n fallback(reason)\n }\n })\n this.onOpen(() => {\n established = true\n if(!primaryTransport){\n // only memorize LP if we never connected to primary\n if(!this.primaryPassedHealthCheck){ this.storeSession(`phx:fallback:${fallbackTransport.name}`, \"true\") }\n return this.log(\"transport\", `established ${fallbackTransport.name} fallback`)\n }\n // if we've established primary, give the fallback a new period to attempt ping\n clearTimeout(this.fallbackTimer)\n this.fallbackTimer = setTimeout(fallback, fallbackThreshold)\n this.ping(rtt => {\n this.log(\"transport\", \"connected to primary after\", rtt)\n this.primaryPassedHealthCheck = true\n clearTimeout(this.fallbackTimer)\n })\n })\n this.transportConnect()\n }\n\n clearHeartbeats(){\n clearTimeout(this.heartbeatTimer)\n clearTimeout(this.heartbeatTimeoutTimer)\n }\n\n onConnOpen(){\n if(this.hasLogger()) this.log(\"transport\", `${this.transport.name} connected to ${this.endPointURL()}`)\n this.closeWasClean = false\n this.establishedConnections++\n this.flushSendBuffer()\n this.reconnectTimer.reset()\n this.resetHeartbeat()\n this.stateChangeCallbacks.open.forEach(([, callback]) => callback())\n }\n\n /**\n * @private\n */\n\n heartbeatTimeout(){\n if(this.pendingHeartbeatRef){\n this.pendingHeartbeatRef = null\n if(this.hasLogger()){ this.log(\"transport\", \"heartbeat timeout. Attempting to re-establish connection\") }\n this.triggerChanError()\n this.closeWasClean = false\n this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, \"heartbeat timeout\")\n }\n }\n\n resetHeartbeat(){\n if(this.conn && this.conn.skipHeartbeat){ return }\n this.pendingHeartbeatRef = null\n this.clearHeartbeats()\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n teardown(callback, code, reason){\n if(!this.conn){\n return callback && callback()\n }\n\n this.waitForBufferDone(() => {\n if(this.conn){\n if(code){ this.conn.close(code, reason || \"\") } else { this.conn.close() }\n }\n\n this.waitForSocketClosed(() => {\n if(this.conn){\n this.conn.onopen = function (){ } // noop\n this.conn.onerror = function (){ } // noop\n this.conn.onmessage = function (){ } // noop\n this.conn.onclose = function (){ } // noop\n this.conn = null\n }\n\n callback && callback()\n })\n })\n }\n\n waitForBufferDone(callback, tries = 1){\n if(tries === 5 || !this.conn || !this.conn.bufferedAmount){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForBufferDone(callback, tries + 1)\n }, 150 * tries)\n }\n\n waitForSocketClosed(callback, tries = 1){\n if(tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForSocketClosed(callback, tries + 1)\n }, 150 * tries)\n }\n\n onConnClose(event){\n let closeCode = event && event.code\n if(this.hasLogger()) this.log(\"transport\", \"close\", event)\n this.triggerChanError()\n this.clearHeartbeats()\n if(!this.closeWasClean && closeCode !== 1000){\n this.reconnectTimer.scheduleTimeout()\n }\n this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event))\n }\n\n /**\n * @private\n */\n onConnError(error){\n if(this.hasLogger()) this.log(\"transport\", error)\n let transportBefore = this.transport\n let establishedBefore = this.establishedConnections\n this.stateChangeCallbacks.error.forEach(([, callback]) => {\n callback(error, transportBefore, establishedBefore)\n })\n if(transportBefore === this.transport || establishedBefore > 0){\n this.triggerChanError()\n }\n }\n\n /**\n * @private\n */\n triggerChanError(){\n this.channels.forEach(channel => {\n if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){\n channel.trigger(CHANNEL_EVENTS.error)\n }\n })\n }\n\n /**\n * @returns {string}\n */\n connectionState(){\n switch(this.conn && this.conn.readyState){\n case SOCKET_STATES.connecting: return \"connecting\"\n case SOCKET_STATES.open: return \"open\"\n case SOCKET_STATES.closing: return \"closing\"\n default: return \"closed\"\n }\n }\n\n /**\n * @returns {boolean}\n */\n isConnected(){ return this.connectionState() === \"open\" }\n\n /**\n * @private\n *\n * @param {Channel}\n */\n remove(channel){\n this.off(channel.stateChangeRefs)\n this.channels = this.channels.filter(c => c !== channel)\n }\n\n /**\n * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.\n *\n * @param {refs} - list of refs returned by calls to\n * `onOpen`, `onClose`, `onError,` and `onMessage`\n */\n off(refs){\n for(let key in this.stateChangeCallbacks){\n this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {\n return refs.indexOf(ref) === -1\n })\n }\n }\n\n /**\n * Initiates a new channel for the given topic\n *\n * @param {string} topic\n * @param {Object} chanParams - Parameters for the channel\n * @returns {Channel}\n */\n channel(topic, chanParams = {}){\n let chan = new Channel(topic, chanParams, this)\n this.channels.push(chan)\n return chan\n }\n\n /**\n * @param {Object} data\n */\n push(data){\n if(this.hasLogger()){\n let {topic, event, payload, ref, join_ref} = data\n this.log(\"push\", `${topic} ${event} (${join_ref}, ${ref})`, payload)\n }\n\n if(this.isConnected()){\n this.encode(data, result => this.conn.send(result))\n } else {\n this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))\n }\n }\n\n /**\n * Return the next message ref, accounting for overflows\n * @returns {string}\n */\n makeRef(){\n let newRef = this.ref + 1\n if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }\n\n return this.ref.toString()\n }\n\n sendHeartbeat(){\n if(this.pendingHeartbeatRef && !this.isConnected()){ return }\n this.pendingHeartbeatRef = this.makeRef()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: this.pendingHeartbeatRef})\n this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)\n }\n\n flushSendBuffer(){\n if(this.isConnected() && this.sendBuffer.length > 0){\n this.sendBuffer.forEach(callback => callback())\n this.sendBuffer = []\n }\n }\n\n onConnMessage(rawMessage){\n this.decode(rawMessage.data, msg => {\n let {topic, event, payload, ref, join_ref} = msg\n if(ref && ref === this.pendingHeartbeatRef){\n this.clearHeartbeats()\n this.pendingHeartbeatRef = null\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n if(this.hasLogger()) this.log(\"receive\", `${payload.status || \"\"} ${topic} ${event} ${ref && \"(\" + ref + \")\" || \"\"}`, payload)\n\n for(let i = 0; i < this.channels.length; i++){\n const channel = this.channels[i]\n if(!channel.isMember(topic, event, payload, join_ref)){ continue }\n channel.trigger(event, payload, ref, join_ref)\n }\n\n for(let i = 0; i < this.stateChangeCallbacks.message.length; i++){\n let [, callback] = this.stateChangeCallbacks.message[i]\n callback(msg)\n }\n })\n }\n\n leaveOpenTopic(topic){\n let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))\n if(dupChannel){\n if(this.hasLogger()) this.log(\"transport\", `leaving duplicate topic \"${topic}\"`)\n dupChannel.leave()\n }\n }\n}\n"],
+ "mappings": "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",
+ "names": ["closure"]
}
diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js
index 879bac837b..7aefa63002 100644
--- a/priv/static/phoenix.js
+++ b/priv/static/phoenix.js
@@ -83,11 +83,18 @@ var Phoenix = (() => {
this.recHooks = [];
this.sent = false;
}
+ /**
+ *
+ * @param {number} timeout
+ */
resend(timeout) {
this.timeout = timeout;
this.reset();
this.send();
}
+ /**
+ *
+ */
send() {
if (this.hasReceived("timeout")) {
return;
@@ -102,6 +109,11 @@ var Phoenix = (() => {
join_ref: this.channel.joinRef()
});
}
+ /**
+ *
+ * @param {*} status
+ * @param {*} callback
+ */
receive(status, callback) {
if (this.hasReceived(status)) {
callback(this.receivedResp.response);
@@ -109,6 +121,9 @@ var Phoenix = (() => {
this.recHooks.push({ status, callback });
return this;
}
+ /**
+ * @private
+ */
reset() {
this.cancelRefEvent();
this.ref = null;
@@ -116,19 +131,31 @@ var Phoenix = (() => {
this.receivedResp = null;
this.sent = false;
}
+ /**
+ * @private
+ */
matchReceive({ status, response, _ref }) {
this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
}
+ /**
+ * @private
+ */
cancelRefEvent() {
if (!this.refEvent) {
return;
}
this.channel.off(this.refEvent);
}
+ /**
+ * @private
+ */
cancelTimeout() {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
+ /**
+ * @private
+ */
startTimeout() {
if (this.timeoutTimer) {
this.cancelTimeout();
@@ -145,9 +172,15 @@ var Phoenix = (() => {
this.trigger("timeout", {});
}, this.timeout);
}
+ /**
+ * @private
+ */
hasReceived(status) {
return this.receivedResp && this.receivedResp.status === status;
}
+ /**
+ * @private
+ */
trigger(status, response) {
this.channel.trigger(this.refEvent, { status, response });
}
@@ -165,6 +198,9 @@ var Phoenix = (() => {
this.tries = 0;
clearTimeout(this.timer);
}
+ /**
+ * Cancels any previous scheduleTimeout and schedules callback
+ */
scheduleTimeout() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
@@ -194,12 +230,14 @@ var Phoenix = (() => {
}
}, this.socket.rejoinAfterMs);
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()));
- this.stateChangeRefs.push(this.socket.onOpen(() => {
- this.rejoinTimer.reset();
- if (this.isErrored()) {
- this.rejoin();
- }
- }));
+ this.stateChangeRefs.push(
+ this.socket.onOpen(() => {
+ this.rejoinTimer.reset();
+ if (this.isErrored()) {
+ this.rejoin();
+ }
+ })
+ );
this.joinPush.receive("ok", () => {
this.state = CHANNEL_STATES.joined;
this.rejoinTimer.reset();
@@ -245,6 +283,11 @@ var Phoenix = (() => {
this.trigger(this.replyEventName(ref), payload);
});
}
+ /**
+ * Join the channel
+ * @param {integer} timeout
+ * @returns {Push}
+ */
join(timeout = this.timeout) {
if (this.joinedOnce) {
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");
@@ -255,25 +298,87 @@ var Phoenix = (() => {
return this.joinPush;
}
}
+ /**
+ * Hook into channel close
+ * @param {Function} callback
+ */
onClose(callback) {
this.on(CHANNEL_EVENTS.close, callback);
}
+ /**
+ * Hook into channel errors
+ * @param {Function} callback
+ */
onError(callback) {
return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason));
}
+ /**
+ * Subscribes on channel events
+ *
+ * Subscription returns a ref counter, which can be used later to
+ * unsubscribe the exact event listener
+ *
+ * @example
+ * const ref1 = channel.on("event", do_stuff)
+ * const ref2 = channel.on("event", do_other_stuff)
+ * channel.off("event", ref1)
+ * // Since unsubscription, do_stuff won't fire,
+ * // while do_other_stuff will keep firing on the "event"
+ *
+ * @param {string} event
+ * @param {Function} callback
+ * @returns {integer} ref
+ */
on(event, callback) {
let ref = this.bindingRef++;
this.bindings.push({ event, ref, callback });
return ref;
}
+ /**
+ * Unsubscribes off of channel events
+ *
+ * Use the ref returned from a channel.on() to unsubscribe one
+ * handler, or pass nothing for the ref to unsubscribe all
+ * handlers for the given event.
+ *
+ * @example
+ * // Unsubscribe the do_stuff handler
+ * const ref1 = channel.on("event", do_stuff)
+ * channel.off("event", ref1)
+ *
+ * // Unsubscribe all handlers from event
+ * channel.off("event")
+ *
+ * @param {string} event
+ * @param {integer} ref
+ */
off(event, ref) {
this.bindings = this.bindings.filter((bind) => {
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref));
});
}
+ /**
+ * @private
+ */
canPush() {
return this.socket.isConnected() && this.isJoined();
}
+ /**
+ * Sends a message `event` to phoenix with the payload `payload`.
+ * Phoenix receives this in the `handle_in(event, payload, socket)`
+ * function. if phoenix replies or it times out (default 10000ms),
+ * then optionally the reply can be received.
+ *
+ * @example
+ * channel.push("event")
+ * .receive("ok", payload => console.log("phoenix replied:", payload))
+ * .receive("error", err => console.log("phoenix errored", err))
+ * .receive("timeout", () => console.log("timed out pushing"))
+ * @param {string} event
+ * @param {Object} payload
+ * @param {number} [timeout]
+ * @returns {Push}
+ */
push(event, payload, timeout = this.timeout) {
payload = payload || {};
if (!this.joinedOnce) {
@@ -290,6 +395,22 @@ var Phoenix = (() => {
}
return pushEvent;
}
+ /** Leaves the channel
+ *
+ * Unsubscribes from server events, and
+ * instructs channel to terminate on server
+ *
+ * Triggers onClose() hooks
+ *
+ * To receive leave acknowledgements, use the `receive`
+ * hook to bind to the server ack, ie:
+ *
+ * @example
+ * channel.leave().receive("ok", () => alert("left!") )
+ *
+ * @param {integer} timeout
+ * @returns {Push}
+ */
leave(timeout = this.timeout) {
this.rejoinTimer.reset();
this.joinPush.cancelTimeout();
@@ -307,9 +428,24 @@ var Phoenix = (() => {
}
return leavePush;
}
+ /**
+ * Overridable message hook
+ *
+ * Receives all events for specialized message handling
+ * before dispatching to the channel callbacks.
+ *
+ * Must return the payload, modified or unmodified
+ * @param {string} event
+ * @param {Object} payload
+ * @param {integer} ref
+ * @returns {Object}
+ */
onMessage(_event, payload, _ref) {
return payload;
}
+ /**
+ * @private
+ */
isMember(topic, event, payload, joinRef) {
if (this.topic !== topic) {
return false;
@@ -322,9 +458,15 @@ var Phoenix = (() => {
return true;
}
}
+ /**
+ * @private
+ */
joinRef() {
return this.joinPush.ref;
}
+ /**
+ * @private
+ */
rejoin(timeout = this.timeout) {
if (this.isLeaving()) {
return;
@@ -333,6 +475,9 @@ var Phoenix = (() => {
this.state = CHANNEL_STATES.joining;
this.joinPush.resend(timeout);
}
+ /**
+ * @private
+ */
trigger(event, payload, ref, joinRef) {
let handledPayload = this.onMessage(event, payload, ref, joinRef);
if (payload && !handledPayload) {
@@ -344,21 +489,39 @@ var Phoenix = (() => {
bind.callback(handledPayload, ref, joinRef || this.joinRef());
}
}
+ /**
+ * @private
+ */
replyEventName(ref) {
return `chan_reply_${ref}`;
}
+ /**
+ * @private
+ */
isClosed() {
return this.state === CHANNEL_STATES.closed;
}
+ /**
+ * @private
+ */
isErrored() {
return this.state === CHANNEL_STATES.errored;
}
+ /**
+ * @private
+ */
isJoined() {
return this.state === CHANNEL_STATES.joined;
}
+ /**
+ * @private
+ */
isJoining() {
return this.state === CHANNEL_STATES.joining;
}
+ /**
+ * @private
+ */
isLeaving() {
return this.state === CHANNEL_STATES.leaving;
}
@@ -473,7 +636,7 @@ var Phoenix = (() => {
};
this.pollEndpoint = this.normalizeEndpoint(endPoint);
this.readyState = SOCKET_STATES.connecting;
- this.poll();
+ setTimeout(() => this.poll(), 0);
}
normalizeEndpoint(endPoint) {
return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
@@ -529,6 +692,9 @@ var Phoenix = (() => {
}
});
}
+ // we collect all pushes within the current event loop by
+ // setTimeout 0, which optimizes back-to-back procedural
+ // pushes against an empty buffer
send(body) {
if (typeof body !== "string") {
body = arrayBufferToBase64(body);
@@ -640,6 +806,15 @@ var Phoenix = (() => {
inPendingSyncState() {
return !this.joinRef || this.joinRef !== this.channel.joinRef();
}
+ // lower-level public static API
+ /**
+ * Used to sync the list of presences on the server
+ * with the client's state. An optional `onJoin` and `onLeave` callback can
+ * be provided to react to changes in the client's local presences across
+ * disconnects and reconnects with the server.
+ *
+ * @returns {Presence}
+ */
static syncState(currentState, newState, onJoin, onLeave) {
let state = this.clone(currentState);
let joins = {};
@@ -670,6 +845,15 @@ var Phoenix = (() => {
});
return this.syncDiff(state, { joins, leaves }, onJoin, onLeave);
}
+ /**
+ *
+ * Used to sync a diff of presence join and leave
+ * events from the server, as they happen. Like `syncState`, `syncDiff`
+ * accepts optional `onJoin` and `onLeave` callbacks to react to a user
+ * joining or leaving from a device.
+ *
+ * @returns {Presence}
+ */
static syncDiff(state, diff, onJoin, onLeave) {
let { joins, leaves } = this.clone(diff);
if (!onJoin) {
@@ -706,6 +890,14 @@ var Phoenix = (() => {
});
return state;
}
+ /**
+ * Returns the array of presences, with selected metadata.
+ *
+ * @param {Object} presences
+ * @param {Function} chooser
+ *
+ * @returns {Presence}
+ */
static list(presences, chooser) {
if (!chooser) {
chooser = function(key, pres) {
@@ -716,6 +908,7 @@ var Phoenix = (() => {
return chooser(key, presence);
});
}
+ // private
static map(obj, func) {
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
}
@@ -745,6 +938,7 @@ var Phoenix = (() => {
return callback({ join_ref, ref, topic, event, payload });
}
},
+ // private
binaryEncode(message) {
let { join_ref, ref, event, topic, payload } = message;
let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length;
@@ -832,6 +1026,10 @@ var Phoenix = (() => {
this.ref = 0;
this.timeout = opts.timeout || DEFAULT_TIMEOUT;
this.transport = opts.transport || global.WebSocket || LongPoll;
+ this.primaryPassedHealthCheck = false;
+ this.longPollFallbackMs = opts.longPollFallbackMs;
+ this.fallbackTimer = null;
+ this.sessionStore = opts.sessionStorage || global && global.sessionStorage;
this.establishedConnections = 0;
this.defaultEncoder = serializer_default.encode.bind(serializer_default);
this.defaultDecoder = serializer_default.decode.bind(serializer_default);
@@ -876,6 +1074,11 @@ var Phoenix = (() => {
}
};
this.logger = opts.logger || null;
+ if (!this.logger && opts.debug) {
+ this.logger = (kind, msg, data) => {
+ console.log(`${kind}: ${msg}`, data);
+ };
+ }
this.longpollerTimeout = opts.longpollerTimeout || 2e4;
this.params = closure(opts.params || {});
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
@@ -887,25 +1090,47 @@ var Phoenix = (() => {
this.teardown(() => this.connect());
}, this.reconnectAfterMs);
}
+ /**
+ * Returns the LongPoll transport reference
+ */
getLongPollTransport() {
return LongPoll;
}
+ /**
+ * Disconnects and replaces the active transport
+ *
+ * @param {Function} newTransport - The new transport class to instantiate
+ *
+ */
replaceTransport(newTransport) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
- this.sendBuffer = [];
if (this.conn) {
this.conn.close();
this.conn = null;
}
this.transport = newTransport;
}
+ /**
+ * Returns the socket protocol
+ *
+ * @returns {string}
+ */
protocol() {
return location.protocol.match(/^https/) ? "wss" : "ws";
}
+ /**
+ * The fully qualified socket url
+ *
+ * @returns {string}
+ */
endPointURL() {
- let uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params()), { vsn: this.vsn });
+ let uri = Ajax.appendParams(
+ Ajax.appendParams(this.endPoint, this.params()),
+ { vsn: this.vsn }
+ );
if (uri.charAt(0) !== "/") {
return uri;
}
@@ -914,12 +1139,29 @@ var Phoenix = (() => {
}
return `${this.protocol()}://${location.host}${uri}`;
}
+ /**
+ * Disconnects the socket
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
+ *
+ * @param {Function} callback - Optional callback which is called after socket is disconnected.
+ * @param {integer} code - A status code for disconnection (Optional).
+ * @param {string} reason - A textual description of the reason to disconnect. (Optional)
+ */
disconnect(callback, code, reason) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
this.teardown(callback, code, reason);
}
+ /**
+ *
+ * @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) {
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
@@ -928,42 +1170,75 @@ var Phoenix = (() => {
if (this.conn) {
return;
}
- this.connectClock++;
- this.closeWasClean = false;
- this.conn = new this.transport(this.endPointURL());
- this.conn.binaryType = this.binaryType;
- this.conn.timeout = this.longpollerTimeout;
- this.conn.onopen = () => this.onConnOpen();
- this.conn.onerror = (error) => this.onConnError(error);
- this.conn.onmessage = (event) => this.onConnMessage(event);
- this.conn.onclose = (event) => this.onConnClose(event);
+ if (this.longPollFallbackMs && this.transport !== LongPoll) {
+ this.connectWithFallback(LongPoll, this.longPollFallbackMs);
+ } else {
+ this.transportConnect();
+ }
}
+ /**
+ * Logs the message. Override `this.logger` for specialized logging. noops by default
+ * @param {string} kind
+ * @param {string} msg
+ * @param {Object} data
+ */
log(kind, msg, data) {
- this.logger(kind, msg, data);
+ this.logger && this.logger(kind, msg, data);
}
+ /**
+ * Returns true if a logger has been set on this socket.
+ */
hasLogger() {
return this.logger !== null;
}
+ /**
+ * Registers callbacks for connection open events
+ *
+ * @example socket.onOpen(function(){ console.info("the socket was opened") })
+ *
+ * @param {Function} callback
+ */
onOpen(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.open.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection close events
+ * @param {Function} callback
+ */
onClose(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.close.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection error events
+ *
+ * @example socket.onError(function(error){ alert("An error occurred") })
+ *
+ * @param {Function} callback
+ */
onError(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.error.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection message events
+ * @param {Function} callback
+ */
onMessage(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.message.push([ref, callback]);
return ref;
}
+ /**
+ * Pings the server and invokes the callback with the RTT in milliseconds
+ * @param {Function} callback
+ *
+ * Returns true if the ping was pushed or false if unable to be pushed.
+ */
ping(callback) {
if (!this.isConnected()) {
return false;
@@ -979,13 +1254,74 @@ var Phoenix = (() => {
});
return true;
}
+ /**
+ * @private
+ */
+ transportConnect() {
+ this.connectClock++;
+ this.closeWasClean = false;
+ this.conn = new this.transport(this.endPointURL());
+ this.conn.binaryType = this.binaryType;
+ this.conn.timeout = this.longpollerTimeout;
+ this.conn.onopen = () => this.onConnOpen();
+ this.conn.onerror = (error) => this.onConnError(error);
+ this.conn.onmessage = (event) => this.onConnMessage(event);
+ this.conn.onclose = (event) => this.onConnClose(event);
+ }
+ getSession(key) {
+ return this.sessionStore && this.sessionStore.getItem(key);
+ }
+ storeSession(key, val) {
+ this.sessionStore && this.sessionStore.setItem(key, val);
+ }
+ connectWithFallback(fallbackTransport, fallbackThreshold = 2500) {
+ clearTimeout(this.fallbackTimer);
+ let established = false;
+ let primaryTransport = true;
+ let openRef, errorRef;
+ let fallback = (reason) => {
+ this.log("transport", `falling back to ${fallbackTransport.name}...`, reason);
+ this.off([openRef, errorRef]);
+ primaryTransport = false;
+ this.replaceTransport(fallbackTransport);
+ this.transportConnect();
+ };
+ if (this.getSession(`phx:fallback:${fallbackTransport.name}`)) {
+ return fallback("memorized");
+ }
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ errorRef = this.onError((reason) => {
+ this.log("transport", "error", reason);
+ if (primaryTransport && !established) {
+ clearTimeout(this.fallbackTimer);
+ fallback(reason);
+ }
+ });
+ this.onOpen(() => {
+ established = true;
+ if (!primaryTransport) {
+ if (!this.primaryPassedHealthCheck) {
+ this.storeSession(`phx:fallback:${fallbackTransport.name}`, "true");
+ }
+ return this.log("transport", `established ${fallbackTransport.name} fallback`);
+ }
+ clearTimeout(this.fallbackTimer);
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ this.ping((rtt) => {
+ this.log("transport", "connected to primary after", rtt);
+ this.primaryPassedHealthCheck = true;
+ clearTimeout(this.fallbackTimer);
+ });
+ });
+ this.transportConnect();
+ }
clearHeartbeats() {
clearTimeout(this.heartbeatTimer);
clearTimeout(this.heartbeatTimeoutTimer);
}
onConnOpen() {
if (this.hasLogger())
- this.log("transport", `connected to ${this.endPointURL()}`);
+ this.log("transport", `${this.transport.name} connected to ${this.endPointURL()}`);
this.closeWasClean = false;
this.establishedConnections++;
this.flushSendBuffer();
@@ -993,6 +1329,9 @@ var Phoenix = (() => {
this.resetHeartbeat();
this.stateChangeCallbacks.open.forEach(([, callback]) => callback());
}
+ /**
+ * @private
+ */
heartbeatTimeout() {
if (this.pendingHeartbeatRef) {
this.pendingHeartbeatRef = null;
@@ -1069,6 +1408,9 @@ var Phoenix = (() => {
}
this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event));
}
+ /**
+ * @private
+ */
onConnError(error) {
if (this.hasLogger())
this.log("transport", error);
@@ -1081,6 +1423,9 @@ var Phoenix = (() => {
this.triggerChanError();
}
}
+ /**
+ * @private
+ */
triggerChanError() {
this.channels.forEach((channel) => {
if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) {
@@ -1088,6 +1433,9 @@ var Phoenix = (() => {
}
});
}
+ /**
+ * @returns {string}
+ */
connectionState() {
switch (this.conn && this.conn.readyState) {
case SOCKET_STATES.connecting:
@@ -1100,13 +1448,27 @@ var Phoenix = (() => {
return "closed";
}
}
+ /**
+ * @returns {boolean}
+ */
isConnected() {
return this.connectionState() === "open";
}
+ /**
+ * @private
+ *
+ * @param {Channel}
+ */
remove(channel) {
this.off(channel.stateChangeRefs);
- this.channels = this.channels.filter((c) => c.joinRef() !== channel.joinRef());
- }
+ this.channels = this.channels.filter((c) => c !== channel);
+ }
+ /**
+ * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
+ *
+ * @param {refs} - list of refs returned by calls to
+ * `onOpen`, `onClose`, `onError,` and `onMessage`
+ */
off(refs) {
for (let key in this.stateChangeCallbacks) {
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
@@ -1114,11 +1476,21 @@ var Phoenix = (() => {
});
}
}
+ /**
+ * Initiates a new channel for the given topic
+ *
+ * @param {string} topic
+ * @param {Object} chanParams - Parameters for the channel
+ * @returns {Channel}
+ */
channel(topic, chanParams = {}) {
let chan = new Channel(topic, chanParams, this);
this.channels.push(chan);
return chan;
}
+ /**
+ * @param {Object} data
+ */
push(data) {
if (this.hasLogger()) {
let { topic, event, payload, ref, join_ref } = data;
@@ -1130,6 +1502,10 @@ var Phoenix = (() => {
this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
}
}
+ /**
+ * Return the next message ref, accounting for overflows
+ * @returns {string}
+ */
makeRef() {
let newRef = this.ref + 1;
if (newRef === this.ref) {
diff --git a/priv/static/phoenix.min.js b/priv/static/phoenix.min.js
index 69bf72a4d8..288fbb929c 100644
--- a/priv/static/phoenix.min.js
+++ b/priv/static/phoenix.min.js
@@ -1,2 +1,2 @@
-var Phoenix=(()=>{var x=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var U=Object.getOwnPropertyNames;var M=Object.prototype.hasOwnProperty;var $=(h,e)=>{for(var t in e)x(h,t,{get:e[t],enumerable:!0})},P=(h,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of U(e))!M.call(h,s)&&s!==t&&x(h,s,{get:()=>e[s],enumerable:!(i=D(e,s))||i.enumerable});return h};var J=h=>P(x({},"__esModule",{value:!0}),h);var W={};$(W,{Channel:()=>j,LongPoll:()=>T,Presence:()=>m,Serializer:()=>y,Socket:()=>w});var S=h=>typeof h=="function"?h:function(){return h};var z=typeof self!="undefined"?self:null,A=typeof window!="undefined"?window:null,R=z||A||R,N="2.0.0",d={connecting:0,open:1,closing:2,closed:3},H=1e4,B=1e3,u={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},p={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},k={longpoll:"longpoll",websocket:"websocket"},O={complete:4};var E=class{constructor(e,t,i,s){this.channel=e,this.event=t,this.payload=i||function(){return{}},this.receivedResp=null,this.timeout=s,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}resend(e){this.timeout=e,this.reset(),this.send()}send(){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()}))}receive(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}matchReceive({status:e,response:t,_ref:i}){this.recHooks.filter(s=>s.status===e).forEach(s=>s.callback(t))}cancelRefEvent(){!this.refEvent||this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,e=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=e,this.matchReceive(e)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(e){return this.receivedResp&&this.receivedResp.status===e}trigger(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}};var b=class{constructor(e,t){this.callback=e,this.timerCalc=t,this.timer=null,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}};var j=class{constructor(e,t,i){this.state=u.closed,this.topic=e,this.params=S(t||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new E(this,p.join,this.params,this.timeout),this.pushBuffer=[],this.stateChangeRefs=[],this.rejoinTimer=new b(()=>{this.socket.isConnected()&&this.rejoin()},this.socket.rejoinAfterMs),this.stateChangeRefs.push(this.socket.onError(()=>this.rejoinTimer.reset())),this.stateChangeRefs.push(this.socket.onOpen(()=>{this.rejoinTimer.reset(),this.isErrored()&&this.rejoin()})),this.joinPush.receive("ok",()=>{this.state=u.joined,this.rejoinTimer.reset(),this.pushBuffer.forEach(s=>s.send()),this.pushBuffer=[]}),this.joinPush.receive("error",()=>{this.state=u.errored,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.hasLogger()&&this.socket.log("channel",`close ${this.topic} ${this.joinRef()}`),this.state=u.closed,this.socket.remove(this)}),this.onError(s=>{this.socket.hasLogger()&&this.socket.log("channel",`error ${this.topic}`,s),this.isJoining()&&this.joinPush.reset(),this.state=u.errored,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.joinPush.receive("timeout",()=>{this.socket.hasLogger()&&this.socket.log("channel",`timeout ${this.topic} (${this.joinRef()})`,this.joinPush.timeout),new E(this,p.leave,S({}),this.timeout).send(),this.state=u.errored,this.joinPush.reset(),this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.on(p.reply,(s,o)=>{this.trigger(this.replyEventName(o),s)})}join(e=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.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}onClose(e){this.on(p.close,e)}onError(e){return this.on(p.error,t=>e(t))}on(e,t){let i=this.bindingRef++;return this.bindings.push({event:e,ref:i,callback:t}),i}off(e,t){this.bindings=this.bindings.filter(i=>!(i.event===e&&(typeof t=="undefined"||t===i.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(e,t,i=this.timeout){if(t=t||{},!this.joinedOnce)throw new Error(`tried to push '${e}' to '${this.topic}' before joining. Use channel.join() before pushing events`);let s=new E(this,e,function(){return t},i);return this.canPush()?s.send():(s.startTimeout(),this.pushBuffer.push(s)),s}leave(e=this.timeout){this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=u.leaving;let t=()=>{this.socket.hasLogger()&&this.socket.log("channel",`leave ${this.topic}`),this.trigger(p.close,"leave")},i=new E(this,p.leave,S({}),e);return i.receive("ok",()=>t()).receive("timeout",()=>t()),i.send(),this.canPush()||i.trigger("ok",{}),i}onMessage(e,t,i){return t}isMember(e,t,i,s){return this.topic!==e?!1:s&&s!==this.joinRef()?(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:i,joinRef:s}),!1):!0}joinRef(){return this.joinPush.ref}rejoin(e=this.timeout){this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=u.joining,this.joinPush.resend(e))}trigger(e,t,i,s){let o=this.onMessage(e,t,i,s);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");let r=this.bindings.filter(n=>n.event===e);for(let n=0;n{let a=this.parseJSON(e.responseText);n&&n(a)},r&&(e.ontimeout=r),e.onprogress=()=>{},e.send(s),e}static xhrRequest(e,t,i,s,o,r,n,a){return e.open(t,i,!0),e.timeout=r,e.setRequestHeader("Content-Type",s),e.onerror=()=>a&&a(null),e.onreadystatechange=()=>{if(e.readyState===O.complete&&a){let l=this.parseJSON(e.responseText);a(l)}},n&&(e.ontimeout=n),e.send(o),e}static parseJSON(e){if(!e||e==="")return null;try{return JSON.parse(e)}catch(t){return console&&console.log("failed to parse JSON response",e),null}}static serialize(e,t){let i=[];for(var s in e){if(!Object.prototype.hasOwnProperty.call(e,s))continue;let o=t?`${t}[${s}]`:s,r=e[s];typeof r=="object"?i.push(this.serialize(r,o)):i.push(encodeURIComponent(o)+"="+encodeURIComponent(r))}return i.join("&")}static appendParams(e,t){if(Object.keys(t).length===0)return e;let i=e.match(/\?/)?"&":"?";return`${e}${i}${this.serialize(t)}`}};var I=h=>{let e="",t=new Uint8Array(h),i=t.byteLength;for(let s=0;sthis.ontimeout(),e=>{if(e){var{status:t,token:i,messages:s}=e;this.token=i}else t=0;switch(t){case 200:s.forEach(o=>{setTimeout(()=>this.onmessage({data:o}),0)}),this.poll();break;case 204:this.poll();break;case 410:this.readyState=d.open,this.onopen({}),this.poll();break;case 403:this.onerror(403),this.close(1008,"forbidden",!1);break;case 0:case 500:this.onerror(500),this.closeAndRetry(1011,"internal server error",500);break;default:throw new Error(`unhandled poll status ${t}`)}})}send(e){typeof e!="string"&&(e=I(e)),this.currentBatch?this.currentBatch.push(e):this.awaitingBatchAck?this.batchBuffer.push(e):(this.currentBatch=[e],this.currentBatchTimer=setTimeout(()=>{this.batchSend(this.currentBatch),this.currentBatch=null},0))}batchSend(e){this.awaitingBatchAck=!0,this.ajax("POST","application/x-ndjson",e.join(`
-`),()=>this.onerror("timeout"),t=>{this.awaitingBatchAck=!1,!t||t.status!==200?(this.onerror(t&&t.status),this.closeAndRetry(1011,"internal server error",!1)):this.batchBuffer.length>0&&(this.batchSend(this.batchBuffer),this.batchBuffer=[])})}close(e,t,i){for(let o of this.reqs)o.abort();this.readyState=d.closed;let s=Object.assign({code:1e3,reason:void 0,wasClean:!0},{code:e,reason:t,wasClean:i});this.batchBuffer=[],clearTimeout(this.currentBatchTimer),this.currentBatchTimer=null,typeof CloseEvent!="undefined"?this.onclose(new CloseEvent("close",s)):this.onclose(s)}ajax(e,t,i,s,o){let r,n=()=>{this.reqs.delete(r),s()};r=g.request(e,this.endpointURL(),t,i,this.timeout,n,a=>{this.reqs.delete(r),this.isActive()&&o(a)}),this.reqs.add(r)}};var m=class{constructor(e,t={}){let i=t.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=e,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(i.state,s=>{let{onJoin:o,onLeave:r,onSync:n}=this.caller;this.joinRef=this.channel.joinRef(),this.state=m.syncState(this.state,s,o,r),this.pendingDiffs.forEach(a=>{this.state=m.syncDiff(this.state,a,o,r)}),this.pendingDiffs=[],n()}),this.channel.on(i.diff,s=>{let{onJoin:o,onLeave:r,onSync:n}=this.caller;this.inPendingSyncState()?this.pendingDiffs.push(s):(this.state=m.syncDiff(this.state,s,o,r),n())})}onJoin(e){this.caller.onJoin=e}onLeave(e){this.caller.onLeave=e}onSync(e){this.caller.onSync=e}list(e){return m.list(this.state,e)}inPendingSyncState(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}static syncState(e,t,i,s){let o=this.clone(e),r={},n={};return this.map(o,(a,l)=>{t[a]||(n[a]=l)}),this.map(t,(a,l)=>{let f=o[a];if(f){let c=l.metas.map(v=>v.phx_ref),C=f.metas.map(v=>v.phx_ref),L=l.metas.filter(v=>C.indexOf(v.phx_ref)<0),_=f.metas.filter(v=>c.indexOf(v.phx_ref)<0);L.length>0&&(r[a]=l,r[a].metas=L),_.length>0&&(n[a]=this.clone(f),n[a].metas=_)}else r[a]=l}),this.syncDiff(o,{joins:r,leaves:n},i,s)}static syncDiff(e,t,i,s){let{joins:o,leaves:r}=this.clone(t);return i||(i=function(){}),s||(s=function(){}),this.map(o,(n,a)=>{let l=e[n];if(e[n]=this.clone(a),l){let f=e[n].metas.map(C=>C.phx_ref),c=l.metas.filter(C=>f.indexOf(C.phx_ref)<0);e[n].metas.unshift(...c)}i(n,l,a)}),this.map(r,(n,a)=>{let l=e[n];if(!l)return;let f=a.metas.map(c=>c.phx_ref);l.metas=l.metas.filter(c=>f.indexOf(c.phx_ref)<0),s(n,l,a),l.metas.length===0&&delete e[n]}),e}static list(e,t){return t||(t=function(i,s){return s}),this.map(e,(i,s)=>t(i,s))}static map(e,t){return Object.getOwnPropertyNames(e).map(i=>t(i,e[i]))}static clone(e){return JSON.parse(JSON.stringify(e))}};var y={HEADER_LENGTH:1,META_LENGTH:4,KINDS:{push:0,reply:1,broadcast:2},encode(h,e){if(h.payload.constructor===ArrayBuffer)return e(this.binaryEncode(h));{let t=[h.join_ref,h.ref,h.topic,h.event,h.payload];return e(JSON.stringify(t))}},decode(h,e){if(h.constructor===ArrayBuffer)return e(this.binaryDecode(h));{let[t,i,s,o,r]=JSON.parse(h);return e({join_ref:t,ref:i,topic:s,event:o,payload:r})}},binaryEncode(h){let{join_ref:e,ref:t,event:i,topic:s,payload:o}=h,r=this.META_LENGTH+e.length+t.length+s.length+i.length,n=new ArrayBuffer(this.HEADER_LENGTH+r),a=new DataView(n),l=0;a.setUint8(l++,this.KINDS.push),a.setUint8(l++,e.length),a.setUint8(l++,t.length),a.setUint8(l++,s.length),a.setUint8(l++,i.length),Array.from(e,c=>a.setUint8(l++,c.charCodeAt(0))),Array.from(t,c=>a.setUint8(l++,c.charCodeAt(0))),Array.from(s,c=>a.setUint8(l++,c.charCodeAt(0))),Array.from(i,c=>a.setUint8(l++,c.charCodeAt(0)));var f=new Uint8Array(n.byteLength+o.byteLength);return f.set(new Uint8Array(n),0),f.set(new Uint8Array(o),n.byteLength),f.buffer},binaryDecode(h){let e=new DataView(h),t=e.getUint8(0),i=new TextDecoder;switch(t){case this.KINDS.push:return this.decodePush(h,e,i);case this.KINDS.reply:return this.decodeReply(h,e,i);case this.KINDS.broadcast:return this.decodeBroadcast(h,e,i)}},decodePush(h,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=e.getUint8(3),r=this.HEADER_LENGTH+this.META_LENGTH-1,n=t.decode(h.slice(r,r+i));r=r+i;let a=t.decode(h.slice(r,r+s));r=r+s;let l=t.decode(h.slice(r,r+o));r=r+o;let f=h.slice(r,h.byteLength);return{join_ref:n,ref:null,topic:a,event:l,payload:f}},decodeReply(h,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=e.getUint8(3),r=e.getUint8(4),n=this.HEADER_LENGTH+this.META_LENGTH,a=t.decode(h.slice(n,n+i));n=n+i;let l=t.decode(h.slice(n,n+s));n=n+s;let f=t.decode(h.slice(n,n+o));n=n+o;let c=t.decode(h.slice(n,n+r));n=n+r;let C=h.slice(n,h.byteLength),L={status:c,response:C};return{join_ref:a,ref:l,topic:f,event:p.reply,payload:L}},decodeBroadcast(h,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=this.HEADER_LENGTH+2,r=t.decode(h.slice(o,o+i));o=o+i;let n=t.decode(h.slice(o,o+s));o=o+s;let a=h.slice(o,h.byteLength);return{join_ref:null,ref:null,topic:r,event:n,payload:a}}};var w=class{constructor(e,t={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=t.timeout||H,this.transport=t.transport||R.WebSocket||T,this.establishedConnections=0,this.defaultEncoder=y.encode.bind(y),this.defaultDecoder=y.decode.bind(y),this.closeWasClean=!1,this.binaryType=t.binaryType||"arraybuffer",this.connectClock=1,this.transport!==T?(this.encode=t.encode||this.defaultEncoder,this.decode=t.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder);let i=null;A&&A.addEventListener&&(A.addEventListener("pagehide",s=>{this.conn&&(this.disconnect(),i=this.connectClock)}),A.addEventListener("pageshow",s=>{i===this.connectClock&&(i=null,this.connect())})),this.heartbeatIntervalMs=t.heartbeatIntervalMs||3e4,this.rejoinAfterMs=s=>t.rejoinAfterMs?t.rejoinAfterMs(s):[1e3,2e3,5e3][s-1]||1e4,this.reconnectAfterMs=s=>t.reconnectAfterMs?t.reconnectAfterMs(s):[10,50,100,150,200,250,500,1e3,2e3][s-1]||5e3,this.logger=t.logger||null,this.longpollerTimeout=t.longpollerTimeout||2e4,this.params=S(t.params||{}),this.endPoint=`${e}/${k.websocket}`,this.vsn=t.vsn||N,this.heartbeatTimeoutTimer=null,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new b(()=>{this.teardown(()=>this.connect())},this.reconnectAfterMs)}getLongPollTransport(){return T}replaceTransport(e){this.connectClock++,this.closeWasClean=!0,this.reconnectTimer.reset(),this.sendBuffer=[],this.conn&&(this.conn.close(),this.conn=null),this.transport=e}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let e=g.appendParams(g.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return e.charAt(0)!=="/"?e:e.charAt(1)==="/"?`${this.protocol()}:${e}`:`${this.protocol()}://${location.host}${e}`}disconnect(e,t,i){this.connectClock++,this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,i)}connect(e){e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=S(e)),!this.conn&&(this.connectClock++,this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=()=>this.onConnOpen(),this.conn.onerror=t=>this.onConnError(t),this.conn.onmessage=t=>this.onConnMessage(t),this.conn.onclose=t=>this.onConnClose(t))}log(e,t,i){this.logger(e,t,i)}hasLogger(){return this.logger!==null}onOpen(e){let t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}onClose(e){let t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}onError(e){let t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}onMessage(e){let t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}ping(e){if(!this.isConnected())return!1;let t=this.makeRef(),i=Date.now();this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:t});let s=this.onMessage(o=>{o.ref===t&&(this.off([s]),e(Date.now()-i))});return!0}clearHeartbeats(){clearTimeout(this.heartbeatTimer),clearTimeout(this.heartbeatTimeoutTimer)}onConnOpen(){this.hasLogger()&&this.log("transport",`connected to ${this.endPointURL()}`),this.closeWasClean=!1,this.establishedConnections++,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach(([,e])=>e())}heartbeatTimeout(){this.pendingHeartbeatRef&&(this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),this.triggerChanError(),this.closeWasClean=!1,this.teardown(()=>this.reconnectTimer.scheduleTimeout(),B,"heartbeat timeout"))}resetHeartbeat(){this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,this.clearHeartbeats(),this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs))}teardown(e,t,i){if(!this.conn)return e&&e();this.waitForBufferDone(()=>{this.conn&&(t?this.conn.close(t,i||""):this.conn.close()),this.waitForSocketClosed(()=>{this.conn&&(this.conn.onopen=function(){},this.conn.onerror=function(){},this.conn.onmessage=function(){},this.conn.onclose=function(){},this.conn=null),e&&e()})})}waitForBufferDone(e,t=1){if(t===5||!this.conn||!this.conn.bufferedAmount){e();return}setTimeout(()=>{this.waitForBufferDone(e,t+1)},150*t)}waitForSocketClosed(e,t=1){if(t===5||!this.conn||this.conn.readyState===d.closed){e();return}setTimeout(()=>{this.waitForSocketClosed(e,t+1)},150*t)}onConnClose(e){let t=e&&e.code;this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),this.clearHeartbeats(),!this.closeWasClean&&t!==1e3&&this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(([,i])=>i(e))}onConnError(e){this.hasLogger()&&this.log("transport",e);let t=this.transport,i=this.establishedConnections;this.stateChangeCallbacks.error.forEach(([,s])=>{s(e,t,i)}),(t===this.transport||i>0)&&this.triggerChanError()}triggerChanError(){this.channels.forEach(e=>{e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(p.error)})}connectionState(){switch(this.conn&&this.conn.readyState){case d.connecting:return"connecting";case d.open:return"open";case d.closing:return"closing";default:return"closed"}}isConnected(){return this.connectionState()==="open"}remove(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter(t=>t.joinRef()!==e.joinRef())}off(e){for(let t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter(([i])=>e.indexOf(i)===-1)}channel(e,t={}){let i=new j(e,t,this);return this.channels.push(i),i}push(e){if(this.hasLogger()){let{topic:t,event:i,payload:s,ref:o,join_ref:r}=e;this.log("push",`${t} ${i} (${r}, ${o})`,s)}this.isConnected()?this.encode(e,t=>this.conn.send(t)):this.sendBuffer.push(()=>this.encode(e,t=>this.conn.send(t)))}makeRef(){let e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}sendHeartbeat(){this.pendingHeartbeatRef&&!this.isConnected()||(this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef}),this.heartbeatTimeoutTimer=setTimeout(()=>this.heartbeatTimeout(),this.heartbeatIntervalMs))}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(e=>e()),this.sendBuffer=[])}onConnMessage(e){this.decode(e.data,t=>{let{topic:i,event:s,payload:o,ref:r,join_ref:n}=t;r&&r===this.pendingHeartbeatRef&&(this.clearHeartbeats(),this.pendingHeartbeatRef=null,this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.hasLogger()&&this.log("receive",`${o.status||""} ${i} ${s} ${r&&"("+r+")"||""}`,o);for(let a=0;ai.topic===e&&(i.isJoined()||i.isJoining()));t&&(this.hasLogger()&&this.log("transport",`leaving duplicate topic "${e}"`),t.leave())}};return J(W);})();
+var Phoenix=(()=>{var x=Object.defineProperty;var $=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var P=Object.prototype.hasOwnProperty;var D=(a,e)=>{for(var t in e)x(a,t,{get:e[t],enumerable:!0})},U=(a,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of M(e))!P.call(a,s)&&s!==t&&x(a,s,{get:()=>e[s],enumerable:!(i=$(e,s))||i.enumerable});return a};var J=a=>U(x({},"__esModule",{value:!0}),a);var F={};D(F,{Channel:()=>y,LongPoll:()=>m,Presence:()=>g,Serializer:()=>j,Socket:()=>w});var v=a=>typeof a=="function"?a:function(){return a};var z=typeof self!="undefined"?self:null,k=typeof window!="undefined"?window:null,T=z||k||T,H="2.0.0",d={connecting:0,open:1,closing:2,closed:3},N=1e4,O=1e3,u={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},p={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},A={longpoll:"longpoll",websocket:"websocket"},B={complete:4};var b=class{constructor(e,t,i,s){this.channel=e,this.event=t,this.payload=i||function(){return{}},this.receivedResp=null,this.timeout=s,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}resend(e){this.timeout=e,this.reset(),this.send()}send(){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()}))}receive(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}matchReceive({status:e,response:t,_ref:i}){this.recHooks.filter(s=>s.status===e).forEach(s=>s.callback(t))}cancelRefEvent(){this.refEvent&&this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,e=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=e,this.matchReceive(e)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(e){return this.receivedResp&&this.receivedResp.status===e}trigger(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}};var R=class{constructor(e,t){this.callback=e,this.timerCalc=t,this.timer=null,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}};var y=class{constructor(e,t,i){this.state=u.closed,this.topic=e,this.params=v(t||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new b(this,p.join,this.params,this.timeout),this.pushBuffer=[],this.stateChangeRefs=[],this.rejoinTimer=new R(()=>{this.socket.isConnected()&&this.rejoin()},this.socket.rejoinAfterMs),this.stateChangeRefs.push(this.socket.onError(()=>this.rejoinTimer.reset())),this.stateChangeRefs.push(this.socket.onOpen(()=>{this.rejoinTimer.reset(),this.isErrored()&&this.rejoin()})),this.joinPush.receive("ok",()=>{this.state=u.joined,this.rejoinTimer.reset(),this.pushBuffer.forEach(s=>s.send()),this.pushBuffer=[]}),this.joinPush.receive("error",()=>{this.state=u.errored,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.hasLogger()&&this.socket.log("channel",`close ${this.topic} ${this.joinRef()}`),this.state=u.closed,this.socket.remove(this)}),this.onError(s=>{this.socket.hasLogger()&&this.socket.log("channel",`error ${this.topic}`,s),this.isJoining()&&this.joinPush.reset(),this.state=u.errored,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.joinPush.receive("timeout",()=>{this.socket.hasLogger()&&this.socket.log("channel",`timeout ${this.topic} (${this.joinRef()})`,this.joinPush.timeout),new b(this,p.leave,v({}),this.timeout).send(),this.state=u.errored,this.joinPush.reset(),this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.on(p.reply,(s,o)=>{this.trigger(this.replyEventName(o),s)})}join(e=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.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}onClose(e){this.on(p.close,e)}onError(e){return this.on(p.error,t=>e(t))}on(e,t){let i=this.bindingRef++;return this.bindings.push({event:e,ref:i,callback:t}),i}off(e,t){this.bindings=this.bindings.filter(i=>!(i.event===e&&(typeof t=="undefined"||t===i.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(e,t,i=this.timeout){if(t=t||{},!this.joinedOnce)throw new Error(`tried to push '${e}' to '${this.topic}' before joining. Use channel.join() before pushing events`);let s=new b(this,e,function(){return t},i);return this.canPush()?s.send():(s.startTimeout(),this.pushBuffer.push(s)),s}leave(e=this.timeout){this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=u.leaving;let t=()=>{this.socket.hasLogger()&&this.socket.log("channel",`leave ${this.topic}`),this.trigger(p.close,"leave")},i=new b(this,p.leave,v({}),e);return i.receive("ok",()=>t()).receive("timeout",()=>t()),i.send(),this.canPush()||i.trigger("ok",{}),i}onMessage(e,t,i){return t}isMember(e,t,i,s){return this.topic!==e?!1:s&&s!==this.joinRef()?(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:i,joinRef:s}),!1):!0}joinRef(){return this.joinPush.ref}rejoin(e=this.timeout){this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=u.joining,this.joinPush.resend(e))}trigger(e,t,i,s){let o=this.onMessage(e,t,i,s);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");let r=this.bindings.filter(n=>n.event===e);for(let n=0;n{let h=this.parseJSON(e.responseText);n&&n(h)},r&&(e.ontimeout=r),e.onprogress=()=>{},e.send(s),e}static xhrRequest(e,t,i,s,o,r,n,h){return e.open(t,i,!0),e.timeout=r,e.setRequestHeader("Content-Type",s),e.onerror=()=>h&&h(null),e.onreadystatechange=()=>{if(e.readyState===B.complete&&h){let l=this.parseJSON(e.responseText);h(l)}},n&&(e.ontimeout=n),e.send(o),e}static parseJSON(e){if(!e||e==="")return null;try{return JSON.parse(e)}catch(t){return console&&console.log("failed to parse JSON response",e),null}}static serialize(e,t){let i=[];for(var s in e){if(!Object.prototype.hasOwnProperty.call(e,s))continue;let o=t?`${t}[${s}]`:s,r=e[s];typeof r=="object"?i.push(this.serialize(r,o)):i.push(encodeURIComponent(o)+"="+encodeURIComponent(r))}return i.join("&")}static appendParams(e,t){if(Object.keys(t).length===0)return e;let i=e.match(/\?/)?"&":"?";return`${e}${i}${this.serialize(t)}`}};var I=a=>{let e="",t=new Uint8Array(a),i=t.byteLength;for(let s=0;sthis.poll(),0)}normalizeEndpoint(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+A.websocket),"$1/"+A.longpoll)}endpointURL(){return C.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(e,t,i){this.close(e,t,i),this.readyState=d.connecting}ontimeout(){this.onerror("timeout"),this.closeAndRetry(1005,"timeout",!1)}isActive(){return this.readyState===d.open||this.readyState===d.connecting}poll(){this.ajax("GET","application/json",null,()=>this.ontimeout(),e=>{if(e){var{status:t,token:i,messages:s}=e;this.token=i}else t=0;switch(t){case 200:s.forEach(o=>{setTimeout(()=>this.onmessage({data:o}),0)}),this.poll();break;case 204:this.poll();break;case 410:this.readyState=d.open,this.onopen({}),this.poll();break;case 403:this.onerror(403),this.close(1008,"forbidden",!1);break;case 0:case 500:this.onerror(500),this.closeAndRetry(1011,"internal server error",500);break;default:throw new Error(`unhandled poll status ${t}`)}})}send(e){typeof e!="string"&&(e=I(e)),this.currentBatch?this.currentBatch.push(e):this.awaitingBatchAck?this.batchBuffer.push(e):(this.currentBatch=[e],this.currentBatchTimer=setTimeout(()=>{this.batchSend(this.currentBatch),this.currentBatch=null},0))}batchSend(e){this.awaitingBatchAck=!0,this.ajax("POST","application/x-ndjson",e.join(`
+`),()=>this.onerror("timeout"),t=>{this.awaitingBatchAck=!1,!t||t.status!==200?(this.onerror(t&&t.status),this.closeAndRetry(1011,"internal server error",!1)):this.batchBuffer.length>0&&(this.batchSend(this.batchBuffer),this.batchBuffer=[])})}close(e,t,i){for(let o of this.reqs)o.abort();this.readyState=d.closed;let s=Object.assign({code:1e3,reason:void 0,wasClean:!0},{code:e,reason:t,wasClean:i});this.batchBuffer=[],clearTimeout(this.currentBatchTimer),this.currentBatchTimer=null,typeof CloseEvent!="undefined"?this.onclose(new CloseEvent("close",s)):this.onclose(s)}ajax(e,t,i,s,o){let r,n=()=>{this.reqs.delete(r),s()};r=C.request(e,this.endpointURL(),t,i,this.timeout,n,h=>{this.reqs.delete(r),this.isActive()&&o(h)}),this.reqs.add(r)}};var g=class{constructor(e,t={}){let i=t.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=e,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(i.state,s=>{let{onJoin:o,onLeave:r,onSync:n}=this.caller;this.joinRef=this.channel.joinRef(),this.state=g.syncState(this.state,s,o,r),this.pendingDiffs.forEach(h=>{this.state=g.syncDiff(this.state,h,o,r)}),this.pendingDiffs=[],n()}),this.channel.on(i.diff,s=>{let{onJoin:o,onLeave:r,onSync:n}=this.caller;this.inPendingSyncState()?this.pendingDiffs.push(s):(this.state=g.syncDiff(this.state,s,o,r),n())})}onJoin(e){this.caller.onJoin=e}onLeave(e){this.caller.onLeave=e}onSync(e){this.caller.onSync=e}list(e){return g.list(this.state,e)}inPendingSyncState(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}static syncState(e,t,i,s){let o=this.clone(e),r={},n={};return this.map(o,(h,l)=>{t[h]||(n[h]=l)}),this.map(t,(h,l)=>{let f=o[h];if(f){let c=l.metas.map(S=>S.phx_ref),E=f.metas.map(S=>S.phx_ref),L=l.metas.filter(S=>E.indexOf(S.phx_ref)<0),_=f.metas.filter(S=>c.indexOf(S.phx_ref)<0);L.length>0&&(r[h]=l,r[h].metas=L),_.length>0&&(n[h]=this.clone(f),n[h].metas=_)}else r[h]=l}),this.syncDiff(o,{joins:r,leaves:n},i,s)}static syncDiff(e,t,i,s){let{joins:o,leaves:r}=this.clone(t);return i||(i=function(){}),s||(s=function(){}),this.map(o,(n,h)=>{let l=e[n];if(e[n]=this.clone(h),l){let f=e[n].metas.map(E=>E.phx_ref),c=l.metas.filter(E=>f.indexOf(E.phx_ref)<0);e[n].metas.unshift(...c)}i(n,l,h)}),this.map(r,(n,h)=>{let l=e[n];if(!l)return;let f=h.metas.map(c=>c.phx_ref);l.metas=l.metas.filter(c=>f.indexOf(c.phx_ref)<0),s(n,l,h),l.metas.length===0&&delete e[n]}),e}static list(e,t){return t||(t=function(i,s){return s}),this.map(e,(i,s)=>t(i,s))}static map(e,t){return Object.getOwnPropertyNames(e).map(i=>t(i,e[i]))}static clone(e){return JSON.parse(JSON.stringify(e))}};var j={HEADER_LENGTH:1,META_LENGTH:4,KINDS:{push:0,reply:1,broadcast:2},encode(a,e){if(a.payload.constructor===ArrayBuffer)return e(this.binaryEncode(a));{let t=[a.join_ref,a.ref,a.topic,a.event,a.payload];return e(JSON.stringify(t))}},decode(a,e){if(a.constructor===ArrayBuffer)return e(this.binaryDecode(a));{let[t,i,s,o,r]=JSON.parse(a);return e({join_ref:t,ref:i,topic:s,event:o,payload:r})}},binaryEncode(a){let{join_ref:e,ref:t,event:i,topic:s,payload:o}=a,r=this.META_LENGTH+e.length+t.length+s.length+i.length,n=new ArrayBuffer(this.HEADER_LENGTH+r),h=new DataView(n),l=0;h.setUint8(l++,this.KINDS.push),h.setUint8(l++,e.length),h.setUint8(l++,t.length),h.setUint8(l++,s.length),h.setUint8(l++,i.length),Array.from(e,c=>h.setUint8(l++,c.charCodeAt(0))),Array.from(t,c=>h.setUint8(l++,c.charCodeAt(0))),Array.from(s,c=>h.setUint8(l++,c.charCodeAt(0))),Array.from(i,c=>h.setUint8(l++,c.charCodeAt(0)));var f=new Uint8Array(n.byteLength+o.byteLength);return f.set(new Uint8Array(n),0),f.set(new Uint8Array(o),n.byteLength),f.buffer},binaryDecode(a){let e=new DataView(a),t=e.getUint8(0),i=new TextDecoder;switch(t){case this.KINDS.push:return this.decodePush(a,e,i);case this.KINDS.reply:return this.decodeReply(a,e,i);case this.KINDS.broadcast:return this.decodeBroadcast(a,e,i)}},decodePush(a,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=e.getUint8(3),r=this.HEADER_LENGTH+this.META_LENGTH-1,n=t.decode(a.slice(r,r+i));r=r+i;let h=t.decode(a.slice(r,r+s));r=r+s;let l=t.decode(a.slice(r,r+o));r=r+o;let f=a.slice(r,a.byteLength);return{join_ref:n,ref:null,topic:h,event:l,payload:f}},decodeReply(a,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=e.getUint8(3),r=e.getUint8(4),n=this.HEADER_LENGTH+this.META_LENGTH,h=t.decode(a.slice(n,n+i));n=n+i;let l=t.decode(a.slice(n,n+s));n=n+s;let f=t.decode(a.slice(n,n+o));n=n+o;let c=t.decode(a.slice(n,n+r));n=n+r;let E=a.slice(n,a.byteLength),L={status:c,response:E};return{join_ref:h,ref:l,topic:f,event:p.reply,payload:L}},decodeBroadcast(a,e,t){let i=e.getUint8(1),s=e.getUint8(2),o=this.HEADER_LENGTH+2,r=t.decode(a.slice(o,o+i));o=o+i;let n=t.decode(a.slice(o,o+s));o=o+s;let h=a.slice(o,a.byteLength);return{join_ref:null,ref:null,topic:r,event:n,payload:h}}};var w=class{constructor(e,t={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=t.timeout||N,this.transport=t.transport||T.WebSocket||m,this.primaryPassedHealthCheck=!1,this.longPollFallbackMs=t.longPollFallbackMs,this.fallbackTimer=null,this.sessionStore=t.sessionStorage||T&&T.sessionStorage,this.establishedConnections=0,this.defaultEncoder=j.encode.bind(j),this.defaultDecoder=j.decode.bind(j),this.closeWasClean=!1,this.binaryType=t.binaryType||"arraybuffer",this.connectClock=1,this.transport!==m?(this.encode=t.encode||this.defaultEncoder,this.decode=t.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder);let i=null;k&&k.addEventListener&&(k.addEventListener("pagehide",s=>{this.conn&&(this.disconnect(),i=this.connectClock)}),k.addEventListener("pageshow",s=>{i===this.connectClock&&(i=null,this.connect())})),this.heartbeatIntervalMs=t.heartbeatIntervalMs||3e4,this.rejoinAfterMs=s=>t.rejoinAfterMs?t.rejoinAfterMs(s):[1e3,2e3,5e3][s-1]||1e4,this.reconnectAfterMs=s=>t.reconnectAfterMs?t.reconnectAfterMs(s):[10,50,100,150,200,250,500,1e3,2e3][s-1]||5e3,this.logger=t.logger||null,!this.logger&&t.debug&&(this.logger=(s,o,r)=>{console.log(`${s}: ${o}`,r)}),this.longpollerTimeout=t.longpollerTimeout||2e4,this.params=v(t.params||{}),this.endPoint=`${e}/${A.websocket}`,this.vsn=t.vsn||H,this.heartbeatTimeoutTimer=null,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new R(()=>{this.teardown(()=>this.connect())},this.reconnectAfterMs)}getLongPollTransport(){return m}replaceTransport(e){this.connectClock++,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.conn&&(this.conn.close(),this.conn=null),this.transport=e}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let e=C.appendParams(C.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return e.charAt(0)!=="/"?e:e.charAt(1)==="/"?`${this.protocol()}:${e}`:`${this.protocol()}://${location.host}${e}`}disconnect(e,t,i){this.connectClock++,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.teardown(e,t,i)}connect(e){e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=v(e)),!this.conn&&(this.longPollFallbackMs&&this.transport!==m?this.connectWithFallback(m,this.longPollFallbackMs):this.transportConnect())}log(e,t,i){this.logger&&this.logger(e,t,i)}hasLogger(){return this.logger!==null}onOpen(e){let t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}onClose(e){let t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}onError(e){let t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}onMessage(e){let t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}ping(e){if(!this.isConnected())return!1;let t=this.makeRef(),i=Date.now();this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:t});let s=this.onMessage(o=>{o.ref===t&&(this.off([s]),e(Date.now()-i))});return!0}transportConnect(){this.connectClock++,this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=()=>this.onConnOpen(),this.conn.onerror=e=>this.onConnError(e),this.conn.onmessage=e=>this.onConnMessage(e),this.conn.onclose=e=>this.onConnClose(e)}getSession(e){return this.sessionStore&&this.sessionStore.getItem(e)}storeSession(e,t){this.sessionStore&&this.sessionStore.setItem(e,t)}connectWithFallback(e,t=2500){clearTimeout(this.fallbackTimer);let i=!1,s=!0,o,r,n=h=>{this.log("transport",`falling back to ${e.name}...`,h),this.off([o,r]),s=!1,this.replaceTransport(e),this.transportConnect()};if(this.getSession(`phx:fallback:${e.name}`))return n("memorized");this.fallbackTimer=setTimeout(n,t),r=this.onError(h=>{this.log("transport","error",h),s&&!i&&(clearTimeout(this.fallbackTimer),n(h))}),this.onOpen(()=>{if(i=!0,!s)return this.primaryPassedHealthCheck||this.storeSession(`phx:fallback:${e.name}`,"true"),this.log("transport",`established ${e.name} fallback`);clearTimeout(this.fallbackTimer),this.fallbackTimer=setTimeout(n,t),this.ping(h=>{this.log("transport","connected to primary after",h),this.primaryPassedHealthCheck=!0,clearTimeout(this.fallbackTimer)})}),this.transportConnect()}clearHeartbeats(){clearTimeout(this.heartbeatTimer),clearTimeout(this.heartbeatTimeoutTimer)}onConnOpen(){this.hasLogger()&&this.log("transport",`${this.transport.name} connected to ${this.endPointURL()}`),this.closeWasClean=!1,this.establishedConnections++,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach(([,e])=>e())}heartbeatTimeout(){this.pendingHeartbeatRef&&(this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),this.triggerChanError(),this.closeWasClean=!1,this.teardown(()=>this.reconnectTimer.scheduleTimeout(),O,"heartbeat timeout"))}resetHeartbeat(){this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,this.clearHeartbeats(),this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs))}teardown(e,t,i){if(!this.conn)return e&&e();this.waitForBufferDone(()=>{this.conn&&(t?this.conn.close(t,i||""):this.conn.close()),this.waitForSocketClosed(()=>{this.conn&&(this.conn.onopen=function(){},this.conn.onerror=function(){},this.conn.onmessage=function(){},this.conn.onclose=function(){},this.conn=null),e&&e()})})}waitForBufferDone(e,t=1){if(t===5||!this.conn||!this.conn.bufferedAmount){e();return}setTimeout(()=>{this.waitForBufferDone(e,t+1)},150*t)}waitForSocketClosed(e,t=1){if(t===5||!this.conn||this.conn.readyState===d.closed){e();return}setTimeout(()=>{this.waitForSocketClosed(e,t+1)},150*t)}onConnClose(e){let t=e&&e.code;this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),this.clearHeartbeats(),!this.closeWasClean&&t!==1e3&&this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(([,i])=>i(e))}onConnError(e){this.hasLogger()&&this.log("transport",e);let t=this.transport,i=this.establishedConnections;this.stateChangeCallbacks.error.forEach(([,s])=>{s(e,t,i)}),(t===this.transport||i>0)&&this.triggerChanError()}triggerChanError(){this.channels.forEach(e=>{e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(p.error)})}connectionState(){switch(this.conn&&this.conn.readyState){case d.connecting:return"connecting";case d.open:return"open";case d.closing:return"closing";default:return"closed"}}isConnected(){return this.connectionState()==="open"}remove(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter(t=>t!==e)}off(e){for(let t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter(([i])=>e.indexOf(i)===-1)}channel(e,t={}){let i=new y(e,t,this);return this.channels.push(i),i}push(e){if(this.hasLogger()){let{topic:t,event:i,payload:s,ref:o,join_ref:r}=e;this.log("push",`${t} ${i} (${r}, ${o})`,s)}this.isConnected()?this.encode(e,t=>this.conn.send(t)):this.sendBuffer.push(()=>this.encode(e,t=>this.conn.send(t)))}makeRef(){let e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}sendHeartbeat(){this.pendingHeartbeatRef&&!this.isConnected()||(this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef}),this.heartbeatTimeoutTimer=setTimeout(()=>this.heartbeatTimeout(),this.heartbeatIntervalMs))}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(e=>e()),this.sendBuffer=[])}onConnMessage(e){this.decode(e.data,t=>{let{topic:i,event:s,payload:o,ref:r,join_ref:n}=t;r&&r===this.pendingHeartbeatRef&&(this.clearHeartbeats(),this.pendingHeartbeatRef=null,this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.hasLogger()&&this.log("receive",`${o.status||""} ${i} ${s} ${r&&"("+r+")"||""}`,o);for(let h=0;hi.topic===e&&(i.isJoined()||i.isJoining()));t&&(this.hasLogger()&&this.log("transport",`leaving duplicate topic "${e}"`),t.leave())}};return J(F);})();
diff --git a/priv/static/phoenix.mjs b/priv/static/phoenix.mjs
index 6533cd58af..ca9ca08f68 100644
--- a/priv/static/phoenix.mjs
+++ b/priv/static/phoenix.mjs
@@ -54,11 +54,18 @@ var Push = class {
this.recHooks = [];
this.sent = false;
}
+ /**
+ *
+ * @param {number} timeout
+ */
resend(timeout) {
this.timeout = timeout;
this.reset();
this.send();
}
+ /**
+ *
+ */
send() {
if (this.hasReceived("timeout")) {
return;
@@ -73,6 +80,11 @@ var Push = class {
join_ref: this.channel.joinRef()
});
}
+ /**
+ *
+ * @param {*} status
+ * @param {*} callback
+ */
receive(status, callback) {
if (this.hasReceived(status)) {
callback(this.receivedResp.response);
@@ -80,6 +92,9 @@ var Push = class {
this.recHooks.push({ status, callback });
return this;
}
+ /**
+ * @private
+ */
reset() {
this.cancelRefEvent();
this.ref = null;
@@ -87,19 +102,31 @@ var Push = class {
this.receivedResp = null;
this.sent = false;
}
+ /**
+ * @private
+ */
matchReceive({ status, response, _ref }) {
this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response));
}
+ /**
+ * @private
+ */
cancelRefEvent() {
if (!this.refEvent) {
return;
}
this.channel.off(this.refEvent);
}
+ /**
+ * @private
+ */
cancelTimeout() {
clearTimeout(this.timeoutTimer);
this.timeoutTimer = null;
}
+ /**
+ * @private
+ */
startTimeout() {
if (this.timeoutTimer) {
this.cancelTimeout();
@@ -116,9 +143,15 @@ var Push = class {
this.trigger("timeout", {});
}, this.timeout);
}
+ /**
+ * @private
+ */
hasReceived(status) {
return this.receivedResp && this.receivedResp.status === status;
}
+ /**
+ * @private
+ */
trigger(status, response) {
this.channel.trigger(this.refEvent, { status, response });
}
@@ -136,6 +169,9 @@ var Timer = class {
this.tries = 0;
clearTimeout(this.timer);
}
+ /**
+ * Cancels any previous scheduleTimeout and schedules callback
+ */
scheduleTimeout() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
@@ -165,12 +201,14 @@ var Channel = class {
}
}, this.socket.rejoinAfterMs);
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()));
- this.stateChangeRefs.push(this.socket.onOpen(() => {
- this.rejoinTimer.reset();
- if (this.isErrored()) {
- this.rejoin();
- }
- }));
+ this.stateChangeRefs.push(
+ this.socket.onOpen(() => {
+ this.rejoinTimer.reset();
+ if (this.isErrored()) {
+ this.rejoin();
+ }
+ })
+ );
this.joinPush.receive("ok", () => {
this.state = CHANNEL_STATES.joined;
this.rejoinTimer.reset();
@@ -216,6 +254,11 @@ var Channel = class {
this.trigger(this.replyEventName(ref), payload);
});
}
+ /**
+ * Join the channel
+ * @param {integer} timeout
+ * @returns {Push}
+ */
join(timeout = this.timeout) {
if (this.joinedOnce) {
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");
@@ -226,25 +269,87 @@ var Channel = class {
return this.joinPush;
}
}
+ /**
+ * Hook into channel close
+ * @param {Function} callback
+ */
onClose(callback) {
this.on(CHANNEL_EVENTS.close, callback);
}
+ /**
+ * Hook into channel errors
+ * @param {Function} callback
+ */
onError(callback) {
return this.on(CHANNEL_EVENTS.error, (reason) => callback(reason));
}
+ /**
+ * Subscribes on channel events
+ *
+ * Subscription returns a ref counter, which can be used later to
+ * unsubscribe the exact event listener
+ *
+ * @example
+ * const ref1 = channel.on("event", do_stuff)
+ * const ref2 = channel.on("event", do_other_stuff)
+ * channel.off("event", ref1)
+ * // Since unsubscription, do_stuff won't fire,
+ * // while do_other_stuff will keep firing on the "event"
+ *
+ * @param {string} event
+ * @param {Function} callback
+ * @returns {integer} ref
+ */
on(event, callback) {
let ref = this.bindingRef++;
this.bindings.push({ event, ref, callback });
return ref;
}
+ /**
+ * Unsubscribes off of channel events
+ *
+ * Use the ref returned from a channel.on() to unsubscribe one
+ * handler, or pass nothing for the ref to unsubscribe all
+ * handlers for the given event.
+ *
+ * @example
+ * // Unsubscribe the do_stuff handler
+ * const ref1 = channel.on("event", do_stuff)
+ * channel.off("event", ref1)
+ *
+ * // Unsubscribe all handlers from event
+ * channel.off("event")
+ *
+ * @param {string} event
+ * @param {integer} ref
+ */
off(event, ref) {
this.bindings = this.bindings.filter((bind) => {
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref));
});
}
+ /**
+ * @private
+ */
canPush() {
return this.socket.isConnected() && this.isJoined();
}
+ /**
+ * Sends a message `event` to phoenix with the payload `payload`.
+ * Phoenix receives this in the `handle_in(event, payload, socket)`
+ * function. if phoenix replies or it times out (default 10000ms),
+ * then optionally the reply can be received.
+ *
+ * @example
+ * channel.push("event")
+ * .receive("ok", payload => console.log("phoenix replied:", payload))
+ * .receive("error", err => console.log("phoenix errored", err))
+ * .receive("timeout", () => console.log("timed out pushing"))
+ * @param {string} event
+ * @param {Object} payload
+ * @param {number} [timeout]
+ * @returns {Push}
+ */
push(event, payload, timeout = this.timeout) {
payload = payload || {};
if (!this.joinedOnce) {
@@ -261,6 +366,22 @@ var Channel = class {
}
return pushEvent;
}
+ /** Leaves the channel
+ *
+ * Unsubscribes from server events, and
+ * instructs channel to terminate on server
+ *
+ * Triggers onClose() hooks
+ *
+ * To receive leave acknowledgements, use the `receive`
+ * hook to bind to the server ack, ie:
+ *
+ * @example
+ * channel.leave().receive("ok", () => alert("left!") )
+ *
+ * @param {integer} timeout
+ * @returns {Push}
+ */
leave(timeout = this.timeout) {
this.rejoinTimer.reset();
this.joinPush.cancelTimeout();
@@ -278,9 +399,24 @@ var Channel = class {
}
return leavePush;
}
+ /**
+ * Overridable message hook
+ *
+ * Receives all events for specialized message handling
+ * before dispatching to the channel callbacks.
+ *
+ * Must return the payload, modified or unmodified
+ * @param {string} event
+ * @param {Object} payload
+ * @param {integer} ref
+ * @returns {Object}
+ */
onMessage(_event, payload, _ref) {
return payload;
}
+ /**
+ * @private
+ */
isMember(topic, event, payload, joinRef) {
if (this.topic !== topic) {
return false;
@@ -293,9 +429,15 @@ var Channel = class {
return true;
}
}
+ /**
+ * @private
+ */
joinRef() {
return this.joinPush.ref;
}
+ /**
+ * @private
+ */
rejoin(timeout = this.timeout) {
if (this.isLeaving()) {
return;
@@ -304,6 +446,9 @@ var Channel = class {
this.state = CHANNEL_STATES.joining;
this.joinPush.resend(timeout);
}
+ /**
+ * @private
+ */
trigger(event, payload, ref, joinRef) {
let handledPayload = this.onMessage(event, payload, ref, joinRef);
if (payload && !handledPayload) {
@@ -315,21 +460,39 @@ var Channel = class {
bind.callback(handledPayload, ref, joinRef || this.joinRef());
}
}
+ /**
+ * @private
+ */
replyEventName(ref) {
return `chan_reply_${ref}`;
}
+ /**
+ * @private
+ */
isClosed() {
return this.state === CHANNEL_STATES.closed;
}
+ /**
+ * @private
+ */
isErrored() {
return this.state === CHANNEL_STATES.errored;
}
+ /**
+ * @private
+ */
isJoined() {
return this.state === CHANNEL_STATES.joined;
}
+ /**
+ * @private
+ */
isJoining() {
return this.state === CHANNEL_STATES.joining;
}
+ /**
+ * @private
+ */
isLeaving() {
return this.state === CHANNEL_STATES.leaving;
}
@@ -444,7 +607,7 @@ var LongPoll = class {
};
this.pollEndpoint = this.normalizeEndpoint(endPoint);
this.readyState = SOCKET_STATES.connecting;
- this.poll();
+ setTimeout(() => this.poll(), 0);
}
normalizeEndpoint(endPoint) {
return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
@@ -500,6 +663,9 @@ var LongPoll = class {
}
});
}
+ // we collect all pushes within the current event loop by
+ // setTimeout 0, which optimizes back-to-back procedural
+ // pushes against an empty buffer
send(body) {
if (typeof body !== "string") {
body = arrayBufferToBase64(body);
@@ -611,6 +777,15 @@ var Presence = class {
inPendingSyncState() {
return !this.joinRef || this.joinRef !== this.channel.joinRef();
}
+ // lower-level public static API
+ /**
+ * Used to sync the list of presences on the server
+ * with the client's state. An optional `onJoin` and `onLeave` callback can
+ * be provided to react to changes in the client's local presences across
+ * disconnects and reconnects with the server.
+ *
+ * @returns {Presence}
+ */
static syncState(currentState, newState, onJoin, onLeave) {
let state = this.clone(currentState);
let joins = {};
@@ -641,6 +816,15 @@ var Presence = class {
});
return this.syncDiff(state, { joins, leaves }, onJoin, onLeave);
}
+ /**
+ *
+ * Used to sync a diff of presence join and leave
+ * events from the server, as they happen. Like `syncState`, `syncDiff`
+ * accepts optional `onJoin` and `onLeave` callbacks to react to a user
+ * joining or leaving from a device.
+ *
+ * @returns {Presence}
+ */
static syncDiff(state, diff, onJoin, onLeave) {
let { joins, leaves } = this.clone(diff);
if (!onJoin) {
@@ -677,6 +861,14 @@ var Presence = class {
});
return state;
}
+ /**
+ * Returns the array of presences, with selected metadata.
+ *
+ * @param {Object} presences
+ * @param {Function} chooser
+ *
+ * @returns {Presence}
+ */
static list(presences, chooser) {
if (!chooser) {
chooser = function(key, pres) {
@@ -687,6 +879,7 @@ var Presence = class {
return chooser(key, presence);
});
}
+ // private
static map(obj, func) {
return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key]));
}
@@ -716,6 +909,7 @@ var serializer_default = {
return callback({ join_ref, ref, topic, event, payload });
}
},
+ // private
binaryEncode(message) {
let { join_ref, ref, event, topic, payload } = message;
let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length;
@@ -803,6 +997,10 @@ var Socket = class {
this.ref = 0;
this.timeout = opts.timeout || DEFAULT_TIMEOUT;
this.transport = opts.transport || global.WebSocket || LongPoll;
+ this.primaryPassedHealthCheck = false;
+ this.longPollFallbackMs = opts.longPollFallbackMs;
+ this.fallbackTimer = null;
+ this.sessionStore = opts.sessionStorage || global && global.sessionStorage;
this.establishedConnections = 0;
this.defaultEncoder = serializer_default.encode.bind(serializer_default);
this.defaultDecoder = serializer_default.decode.bind(serializer_default);
@@ -847,6 +1045,11 @@ var Socket = class {
}
};
this.logger = opts.logger || null;
+ if (!this.logger && opts.debug) {
+ this.logger = (kind, msg, data) => {
+ console.log(`${kind}: ${msg}`, data);
+ };
+ }
this.longpollerTimeout = opts.longpollerTimeout || 2e4;
this.params = closure(opts.params || {});
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`;
@@ -858,25 +1061,47 @@ var Socket = class {
this.teardown(() => this.connect());
}, this.reconnectAfterMs);
}
+ /**
+ * Returns the LongPoll transport reference
+ */
getLongPollTransport() {
return LongPoll;
}
+ /**
+ * Disconnects and replaces the active transport
+ *
+ * @param {Function} newTransport - The new transport class to instantiate
+ *
+ */
replaceTransport(newTransport) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
- this.sendBuffer = [];
if (this.conn) {
this.conn.close();
this.conn = null;
}
this.transport = newTransport;
}
+ /**
+ * Returns the socket protocol
+ *
+ * @returns {string}
+ */
protocol() {
return location.protocol.match(/^https/) ? "wss" : "ws";
}
+ /**
+ * The fully qualified socket url
+ *
+ * @returns {string}
+ */
endPointURL() {
- let uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params()), { vsn: this.vsn });
+ let uri = Ajax.appendParams(
+ Ajax.appendParams(this.endPoint, this.params()),
+ { vsn: this.vsn }
+ );
if (uri.charAt(0) !== "/") {
return uri;
}
@@ -885,12 +1110,29 @@ var Socket = class {
}
return `${this.protocol()}://${location.host}${uri}`;
}
+ /**
+ * Disconnects the socket
+ *
+ * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
+ *
+ * @param {Function} callback - Optional callback which is called after socket is disconnected.
+ * @param {integer} code - A status code for disconnection (Optional).
+ * @param {string} reason - A textual description of the reason to disconnect. (Optional)
+ */
disconnect(callback, code, reason) {
this.connectClock++;
this.closeWasClean = true;
+ clearTimeout(this.fallbackTimer);
this.reconnectTimer.reset();
this.teardown(callback, code, reason);
}
+ /**
+ *
+ * @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) {
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
@@ -899,42 +1141,75 @@ var Socket = class {
if (this.conn) {
return;
}
- this.connectClock++;
- this.closeWasClean = false;
- this.conn = new this.transport(this.endPointURL());
- this.conn.binaryType = this.binaryType;
- this.conn.timeout = this.longpollerTimeout;
- this.conn.onopen = () => this.onConnOpen();
- this.conn.onerror = (error) => this.onConnError(error);
- this.conn.onmessage = (event) => this.onConnMessage(event);
- this.conn.onclose = (event) => this.onConnClose(event);
+ if (this.longPollFallbackMs && this.transport !== LongPoll) {
+ this.connectWithFallback(LongPoll, this.longPollFallbackMs);
+ } else {
+ this.transportConnect();
+ }
}
+ /**
+ * Logs the message. Override `this.logger` for specialized logging. noops by default
+ * @param {string} kind
+ * @param {string} msg
+ * @param {Object} data
+ */
log(kind, msg, data) {
- this.logger(kind, msg, data);
+ this.logger && this.logger(kind, msg, data);
}
+ /**
+ * Returns true if a logger has been set on this socket.
+ */
hasLogger() {
return this.logger !== null;
}
+ /**
+ * Registers callbacks for connection open events
+ *
+ * @example socket.onOpen(function(){ console.info("the socket was opened") })
+ *
+ * @param {Function} callback
+ */
onOpen(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.open.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection close events
+ * @param {Function} callback
+ */
onClose(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.close.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection error events
+ *
+ * @example socket.onError(function(error){ alert("An error occurred") })
+ *
+ * @param {Function} callback
+ */
onError(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.error.push([ref, callback]);
return ref;
}
+ /**
+ * Registers callbacks for connection message events
+ * @param {Function} callback
+ */
onMessage(callback) {
let ref = this.makeRef();
this.stateChangeCallbacks.message.push([ref, callback]);
return ref;
}
+ /**
+ * Pings the server and invokes the callback with the RTT in milliseconds
+ * @param {Function} callback
+ *
+ * Returns true if the ping was pushed or false if unable to be pushed.
+ */
ping(callback) {
if (!this.isConnected()) {
return false;
@@ -950,13 +1225,74 @@ var Socket = class {
});
return true;
}
+ /**
+ * @private
+ */
+ transportConnect() {
+ this.connectClock++;
+ this.closeWasClean = false;
+ this.conn = new this.transport(this.endPointURL());
+ this.conn.binaryType = this.binaryType;
+ this.conn.timeout = this.longpollerTimeout;
+ this.conn.onopen = () => this.onConnOpen();
+ this.conn.onerror = (error) => this.onConnError(error);
+ this.conn.onmessage = (event) => this.onConnMessage(event);
+ this.conn.onclose = (event) => this.onConnClose(event);
+ }
+ getSession(key) {
+ return this.sessionStore && this.sessionStore.getItem(key);
+ }
+ storeSession(key, val) {
+ this.sessionStore && this.sessionStore.setItem(key, val);
+ }
+ connectWithFallback(fallbackTransport, fallbackThreshold = 2500) {
+ clearTimeout(this.fallbackTimer);
+ let established = false;
+ let primaryTransport = true;
+ let openRef, errorRef;
+ let fallback = (reason) => {
+ this.log("transport", `falling back to ${fallbackTransport.name}...`, reason);
+ this.off([openRef, errorRef]);
+ primaryTransport = false;
+ this.replaceTransport(fallbackTransport);
+ this.transportConnect();
+ };
+ if (this.getSession(`phx:fallback:${fallbackTransport.name}`)) {
+ return fallback("memorized");
+ }
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ errorRef = this.onError((reason) => {
+ this.log("transport", "error", reason);
+ if (primaryTransport && !established) {
+ clearTimeout(this.fallbackTimer);
+ fallback(reason);
+ }
+ });
+ this.onOpen(() => {
+ established = true;
+ if (!primaryTransport) {
+ if (!this.primaryPassedHealthCheck) {
+ this.storeSession(`phx:fallback:${fallbackTransport.name}`, "true");
+ }
+ return this.log("transport", `established ${fallbackTransport.name} fallback`);
+ }
+ clearTimeout(this.fallbackTimer);
+ this.fallbackTimer = setTimeout(fallback, fallbackThreshold);
+ this.ping((rtt) => {
+ this.log("transport", "connected to primary after", rtt);
+ this.primaryPassedHealthCheck = true;
+ clearTimeout(this.fallbackTimer);
+ });
+ });
+ this.transportConnect();
+ }
clearHeartbeats() {
clearTimeout(this.heartbeatTimer);
clearTimeout(this.heartbeatTimeoutTimer);
}
onConnOpen() {
if (this.hasLogger())
- this.log("transport", `connected to ${this.endPointURL()}`);
+ this.log("transport", `${this.transport.name} connected to ${this.endPointURL()}`);
this.closeWasClean = false;
this.establishedConnections++;
this.flushSendBuffer();
@@ -964,6 +1300,9 @@ var Socket = class {
this.resetHeartbeat();
this.stateChangeCallbacks.open.forEach(([, callback]) => callback());
}
+ /**
+ * @private
+ */
heartbeatTimeout() {
if (this.pendingHeartbeatRef) {
this.pendingHeartbeatRef = null;
@@ -1040,6 +1379,9 @@ var Socket = class {
}
this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event));
}
+ /**
+ * @private
+ */
onConnError(error) {
if (this.hasLogger())
this.log("transport", error);
@@ -1052,6 +1394,9 @@ var Socket = class {
this.triggerChanError();
}
}
+ /**
+ * @private
+ */
triggerChanError() {
this.channels.forEach((channel) => {
if (!(channel.isErrored() || channel.isLeaving() || channel.isClosed())) {
@@ -1059,6 +1404,9 @@ var Socket = class {
}
});
}
+ /**
+ * @returns {string}
+ */
connectionState() {
switch (this.conn && this.conn.readyState) {
case SOCKET_STATES.connecting:
@@ -1071,13 +1419,27 @@ var Socket = class {
return "closed";
}
}
+ /**
+ * @returns {boolean}
+ */
isConnected() {
return this.connectionState() === "open";
}
+ /**
+ * @private
+ *
+ * @param {Channel}
+ */
remove(channel) {
this.off(channel.stateChangeRefs);
- this.channels = this.channels.filter((c) => c.joinRef() !== channel.joinRef());
- }
+ this.channels = this.channels.filter((c) => c !== channel);
+ }
+ /**
+ * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
+ *
+ * @param {refs} - list of refs returned by calls to
+ * `onOpen`, `onClose`, `onError,` and `onMessage`
+ */
off(refs) {
for (let key in this.stateChangeCallbacks) {
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
@@ -1085,11 +1447,21 @@ var Socket = class {
});
}
}
+ /**
+ * Initiates a new channel for the given topic
+ *
+ * @param {string} topic
+ * @param {Object} chanParams - Parameters for the channel
+ * @returns {Channel}
+ */
channel(topic, chanParams = {}) {
let chan = new Channel(topic, chanParams, this);
this.channels.push(chan);
return chan;
}
+ /**
+ * @param {Object} data
+ */
push(data) {
if (this.hasLogger()) {
let { topic, event, payload, ref, join_ref } = data;
@@ -1101,6 +1473,10 @@ var Socket = class {
this.sendBuffer.push(() => this.encode(data, (result) => this.conn.send(result)));
}
}
+ /**
+ * Return the next message ref, accounting for overflows
+ * @returns {string}
+ */
makeRef() {
let newRef = this.ref + 1;
if (newRef === this.ref) {
diff --git a/priv/static/phoenix.mjs.map b/priv/static/phoenix.mjs.map
index 3b4117c5be..04797b1753 100644
--- a/priv/static/phoenix.mjs.map
+++ b/priv/static/phoenix.mjs.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../assets/js/phoenix/utils.js", "../../assets/js/phoenix/constants.js", "../../assets/js/phoenix/push.js", "../../assets/js/phoenix/timer.js", "../../assets/js/phoenix/channel.js", "../../assets/js/phoenix/ajax.js", "../../assets/js/phoenix/longpoll.js", "../../assets/js/phoenix/presence.js", "../../assets/js/phoenix/serializer.js", "../../assets/js/phoenix/socket.js"],
- "sourcesContent": ["// wraps value in closure or returns closure\nexport let closure = (value) => {\n if(typeof value === \"function\"){\n return value\n } else {\n let closure = function (){ return value }\n return closure\n }\n}\n", "export const globalSelf = typeof self !== \"undefined\" ? self : null\nexport const phxWindow = typeof window !== \"undefined\" ? window : null\nexport const global = globalSelf || phxWindow || global\nexport const DEFAULT_VSN = \"2.0.0\"\nexport const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}\nexport const DEFAULT_TIMEOUT = 10000\nexport const WS_CLOSE_NORMAL = 1000\nexport const CHANNEL_STATES = {\n closed: \"closed\",\n errored: \"errored\",\n joined: \"joined\",\n joining: \"joining\",\n leaving: \"leaving\",\n}\nexport const CHANNEL_EVENTS = {\n close: \"phx_close\",\n error: \"phx_error\",\n join: \"phx_join\",\n reply: \"phx_reply\",\n leave: \"phx_leave\"\n}\n\nexport const TRANSPORTS = {\n longpoll: \"longpoll\",\n websocket: \"websocket\"\n}\nexport const XHR_STATES = {\n complete: 4\n}\n", "/**\n * Initializes the Push\n * @param {Channel} channel - The Channel\n * @param {string} event - The event, for example `\"phx_join\"`\n * @param {Object} payload - The payload, for example `{user_id: 123}`\n * @param {number} timeout - The push timeout in milliseconds\n */\nexport default class Push {\n constructor(channel, event, payload, timeout){\n this.channel = channel\n this.event = event\n this.payload = payload || function (){ return {} }\n this.receivedResp = null\n this.timeout = timeout\n this.timeoutTimer = null\n this.recHooks = []\n this.sent = false\n }\n\n /**\n *\n * @param {number} timeout\n */\n resend(timeout){\n this.timeout = timeout\n this.reset()\n this.send()\n }\n\n /**\n *\n */\n send(){\n if(this.hasReceived(\"timeout\")){ return }\n this.startTimeout()\n this.sent = true\n this.channel.socket.push({\n topic: this.channel.topic,\n event: this.event,\n payload: this.payload(),\n ref: this.ref,\n join_ref: this.channel.joinRef()\n })\n }\n\n /**\n *\n * @param {*} status\n * @param {*} callback\n */\n receive(status, callback){\n if(this.hasReceived(status)){\n callback(this.receivedResp.response)\n }\n\n this.recHooks.push({status, callback})\n return this\n }\n\n /**\n * @private\n */\n reset(){\n this.cancelRefEvent()\n this.ref = null\n this.refEvent = null\n this.receivedResp = null\n this.sent = false\n }\n\n /**\n * @private\n */\n matchReceive({status, response, _ref}){\n this.recHooks.filter(h => h.status === status)\n .forEach(h => h.callback(response))\n }\n\n /**\n * @private\n */\n cancelRefEvent(){\n if(!this.refEvent){ return }\n this.channel.off(this.refEvent)\n }\n\n /**\n * @private\n */\n cancelTimeout(){\n clearTimeout(this.timeoutTimer)\n this.timeoutTimer = null\n }\n\n /**\n * @private\n */\n startTimeout(){\n if(this.timeoutTimer){ this.cancelTimeout() }\n this.ref = this.channel.socket.makeRef()\n this.refEvent = this.channel.replyEventName(this.ref)\n\n this.channel.on(this.refEvent, payload => {\n this.cancelRefEvent()\n this.cancelTimeout()\n this.receivedResp = payload\n this.matchReceive(payload)\n })\n\n this.timeoutTimer = setTimeout(() => {\n this.trigger(\"timeout\", {})\n }, this.timeout)\n }\n\n /**\n * @private\n */\n hasReceived(status){\n return this.receivedResp && this.receivedResp.status === status\n }\n\n /**\n * @private\n */\n trigger(status, response){\n this.channel.trigger(this.refEvent, {status, response})\n }\n}\n", "/**\n *\n * Creates a timer that accepts a `timerCalc` function to perform\n * calculated timeout retries, such as exponential backoff.\n *\n * @example\n * let reconnectTimer = new Timer(() => this.connect(), function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * })\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * reconnectTimer.scheduleTimeout() // fires after 5000\n * reconnectTimer.reset()\n * reconnectTimer.scheduleTimeout() // fires after 1000\n *\n * @param {Function} callback\n * @param {Function} timerCalc\n */\nexport default class Timer {\n constructor(callback, timerCalc){\n this.callback = callback\n this.timerCalc = timerCalc\n this.timer = null\n this.tries = 0\n }\n\n reset(){\n this.tries = 0\n clearTimeout(this.timer)\n }\n\n /**\n * Cancels any previous scheduleTimeout and schedules callback\n */\n scheduleTimeout(){\n clearTimeout(this.timer)\n\n this.timer = setTimeout(() => {\n this.tries = this.tries + 1\n this.callback()\n }, this.timerCalc(this.tries + 1))\n }\n}\n", "import {closure} from \"./utils\"\nimport {\n CHANNEL_EVENTS,\n CHANNEL_STATES,\n} from \"./constants\"\n\nimport Push from \"./push\"\nimport Timer from \"./timer\"\n\n/**\n *\n * @param {string} topic\n * @param {(Object|function)} params\n * @param {Socket} socket\n */\nexport default class Channel {\n constructor(topic, params, socket){\n this.state = CHANNEL_STATES.closed\n this.topic = topic\n this.params = closure(params || {})\n this.socket = socket\n this.bindings = []\n this.bindingRef = 0\n this.timeout = this.socket.timeout\n this.joinedOnce = false\n this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)\n this.pushBuffer = []\n this.stateChangeRefs = []\n\n this.rejoinTimer = new Timer(() => {\n if(this.socket.isConnected()){ this.rejoin() }\n }, this.socket.rejoinAfterMs)\n this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))\n this.stateChangeRefs.push(this.socket.onOpen(() => {\n this.rejoinTimer.reset()\n if(this.isErrored()){ this.rejoin() }\n })\n )\n this.joinPush.receive(\"ok\", () => {\n this.state = CHANNEL_STATES.joined\n this.rejoinTimer.reset()\n this.pushBuffer.forEach(pushEvent => pushEvent.send())\n this.pushBuffer = []\n })\n this.joinPush.receive(\"error\", () => {\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.onClose(() => {\n this.rejoinTimer.reset()\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `close ${this.topic} ${this.joinRef()}`)\n this.state = CHANNEL_STATES.closed\n this.socket.remove(this)\n })\n this.onError(reason => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `error ${this.topic}`, reason)\n if(this.isJoining()){ this.joinPush.reset() }\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.joinPush.receive(\"timeout\", () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)\n leavePush.send()\n this.state = CHANNEL_STATES.errored\n this.joinPush.reset()\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.on(CHANNEL_EVENTS.reply, (payload, ref) => {\n this.trigger(this.replyEventName(ref), payload)\n })\n }\n\n /**\n * Join the channel\n * @param {integer} timeout\n * @returns {Push}\n */\n join(timeout = this.timeout){\n if(this.joinedOnce){\n throw new Error(\"tried to join multiple times. 'join' can only be called a single time per channel instance\")\n } else {\n this.timeout = timeout\n this.joinedOnce = true\n this.rejoin()\n return this.joinPush\n }\n }\n\n /**\n * Hook into channel close\n * @param {Function} callback\n */\n onClose(callback){\n this.on(CHANNEL_EVENTS.close, callback)\n }\n\n /**\n * Hook into channel errors\n * @param {Function} callback\n */\n onError(callback){\n return this.on(CHANNEL_EVENTS.error, reason => callback(reason))\n }\n\n /**\n * Subscribes on channel events\n *\n * Subscription returns a ref counter, which can be used later to\n * unsubscribe the exact event listener\n *\n * @example\n * const ref1 = channel.on(\"event\", do_stuff)\n * const ref2 = channel.on(\"event\", do_other_stuff)\n * channel.off(\"event\", ref1)\n * // Since unsubscription, do_stuff won't fire,\n * // while do_other_stuff will keep firing on the \"event\"\n *\n * @param {string} event\n * @param {Function} callback\n * @returns {integer} ref\n */\n on(event, callback){\n let ref = this.bindingRef++\n this.bindings.push({event, ref, callback})\n return ref\n }\n\n /**\n * Unsubscribes off of channel events\n *\n * Use the ref returned from a channel.on() to unsubscribe one\n * handler, or pass nothing for the ref to unsubscribe all\n * handlers for the given event.\n *\n * @example\n * // Unsubscribe the do_stuff handler\n * const ref1 = channel.on(\"event\", do_stuff)\n * channel.off(\"event\", ref1)\n *\n * // Unsubscribe all handlers from event\n * channel.off(\"event\")\n *\n * @param {string} event\n * @param {integer} ref\n */\n off(event, ref){\n this.bindings = this.bindings.filter((bind) => {\n return !(bind.event === event && (typeof ref === \"undefined\" || ref === bind.ref))\n })\n }\n\n /**\n * @private\n */\n canPush(){ return this.socket.isConnected() && this.isJoined() }\n\n /**\n * Sends a message `event` to phoenix with the payload `payload`.\n * Phoenix receives this in the `handle_in(event, payload, socket)`\n * function. if phoenix replies or it times out (default 10000ms),\n * then optionally the reply can be received.\n *\n * @example\n * channel.push(\"event\")\n * .receive(\"ok\", payload => console.log(\"phoenix replied:\", payload))\n * .receive(\"error\", err => console.log(\"phoenix errored\", err))\n * .receive(\"timeout\", () => console.log(\"timed out pushing\"))\n * @param {string} event\n * @param {Object} payload\n * @param {number} [timeout]\n * @returns {Push}\n */\n push(event, payload, timeout = this.timeout){\n payload = payload || {}\n if(!this.joinedOnce){\n throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)\n }\n let pushEvent = new Push(this, event, function (){ return payload }, timeout)\n if(this.canPush()){\n pushEvent.send()\n } else {\n pushEvent.startTimeout()\n this.pushBuffer.push(pushEvent)\n }\n\n return pushEvent\n }\n\n /** Leaves the channel\n *\n * Unsubscribes from server events, and\n * instructs channel to terminate on server\n *\n * Triggers onClose() hooks\n *\n * To receive leave acknowledgements, use the `receive`\n * hook to bind to the server ack, ie:\n *\n * @example\n * channel.leave().receive(\"ok\", () => alert(\"left!\") )\n *\n * @param {integer} timeout\n * @returns {Push}\n */\n leave(timeout = this.timeout){\n this.rejoinTimer.reset()\n this.joinPush.cancelTimeout()\n\n this.state = CHANNEL_STATES.leaving\n let onClose = () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `leave ${this.topic}`)\n this.trigger(CHANNEL_EVENTS.close, \"leave\")\n }\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)\n leavePush.receive(\"ok\", () => onClose())\n .receive(\"timeout\", () => onClose())\n leavePush.send()\n if(!this.canPush()){ leavePush.trigger(\"ok\", {}) }\n\n return leavePush\n }\n\n /**\n * Overridable message hook\n *\n * Receives all events for specialized message handling\n * before dispatching to the channel callbacks.\n *\n * Must return the payload, modified or unmodified\n * @param {string} event\n * @param {Object} payload\n * @param {integer} ref\n * @returns {Object}\n */\n onMessage(_event, payload, _ref){ return payload }\n\n /**\n * @private\n */\n isMember(topic, event, payload, joinRef){\n if(this.topic !== topic){ return false }\n\n if(joinRef && joinRef !== this.joinRef()){\n if(this.socket.hasLogger()) this.socket.log(\"channel\", \"dropping outdated message\", {topic, event, payload, joinRef})\n return false\n } else {\n return true\n }\n }\n\n /**\n * @private\n */\n joinRef(){ return this.joinPush.ref }\n\n /**\n * @private\n */\n rejoin(timeout = this.timeout){\n if(this.isLeaving()){ return }\n this.socket.leaveOpenTopic(this.topic)\n this.state = CHANNEL_STATES.joining\n this.joinPush.resend(timeout)\n }\n\n /**\n * @private\n */\n trigger(event, payload, ref, joinRef){\n let handledPayload = this.onMessage(event, payload, ref, joinRef)\n if(payload && !handledPayload){ throw new Error(\"channel onMessage callbacks must return the payload, modified or unmodified\") }\n\n let eventBindings = this.bindings.filter(bind => bind.event === event)\n\n for(let i = 0; i < eventBindings.length; i++){\n let bind = eventBindings[i]\n bind.callback(handledPayload, ref, joinRef || this.joinRef())\n }\n }\n\n /**\n * @private\n */\n replyEventName(ref){ return `chan_reply_${ref}` }\n\n /**\n * @private\n */\n isClosed(){ return this.state === CHANNEL_STATES.closed }\n\n /**\n * @private\n */\n isErrored(){ return this.state === CHANNEL_STATES.errored }\n\n /**\n * @private\n */\n isJoined(){ return this.state === CHANNEL_STATES.joined }\n\n /**\n * @private\n */\n isJoining(){ return this.state === CHANNEL_STATES.joining }\n\n /**\n * @private\n */\n isLeaving(){ return this.state === CHANNEL_STATES.leaving }\n}\n", "import {\n global,\n XHR_STATES\n} from \"./constants\"\n\nexport default class Ajax {\n\n static request(method, endPoint, accept, body, timeout, ontimeout, callback){\n if(global.XDomainRequest){\n let req = new global.XDomainRequest() // IE8, IE9\n return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)\n } else {\n let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari\n return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)\n }\n }\n\n static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){\n req.timeout = timeout\n req.open(method, endPoint)\n req.onload = () => {\n let response = this.parseJSON(req.responseText)\n callback && callback(response)\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n // Work around bug in IE9 that requires an attached onprogress handler\n req.onprogress = () => { }\n\n req.send(body)\n return req\n }\n\n static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){\n req.open(method, endPoint, true)\n req.timeout = timeout\n req.setRequestHeader(\"Content-Type\", accept)\n req.onerror = () => callback && callback(null)\n req.onreadystatechange = () => {\n if(req.readyState === XHR_STATES.complete && callback){\n let response = this.parseJSON(req.responseText)\n callback(response)\n }\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n req.send(body)\n return req\n }\n\n static parseJSON(resp){\n if(!resp || resp === \"\"){ return null }\n\n try {\n return JSON.parse(resp)\n } catch (e){\n console && console.log(\"failed to parse JSON response\", resp)\n return null\n }\n }\n\n static serialize(obj, parentKey){\n let queryStr = []\n for(var key in obj){\n if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }\n let paramKey = parentKey ? `${parentKey}[${key}]` : key\n let paramVal = obj[key]\n if(typeof paramVal === \"object\"){\n queryStr.push(this.serialize(paramVal, paramKey))\n } else {\n queryStr.push(encodeURIComponent(paramKey) + \"=\" + encodeURIComponent(paramVal))\n }\n }\n return queryStr.join(\"&\")\n }\n\n static appendParams(url, params){\n if(Object.keys(params).length === 0){ return url }\n\n let prefix = url.match(/\\?/) ? \"&\" : \"?\"\n return `${url}${prefix}${this.serialize(params)}`\n }\n}\n", "import {\n SOCKET_STATES,\n TRANSPORTS\n} from \"./constants\"\n\nimport Ajax from \"./ajax\"\n\nlet arrayBufferToBase64 = (buffer) => {\n let binary = \"\"\n let bytes = new Uint8Array(buffer)\n let len = bytes.byteLength\n for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }\n return btoa(binary)\n}\n\nexport default class LongPoll {\n\n constructor(endPoint){\n this.endPoint = null\n this.token = null\n this.skipHeartbeat = true\n this.reqs = new Set()\n this.awaitingBatchAck = false\n this.currentBatch = null\n this.currentBatchTimer = null\n this.batchBuffer = []\n this.onopen = function (){ } // noop\n this.onerror = function (){ } // noop\n this.onmessage = function (){ } // noop\n this.onclose = function (){ } // noop\n this.pollEndpoint = this.normalizeEndpoint(endPoint)\n this.readyState = SOCKET_STATES.connecting\n this.poll()\n }\n\n normalizeEndpoint(endPoint){\n return (endPoint\n .replace(\"ws://\", \"http://\")\n .replace(\"wss://\", \"https://\")\n .replace(new RegExp(\"(.*)\\/\" + TRANSPORTS.websocket), \"$1/\" + TRANSPORTS.longpoll))\n }\n\n endpointURL(){\n return Ajax.appendParams(this.pollEndpoint, {token: this.token})\n }\n\n closeAndRetry(code, reason, wasClean){\n this.close(code, reason, wasClean)\n this.readyState = SOCKET_STATES.connecting\n }\n\n ontimeout(){\n this.onerror(\"timeout\")\n this.closeAndRetry(1005, \"timeout\", false)\n }\n\n isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }\n\n poll(){\n this.ajax(\"GET\", \"application/json\", null, () => this.ontimeout(), resp => {\n if(resp){\n var {status, token, messages} = resp\n this.token = token\n } else {\n status = 0\n }\n\n switch(status){\n case 200:\n messages.forEach(msg => {\n // Tasks are what things like event handlers, setTimeout callbacks,\n // promise resolves and more are run within.\n // In modern browsers, there are two different kinds of tasks,\n // microtasks and macrotasks.\n // Microtasks are mainly used for Promises, while macrotasks are\n // used for everything else.\n // Microtasks always have priority over macrotasks. If the JS engine\n // is looking for a task to run, it will always try to empty the\n // microtask queue before attempting to run anything from the\n // macrotask queue.\n //\n // For the WebSocket transport, messages always arrive in their own\n // event. This means that if any promises are resolved from within,\n // their callbacks will always finish execution by the time the\n // next message event handler is run.\n //\n // In order to emulate this behaviour, we need to make sure each\n // onmessage handler is run within its own macrotask.\n setTimeout(() => this.onmessage({data: msg}), 0)\n })\n this.poll()\n break\n case 204:\n this.poll()\n break\n case 410:\n this.readyState = SOCKET_STATES.open\n this.onopen({})\n this.poll()\n break\n case 403:\n this.onerror(403)\n this.close(1008, \"forbidden\", false)\n break\n case 0:\n case 500:\n this.onerror(500)\n this.closeAndRetry(1011, \"internal server error\", 500)\n break\n default: throw new Error(`unhandled poll status ${status}`)\n }\n })\n }\n\n // we collect all pushes within the current event loop by\n // setTimeout 0, which optimizes back-to-back procedural\n // pushes against an empty buffer\n\n send(body){\n if(typeof(body) !== \"string\"){ body = arrayBufferToBase64(body) }\n if(this.currentBatch){\n this.currentBatch.push(body)\n } else if(this.awaitingBatchAck){\n this.batchBuffer.push(body)\n } else {\n this.currentBatch = [body]\n this.currentBatchTimer = setTimeout(() => {\n this.batchSend(this.currentBatch)\n this.currentBatch = null\n }, 0)\n }\n }\n\n batchSend(messages){\n this.awaitingBatchAck = true\n this.ajax(\"POST\", \"application/x-ndjson\", messages.join(\"\\n\"), () => this.onerror(\"timeout\"), resp => {\n this.awaitingBatchAck = false\n if(!resp || resp.status !== 200){\n this.onerror(resp && resp.status)\n this.closeAndRetry(1011, \"internal server error\", false)\n } else if(this.batchBuffer.length > 0){\n this.batchSend(this.batchBuffer)\n this.batchBuffer = []\n }\n })\n }\n\n close(code, reason, wasClean){\n for(let req of this.reqs){ req.abort() }\n this.readyState = SOCKET_STATES.closed\n let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})\n this.batchBuffer = []\n clearTimeout(this.currentBatchTimer)\n this.currentBatchTimer = null\n if(typeof(CloseEvent) !== \"undefined\"){\n this.onclose(new CloseEvent(\"close\", opts))\n } else {\n this.onclose(opts)\n }\n }\n\n ajax(method, contentType, body, onCallerTimeout, callback){\n let req\n let ontimeout = () => {\n this.reqs.delete(req)\n onCallerTimeout()\n }\n req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {\n this.reqs.delete(req)\n if(this.isActive()){ callback(resp) }\n })\n this.reqs.add(req)\n }\n}\n", "/**\n * Initializes the Presence\n * @param {Channel} channel - The Channel\n * @param {Object} opts - The options,\n * for example `{events: {state: \"state\", diff: \"diff\"}}`\n */\nexport default class Presence {\n\n constructor(channel, opts = {}){\n let events = opts.events || {state: \"presence_state\", diff: \"presence_diff\"}\n this.state = {}\n this.pendingDiffs = []\n this.channel = channel\n this.joinRef = null\n this.caller = {\n onJoin: function (){ },\n onLeave: function (){ },\n onSync: function (){ }\n }\n\n this.channel.on(events.state, newState => {\n let {onJoin, onLeave, onSync} = this.caller\n\n this.joinRef = this.channel.joinRef()\n this.state = Presence.syncState(this.state, newState, onJoin, onLeave)\n\n this.pendingDiffs.forEach(diff => {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n })\n this.pendingDiffs = []\n onSync()\n })\n\n this.channel.on(events.diff, diff => {\n let {onJoin, onLeave, onSync} = this.caller\n\n if(this.inPendingSyncState()){\n this.pendingDiffs.push(diff)\n } else {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n onSync()\n }\n })\n }\n\n onJoin(callback){ this.caller.onJoin = callback }\n\n onLeave(callback){ this.caller.onLeave = callback }\n\n onSync(callback){ this.caller.onSync = callback }\n\n list(by){ return Presence.list(this.state, by) }\n\n inPendingSyncState(){\n return !this.joinRef || (this.joinRef !== this.channel.joinRef())\n }\n\n // lower-level public static API\n\n /**\n * Used to sync the list of presences on the server\n * with the client's state. An optional `onJoin` and `onLeave` callback can\n * be provided to react to changes in the client's local presences across\n * disconnects and reconnects with the server.\n *\n * @returns {Presence}\n */\n static syncState(currentState, newState, onJoin, onLeave){\n let state = this.clone(currentState)\n let joins = {}\n let leaves = {}\n\n this.map(state, (key, presence) => {\n if(!newState[key]){\n leaves[key] = presence\n }\n })\n this.map(newState, (key, newPresence) => {\n let currentPresence = state[key]\n if(currentPresence){\n let newRefs = newPresence.metas.map(m => m.phx_ref)\n let curRefs = currentPresence.metas.map(m => m.phx_ref)\n let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)\n let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)\n if(joinedMetas.length > 0){\n joins[key] = newPresence\n joins[key].metas = joinedMetas\n }\n if(leftMetas.length > 0){\n leaves[key] = this.clone(currentPresence)\n leaves[key].metas = leftMetas\n }\n } else {\n joins[key] = newPresence\n }\n })\n return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)\n }\n\n /**\n *\n * Used to sync a diff of presence join and leave\n * events from the server, as they happen. Like `syncState`, `syncDiff`\n * accepts optional `onJoin` and `onLeave` callbacks to react to a user\n * joining or leaving from a device.\n *\n * @returns {Presence}\n */\n static syncDiff(state, diff, onJoin, onLeave){\n let {joins, leaves} = this.clone(diff)\n if(!onJoin){ onJoin = function (){ } }\n if(!onLeave){ onLeave = function (){ } }\n\n this.map(joins, (key, newPresence) => {\n let currentPresence = state[key]\n state[key] = this.clone(newPresence)\n if(currentPresence){\n let joinedRefs = state[key].metas.map(m => m.phx_ref)\n let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)\n state[key].metas.unshift(...curMetas)\n }\n onJoin(key, currentPresence, newPresence)\n })\n this.map(leaves, (key, leftPresence) => {\n let currentPresence = state[key]\n if(!currentPresence){ return }\n let refsToRemove = leftPresence.metas.map(m => m.phx_ref)\n currentPresence.metas = currentPresence.metas.filter(p => {\n return refsToRemove.indexOf(p.phx_ref) < 0\n })\n onLeave(key, currentPresence, leftPresence)\n if(currentPresence.metas.length === 0){\n delete state[key]\n }\n })\n return state\n }\n\n /**\n * Returns the array of presences, with selected metadata.\n *\n * @param {Object} presences\n * @param {Function} chooser\n *\n * @returns {Presence}\n */\n static list(presences, chooser){\n if(!chooser){ chooser = function (key, pres){ return pres } }\n\n return this.map(presences, (key, presence) => {\n return chooser(key, presence)\n })\n }\n\n // private\n\n static map(obj, func){\n return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))\n }\n\n static clone(obj){ return JSON.parse(JSON.stringify(obj)) }\n}\n", "/* The default serializer for encoding and decoding messages */\nimport {\n CHANNEL_EVENTS\n} from \"./constants\"\n\nexport default {\n HEADER_LENGTH: 1,\n META_LENGTH: 4,\n KINDS: {push: 0, reply: 1, broadcast: 2},\n\n encode(msg, callback){\n if(msg.payload.constructor === ArrayBuffer){\n return callback(this.binaryEncode(msg))\n } else {\n let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]\n return callback(JSON.stringify(payload))\n }\n },\n\n decode(rawPayload, callback){\n if(rawPayload.constructor === ArrayBuffer){\n return callback(this.binaryDecode(rawPayload))\n } else {\n let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)\n return callback({join_ref, ref, topic, event, payload})\n }\n },\n\n // private\n\n binaryEncode(message){\n let {join_ref, ref, event, topic, payload} = message\n let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length\n let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)\n let view = new DataView(header)\n let offset = 0\n\n view.setUint8(offset++, this.KINDS.push) // kind\n view.setUint8(offset++, join_ref.length)\n view.setUint8(offset++, ref.length)\n view.setUint8(offset++, topic.length)\n view.setUint8(offset++, event.length)\n Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0)))\n\n var combined = new Uint8Array(header.byteLength + payload.byteLength)\n combined.set(new Uint8Array(header), 0)\n combined.set(new Uint8Array(payload), header.byteLength)\n\n return combined.buffer\n },\n\n binaryDecode(buffer){\n let view = new DataView(buffer)\n let kind = view.getUint8(0)\n let decoder = new TextDecoder()\n switch(kind){\n case this.KINDS.push: return this.decodePush(buffer, view, decoder)\n case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)\n case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)\n }\n },\n\n decodePush(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let topicSize = view.getUint8(2)\n let eventSize = view.getUint8(3)\n let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}\n },\n\n decodeReply(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let refSize = view.getUint8(2)\n let topicSize = view.getUint8(3)\n let eventSize = view.getUint8(4)\n let offset = this.HEADER_LENGTH + this.META_LENGTH\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let ref = decoder.decode(buffer.slice(offset, offset + refSize))\n offset = offset + refSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n let payload = {status: event, response: data}\n return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}\n },\n\n decodeBroadcast(buffer, view, decoder){\n let topicSize = view.getUint8(1)\n let eventSize = view.getUint8(2)\n let offset = this.HEADER_LENGTH + 2\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n\n return {join_ref: null, ref: null, topic: topic, event: event, payload: data}\n }\n}\n", "import {\n global,\n phxWindow,\n CHANNEL_EVENTS,\n DEFAULT_TIMEOUT,\n DEFAULT_VSN,\n SOCKET_STATES,\n TRANSPORTS,\n WS_CLOSE_NORMAL\n} from \"./constants\"\n\nimport {\n closure\n} from \"./utils\"\n\nimport Ajax from \"./ajax\"\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Serializer from \"./serializer\"\nimport Timer from \"./timer\"\n\n/** Initializes the Socket *\n *\n * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)\n *\n * @param {string} endPoint - The string WebSocket endpoint, ie, `\"ws://example.com/socket\"`,\n * `\"wss://example.com\"`\n * `\"/socket\"` (inherited host & protocol)\n * @param {Object} [opts] - Optional configuration\n * @param {Function} [opts.transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.\n *\n * Defaults to WebSocket with automatic LongPoll fallback.\n * @param {Function} [opts.encode] - The function to encode outgoing messages.\n *\n * Defaults to JSON encoder.\n *\n * @param {Function} [opts.decode] - The function to decode incoming messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.parse(payload))\n * ```\n *\n * @param {number} [opts.timeout] - The default timeout in milliseconds to trigger push timeouts.\n *\n * Defaults `DEFAULT_TIMEOUT`\n * @param {number} [opts.heartbeatIntervalMs] - The millisec interval to send a heartbeat message\n * @param {number} [opts.reconnectAfterMs] - The optional function that returns the millisec\n * socket reconnect interval.\n *\n * Defaults to stepped backoff of:\n *\n * ```javascript\n * function(tries){\n * return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n * }\n * ````\n *\n * @param {number} [opts.rejoinAfterMs] - The optional function that returns the millisec\n * rejoin interval for individual channels.\n *\n * ```javascript\n * function(tries){\n * return [1000, 2000, 5000][tries - 1] || 10000\n * }\n * ````\n *\n * @param {Function} [opts.logger] - The optional function for specialized logging, ie:\n *\n * ```javascript\n * function(kind, msg, data) {\n * console.log(`${kind}: ${msg}`, data)\n * }\n * ```\n *\n * @param {number} [opts.longpollerTimeout] - The maximum timeout of a long poll AJAX request.\n *\n * Defaults to 20s (double the server long poll timer).\n *\n * @param {(Object|function)} [opts.params] - The optional params to pass when connecting\n * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.\n *\n * Defaults to \"arraybuffer\"\n *\n * @param {vsn} [opts.vsn] - The serializer's protocol version to send on connect.\n *\n * Defaults to DEFAULT_VSN.\n*/\nexport default class Socket {\n constructor(endPoint, opts = {}){\n this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}\n this.channels = []\n this.sendBuffer = []\n this.ref = 0\n this.timeout = opts.timeout || DEFAULT_TIMEOUT\n this.transport = opts.transport || global.WebSocket || LongPoll\n this.establishedConnections = 0\n this.defaultEncoder = Serializer.encode.bind(Serializer)\n this.defaultDecoder = Serializer.decode.bind(Serializer)\n this.closeWasClean = false\n this.binaryType = opts.binaryType || \"arraybuffer\"\n this.connectClock = 1\n if(this.transport !== LongPoll){\n this.encode = opts.encode || this.defaultEncoder\n this.decode = opts.decode || this.defaultDecoder\n } else {\n this.encode = this.defaultEncoder\n this.decode = this.defaultDecoder\n }\n let awaitingConnectionOnPageShow = null\n if(phxWindow && phxWindow.addEventListener){\n phxWindow.addEventListener(\"pagehide\", _e => {\n if(this.conn){\n this.disconnect()\n awaitingConnectionOnPageShow = this.connectClock\n }\n })\n phxWindow.addEventListener(\"pageshow\", _e => {\n if(awaitingConnectionOnPageShow === this.connectClock){\n awaitingConnectionOnPageShow = null\n this.connect()\n }\n })\n }\n this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000\n this.rejoinAfterMs = (tries) => {\n if(opts.rejoinAfterMs){\n return opts.rejoinAfterMs(tries)\n } else {\n return [1000, 2000, 5000][tries - 1] || 10000\n }\n }\n this.reconnectAfterMs = (tries) => {\n if(opts.reconnectAfterMs){\n return opts.reconnectAfterMs(tries)\n } else {\n return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n }\n }\n this.logger = opts.logger || null\n this.longpollerTimeout = opts.longpollerTimeout || 20000\n this.params = closure(opts.params || {})\n this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`\n this.vsn = opts.vsn || DEFAULT_VSN\n this.heartbeatTimeoutTimer = null\n this.heartbeatTimer = null\n this.pendingHeartbeatRef = null\n this.reconnectTimer = new Timer(() => {\n this.teardown(() => this.connect())\n }, this.reconnectAfterMs)\n }\n\n /**\n * Returns the LongPoll transport reference\n */\n getLongPollTransport(){ return LongPoll }\n\n /**\n * Disconnects and replaces the active transport\n *\n * @param {Function} newTransport - The new transport class to instantiate\n *\n */\n replaceTransport(newTransport){\n this.connectClock++\n this.closeWasClean = true\n this.reconnectTimer.reset()\n this.sendBuffer = []\n if(this.conn){\n this.conn.close()\n this.conn = null\n }\n this.transport = newTransport\n }\n\n /**\n * Returns the socket protocol\n *\n * @returns {string}\n */\n protocol(){ return location.protocol.match(/^https/) ? \"wss\" : \"ws\" }\n\n /**\n * The fully qualified socket url\n *\n * @returns {string}\n */\n endPointURL(){\n let uri = Ajax.appendParams(\n Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})\n if(uri.charAt(0) !== \"/\"){ return uri }\n if(uri.charAt(1) === \"/\"){ return `${this.protocol()}:${uri}` }\n\n return `${this.protocol()}://${location.host}${uri}`\n }\n\n /**\n * Disconnects the socket\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.\n *\n * @param {Function} callback - Optional callback which is called after socket is disconnected.\n * @param {integer} code - A status code for disconnection (Optional).\n * @param {string} reason - A textual description of the reason to disconnect. (Optional)\n */\n disconnect(callback, code, reason){\n this.connectClock++\n this.closeWasClean = true\n this.reconnectTimer.reset()\n this.teardown(callback, code, reason)\n }\n\n /**\n *\n * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`\n *\n * Passing params to connect is deprecated; pass them in the Socket constructor instead:\n * `new Socket(\"/socket\", {params: {user_id: userToken}})`.\n */\n connect(params){\n if(params){\n console && console.log(\"passing params to connect is deprecated. Instead pass :params to the Socket constructor\")\n this.params = closure(params)\n }\n if(this.conn){ return }\n\n this.connectClock++\n this.closeWasClean = false\n this.conn = new this.transport(this.endPointURL())\n this.conn.binaryType = this.binaryType\n this.conn.timeout = this.longpollerTimeout\n this.conn.onopen = () => this.onConnOpen()\n this.conn.onerror = error => this.onConnError(error)\n this.conn.onmessage = event => this.onConnMessage(event)\n this.conn.onclose = event => this.onConnClose(event)\n }\n\n /**\n * Logs the message. Override `this.logger` for specialized logging. noops by default\n * @param {string} kind\n * @param {string} msg\n * @param {Object} data\n */\n log(kind, msg, data){ this.logger(kind, msg, data) }\n\n /**\n * Returns true if a logger has been set on this socket.\n */\n hasLogger(){ return this.logger !== null }\n\n /**\n * Registers callbacks for connection open events\n *\n * @example socket.onOpen(function(){ console.info(\"the socket was opened\") })\n *\n * @param {Function} callback\n */\n onOpen(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.open.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection close events\n * @param {Function} callback\n */\n onClose(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.close.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection error events\n *\n * @example socket.onError(function(error){ alert(\"An error occurred\") })\n *\n * @param {Function} callback\n */\n onError(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.error.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection message events\n * @param {Function} callback\n */\n onMessage(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.message.push([ref, callback])\n return ref\n }\n\n /**\n * Pings the server and invokes the callback with the RTT in milliseconds\n * @param {Function} callback\n *\n * Returns true if the ping was pushed or false if unable to be pushed.\n */\n ping(callback){\n if(!this.isConnected()){ return false }\n let ref = this.makeRef()\n let startTime = Date.now()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: ref})\n let onMsgRef = this.onMessage(msg => {\n if(msg.ref === ref){\n this.off([onMsgRef])\n callback(Date.now() - startTime)\n }\n })\n return true\n }\n\n /**\n * @private\n */\n\n clearHeartbeats(){\n clearTimeout(this.heartbeatTimer)\n clearTimeout(this.heartbeatTimeoutTimer)\n }\n\n onConnOpen(){\n if(this.hasLogger()) this.log(\"transport\", `connected to ${this.endPointURL()}`)\n this.closeWasClean = false\n this.establishedConnections++\n this.flushSendBuffer()\n this.reconnectTimer.reset()\n this.resetHeartbeat()\n this.stateChangeCallbacks.open.forEach(([, callback]) => callback())\n }\n\n /**\n * @private\n */\n\n heartbeatTimeout(){\n if(this.pendingHeartbeatRef){\n this.pendingHeartbeatRef = null\n if(this.hasLogger()){ this.log(\"transport\", \"heartbeat timeout. Attempting to re-establish connection\") }\n this.triggerChanError()\n this.closeWasClean = false\n this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, \"heartbeat timeout\")\n }\n }\n\n resetHeartbeat(){\n if(this.conn && this.conn.skipHeartbeat){ return }\n this.pendingHeartbeatRef = null\n this.clearHeartbeats()\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n teardown(callback, code, reason){\n if(!this.conn){\n return callback && callback()\n }\n\n this.waitForBufferDone(() => {\n if(this.conn){\n if(code){ this.conn.close(code, reason || \"\") } else { this.conn.close() }\n }\n\n this.waitForSocketClosed(() => {\n if(this.conn){\n this.conn.onopen = function (){ } // noop\n this.conn.onerror = function (){ } // noop\n this.conn.onmessage = function (){ } // noop\n this.conn.onclose = function (){ } // noop\n this.conn = null\n }\n\n callback && callback()\n })\n })\n }\n\n waitForBufferDone(callback, tries = 1){\n if(tries === 5 || !this.conn || !this.conn.bufferedAmount){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForBufferDone(callback, tries + 1)\n }, 150 * tries)\n }\n\n waitForSocketClosed(callback, tries = 1){\n if(tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForSocketClosed(callback, tries + 1)\n }, 150 * tries)\n }\n\n onConnClose(event){\n let closeCode = event && event.code\n if(this.hasLogger()) this.log(\"transport\", \"close\", event)\n this.triggerChanError()\n this.clearHeartbeats()\n if(!this.closeWasClean && closeCode !== 1000){\n this.reconnectTimer.scheduleTimeout()\n }\n this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event))\n }\n\n /**\n * @private\n */\n onConnError(error){\n if(this.hasLogger()) this.log(\"transport\", error)\n let transportBefore = this.transport\n let establishedBefore = this.establishedConnections\n this.stateChangeCallbacks.error.forEach(([, callback]) => {\n callback(error, transportBefore, establishedBefore)\n })\n if(transportBefore === this.transport || establishedBefore > 0){\n this.triggerChanError()\n }\n }\n\n /**\n * @private\n */\n triggerChanError(){\n this.channels.forEach(channel => {\n if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){\n channel.trigger(CHANNEL_EVENTS.error)\n }\n })\n }\n\n /**\n * @returns {string}\n */\n connectionState(){\n switch(this.conn && this.conn.readyState){\n case SOCKET_STATES.connecting: return \"connecting\"\n case SOCKET_STATES.open: return \"open\"\n case SOCKET_STATES.closing: return \"closing\"\n default: return \"closed\"\n }\n }\n\n /**\n * @returns {boolean}\n */\n isConnected(){ return this.connectionState() === \"open\" }\n\n /**\n * @private\n *\n * @param {Channel}\n */\n remove(channel){\n this.off(channel.stateChangeRefs)\n this.channels = this.channels.filter(c => c.joinRef() !== channel.joinRef())\n }\n\n /**\n * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.\n *\n * @param {refs} - list of refs returned by calls to\n * `onOpen`, `onClose`, `onError,` and `onMessage`\n */\n off(refs){\n for(let key in this.stateChangeCallbacks){\n this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {\n return refs.indexOf(ref) === -1\n })\n }\n }\n\n /**\n * Initiates a new channel for the given topic\n *\n * @param {string} topic\n * @param {Object} chanParams - Parameters for the channel\n * @returns {Channel}\n */\n channel(topic, chanParams = {}){\n let chan = new Channel(topic, chanParams, this)\n this.channels.push(chan)\n return chan\n }\n\n /**\n * @param {Object} data\n */\n push(data){\n if(this.hasLogger()){\n let {topic, event, payload, ref, join_ref} = data\n this.log(\"push\", `${topic} ${event} (${join_ref}, ${ref})`, payload)\n }\n\n if(this.isConnected()){\n this.encode(data, result => this.conn.send(result))\n } else {\n this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))\n }\n }\n\n /**\n * Return the next message ref, accounting for overflows\n * @returns {string}\n */\n makeRef(){\n let newRef = this.ref + 1\n if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }\n\n return this.ref.toString()\n }\n\n sendHeartbeat(){\n if(this.pendingHeartbeatRef && !this.isConnected()){ return }\n this.pendingHeartbeatRef = this.makeRef()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: this.pendingHeartbeatRef})\n this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)\n }\n\n flushSendBuffer(){\n if(this.isConnected() && this.sendBuffer.length > 0){\n this.sendBuffer.forEach(callback => callback())\n this.sendBuffer = []\n }\n }\n\n onConnMessage(rawMessage){\n this.decode(rawMessage.data, msg => {\n let {topic, event, payload, ref, join_ref} = msg\n if(ref && ref === this.pendingHeartbeatRef){\n this.clearHeartbeats()\n this.pendingHeartbeatRef = null\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n if(this.hasLogger()) this.log(\"receive\", `${payload.status || \"\"} ${topic} ${event} ${ref && \"(\" + ref + \")\" || \"\"}`, payload)\n\n for(let i = 0; i < this.channels.length; i++){\n const channel = this.channels[i]\n if(!channel.isMember(topic, event, payload, join_ref)){ continue }\n channel.trigger(event, payload, ref, join_ref)\n }\n\n for(let i = 0; i < this.stateChangeCallbacks.message.length; i++){\n let [, callback] = this.stateChangeCallbacks.message[i]\n callback(msg)\n }\n })\n }\n\n leaveOpenTopic(topic){\n let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))\n if(dupChannel){\n if(this.hasLogger()) this.log(\"transport\", `leaving duplicate topic \"${topic}\"`)\n dupChannel.leave()\n }\n }\n}\n"],
- "mappings": "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",
- "names": []
+ "sourcesContent": ["// wraps value in closure or returns closure\nexport let closure = (value) => {\n if(typeof value === \"function\"){\n return value\n } else {\n let closure = function (){ return value }\n return closure\n }\n}\n", "export const globalSelf = typeof self !== \"undefined\" ? self : null\nexport const phxWindow = typeof window !== \"undefined\" ? window : null\nexport const global = globalSelf || phxWindow || global\nexport const DEFAULT_VSN = \"2.0.0\"\nexport const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}\nexport const DEFAULT_TIMEOUT = 10000\nexport const WS_CLOSE_NORMAL = 1000\nexport const CHANNEL_STATES = {\n closed: \"closed\",\n errored: \"errored\",\n joined: \"joined\",\n joining: \"joining\",\n leaving: \"leaving\",\n}\nexport const CHANNEL_EVENTS = {\n close: \"phx_close\",\n error: \"phx_error\",\n join: \"phx_join\",\n reply: \"phx_reply\",\n leave: \"phx_leave\"\n}\n\nexport const TRANSPORTS = {\n longpoll: \"longpoll\",\n websocket: \"websocket\"\n}\nexport const XHR_STATES = {\n complete: 4\n}\n", "/**\n * Initializes the Push\n * @param {Channel} channel - The Channel\n * @param {string} event - The event, for example `\"phx_join\"`\n * @param {Object} payload - The payload, for example `{user_id: 123}`\n * @param {number} timeout - The push timeout in milliseconds\n */\nexport default class Push {\n constructor(channel, event, payload, timeout){\n this.channel = channel\n this.event = event\n this.payload = payload || function (){ return {} }\n this.receivedResp = null\n this.timeout = timeout\n this.timeoutTimer = null\n this.recHooks = []\n this.sent = false\n }\n\n /**\n *\n * @param {number} timeout\n */\n resend(timeout){\n this.timeout = timeout\n this.reset()\n this.send()\n }\n\n /**\n *\n */\n send(){\n if(this.hasReceived(\"timeout\")){ return }\n this.startTimeout()\n this.sent = true\n this.channel.socket.push({\n topic: this.channel.topic,\n event: this.event,\n payload: this.payload(),\n ref: this.ref,\n join_ref: this.channel.joinRef()\n })\n }\n\n /**\n *\n * @param {*} status\n * @param {*} callback\n */\n receive(status, callback){\n if(this.hasReceived(status)){\n callback(this.receivedResp.response)\n }\n\n this.recHooks.push({status, callback})\n return this\n }\n\n /**\n * @private\n */\n reset(){\n this.cancelRefEvent()\n this.ref = null\n this.refEvent = null\n this.receivedResp = null\n this.sent = false\n }\n\n /**\n * @private\n */\n matchReceive({status, response, _ref}){\n this.recHooks.filter(h => h.status === status)\n .forEach(h => h.callback(response))\n }\n\n /**\n * @private\n */\n cancelRefEvent(){\n if(!this.refEvent){ return }\n this.channel.off(this.refEvent)\n }\n\n /**\n * @private\n */\n cancelTimeout(){\n clearTimeout(this.timeoutTimer)\n this.timeoutTimer = null\n }\n\n /**\n * @private\n */\n startTimeout(){\n if(this.timeoutTimer){ this.cancelTimeout() }\n this.ref = this.channel.socket.makeRef()\n this.refEvent = this.channel.replyEventName(this.ref)\n\n this.channel.on(this.refEvent, payload => {\n this.cancelRefEvent()\n this.cancelTimeout()\n this.receivedResp = payload\n this.matchReceive(payload)\n })\n\n this.timeoutTimer = setTimeout(() => {\n this.trigger(\"timeout\", {})\n }, this.timeout)\n }\n\n /**\n * @private\n */\n hasReceived(status){\n return this.receivedResp && this.receivedResp.status === status\n }\n\n /**\n * @private\n */\n trigger(status, response){\n this.channel.trigger(this.refEvent, {status, response})\n }\n}\n", "/**\n *\n * Creates a timer that accepts a `timerCalc` function to perform\n * calculated timeout retries, such as exponential backoff.\n *\n * @example\n * let reconnectTimer = new Timer(() => this.connect(), function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * })\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * reconnectTimer.scheduleTimeout() // fires after 5000\n * reconnectTimer.reset()\n * reconnectTimer.scheduleTimeout() // fires after 1000\n *\n * @param {Function} callback\n * @param {Function} timerCalc\n */\nexport default class Timer {\n constructor(callback, timerCalc){\n this.callback = callback\n this.timerCalc = timerCalc\n this.timer = null\n this.tries = 0\n }\n\n reset(){\n this.tries = 0\n clearTimeout(this.timer)\n }\n\n /**\n * Cancels any previous scheduleTimeout and schedules callback\n */\n scheduleTimeout(){\n clearTimeout(this.timer)\n\n this.timer = setTimeout(() => {\n this.tries = this.tries + 1\n this.callback()\n }, this.timerCalc(this.tries + 1))\n }\n}\n", "import {closure} from \"./utils\"\nimport {\n CHANNEL_EVENTS,\n CHANNEL_STATES,\n} from \"./constants\"\n\nimport Push from \"./push\"\nimport Timer from \"./timer\"\n\n/**\n *\n * @param {string} topic\n * @param {(Object|function)} params\n * @param {Socket} socket\n */\nexport default class Channel {\n constructor(topic, params, socket){\n this.state = CHANNEL_STATES.closed\n this.topic = topic\n this.params = closure(params || {})\n this.socket = socket\n this.bindings = []\n this.bindingRef = 0\n this.timeout = this.socket.timeout\n this.joinedOnce = false\n this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)\n this.pushBuffer = []\n this.stateChangeRefs = []\n\n this.rejoinTimer = new Timer(() => {\n if(this.socket.isConnected()){ this.rejoin() }\n }, this.socket.rejoinAfterMs)\n this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))\n this.stateChangeRefs.push(this.socket.onOpen(() => {\n this.rejoinTimer.reset()\n if(this.isErrored()){ this.rejoin() }\n })\n )\n this.joinPush.receive(\"ok\", () => {\n this.state = CHANNEL_STATES.joined\n this.rejoinTimer.reset()\n this.pushBuffer.forEach(pushEvent => pushEvent.send())\n this.pushBuffer = []\n })\n this.joinPush.receive(\"error\", () => {\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.onClose(() => {\n this.rejoinTimer.reset()\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `close ${this.topic} ${this.joinRef()}`)\n this.state = CHANNEL_STATES.closed\n this.socket.remove(this)\n })\n this.onError(reason => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `error ${this.topic}`, reason)\n if(this.isJoining()){ this.joinPush.reset() }\n this.state = CHANNEL_STATES.errored\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.joinPush.receive(\"timeout\", () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)\n leavePush.send()\n this.state = CHANNEL_STATES.errored\n this.joinPush.reset()\n if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }\n })\n this.on(CHANNEL_EVENTS.reply, (payload, ref) => {\n this.trigger(this.replyEventName(ref), payload)\n })\n }\n\n /**\n * Join the channel\n * @param {integer} timeout\n * @returns {Push}\n */\n join(timeout = this.timeout){\n if(this.joinedOnce){\n throw new Error(\"tried to join multiple times. 'join' can only be called a single time per channel instance\")\n } else {\n this.timeout = timeout\n this.joinedOnce = true\n this.rejoin()\n return this.joinPush\n }\n }\n\n /**\n * Hook into channel close\n * @param {Function} callback\n */\n onClose(callback){\n this.on(CHANNEL_EVENTS.close, callback)\n }\n\n /**\n * Hook into channel errors\n * @param {Function} callback\n */\n onError(callback){\n return this.on(CHANNEL_EVENTS.error, reason => callback(reason))\n }\n\n /**\n * Subscribes on channel events\n *\n * Subscription returns a ref counter, which can be used later to\n * unsubscribe the exact event listener\n *\n * @example\n * const ref1 = channel.on(\"event\", do_stuff)\n * const ref2 = channel.on(\"event\", do_other_stuff)\n * channel.off(\"event\", ref1)\n * // Since unsubscription, do_stuff won't fire,\n * // while do_other_stuff will keep firing on the \"event\"\n *\n * @param {string} event\n * @param {Function} callback\n * @returns {integer} ref\n */\n on(event, callback){\n let ref = this.bindingRef++\n this.bindings.push({event, ref, callback})\n return ref\n }\n\n /**\n * Unsubscribes off of channel events\n *\n * Use the ref returned from a channel.on() to unsubscribe one\n * handler, or pass nothing for the ref to unsubscribe all\n * handlers for the given event.\n *\n * @example\n * // Unsubscribe the do_stuff handler\n * const ref1 = channel.on(\"event\", do_stuff)\n * channel.off(\"event\", ref1)\n *\n * // Unsubscribe all handlers from event\n * channel.off(\"event\")\n *\n * @param {string} event\n * @param {integer} ref\n */\n off(event, ref){\n this.bindings = this.bindings.filter((bind) => {\n return !(bind.event === event && (typeof ref === \"undefined\" || ref === bind.ref))\n })\n }\n\n /**\n * @private\n */\n canPush(){ return this.socket.isConnected() && this.isJoined() }\n\n /**\n * Sends a message `event` to phoenix with the payload `payload`.\n * Phoenix receives this in the `handle_in(event, payload, socket)`\n * function. if phoenix replies or it times out (default 10000ms),\n * then optionally the reply can be received.\n *\n * @example\n * channel.push(\"event\")\n * .receive(\"ok\", payload => console.log(\"phoenix replied:\", payload))\n * .receive(\"error\", err => console.log(\"phoenix errored\", err))\n * .receive(\"timeout\", () => console.log(\"timed out pushing\"))\n * @param {string} event\n * @param {Object} payload\n * @param {number} [timeout]\n * @returns {Push}\n */\n push(event, payload, timeout = this.timeout){\n payload = payload || {}\n if(!this.joinedOnce){\n throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)\n }\n let pushEvent = new Push(this, event, function (){ return payload }, timeout)\n if(this.canPush()){\n pushEvent.send()\n } else {\n pushEvent.startTimeout()\n this.pushBuffer.push(pushEvent)\n }\n\n return pushEvent\n }\n\n /** Leaves the channel\n *\n * Unsubscribes from server events, and\n * instructs channel to terminate on server\n *\n * Triggers onClose() hooks\n *\n * To receive leave acknowledgements, use the `receive`\n * hook to bind to the server ack, ie:\n *\n * @example\n * channel.leave().receive(\"ok\", () => alert(\"left!\") )\n *\n * @param {integer} timeout\n * @returns {Push}\n */\n leave(timeout = this.timeout){\n this.rejoinTimer.reset()\n this.joinPush.cancelTimeout()\n\n this.state = CHANNEL_STATES.leaving\n let onClose = () => {\n if(this.socket.hasLogger()) this.socket.log(\"channel\", `leave ${this.topic}`)\n this.trigger(CHANNEL_EVENTS.close, \"leave\")\n }\n let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)\n leavePush.receive(\"ok\", () => onClose())\n .receive(\"timeout\", () => onClose())\n leavePush.send()\n if(!this.canPush()){ leavePush.trigger(\"ok\", {}) }\n\n return leavePush\n }\n\n /**\n * Overridable message hook\n *\n * Receives all events for specialized message handling\n * before dispatching to the channel callbacks.\n *\n * Must return the payload, modified or unmodified\n * @param {string} event\n * @param {Object} payload\n * @param {integer} ref\n * @returns {Object}\n */\n onMessage(_event, payload, _ref){ return payload }\n\n /**\n * @private\n */\n isMember(topic, event, payload, joinRef){\n if(this.topic !== topic){ return false }\n\n if(joinRef && joinRef !== this.joinRef()){\n if(this.socket.hasLogger()) this.socket.log(\"channel\", \"dropping outdated message\", {topic, event, payload, joinRef})\n return false\n } else {\n return true\n }\n }\n\n /**\n * @private\n */\n joinRef(){ return this.joinPush.ref }\n\n /**\n * @private\n */\n rejoin(timeout = this.timeout){\n if(this.isLeaving()){ return }\n this.socket.leaveOpenTopic(this.topic)\n this.state = CHANNEL_STATES.joining\n this.joinPush.resend(timeout)\n }\n\n /**\n * @private\n */\n trigger(event, payload, ref, joinRef){\n let handledPayload = this.onMessage(event, payload, ref, joinRef)\n if(payload && !handledPayload){ throw new Error(\"channel onMessage callbacks must return the payload, modified or unmodified\") }\n\n let eventBindings = this.bindings.filter(bind => bind.event === event)\n\n for(let i = 0; i < eventBindings.length; i++){\n let bind = eventBindings[i]\n bind.callback(handledPayload, ref, joinRef || this.joinRef())\n }\n }\n\n /**\n * @private\n */\n replyEventName(ref){ return `chan_reply_${ref}` }\n\n /**\n * @private\n */\n isClosed(){ return this.state === CHANNEL_STATES.closed }\n\n /**\n * @private\n */\n isErrored(){ return this.state === CHANNEL_STATES.errored }\n\n /**\n * @private\n */\n isJoined(){ return this.state === CHANNEL_STATES.joined }\n\n /**\n * @private\n */\n isJoining(){ return this.state === CHANNEL_STATES.joining }\n\n /**\n * @private\n */\n isLeaving(){ return this.state === CHANNEL_STATES.leaving }\n}\n", "import {\n global,\n XHR_STATES\n} from \"./constants\"\n\nexport default class Ajax {\n\n static request(method, endPoint, accept, body, timeout, ontimeout, callback){\n if(global.XDomainRequest){\n let req = new global.XDomainRequest() // IE8, IE9\n return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)\n } else {\n let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari\n return this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)\n }\n }\n\n static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){\n req.timeout = timeout\n req.open(method, endPoint)\n req.onload = () => {\n let response = this.parseJSON(req.responseText)\n callback && callback(response)\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n // Work around bug in IE9 that requires an attached onprogress handler\n req.onprogress = () => { }\n\n req.send(body)\n return req\n }\n\n static xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback){\n req.open(method, endPoint, true)\n req.timeout = timeout\n req.setRequestHeader(\"Content-Type\", accept)\n req.onerror = () => callback && callback(null)\n req.onreadystatechange = () => {\n if(req.readyState === XHR_STATES.complete && callback){\n let response = this.parseJSON(req.responseText)\n callback(response)\n }\n }\n if(ontimeout){ req.ontimeout = ontimeout }\n\n req.send(body)\n return req\n }\n\n static parseJSON(resp){\n if(!resp || resp === \"\"){ return null }\n\n try {\n return JSON.parse(resp)\n } catch (e){\n console && console.log(\"failed to parse JSON response\", resp)\n return null\n }\n }\n\n static serialize(obj, parentKey){\n let queryStr = []\n for(var key in obj){\n if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }\n let paramKey = parentKey ? `${parentKey}[${key}]` : key\n let paramVal = obj[key]\n if(typeof paramVal === \"object\"){\n queryStr.push(this.serialize(paramVal, paramKey))\n } else {\n queryStr.push(encodeURIComponent(paramKey) + \"=\" + encodeURIComponent(paramVal))\n }\n }\n return queryStr.join(\"&\")\n }\n\n static appendParams(url, params){\n if(Object.keys(params).length === 0){ return url }\n\n let prefix = url.match(/\\?/) ? \"&\" : \"?\"\n return `${url}${prefix}${this.serialize(params)}`\n }\n}\n", "import {\n SOCKET_STATES,\n TRANSPORTS\n} from \"./constants\"\n\nimport Ajax from \"./ajax\"\n\nlet arrayBufferToBase64 = (buffer) => {\n let binary = \"\"\n let bytes = new Uint8Array(buffer)\n let len = bytes.byteLength\n for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }\n return btoa(binary)\n}\n\nexport default class LongPoll {\n\n constructor(endPoint){\n this.endPoint = null\n this.token = null\n this.skipHeartbeat = true\n this.reqs = new Set()\n this.awaitingBatchAck = false\n this.currentBatch = null\n this.currentBatchTimer = null\n this.batchBuffer = []\n this.onopen = function (){ } // noop\n this.onerror = function (){ } // noop\n this.onmessage = function (){ } // noop\n this.onclose = function (){ } // noop\n this.pollEndpoint = this.normalizeEndpoint(endPoint)\n this.readyState = SOCKET_STATES.connecting\n // we must wait for the caller to finish setting up our callbacks and timeout properties\n setTimeout(() => this.poll(), 0)\n }\n\n normalizeEndpoint(endPoint){\n return (endPoint\n .replace(\"ws://\", \"http://\")\n .replace(\"wss://\", \"https://\")\n .replace(new RegExp(\"(.*)\\/\" + TRANSPORTS.websocket), \"$1/\" + TRANSPORTS.longpoll))\n }\n\n endpointURL(){\n return Ajax.appendParams(this.pollEndpoint, {token: this.token})\n }\n\n closeAndRetry(code, reason, wasClean){\n this.close(code, reason, wasClean)\n this.readyState = SOCKET_STATES.connecting\n }\n\n ontimeout(){\n this.onerror(\"timeout\")\n this.closeAndRetry(1005, \"timeout\", false)\n }\n\n isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }\n\n poll(){\n this.ajax(\"GET\", \"application/json\", null, () => this.ontimeout(), resp => {\n if(resp){\n var {status, token, messages} = resp\n this.token = token\n } else {\n status = 0\n }\n\n switch(status){\n case 200:\n messages.forEach(msg => {\n // Tasks are what things like event handlers, setTimeout callbacks,\n // promise resolves and more are run within.\n // In modern browsers, there are two different kinds of tasks,\n // microtasks and macrotasks.\n // Microtasks are mainly used for Promises, while macrotasks are\n // used for everything else.\n // Microtasks always have priority over macrotasks. If the JS engine\n // is looking for a task to run, it will always try to empty the\n // microtask queue before attempting to run anything from the\n // macrotask queue.\n //\n // For the WebSocket transport, messages always arrive in their own\n // event. This means that if any promises are resolved from within,\n // their callbacks will always finish execution by the time the\n // next message event handler is run.\n //\n // In order to emulate this behaviour, we need to make sure each\n // onmessage handler is run within its own macrotask.\n setTimeout(() => this.onmessage({data: msg}), 0)\n })\n this.poll()\n break\n case 204:\n this.poll()\n break\n case 410:\n this.readyState = SOCKET_STATES.open\n this.onopen({})\n this.poll()\n break\n case 403:\n this.onerror(403)\n this.close(1008, \"forbidden\", false)\n break\n case 0:\n case 500:\n this.onerror(500)\n this.closeAndRetry(1011, \"internal server error\", 500)\n break\n default: throw new Error(`unhandled poll status ${status}`)\n }\n })\n }\n\n // we collect all pushes within the current event loop by\n // setTimeout 0, which optimizes back-to-back procedural\n // pushes against an empty buffer\n\n send(body){\n if(typeof(body) !== \"string\"){ body = arrayBufferToBase64(body) }\n if(this.currentBatch){\n this.currentBatch.push(body)\n } else if(this.awaitingBatchAck){\n this.batchBuffer.push(body)\n } else {\n this.currentBatch = [body]\n this.currentBatchTimer = setTimeout(() => {\n this.batchSend(this.currentBatch)\n this.currentBatch = null\n }, 0)\n }\n }\n\n batchSend(messages){\n this.awaitingBatchAck = true\n this.ajax(\"POST\", \"application/x-ndjson\", messages.join(\"\\n\"), () => this.onerror(\"timeout\"), resp => {\n this.awaitingBatchAck = false\n if(!resp || resp.status !== 200){\n this.onerror(resp && resp.status)\n this.closeAndRetry(1011, \"internal server error\", false)\n } else if(this.batchBuffer.length > 0){\n this.batchSend(this.batchBuffer)\n this.batchBuffer = []\n }\n })\n }\n\n close(code, reason, wasClean){\n for(let req of this.reqs){ req.abort() }\n this.readyState = SOCKET_STATES.closed\n let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})\n this.batchBuffer = []\n clearTimeout(this.currentBatchTimer)\n this.currentBatchTimer = null\n if(typeof(CloseEvent) !== \"undefined\"){\n this.onclose(new CloseEvent(\"close\", opts))\n } else {\n this.onclose(opts)\n }\n }\n\n ajax(method, contentType, body, onCallerTimeout, callback){\n let req\n let ontimeout = () => {\n this.reqs.delete(req)\n onCallerTimeout()\n }\n req = Ajax.request(method, this.endpointURL(), contentType, body, this.timeout, ontimeout, resp => {\n this.reqs.delete(req)\n if(this.isActive()){ callback(resp) }\n })\n this.reqs.add(req)\n }\n}\n", "/**\n * Initializes the Presence\n * @param {Channel} channel - The Channel\n * @param {Object} opts - The options,\n * for example `{events: {state: \"state\", diff: \"diff\"}}`\n */\nexport default class Presence {\n\n constructor(channel, opts = {}){\n let events = opts.events || {state: \"presence_state\", diff: \"presence_diff\"}\n this.state = {}\n this.pendingDiffs = []\n this.channel = channel\n this.joinRef = null\n this.caller = {\n onJoin: function (){ },\n onLeave: function (){ },\n onSync: function (){ }\n }\n\n this.channel.on(events.state, newState => {\n let {onJoin, onLeave, onSync} = this.caller\n\n this.joinRef = this.channel.joinRef()\n this.state = Presence.syncState(this.state, newState, onJoin, onLeave)\n\n this.pendingDiffs.forEach(diff => {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n })\n this.pendingDiffs = []\n onSync()\n })\n\n this.channel.on(events.diff, diff => {\n let {onJoin, onLeave, onSync} = this.caller\n\n if(this.inPendingSyncState()){\n this.pendingDiffs.push(diff)\n } else {\n this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)\n onSync()\n }\n })\n }\n\n onJoin(callback){ this.caller.onJoin = callback }\n\n onLeave(callback){ this.caller.onLeave = callback }\n\n onSync(callback){ this.caller.onSync = callback }\n\n list(by){ return Presence.list(this.state, by) }\n\n inPendingSyncState(){\n return !this.joinRef || (this.joinRef !== this.channel.joinRef())\n }\n\n // lower-level public static API\n\n /**\n * Used to sync the list of presences on the server\n * with the client's state. An optional `onJoin` and `onLeave` callback can\n * be provided to react to changes in the client's local presences across\n * disconnects and reconnects with the server.\n *\n * @returns {Presence}\n */\n static syncState(currentState, newState, onJoin, onLeave){\n let state = this.clone(currentState)\n let joins = {}\n let leaves = {}\n\n this.map(state, (key, presence) => {\n if(!newState[key]){\n leaves[key] = presence\n }\n })\n this.map(newState, (key, newPresence) => {\n let currentPresence = state[key]\n if(currentPresence){\n let newRefs = newPresence.metas.map(m => m.phx_ref)\n let curRefs = currentPresence.metas.map(m => m.phx_ref)\n let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)\n let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)\n if(joinedMetas.length > 0){\n joins[key] = newPresence\n joins[key].metas = joinedMetas\n }\n if(leftMetas.length > 0){\n leaves[key] = this.clone(currentPresence)\n leaves[key].metas = leftMetas\n }\n } else {\n joins[key] = newPresence\n }\n })\n return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)\n }\n\n /**\n *\n * Used to sync a diff of presence join and leave\n * events from the server, as they happen. Like `syncState`, `syncDiff`\n * accepts optional `onJoin` and `onLeave` callbacks to react to a user\n * joining or leaving from a device.\n *\n * @returns {Presence}\n */\n static syncDiff(state, diff, onJoin, onLeave){\n let {joins, leaves} = this.clone(diff)\n if(!onJoin){ onJoin = function (){ } }\n if(!onLeave){ onLeave = function (){ } }\n\n this.map(joins, (key, newPresence) => {\n let currentPresence = state[key]\n state[key] = this.clone(newPresence)\n if(currentPresence){\n let joinedRefs = state[key].metas.map(m => m.phx_ref)\n let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)\n state[key].metas.unshift(...curMetas)\n }\n onJoin(key, currentPresence, newPresence)\n })\n this.map(leaves, (key, leftPresence) => {\n let currentPresence = state[key]\n if(!currentPresence){ return }\n let refsToRemove = leftPresence.metas.map(m => m.phx_ref)\n currentPresence.metas = currentPresence.metas.filter(p => {\n return refsToRemove.indexOf(p.phx_ref) < 0\n })\n onLeave(key, currentPresence, leftPresence)\n if(currentPresence.metas.length === 0){\n delete state[key]\n }\n })\n return state\n }\n\n /**\n * Returns the array of presences, with selected metadata.\n *\n * @param {Object} presences\n * @param {Function} chooser\n *\n * @returns {Presence}\n */\n static list(presences, chooser){\n if(!chooser){ chooser = function (key, pres){ return pres } }\n\n return this.map(presences, (key, presence) => {\n return chooser(key, presence)\n })\n }\n\n // private\n\n static map(obj, func){\n return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))\n }\n\n static clone(obj){ return JSON.parse(JSON.stringify(obj)) }\n}\n", "/* The default serializer for encoding and decoding messages */\nimport {\n CHANNEL_EVENTS\n} from \"./constants\"\n\nexport default {\n HEADER_LENGTH: 1,\n META_LENGTH: 4,\n KINDS: {push: 0, reply: 1, broadcast: 2},\n\n encode(msg, callback){\n if(msg.payload.constructor === ArrayBuffer){\n return callback(this.binaryEncode(msg))\n } else {\n let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]\n return callback(JSON.stringify(payload))\n }\n },\n\n decode(rawPayload, callback){\n if(rawPayload.constructor === ArrayBuffer){\n return callback(this.binaryDecode(rawPayload))\n } else {\n let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)\n return callback({join_ref, ref, topic, event, payload})\n }\n },\n\n // private\n\n binaryEncode(message){\n let {join_ref, ref, event, topic, payload} = message\n let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length\n let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)\n let view = new DataView(header)\n let offset = 0\n\n view.setUint8(offset++, this.KINDS.push) // kind\n view.setUint8(offset++, join_ref.length)\n view.setUint8(offset++, ref.length)\n view.setUint8(offset++, topic.length)\n view.setUint8(offset++, event.length)\n Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0)))\n Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0)))\n\n var combined = new Uint8Array(header.byteLength + payload.byteLength)\n combined.set(new Uint8Array(header), 0)\n combined.set(new Uint8Array(payload), header.byteLength)\n\n return combined.buffer\n },\n\n binaryDecode(buffer){\n let view = new DataView(buffer)\n let kind = view.getUint8(0)\n let decoder = new TextDecoder()\n switch(kind){\n case this.KINDS.push: return this.decodePush(buffer, view, decoder)\n case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)\n case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)\n }\n },\n\n decodePush(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let topicSize = view.getUint8(2)\n let eventSize = view.getUint8(3)\n let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}\n },\n\n decodeReply(buffer, view, decoder){\n let joinRefSize = view.getUint8(1)\n let refSize = view.getUint8(2)\n let topicSize = view.getUint8(3)\n let eventSize = view.getUint8(4)\n let offset = this.HEADER_LENGTH + this.META_LENGTH\n let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))\n offset = offset + joinRefSize\n let ref = decoder.decode(buffer.slice(offset, offset + refSize))\n offset = offset + refSize\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n let payload = {status: event, response: data}\n return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}\n },\n\n decodeBroadcast(buffer, view, decoder){\n let topicSize = view.getUint8(1)\n let eventSize = view.getUint8(2)\n let offset = this.HEADER_LENGTH + 2\n let topic = decoder.decode(buffer.slice(offset, offset + topicSize))\n offset = offset + topicSize\n let event = decoder.decode(buffer.slice(offset, offset + eventSize))\n offset = offset + eventSize\n let data = buffer.slice(offset, buffer.byteLength)\n\n return {join_ref: null, ref: null, topic: topic, event: event, payload: data}\n }\n}\n", "import {\n global,\n phxWindow,\n CHANNEL_EVENTS,\n DEFAULT_TIMEOUT,\n DEFAULT_VSN,\n SOCKET_STATES,\n TRANSPORTS,\n WS_CLOSE_NORMAL\n} from \"./constants\"\n\nimport {\n closure\n} from \"./utils\"\n\nimport Ajax from \"./ajax\"\nimport Channel from \"./channel\"\nimport LongPoll from \"./longpoll\"\nimport Serializer from \"./serializer\"\nimport Timer from \"./timer\"\n\n/** Initializes the Socket *\n *\n * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)\n *\n * @param {string} endPoint - The string WebSocket endpoint, ie, `\"ws://example.com/socket\"`,\n * `\"wss://example.com\"`\n * `\"/socket\"` (inherited host & protocol)\n * @param {Object} [opts] - Optional configuration\n * @param {Function} [opts.transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.\n *\n * Defaults to WebSocket with automatic LongPoll fallback if WebSocket is not defined.\n * To fallback to LongPoll when WebSocket attempts fail, use `longPollFallbackMs: 2500`.\n *\n * @param {Function} [opts.longPollFallbackMs] - The millisecond time to attempt the primary transport\n * before falling back to the LongPoll transport. Disabled by default.\n *\n * @param {Function} [opts.debug] - When true, enables debug logging. Default false.\n *\n * @param {Function} [opts.encode] - The function to encode outgoing messages.\n *\n * Defaults to JSON encoder.\n *\n * @param {Function} [opts.decode] - The function to decode incoming messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.parse(payload))\n * ```\n *\n * @param {number} [opts.timeout] - The default timeout in milliseconds to trigger push timeouts.\n *\n * Defaults `DEFAULT_TIMEOUT`\n * @param {number} [opts.heartbeatIntervalMs] - The millisec interval to send a heartbeat message\n * @param {number} [opts.reconnectAfterMs] - The optional function that returns the millisec\n * socket reconnect interval.\n *\n * Defaults to stepped backoff of:\n *\n * ```javascript\n * function(tries){\n * return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n * }\n * ````\n *\n * @param {number} [opts.rejoinAfterMs] - The optional function that returns the millisec\n * rejoin interval for individual channels.\n *\n * ```javascript\n * function(tries){\n * return [1000, 2000, 5000][tries - 1] || 10000\n * }\n * ````\n *\n * @param {Function} [opts.logger] - The optional function for specialized logging, ie:\n *\n * ```javascript\n * function(kind, msg, data) {\n * console.log(`${kind}: ${msg}`, data)\n * }\n * ```\n *\n * @param {number} [opts.longpollerTimeout] - The maximum timeout of a long poll AJAX request.\n *\n * Defaults to 20s (double the server long poll timer).\n *\n * @param {(Object|function)} [opts.params] - The optional params to pass when connecting\n * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.\n *\n * Defaults to \"arraybuffer\"\n *\n * @param {vsn} [opts.vsn] - The serializer's protocol version to send on connect.\n *\n * Defaults to DEFAULT_VSN.\n *\n * @param {Object} [opts.sessionStorage] - An optional Storage compatible object\n * Phoenix uses sessionStorage for longpoll fallback history. Overriding the store is\n * useful when Phoenix won't have access to `sessionStorage`. For example, This could\n * happen if a site loads a cross-domain channel in an iframe. Example usage:\n *\n * class InMemoryStorage {\n * constructor() { this.storage = {} }\n * getItem(keyName) { return this.storage[keyName] || null }\n * removeItem(keyName) { delete this.storage[keyName] }\n * setItem(keyName, keyValue) { this.storage[keyName] = keyValue }\n * }\n *\n*/\nexport default class Socket {\n constructor(endPoint, opts = {}){\n this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}\n this.channels = []\n this.sendBuffer = []\n this.ref = 0\n this.timeout = opts.timeout || DEFAULT_TIMEOUT\n this.transport = opts.transport || global.WebSocket || LongPoll\n this.primaryPassedHealthCheck = false\n this.longPollFallbackMs = opts.longPollFallbackMs\n this.fallbackTimer = null\n this.sessionStore = opts.sessionStorage || (global && global.sessionStorage)\n this.establishedConnections = 0\n this.defaultEncoder = Serializer.encode.bind(Serializer)\n this.defaultDecoder = Serializer.decode.bind(Serializer)\n this.closeWasClean = false\n this.binaryType = opts.binaryType || \"arraybuffer\"\n this.connectClock = 1\n if(this.transport !== LongPoll){\n this.encode = opts.encode || this.defaultEncoder\n this.decode = opts.decode || this.defaultDecoder\n } else {\n this.encode = this.defaultEncoder\n this.decode = this.defaultDecoder\n }\n let awaitingConnectionOnPageShow = null\n if(phxWindow && phxWindow.addEventListener){\n phxWindow.addEventListener(\"pagehide\", _e => {\n if(this.conn){\n this.disconnect()\n awaitingConnectionOnPageShow = this.connectClock\n }\n })\n phxWindow.addEventListener(\"pageshow\", _e => {\n if(awaitingConnectionOnPageShow === this.connectClock){\n awaitingConnectionOnPageShow = null\n this.connect()\n }\n })\n }\n this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000\n this.rejoinAfterMs = (tries) => {\n if(opts.rejoinAfterMs){\n return opts.rejoinAfterMs(tries)\n } else {\n return [1000, 2000, 5000][tries - 1] || 10000\n }\n }\n this.reconnectAfterMs = (tries) => {\n if(opts.reconnectAfterMs){\n return opts.reconnectAfterMs(tries)\n } else {\n return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000\n }\n }\n this.logger = opts.logger || null\n if(!this.logger && opts.debug){\n this.logger = (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }\n }\n this.longpollerTimeout = opts.longpollerTimeout || 20000\n this.params = closure(opts.params || {})\n this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`\n this.vsn = opts.vsn || DEFAULT_VSN\n this.heartbeatTimeoutTimer = null\n this.heartbeatTimer = null\n this.pendingHeartbeatRef = null\n this.reconnectTimer = new Timer(() => {\n this.teardown(() => this.connect())\n }, this.reconnectAfterMs)\n }\n\n /**\n * Returns the LongPoll transport reference\n */\n getLongPollTransport(){ return LongPoll }\n\n /**\n * Disconnects and replaces the active transport\n *\n * @param {Function} newTransport - The new transport class to instantiate\n *\n */\n replaceTransport(newTransport){\n this.connectClock++\n this.closeWasClean = true\n clearTimeout(this.fallbackTimer)\n this.reconnectTimer.reset()\n if(this.conn){\n this.conn.close()\n this.conn = null\n }\n this.transport = newTransport\n }\n\n /**\n * Returns the socket protocol\n *\n * @returns {string}\n */\n protocol(){ return location.protocol.match(/^https/) ? \"wss\" : \"ws\" }\n\n /**\n * The fully qualified socket url\n *\n * @returns {string}\n */\n endPointURL(){\n let uri = Ajax.appendParams(\n Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})\n if(uri.charAt(0) !== \"/\"){ return uri }\n if(uri.charAt(1) === \"/\"){ return `${this.protocol()}:${uri}` }\n\n return `${this.protocol()}://${location.host}${uri}`\n }\n\n /**\n * Disconnects the socket\n *\n * See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.\n *\n * @param {Function} callback - Optional callback which is called after socket is disconnected.\n * @param {integer} code - A status code for disconnection (Optional).\n * @param {string} reason - A textual description of the reason to disconnect. (Optional)\n */\n disconnect(callback, code, reason){\n this.connectClock++\n this.closeWasClean = true\n clearTimeout(this.fallbackTimer)\n this.reconnectTimer.reset()\n this.teardown(callback, code, reason)\n }\n\n /**\n *\n * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`\n *\n * Passing params to connect is deprecated; pass them in the Socket constructor instead:\n * `new Socket(\"/socket\", {params: {user_id: userToken}})`.\n */\n connect(params){\n if(params){\n console && console.log(\"passing params to connect is deprecated. Instead pass :params to the Socket constructor\")\n this.params = closure(params)\n }\n if(this.conn){ return }\n if(this.longPollFallbackMs && this.transport !== LongPoll){\n this.connectWithFallback(LongPoll, this.longPollFallbackMs)\n } else {\n this.transportConnect()\n }\n }\n\n /**\n * Logs the message. Override `this.logger` for specialized logging. noops by default\n * @param {string} kind\n * @param {string} msg\n * @param {Object} data\n */\n log(kind, msg, data){ this.logger && this.logger(kind, msg, data) }\n\n /**\n * Returns true if a logger has been set on this socket.\n */\n hasLogger(){ return this.logger !== null }\n\n /**\n * Registers callbacks for connection open events\n *\n * @example socket.onOpen(function(){ console.info(\"the socket was opened\") })\n *\n * @param {Function} callback\n */\n onOpen(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.open.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection close events\n * @param {Function} callback\n */\n onClose(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.close.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection error events\n *\n * @example socket.onError(function(error){ alert(\"An error occurred\") })\n *\n * @param {Function} callback\n */\n onError(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.error.push([ref, callback])\n return ref\n }\n\n /**\n * Registers callbacks for connection message events\n * @param {Function} callback\n */\n onMessage(callback){\n let ref = this.makeRef()\n this.stateChangeCallbacks.message.push([ref, callback])\n return ref\n }\n\n /**\n * Pings the server and invokes the callback with the RTT in milliseconds\n * @param {Function} callback\n *\n * Returns true if the ping was pushed or false if unable to be pushed.\n */\n ping(callback){\n if(!this.isConnected()){ return false }\n let ref = this.makeRef()\n let startTime = Date.now()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: ref})\n let onMsgRef = this.onMessage(msg => {\n if(msg.ref === ref){\n this.off([onMsgRef])\n callback(Date.now() - startTime)\n }\n })\n return true\n }\n\n /**\n * @private\n */\n\n transportConnect(){\n this.connectClock++\n this.closeWasClean = false\n this.conn = new this.transport(this.endPointURL())\n this.conn.binaryType = this.binaryType\n this.conn.timeout = this.longpollerTimeout\n this.conn.onopen = () => this.onConnOpen()\n this.conn.onerror = error => this.onConnError(error)\n this.conn.onmessage = event => this.onConnMessage(event)\n this.conn.onclose = event => this.onConnClose(event)\n }\n\n getSession(key){ return this.sessionStore && this.sessionStore.getItem(key) }\n\n storeSession(key, val){ this.sessionStore && this.sessionStore.setItem(key, val) }\n\n connectWithFallback(fallbackTransport, fallbackThreshold = 2500){\n clearTimeout(this.fallbackTimer)\n let established = false\n let primaryTransport = true\n let openRef, errorRef\n let fallback = (reason) => {\n this.log(\"transport\", `falling back to ${fallbackTransport.name}...`, reason)\n this.off([openRef, errorRef])\n primaryTransport = false\n this.replaceTransport(fallbackTransport)\n this.transportConnect()\n }\n if(this.getSession(`phx:fallback:${fallbackTransport.name}`)){ return fallback(\"memorized\") }\n\n this.fallbackTimer = setTimeout(fallback, fallbackThreshold)\n\n errorRef = this.onError(reason => {\n this.log(\"transport\", \"error\", reason)\n if(primaryTransport && !established){\n clearTimeout(this.fallbackTimer)\n fallback(reason)\n }\n })\n this.onOpen(() => {\n established = true\n if(!primaryTransport){\n // only memorize LP if we never connected to primary\n if(!this.primaryPassedHealthCheck){ this.storeSession(`phx:fallback:${fallbackTransport.name}`, \"true\") }\n return this.log(\"transport\", `established ${fallbackTransport.name} fallback`)\n }\n // if we've established primary, give the fallback a new period to attempt ping\n clearTimeout(this.fallbackTimer)\n this.fallbackTimer = setTimeout(fallback, fallbackThreshold)\n this.ping(rtt => {\n this.log(\"transport\", \"connected to primary after\", rtt)\n this.primaryPassedHealthCheck = true\n clearTimeout(this.fallbackTimer)\n })\n })\n this.transportConnect()\n }\n\n clearHeartbeats(){\n clearTimeout(this.heartbeatTimer)\n clearTimeout(this.heartbeatTimeoutTimer)\n }\n\n onConnOpen(){\n if(this.hasLogger()) this.log(\"transport\", `${this.transport.name} connected to ${this.endPointURL()}`)\n this.closeWasClean = false\n this.establishedConnections++\n this.flushSendBuffer()\n this.reconnectTimer.reset()\n this.resetHeartbeat()\n this.stateChangeCallbacks.open.forEach(([, callback]) => callback())\n }\n\n /**\n * @private\n */\n\n heartbeatTimeout(){\n if(this.pendingHeartbeatRef){\n this.pendingHeartbeatRef = null\n if(this.hasLogger()){ this.log(\"transport\", \"heartbeat timeout. Attempting to re-establish connection\") }\n this.triggerChanError()\n this.closeWasClean = false\n this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, \"heartbeat timeout\")\n }\n }\n\n resetHeartbeat(){\n if(this.conn && this.conn.skipHeartbeat){ return }\n this.pendingHeartbeatRef = null\n this.clearHeartbeats()\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n teardown(callback, code, reason){\n if(!this.conn){\n return callback && callback()\n }\n\n this.waitForBufferDone(() => {\n if(this.conn){\n if(code){ this.conn.close(code, reason || \"\") } else { this.conn.close() }\n }\n\n this.waitForSocketClosed(() => {\n if(this.conn){\n this.conn.onopen = function (){ } // noop\n this.conn.onerror = function (){ } // noop\n this.conn.onmessage = function (){ } // noop\n this.conn.onclose = function (){ } // noop\n this.conn = null\n }\n\n callback && callback()\n })\n })\n }\n\n waitForBufferDone(callback, tries = 1){\n if(tries === 5 || !this.conn || !this.conn.bufferedAmount){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForBufferDone(callback, tries + 1)\n }, 150 * tries)\n }\n\n waitForSocketClosed(callback, tries = 1){\n if(tries === 5 || !this.conn || this.conn.readyState === SOCKET_STATES.closed){\n callback()\n return\n }\n\n setTimeout(() => {\n this.waitForSocketClosed(callback, tries + 1)\n }, 150 * tries)\n }\n\n onConnClose(event){\n let closeCode = event && event.code\n if(this.hasLogger()) this.log(\"transport\", \"close\", event)\n this.triggerChanError()\n this.clearHeartbeats()\n if(!this.closeWasClean && closeCode !== 1000){\n this.reconnectTimer.scheduleTimeout()\n }\n this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event))\n }\n\n /**\n * @private\n */\n onConnError(error){\n if(this.hasLogger()) this.log(\"transport\", error)\n let transportBefore = this.transport\n let establishedBefore = this.establishedConnections\n this.stateChangeCallbacks.error.forEach(([, callback]) => {\n callback(error, transportBefore, establishedBefore)\n })\n if(transportBefore === this.transport || establishedBefore > 0){\n this.triggerChanError()\n }\n }\n\n /**\n * @private\n */\n triggerChanError(){\n this.channels.forEach(channel => {\n if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){\n channel.trigger(CHANNEL_EVENTS.error)\n }\n })\n }\n\n /**\n * @returns {string}\n */\n connectionState(){\n switch(this.conn && this.conn.readyState){\n case SOCKET_STATES.connecting: return \"connecting\"\n case SOCKET_STATES.open: return \"open\"\n case SOCKET_STATES.closing: return \"closing\"\n default: return \"closed\"\n }\n }\n\n /**\n * @returns {boolean}\n */\n isConnected(){ return this.connectionState() === \"open\" }\n\n /**\n * @private\n *\n * @param {Channel}\n */\n remove(channel){\n this.off(channel.stateChangeRefs)\n this.channels = this.channels.filter(c => c !== channel)\n }\n\n /**\n * Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.\n *\n * @param {refs} - list of refs returned by calls to\n * `onOpen`, `onClose`, `onError,` and `onMessage`\n */\n off(refs){\n for(let key in this.stateChangeCallbacks){\n this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {\n return refs.indexOf(ref) === -1\n })\n }\n }\n\n /**\n * Initiates a new channel for the given topic\n *\n * @param {string} topic\n * @param {Object} chanParams - Parameters for the channel\n * @returns {Channel}\n */\n channel(topic, chanParams = {}){\n let chan = new Channel(topic, chanParams, this)\n this.channels.push(chan)\n return chan\n }\n\n /**\n * @param {Object} data\n */\n push(data){\n if(this.hasLogger()){\n let {topic, event, payload, ref, join_ref} = data\n this.log(\"push\", `${topic} ${event} (${join_ref}, ${ref})`, payload)\n }\n\n if(this.isConnected()){\n this.encode(data, result => this.conn.send(result))\n } else {\n this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))\n }\n }\n\n /**\n * Return the next message ref, accounting for overflows\n * @returns {string}\n */\n makeRef(){\n let newRef = this.ref + 1\n if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }\n\n return this.ref.toString()\n }\n\n sendHeartbeat(){\n if(this.pendingHeartbeatRef && !this.isConnected()){ return }\n this.pendingHeartbeatRef = this.makeRef()\n this.push({topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: this.pendingHeartbeatRef})\n this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)\n }\n\n flushSendBuffer(){\n if(this.isConnected() && this.sendBuffer.length > 0){\n this.sendBuffer.forEach(callback => callback())\n this.sendBuffer = []\n }\n }\n\n onConnMessage(rawMessage){\n this.decode(rawMessage.data, msg => {\n let {topic, event, payload, ref, join_ref} = msg\n if(ref && ref === this.pendingHeartbeatRef){\n this.clearHeartbeats()\n this.pendingHeartbeatRef = null\n this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)\n }\n\n if(this.hasLogger()) this.log(\"receive\", `${payload.status || \"\"} ${topic} ${event} ${ref && \"(\" + ref + \")\" || \"\"}`, payload)\n\n for(let i = 0; i < this.channels.length; i++){\n const channel = this.channels[i]\n if(!channel.isMember(topic, event, payload, join_ref)){ continue }\n channel.trigger(event, payload, ref, join_ref)\n }\n\n for(let i = 0; i < this.stateChangeCallbacks.message.length; i++){\n let [, callback] = this.stateChangeCallbacks.message[i]\n callback(msg)\n }\n })\n }\n\n leaveOpenTopic(topic){\n let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))\n if(dupChannel){\n if(this.hasLogger()) this.log(\"transport\", `leaving duplicate topic \"${topic}\"`)\n dupChannel.leave()\n }\n }\n}\n"],
+ "mappings": "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",
+ "names": ["closure"]
}
diff --git a/priv/templates/phx.gen.auth/auth.ex b/priv/templates/phx.gen.auth/auth.ex
index b32758a75d..af36929d21 100644
--- a/priv/templates/phx.gen.auth/auth.ex
+++ b/priv/templates/phx.gen.auth/auth.ex
@@ -60,6 +60,8 @@ defmodule <%= inspect auth_module %> do
# end
#
defp renew_session(conn) do
+ delete_csrf_token()
+
conn
|> configure_session(renew: true)
|> clear_session()
@@ -156,7 +158,7 @@ defmodule <%= inspect auth_module %> do
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
- |> Phoenix.LiveView.redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+ |> Phoenix.LiveView.redirect(to: ~p"<%= schema.route_prefix %>/log-in")
{:halt, socket}
end
@@ -206,7 +208,7 @@ defmodule <%= inspect auth_module %> do
conn
|> put_flash(:error, "You must log in to access this page.")
|> maybe_store_return_to()
- |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+ |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
|> halt()
end
end
diff --git a/priv/templates/phx.gen.auth/auth_test.exs b/priv/templates/phx.gen.auth/auth_test.exs
index ef8c12ee99..cb7943b5e4 100644
--- a/priv/templates/phx.gen.auth/auth_test.exs
+++ b/priv/templates/phx.gen.auth/auth_test.exs
@@ -117,7 +117,7 @@ defmodule <%= inspect auth_module %>Test do
end
end
- describe "on_mount: mount_current_<%= schema.singular %>" do
+ describe "on_mount :mount_current_<%= schema.singular %>" do
test "assigns current_<%= schema.singular %> based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
<%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
@@ -148,7 +148,7 @@ defmodule <%= inspect auth_module %>Test do
end
end
- describe "on_mount: ensure_authenticated" do
+ describe "on_mount :ensure_authenticated" do
test "authenticates current_<%= schema.singular %> based on a valid <%= schema.singular %>_token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
<%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
@@ -185,7 +185,7 @@ defmodule <%= inspect auth_module %>Test do
end
end
- describe "on_mount: :redirect_if_<%= schema.singular %>_is_authenticated" do
+ describe "on_mount :redirect_if_<%= schema.singular %>_is_authenticated" do
test "redirects if there is an authenticated <%= schema.singular %> ", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
<%= schema.singular %>_token = <%= inspect context.alias %>.generate_<%= schema.singular %>_session_token(<%= schema.singular %>)
session = conn |> put_session(:<%= schema.singular %>_token, <%= schema.singular %>_token) |> get_session()
@@ -231,7 +231,7 @@ defmodule <%= inspect auth_module %>Test do
conn = conn |> fetch_flash() |> <%= inspect schema.alias %>Auth.require_authenticated_<%= schema.singular %>([])
assert conn.halted
- assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+ assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
assert Phoenix.Flash.get(conn.assigns.flash, :error) ==
"You must log in to access this page."
diff --git a/priv/templates/phx.gen.auth/confirmation_controller_test.exs b/priv/templates/phx.gen.auth/confirmation_controller_test.exs
index 09390630fb..d38e11f889 100644
--- a/priv/templates/phx.gen.auth/confirmation_controller_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_controller_test.exs
@@ -2,7 +2,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
alias <%= inspect context.module %>
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
import <%= inspect context.module %>Fixtures
setup do
diff --git a/priv/templates/phx.gen.auth/confirmation_edit.html.heex b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
index ea7d8d1704..0f6d96d416 100644
--- a/priv/templates/phx.gen.auth/confirmation_edit.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_edit.html.heex
@@ -7,8 +7,8 @@
-
+
} class="text-center mt-4">
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
index 8d7b32b47c..bf54a99333 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationInstructionsLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructions do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -26,9 +26,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
-
+
} class="text-center mt-4">
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
"""
diff --git a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
index 4a9488d020..d8d1d23763 100644
--- a/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_instructions_live_test.exs
@@ -1,11 +1,11 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationInstructionsLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationInstructionsTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
alias <%= inspect context.module %>
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
setup do
%{<%= schema.singular %>: <%= schema.singular %>_fixture()}
diff --git a/priv/templates/phx.gen.auth/confirmation_live.ex b/priv/templates/phx.gen.auth/confirmation_live.ex
index 677dd1e7a6..251afacec0 100644
--- a/priv/templates/phx.gen.auth/confirmation_live.ex
+++ b/priv/templates/phx.gen.auth/confirmation_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Confirmation do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -9,15 +9,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<.header class="text-center">Confirm Account
<.simple_form for={@form} id="confirmation_form" phx-submit="confirm_account">
- <.input field={@form[:token]} type="hidden" />
+
<:actions>
<.button phx-disable-with="Confirming..." class="w-full">Confirm my account
-
+
} class="text-center mt-4">
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
"""
diff --git a/priv/templates/phx.gen.auth/confirmation_live_test.exs b/priv/templates/phx.gen.auth/confirmation_live_test.exs
index 421102141c..0eac2e43c0 100644
--- a/priv/templates/phx.gen.auth/confirmation_live_test.exs
+++ b/priv/templates/phx.gen.auth/confirmation_live_test.exs
@@ -1,11 +1,11 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ConfirmationLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ConfirmationTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
alias <%= inspect context.module %>
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
setup do
%{<%= schema.singular %>: <%= schema.singular %>_fixture()}
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
lv
|> form("#confirmation_form")
|> render_submit()
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert {:ok, conn} = result
@@ -47,7 +47,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
lv
|> form("#confirmation_form")
|> render_submit()
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert {:ok, conn} = result
@@ -55,16 +55,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
"<%= inspect schema.alias %> confirmation link is invalid or it has expired"
# when logged in
- {:ok, lv, _html} =
+ conn =
build_conn()
|> log_in_<%= schema.singular %>(<%= schema.singular %>)
- |> live(~p"<%= schema.route_prefix %>/confirm/#{token}")
+
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/confirm/#{token}")
result =
lv
|> form("#confirmation_form")
|> render_submit()
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert {:ok, conn} = result
refute Phoenix.Flash.get(conn.assigns.flash, :error)
diff --git a/priv/templates/phx.gen.auth/confirmation_new.html.heex b/priv/templates/phx.gen.auth/confirmation_new.html.heex
index e31f6db4b2..7edb1088c4 100644
--- a/priv/templates/phx.gen.auth/confirmation_new.html.heex
+++ b/priv/templates/phx.gen.auth/confirmation_new.html.heex
@@ -13,8 +13,8 @@
-
+
} class="text-center mt-4">
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
diff --git a/priv/templates/phx.gen.auth/context_functions.ex b/priv/templates/phx.gen.auth/context_functions.ex
index 2f9b1bed3a..e3a2c090f1 100644
--- a/priv/templates/phx.gen.auth/context_functions.ex
+++ b/priv/templates/phx.gen.auth/context_functions.ex
@@ -146,7 +146,7 @@
Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, changeset)
- |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [context]))
+ |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [context]))
end
@doc ~S"""
@@ -154,7 +154,7 @@
## Examples
- iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1})")
+ iex> deliver_<%= schema.singular %>_update_email_instructions(<%= schema.singular %>, current_email, &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}"))
{:ok, %{to: ..., body: ...}}
"""
@@ -199,7 +199,7 @@
Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, changeset)
- |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
+ |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
|> Repo.transaction()
|> case do
{:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
@@ -230,7 +230,7 @@
Deletes the signed token with the given context.
"""
def delete_<%= schema.singular %>_session_token(token) do
- Repo.delete_all(<%= inspect schema.alias %>Token.token_and_context_query(token, "session"))
+ Repo.delete_all(<%= inspect schema.alias %>Token.by_token_and_context_query(token, "session"))
:ok
end
@@ -278,7 +278,7 @@
defp confirm_<%= schema.singular %>_multi(<%= schema.singular %>) do
Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, <%= inspect schema.alias %>.confirm_changeset(<%= schema.singular %>))
- |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, ["confirm"]))
+ |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, ["confirm"]))
end
## Reset password
@@ -288,7 +288,7 @@
## Examples
- iex> deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}"))
+ iex> deliver_<%= schema.singular %>_reset_password_instructions(<%= schema.singular %>, &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}"))
{:ok, %{to: ..., body: ...}}
"""
@@ -335,7 +335,7 @@
def reset_<%= schema.singular %>_password(<%= schema.singular %>, attrs) do
Ecto.Multi.new()
|> Ecto.Multi.update(:<%= schema.singular %>, <%= inspect schema.alias %>.password_changeset(<%= schema.singular %>, attrs))
- |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
+ |> Ecto.Multi.delete_all(:tokens, <%= inspect schema.alias %>Token.by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all))
|> Repo.transaction()
|> case do
{:ok, %{<%= schema.singular %>: <%= schema.singular %>}} -> {:ok, <%= schema.singular %>}
diff --git a/priv/templates/phx.gen.auth/forgot_password_live.ex b/priv/templates/phx.gen.auth/forgot_password_live.ex
index d1461789b1..9c30c3c90e 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live.ex
+++ b/priv/templates/phx.gen.auth/forgot_password_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ForgotPasswordLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPassword do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -27,7 +27,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
"""
@@ -41,7 +41,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
<%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
<%= schema.singular %>,
- &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}")
+ &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
)
end
diff --git a/priv/templates/phx.gen.auth/forgot_password_live_test.exs b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
index 7dd7cd6b42..2a7f5b3620 100644
--- a/priv/templates/phx.gen.auth/forgot_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/forgot_password_live_test.exs
@@ -1,26 +1,26 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ForgotPasswordLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ForgotPasswordTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
alias <%= inspect context.module %>
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
describe "Forgot password page" do
test "renders email page", %{conn: conn} do
- {:ok, lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+ {:ok, lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
assert html =~ "Forgot your password?"
assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/register"}"]|, "Register")
- assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/log_in"}"]|, "Log in")
+ assert has_element?(lv, ~s|a[href="#{~p"<%= schema.route_prefix %>/log-in"}"]|, "Log in")
end
test "redirects if already logged in", %{conn: conn} do
result =
conn
|> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
- |> live(~p"<%= schema.route_prefix %>/reset_password")
+ |> live(~p"<%= schema.route_prefix %>/reset-password")
|> follow_redirect(conn, ~p"/")
assert {:ok, _conn} = result
@@ -33,13 +33,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
{:ok, conn} =
lv
|> form("#reset_password_form", <%= schema.singular %>: %{"email" => <%= schema.singular %>.email})
|> render_submit()
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
@@ -48,13 +48,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "does not send reset password token if email is invalid", %{conn: conn} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password")
{:ok, conn} =
lv
|> form("#reset_password_form", <%= schema.singular %>: %{"email" => "unknown@example.com"})
|> render_submit()
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "If your email is in our system"
assert Repo.all(<%= inspect context.alias %>.<%= inspect schema.alias %>Token) == []
diff --git a/priv/templates/phx.gen.auth/login_live.ex b/priv/templates/phx.gen.auth/login_live.ex
index b7efa0104e..77bb16d791 100644
--- a/priv/templates/phx.gen.auth/login_live.ex
+++ b/priv/templates/phx.gen.auth/login_live.ex
@@ -1,11 +1,11 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LoginLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Login do
use <%= inspect context.web_module %>, :live_view
def render(assigns) do
~H"""
<.header class="text-center">
- Sign in to account
+ Log in to account
<:subtitle>
Don't have an account?
<.link navigate={~p"<%= schema.route_prefix %>/register"} class="font-semibold text-brand hover:underline">
@@ -15,7 +15,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
- <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log_in"} phx-update="ignore">
+ <.simple_form for={@form} id="login_form" action={~p"<%= schema.route_prefix %>/log-in"} phx-update="ignore">
<.input field={@form[:email]} type="email" label="Email" autocomplete="username" required />
<.input
field={@form[:password]}
@@ -27,13 +27,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<:actions>
<.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
- <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
+ <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
Forgot your password?
<:actions>
- <.button phx-disable-with="Signing in..." class="w-full">
- Sign in
→
+ <.button class="w-full">
+ Log in
→
@@ -42,7 +42,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
def mount(_params, _session, socket) do
- email = live_flash(socket.assigns.flash, :email)
+ email = Phoenix.Flash.get(socket.assigns.flash, :email)
form = to_form(%{"email" => email}, as: "<%= schema.singular %>")
{:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end
diff --git a/priv/templates/phx.gen.auth/login_live_test.exs b/priv/templates/phx.gen.auth/login_live_test.exs
index d9aa306669..11516b78ac 100644
--- a/priv/templates/phx.gen.auth/login_live_test.exs
+++ b/priv/templates/phx.gen.auth/login_live_test.exs
@@ -1,12 +1,12 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LoginLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.LoginTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
describe "Log in page" do
test "renders log in page", %{conn: conn} do
- {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+ {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
assert html =~ "Log in"
assert html =~ "Register"
@@ -17,8 +17,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
result =
conn
|> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
- |> live(~p"<%= schema.route_prefix %>/log_in")
- |> follow_redirect(conn, "/")
+ |> live(~p"<%= schema.route_prefix %>/log-in")
+ |> follow_redirect(conn, ~p"/")
assert {:ok, _conn} = result
end
@@ -29,7 +29,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
password = "123456789abcd"
<%= schema.singular %> = <%= schema.singular %>_fixture(%{password: password})
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
form =
form(lv, "#login_form", <%= schema.singular %>: %{email: <%= schema.singular %>.email, password: password, remember_me: true})
@@ -42,7 +42,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects to login page with a flash error if there are no valid credentials", %{
conn: conn
} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
form =
form(lv, "#login_form",
@@ -53,13 +53,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
- assert redirected_to(conn) == "<%= schema.route_prefix %>/log_in"
+ assert redirected_to(conn) == "<%= schema.route_prefix %>/log-in"
end
end
describe "login navigation" do
test "redirects to registration page when the Register button is clicked", %{conn: conn} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
{:ok, _login_live, login_html} =
lv
@@ -73,13 +73,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects to forgot password page when the Forgot Password button is clicked", %{
conn: conn
} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log_in")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/log-in")
{:ok, conn} =
lv
|> element(~s|main a:fl-contains("Forgot your password?")|)
|> render_click()
- |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/reset_password")
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/reset-password")
assert conn.resp_body =~ "Forgot your password?"
end
diff --git a/priv/templates/phx.gen.auth/migration.ex b/priv/templates/phx.gen.auth/migration.ex
index e8cdd476a1..ab8d4b918f 100644
--- a/priv/templates/phx.gen.auth/migration.ex
+++ b/priv/templates/phx.gen.auth/migration.ex
@@ -8,8 +8,9 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
<%= if schema.binary_id do %> add :id, :binary_id, primary_key: true
<% end %> <%= migration.column_definitions[:email] %>
add :hashed_password, :string, null: false
- add :confirmed_at, :naive_datetime
- timestamps()
+ add :confirmed_at, <%= inspect schema.timestamp_type %>
+
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
end
create unique_index(:<%= schema.table %>, [:email])
@@ -20,7 +21,8 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
<%= migration.column_definitions[:token] %>
add :context, :string, null: false
add :sent_to, :string
- timestamps(updated_at: false)
+
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}, " %>updated_at: false)
end
create index(:<%= schema.table %>_tokens, [:<%= schema.singular %>_id])
diff --git a/priv/templates/phx.gen.auth/registration_controller_test.exs b/priv/templates/phx.gen.auth/registration_controller_test.exs
index 2682277b08..53dfbb2e66 100644
--- a/priv/templates/phx.gen.auth/registration_controller_test.exs
+++ b/priv/templates/phx.gen.auth/registration_controller_test.exs
@@ -8,7 +8,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
conn = get(conn, ~p"<%= schema.route_prefix %>/register")
response = html_response(conn, 200)
assert response =~ "Register"
- assert response =~ ~p"<%= schema.route_prefix %>/log_in"
+ assert response =~ ~p"<%= schema.route_prefix %>/log-in"
assert response =~ ~p"<%= schema.route_prefix %>/register"
end
@@ -37,7 +37,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
response = html_response(conn, 200)
assert response =~ email
assert response =~ ~p"<%= schema.route_prefix %>/settings"
- assert response =~ ~p"<%= schema.route_prefix %>/log_out"
+ assert response =~ ~p"<%= schema.route_prefix %>/log-out"
end
test "render errors for invalid data", %{conn: conn} do
diff --git a/priv/templates/phx.gen.auth/registration_live.ex b/priv/templates/phx.gen.auth/registration_live.ex
index 29629edf19..31cb8ad8e8 100644
--- a/priv/templates/phx.gen.auth/registration_live.ex
+++ b/priv/templates/phx.gen.auth/registration_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Registration do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -11,8 +11,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
Register for an account
<:subtitle>
Already registered?
- <.link navigate={~p"<%= schema.route_prefix %>/log_in"} class="font-semibold text-brand hover:underline">
- Sign in
+ <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
+ Log in
to your account now.
@@ -24,7 +24,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
phx-submit="save"
phx-change="validate"
phx-trigger-action={@trigger_submit}
- action={~p"<%= schema.route_prefix %>/log_in?_action=registered"}
+ action={~p"<%= schema.route_prefix %>/log-in?_action=registered"}
method="post"
>
<.error :if={@check_errors}>
diff --git a/priv/templates/phx.gen.auth/registration_live_test.exs b/priv/templates/phx.gen.auth/registration_live_test.exs
index 0f97ded814..8c878d3015 100644
--- a/priv/templates/phx.gen.auth/registration_live_test.exs
+++ b/priv/templates/phx.gen.auth/registration_live_test.exs
@@ -1,5 +1,5 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>RegistrationLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.RegistrationTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
@@ -17,7 +17,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
conn
|> log_in_<%= schema.singular %>(<%= schema.singular %>_fixture())
|> live(~p"<%= schema.route_prefix %>/register")
- |> follow_redirect(conn, "/")
+ |> follow_redirect(conn, ~p"/")
assert {:ok, _conn} = result
end
@@ -48,7 +48,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
assert redirected_to(conn) == ~p"/"
# Now do a logged in request and assert on the menu
- conn = get(conn, "/")
+ conn = get(conn, ~p"/")
response = html_response(conn, 200)
assert response =~ email
assert response =~ "Settings"
@@ -77,9 +77,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
{:ok, _login_live, login_html} =
lv
- |> element(~s|main a:fl-contains("Sign in")|)
+ |> element(~s|main a:fl-contains("Log in")|)
|> render_click()
- |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
assert login_html =~ "Log in"
end
diff --git a/priv/templates/phx.gen.auth/registration_new.html.heex b/priv/templates/phx.gen.auth/registration_new.html.heex
index 846beba88d..cff2b38196 100644
--- a/priv/templates/phx.gen.auth/registration_new.html.heex
+++ b/priv/templates/phx.gen.auth/registration_new.html.heex
@@ -3,8 +3,8 @@
Register for an account
<:subtitle>
Already registered?
- <.link navigate={~p"<%= schema.route_prefix %>/log_in"} class="font-semibold text-brand hover:underline">
- Sign in
+ <.link navigate={~p"<%= schema.route_prefix %>/log-in"} class="font-semibold text-brand hover:underline">
+ Log in
to your account now.
diff --git a/priv/templates/phx.gen.auth/reset_password_controller.ex b/priv/templates/phx.gen.auth/reset_password_controller.ex
index 5dc1c60fc0..e572e4d5f1 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller.ex
+++ b/priv/templates/phx.gen.auth/reset_password_controller.ex
@@ -13,7 +13,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
if <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email) do
<%= inspect context.alias %>.deliver_<%= schema.singular %>_reset_password_instructions(
<%= schema.singular %>,
- &url(~p"<%= schema.route_prefix %>/reset_password/#{&1}")
+ &url(~p"<%= schema.route_prefix %>/reset-password/#{&1}")
)
end
@@ -36,7 +36,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
{:ok, _} ->
conn
|> put_flash(:info, "Password reset successfully.")
- |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+ |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
{:error, changeset} ->
render(conn, :edit, changeset: changeset)
diff --git a/priv/templates/phx.gen.auth/reset_password_controller_test.exs b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
index 99594fcbc7..dcf0dc5c96 100644
--- a/priv/templates/phx.gen.auth/reset_password_controller_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_controller_test.exs
@@ -2,26 +2,26 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
alias <%= inspect context.module %>
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
import <%= inspect context.module %>Fixtures
setup do
%{<%= schema.singular %>: <%= schema.singular %>_fixture()}
end
- describe "GET <%= schema.route_prefix %>/reset_password" do
+ describe "GET <%= schema.route_prefix %>/reset-password" do
test "renders the reset password page", %{conn: conn} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password")
response = html_response(conn, 200)
assert response =~ "Forgot your password?"
end
end
- describe "POST <%= schema.route_prefix %>/reset_password" do
+ describe "POST <%= schema.route_prefix %>/reset-password" do
@tag :capture_log
test "sends a new reset password token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/reset_password", %{
+ post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
"<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email}
})
@@ -35,7 +35,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "does not send reset password token if email is invalid", %{conn: conn} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/reset_password", %{
+ post(conn, ~p"<%= schema.route_prefix %>/reset-password", %{
"<%= schema.singular %>" => %{"email" => "unknown@example.com"}
})
@@ -48,7 +48,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
end
- describe "GET <%= schema.route_prefix %>/reset_password/:token" do
+ describe "GET <%= schema.route_prefix %>/reset-password/:token" do
setup %{<%= schema.singular %>: <%= schema.singular %>} do
token =
extract_<%= schema.singular %>_token(fn url ->
@@ -59,12 +59,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "renders reset password", %{conn: conn, token: token} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
assert html_response(conn, 200) =~ "Reset password"
end
test "does not render reset password with invalid token", %{conn: conn} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/reset_password/oops")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
assert redirected_to(conn) == ~p"/"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
@@ -72,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
end
- describe "PUT <%= schema.route_prefix %>/reset_password/:token" do
+ describe "PUT <%= schema.route_prefix %>/reset-password/:token" do
setup %{<%= schema.singular %>: <%= schema.singular %>} do
token =
extract_<%= schema.singular %>_token(fn url ->
@@ -84,14 +84,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "resets password once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token} do
conn =
- put(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}", %{
+ put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
"<%= schema.singular %>" => %{
"password" => "new valid password",
"password_confirmation" => "new valid password"
}
})
- assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+ assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
refute get_session(conn, :<%= schema.singular %>_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
@@ -102,7 +102,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "does not reset password on invalid data", %{conn: conn, token: token} do
conn =
- put(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}", %{
+ put(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}", %{
"<%= schema.singular %>" => %{
"password" => "too short",
"password_confirmation" => "does not match"
@@ -113,7 +113,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "does not reset password with invalid token", %{conn: conn} do
- conn = put(conn, ~p"<%= schema.route_prefix %>/reset_password/oops")
+ conn = put(conn, ~p"<%= schema.route_prefix %>/reset-password/oops")
assert redirected_to(conn) == ~p"/"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
diff --git a/priv/templates/phx.gen.auth/reset_password_edit.html.heex b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
index d71705579e..729b497c42 100644
--- a/priv/templates/phx.gen.auth/reset_password_edit.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_edit.html.heex
@@ -3,7 +3,7 @@
Reset Password
- <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset_password/#{@token}"}>
+ <.simple_form :let={f} for={@changeset} action={~p"<%= schema.route_prefix %>/reset-password/#{@token}"}>
<.error :if={@changeset.action}>
Oops, something went wrong! Please check the errors below.
@@ -31,6 +31,6 @@
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
diff --git a/priv/templates/phx.gen.auth/reset_password_live.ex b/priv/templates/phx.gen.auth/reset_password_live.ex
index 53fd4ee4ca..02ae26850f 100644
--- a/priv/templates/phx.gen.auth/reset_password_live.ex
+++ b/priv/templates/phx.gen.auth/reset_password_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPassword do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -39,7 +39,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
"""
@@ -68,7 +68,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
{:noreply,
socket
|> put_flash(:info, "Password reset successfully.")
- |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")}
+ |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")}
{:error, changeset} ->
{:noreply, assign_form(socket, Map.put(changeset, :action, :insert))}
diff --git a/priv/templates/phx.gen.auth/reset_password_live_test.exs b/priv/templates/phx.gen.auth/reset_password_live_test.exs
index 31ef692b29..6e73bfe8e9 100644
--- a/priv/templates/phx.gen.auth/reset_password_live_test.exs
+++ b/priv/templates/phx.gen.auth/reset_password_live_test.exs
@@ -1,5 +1,5 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>ResetPasswordLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.ResetPasswordTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
import Phoenix.LiveViewTest
import <%= inspect context.module %>Fixtures
@@ -19,13 +19,13 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
describe "Reset password page" do
test "renders reset password with valid token", %{conn: conn, token: token} do
- {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, _lv, html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
assert html =~ "Reset Password"
end
test "does not render reset password with invalid token", %{conn: conn} do
- {:error, {:redirect, to}} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/invalid")
+ {:error, {:redirect, to}} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/invalid")
assert to == %{
flash: %{"error" => "Reset password link is invalid or it has expired."},
@@ -34,7 +34,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "renders errors for invalid data", %{conn: conn, token: token} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
result =
lv
@@ -50,7 +50,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
describe "Reset Password" do
test "resets password once", %{conn: conn, token: token, <%= schema.singular %>: <%= schema.singular %>} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
{:ok, conn} =
lv
@@ -61,7 +61,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
}
)
|> render_submit()
- |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
refute get_session(conn, :<%= schema.singular %>_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Password reset successfully"
@@ -69,7 +69,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "does not reset password on invalid data", %{conn: conn, token: token} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
result =
lv
@@ -89,22 +89,22 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
describe "Reset password navigation" do
test "redirects to login page when the Log in button is clicked", %{conn: conn, token: token} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
{:ok, conn} =
lv
|> element(~s|main a:fl-contains("Log in")|)
|> render_click()
- |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log_in")
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/log-in")
assert conn.resp_body =~ "Log in"
end
- test "redirects to password reset page when the Register button is clicked", %{
+ test "redirects to registration page when the Register button is clicked", %{
conn: conn,
token: token
} do
- {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset_password/#{token}")
+ {:ok, lv, _html} = live(conn, ~p"<%= schema.route_prefix %>/reset-password/#{token}")
{:ok, conn} =
lv
diff --git a/priv/templates/phx.gen.auth/reset_password_new.html.heex b/priv/templates/phx.gen.auth/reset_password_new.html.heex
index e4c52a1b55..6f254d1e8c 100644
--- a/priv/templates/phx.gen.auth/reset_password_new.html.heex
+++ b/priv/templates/phx.gen.auth/reset_password_new.html.heex
@@ -4,7 +4,7 @@
<:subtitle>We'll send a password reset link to your inbox
- <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset_password"}>
+ <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/reset-password"}>
<.input field={f[:email]} type="email" placeholder="Email" autocomplete="username" required />
<:actions>
<.button phx-disable-with="Sending..." class="w-full">
@@ -15,6 +15,6 @@
<.link href={~p"<%= schema.route_prefix %>/register"}>Register
- | <.link href={~p"<%= schema.route_prefix %>/log_in"}>Log in
+ | <.link href={~p"<%= schema.route_prefix %>/log-in"}>Log in
diff --git a/priv/templates/phx.gen.auth/routes.ex b/priv/templates/phx.gen.auth/routes.ex
index 60ced33601..928ea70c43 100644
--- a/priv/templates/phx.gen.auth/routes.ex
+++ b/priv/templates/phx.gen.auth/routes.ex
@@ -6,22 +6,22 @@
live_session :redirect_if_<%= schema.singular %>_is_authenticated,
on_mount: [{<%= inspect auth_module %>, :redirect_if_<%= schema.singular %>_is_authenticated}] do
- live "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationLive, :new
- live "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>LoginLive, :new
- live "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ForgotPasswordLive, :new
- live "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordLive, :edit
+ live "/<%= schema.plural %>/register", <%= inspect schema.alias %>Live.Registration, :new
+ live "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>Live.Login, :new
+ live "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>Live.ForgotPassword, :new
+ live "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>Live.ResetPassword, :edit
end
- post "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :create<% else %>
+ post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create<% else %>
get "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :new
post "/<%= schema.plural %>/register", <%= inspect schema.alias %>RegistrationController, :create
- get "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :new
- post "/<%= schema.plural %>/log_in", <%= inspect schema.alias %>SessionController, :create
- get "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ResetPasswordController, :new
- post "/<%= schema.plural %>/reset_password", <%= inspect schema.alias %>ResetPasswordController, :create
- get "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordController, :edit
- put "/<%= schema.plural %>/reset_password/:token", <%= inspect schema.alias %>ResetPasswordController, :update<% end %>
+ get "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :new
+ post "/<%= schema.plural %>/log-in", <%= inspect schema.alias %>SessionController, :create
+ get "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :new
+ post "/<%= schema.plural %>/reset-password", <%= inspect schema.alias %>ResetPasswordController, :create
+ get "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :edit
+ put "/<%= schema.plural %>/reset-password/:token", <%= inspect schema.alias %>ResetPasswordController, :update<% end %>
end
scope <%= router_scope %> do
@@ -29,24 +29,24 @@
live_session :require_authenticated_<%= schema.singular %>,
on_mount: [{<%= inspect auth_module %>, :ensure_authenticated}] do
- live "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsLive, :edit
- live "/<%= schema.plural %>/settings/confirm_email/:token", <%= inspect schema.alias %>SettingsLive, :confirm_email
+ live "/<%= schema.plural %>/settings", <%= inspect schema.alias %>Live.Settings, :edit
+ live "/<%= schema.plural %>/settings/confirm-email/:token", <%= inspect schema.alias %>Live.Settings, :confirm_email
end<% else %>
get "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :edit
put "/<%= schema.plural %>/settings", <%= inspect schema.alias %>SettingsController, :update
- get "/<%= schema.plural %>/settings/confirm_email/:token", <%= inspect schema.alias %>SettingsController, :confirm_email<% end %>
+ get "/<%= schema.plural %>/settings/confirm-email/:token", <%= inspect schema.alias %>SettingsController, :confirm_email<% end %>
end
scope <%= router_scope %> do
pipe_through [:browser]
- delete "/<%= schema.plural %>/log_out", <%= inspect schema.alias %>SessionController, :delete<%= if live? do %>
+ delete "/<%= schema.plural %>/log-out", <%= inspect schema.alias %>SessionController, :delete<%= if live? do %>
live_session :current_<%= schema.singular %>,
on_mount: [{<%= inspect auth_module %>, :mount_current_<%= schema.singular %>}] do
- live "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>ConfirmationLive, :edit
- live "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationInstructionsLive, :new
+ live "/<%= schema.plural %>/confirm/:token", <%= inspect schema.alias %>Live.Confirmation, :edit
+ live "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>Live.ConfirmationInstructions, :new
end<% else %>
get "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :new
post "/<%= schema.plural %>/confirm", <%= inspect schema.alias %>ConfirmationController, :create
diff --git a/priv/templates/phx.gen.auth/schema.ex b/priv/templates/phx.gen.auth/schema.ex
index 2624e96f3a..13ff8f1db5 100644
--- a/priv/templates/phx.gen.auth/schema.ex
+++ b/priv/templates/phx.gen.auth/schema.ex
@@ -7,9 +7,10 @@ defmodule <%= inspect schema.module %> do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
- field :confirmed_at, :naive_datetime
+ field :current_password, :string, virtual: true, redact: true
+ field :confirmed_at, <%= inspect schema.timestamp_type %>
- timestamps()
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
end
@doc """
@@ -126,8 +127,11 @@ defmodule <%= inspect schema.module %> do
Confirms the account by setting `confirmed_at`.
"""
def confirm_changeset(<%= schema.singular %>) do
- now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
- change(<%= schema.singular %>, confirmed_at: now)
+ <%= case schema.timestamp_type do %>
+ <% :naive_datetime -> %>now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+ <% :utc_datetime -> %>now = DateTime.utc_now() |> DateTime.truncate(:second)
+ <% :utc_datetime_usec -> %>now = DateTime.utc_now() |> DateTime.truncate(:microsecond)
+ <% end %>change(<%= schema.singular %>, confirmed_at: now)
end
@doc """
@@ -150,6 +154,8 @@ defmodule <%= inspect schema.module %> do
Validates the current password otherwise adds an error to the changeset.
"""
def validate_current_password(changeset, password) do
+ changeset = cast(changeset, %{current_password: password}, [:current_password])
+
if valid_password?(changeset.data, password) do
changeset
else
diff --git a/priv/templates/phx.gen.auth/schema_token.ex b/priv/templates/phx.gen.auth/schema_token.ex
index 71bdc954ac..d862d1d288 100644
--- a/priv/templates/phx.gen.auth/schema_token.ex
+++ b/priv/templates/phx.gen.auth/schema_token.ex
@@ -21,7 +21,7 @@ defmodule <%= inspect schema.module %>Token do
field :sent_to, :string
belongs_to :<%= schema.singular %>, <%= inspect schema.module %>
- timestamps(updated_at: false)
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}, " %>updated_at: false)
end
@doc """
@@ -58,7 +58,7 @@ defmodule <%= inspect schema.module %>Token do
"""
def verify_session_token_query(token) do
query =
- from token in token_and_context_query(token, "session"),
+ from token in by_token_and_context_query(token, "session"),
join: <%= schema.singular %> in assoc(token, :<%= schema.singular %>),
where: token.inserted_at > ago(@session_validity_in_days, "day"),
select: <%= schema.singular %>
@@ -116,7 +116,7 @@ defmodule <%= inspect schema.module %>Token do
days = days_for_context(context)
query =
- from token in token_and_context_query(hashed_token, context),
+ from token in by_token_and_context_query(hashed_token, context),
join: <%= schema.singular %> in assoc(token, :<%= schema.singular %>),
where: token.inserted_at > ago(^days, "day") and token.sent_to == <%= schema.singular %>.email,
select: <%= schema.singular %>
@@ -134,7 +134,7 @@ defmodule <%= inspect schema.module %>Token do
@doc """
Checks if the token is valid and returns its underlying lookup query.
- The query returns the <%= schema.singular %> found by the token, if any.
+ The query returns the <%= schema.singular %>_token found by the token, if any.
This is used to validate requests to change the <%= schema.singular %>
email. It is different from `verify_email_token_query/2` precisely because
@@ -151,7 +151,7 @@ defmodule <%= inspect schema.module %>Token do
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
- from token in token_and_context_query(hashed_token, context),
+ from token in by_token_and_context_query(hashed_token, context),
where: token.inserted_at > ago(@change_email_validity_in_days, "day")
{:ok, query}
@@ -164,18 +164,18 @@ defmodule <%= inspect schema.module %>Token do
@doc """
Returns the token struct for the given token value and context.
"""
- def token_and_context_query(token, context) do
+ def by_token_and_context_query(token, context) do
from <%= inspect schema.alias %>Token, where: [token: ^token, context: ^context]
end
@doc """
Gets all tokens for the given <%= schema.singular %> for the given contexts.
"""
- def <%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all) do
+ def by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, :all) do
from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id
end
- def <%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [_ | _] = contexts) do
+ def by_<%= schema.singular %>_and_contexts_query(<%= schema.singular %>, [_ | _] = contexts) do
from t in <%= inspect schema.alias %>Token, where: t.<%= schema.singular %>_id == ^<%= schema.singular %>.id and t.context in ^contexts
end
end
diff --git a/priv/templates/phx.gen.auth/session_controller.ex b/priv/templates/phx.gen.auth/session_controller.ex
index cb76ac3ea9..bab413a3c2 100644
--- a/priv/templates/phx.gen.auth/session_controller.ex
+++ b/priv/templates/phx.gen.auth/session_controller.ex
@@ -8,7 +8,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
create(conn, params, "Account created successfully!")
end
- def create(conn, %{"_action" => "password_updated"} = params) do
+ def create(conn, %{"_action" => "password-updated"} = params) do
conn
|> put_session(:<%= schema.singular %>_return_to, ~p"<%= schema.route_prefix %>/settings")
|> create(params, "Password updated successfully!")
@@ -30,7 +30,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:email, String.slice(email, 0, 160))
- |> redirect(to: ~p"<%= schema.route_prefix %>/log_in")
+ |> redirect(to: ~p"<%= schema.route_prefix %>/log-in")
end
end<% else %>
diff --git a/priv/templates/phx.gen.auth/session_controller_test.exs b/priv/templates/phx.gen.auth/session_controller_test.exs
index 70473c2cea..8004208b15 100644
--- a/priv/templates/phx.gen.auth/session_controller_test.exs
+++ b/priv/templates/phx.gen.auth/session_controller_test.exs
@@ -7,9 +7,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
%{<%= schema.singular %>: <%= schema.singular %>_fixture()}
end<%= if not live? do %>
- describe "GET <%= schema.route_prefix %>/log_in" do
+ describe "GET <%= schema.route_prefix %>/log-in" do
test "renders log in page", %{conn: conn} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/log_in")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/log-in")
response = html_response(conn, 200)
assert response =~ "Log in"
assert response =~ ~p"<%= schema.route_prefix %>/register"
@@ -17,15 +17,15 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "redirects if already logged in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
- conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> get(~p"<%= schema.route_prefix %>/log_in")
+ conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> get(~p"<%= schema.route_prefix %>/log-in")
assert redirected_to(conn) == ~p"/"
end
end<% end %>
- describe "POST <%= schema.route_prefix %>/log_in" do
+ describe "POST <%= schema.route_prefix %>/log-in" do
test "logs the <%= schema.singular %> in", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+ post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
"<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => valid_<%= schema.singular %>_password()}
})
@@ -37,12 +37,12 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
response = html_response(conn, 200)
assert response =~ <%= schema.singular %>.email
assert response =~ ~p"<%= schema.route_prefix %>/settings"
- assert response =~ ~p"<%= schema.route_prefix %>/log_out"
+ assert response =~ ~p"<%= schema.route_prefix %>/log-out"
end
test "logs the <%= schema.singular %> in with remember me", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+ post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
"<%= schema.singular %>" => %{
"email" => <%= schema.singular %>.email,
"password" => valid_<%= schema.singular %>_password(),
@@ -58,7 +58,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
conn =
conn
|> init_test_session(<%= schema.singular %>_return_to: "/foo/bar")
- |> post(~p"<%= schema.route_prefix %>/log_in", %{
+ |> post(~p"<%= schema.route_prefix %>/log-in", %{
"<%= schema.singular %>" => %{
"email" => <%= schema.singular %>.email,
"password" => valid_<%= schema.singular %>_password()
@@ -72,7 +72,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "login following registration", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
conn
- |> post(~p"<%= schema.route_prefix %>/log_in", %{
+ |> post(~p"<%= schema.route_prefix %>/log-in", %{
"_action" => "registered",
"<%= schema.singular %>" => %{
"email" => <%= schema.singular %>.email,
@@ -87,8 +87,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "login following password update", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
conn
- |> post(~p"<%= schema.route_prefix %>/log_in", %{
- "_action" => "password_updated",
+ |> post(~p"<%= schema.route_prefix %>/log-in", %{
+ "_action" => "password-updated",
"<%= schema.singular %>" => %{
"email" => <%= schema.singular %>.email,
"password" => valid_<%= schema.singular %>_password()
@@ -101,17 +101,17 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects to login page with invalid credentials", %{conn: conn} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+ post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
"<%= schema.singular %>" => %{"email" => "invalid@email.com", "password" => "invalid_password"}
})
assert Phoenix.Flash.get(conn.assigns.flash, :error) == "Invalid email or password"
- assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+ assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
end<% else %>
test "emits error message with invalid credentials", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
conn =
- post(conn, ~p"<%= schema.route_prefix %>/log_in", %{
+ post(conn, ~p"<%= schema.route_prefix %>/log-in", %{
"<%= schema.singular %>" => %{"email" => <%= schema.singular %>.email, "password" => "invalid_password"}
})
@@ -121,16 +121,16 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end<% end %>
end
- describe "DELETE <%= schema.route_prefix %>/log_out" do
+ describe "DELETE <%= schema.route_prefix %>/log-out" do
test "logs the <%= schema.singular %> out", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
- conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(~p"<%= schema.route_prefix %>/log_out")
+ conn = conn |> log_in_<%= schema.singular %>(<%= schema.singular %>) |> delete(~p"<%= schema.route_prefix %>/log-out")
assert redirected_to(conn) == ~p"/"
refute get_session(conn, :<%= schema.singular %>_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
end
test "succeeds even if the <%= schema.singular %> is not logged in", %{conn: conn} do
- conn = delete(conn, ~p"<%= schema.route_prefix %>/log_out")
+ conn = delete(conn, ~p"<%= schema.route_prefix %>/log-out")
assert redirected_to(conn) == ~p"/"
refute get_session(conn, :<%= schema.singular %>_token)
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Logged out successfully"
diff --git a/priv/templates/phx.gen.auth/session_new.html.heex b/priv/templates/phx.gen.auth/session_new.html.heex
index ac2bfeb164..1e7abc4fcd 100644
--- a/priv/templates/phx.gen.auth/session_new.html.heex
+++ b/priv/templates/phx.gen.auth/session_new.html.heex
@@ -1,6 +1,6 @@
<.header class="text-center">
- Sign in to account
+ Log in to account
<:subtitle>
Don't have an account?
<.link navigate={~p"<%= schema.route_prefix %>/register"} class="font-semibold text-brand hover:underline">
@@ -10,7 +10,7 @@
- <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log_in"}>
+ <.simple_form :let={f} for={@conn.params["<%= schema.singular %>"]} as={:<%= schema.singular %>} action={~p"<%= schema.route_prefix %>/log-in"}>
<.error :if={@error_message}><%%= @error_message %>
<.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
@@ -24,13 +24,13 @@
<:actions :let={f}>
<.input field={f[:remember_me]} type="checkbox" label="Keep me logged in" />
- <.link href={~p"<%= schema.route_prefix %>/reset_password"} class="text-sm font-semibold">
+ <.link href={~p"<%= schema.route_prefix %>/reset-password"} class="text-sm font-semibold">
Forgot your password?
<:actions>
- <.button phx-disable-with="Signing in..." class="w-full">
- Sign in
→
+ <.button phx-disable-with="Logging in..." class="w-full">
+ Log in
→
diff --git a/priv/templates/phx.gen.auth/settings_controller.ex b/priv/templates/phx.gen.auth/settings_controller.ex
index 971c12df00..c046cf7796 100644
--- a/priv/templates/phx.gen.auth/settings_controller.ex
+++ b/priv/templates/phx.gen.auth/settings_controller.ex
@@ -19,7 +19,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
applied_<%= schema.singular %>,
<%= schema.singular %>.email,
- &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1}")
+ &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
)
conn
diff --git a/priv/templates/phx.gen.auth/settings_controller_test.exs b/priv/templates/phx.gen.auth/settings_controller_test.exs
index 7c83ed5544..6f5ce220d0 100644
--- a/priv/templates/phx.gen.auth/settings_controller_test.exs
+++ b/priv/templates/phx.gen.auth/settings_controller_test.exs
@@ -16,7 +16,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects if <%= schema.singular %> is not logged in" do
conn = build_conn()
conn = get(conn, ~p"<%= schema.route_prefix %>/settings")
- assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+ assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
end
end
@@ -96,7 +96,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
end
- describe "GET <%= schema.route_prefix %>/settings/confirm_email/:token" do
+ describe "GET <%= schema.route_prefix %>/settings/confirm-email/:token" do
setup %{<%= schema.singular %>: <%= schema.singular %>} do
email = unique_<%= schema.singular %>_email()
@@ -109,7 +109,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "updates the <%= schema.singular %> email once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token, email: email} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
assert Phoenix.Flash.get(conn.assigns.flash, :info) =~
@@ -118,7 +118,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
refute <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(<%= schema.singular %>.email)
assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email)
- conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
@@ -127,7 +127,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "does not update email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
- conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/oops")
+ conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/oops")
assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/settings"
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~
@@ -138,8 +138,8 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects if <%= schema.singular %> is not logged in", %{token: token} do
conn = build_conn()
- conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
- assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log_in"
+ conn = get(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
+ assert redirected_to(conn) == ~p"<%= schema.route_prefix %>/log-in"
end
end
end
diff --git a/priv/templates/phx.gen.auth/settings_edit.html.heex b/priv/templates/phx.gen.auth/settings_edit.html.heex
index 028451faf6..e741536a90 100644
--- a/priv/templates/phx.gen.auth/settings_edit.html.heex
+++ b/priv/templates/phx.gen.auth/settings_edit.html.heex
@@ -10,7 +10,7 @@
Oops, something went wrong! Please check the errors below.
- <.input field={f[:action]} type="hidden" name="action" value="update_email" />
+
<.input field={f[:email]} type="email" label="Email" autocomplete="username" required />
<.input
@@ -38,7 +38,7 @@
Oops, something went wrong! Please check the errors below.
- <.input field={f[:action]} type="hidden" name="action" value="update_password" />
+
<.input
field={f[:password]}
diff --git a/priv/templates/phx.gen.auth/settings_live.ex b/priv/templates/phx.gen.auth/settings_live.ex
index 97f2fcd8af..7dc7c999bf 100644
--- a/priv/templates/phx.gen.auth/settings_live.ex
+++ b/priv/templates/phx.gen.auth/settings_live.ex
@@ -1,4 +1,4 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SettingsLive do
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Settings do
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
@@ -44,14 +44,14 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
<.simple_form
for={@password_form}
id="password_form"
- action={~p"<%= schema.route_prefix %>/log_in?_action=password_updated"}
+ action={~p"<%= schema.route_prefix %>/log-in?_action=password-updated"}
method="post"
phx-change="validate_password"
phx-submit="update_password"
phx-trigger-action={@trigger_submit}
>
- <.input
- field={@password_form[:email]}
+
.<%= inspect Module.concat(schema.web
<%= inspect context.alias %>.deliver_<%= schema.singular %>_update_email_instructions(
applied_<%= schema.singular %>,
<%= schema.singular %>.email,
- &url(~p"<%= schema.route_prefix %>/settings/confirm_email/#{&1}")
+ &url(~p"<%= schema.route_prefix %>/settings/confirm-email/#{&1}")
)
info = "A link to confirm your email change has been sent to the new address."
diff --git a/priv/templates/phx.gen.auth/settings_live_test.exs b/priv/templates/phx.gen.auth/settings_live_test.exs
index 45503e3fda..32f4ebd929 100644
--- a/priv/templates/phx.gen.auth/settings_live_test.exs
+++ b/priv/templates/phx.gen.auth/settings_live_test.exs
@@ -1,5 +1,5 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>SettingsLiveTest do
- use <%= inspect context.web_module %>.ConnCase
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.SettingsTest do
+ use <%= inspect context.web_module %>.ConnCase<%= test_case_options %>
alias <%= inspect context.module %>
import Phoenix.LiveViewTest
@@ -20,7 +20,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
assert {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings")
assert {:redirect, %{to: path, flash: flash}} = redirect
- assert path == ~p"<%= schema.route_prefix %>/log_in"
+ assert path == ~p"<%= schema.route_prefix %>/log-in"
assert %{"error" => "You must log in to access this page."} = flash
end
end
@@ -172,7 +172,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "updates the <%= schema.singular %> email once", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>, token: token, email: email} do
- {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+ {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
assert {:live_redirect, %{to: path, flash: flash}} = redirect
assert path == ~p"<%= schema.route_prefix %>/settings"
@@ -182,7 +182,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
assert <%= inspect context.alias %>.get_<%= schema.singular %>_by_email(email)
# use confirm token again
- {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+ {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
assert {:live_redirect, %{to: path, flash: flash}} = redirect
assert path == ~p"<%= schema.route_prefix %>/settings"
assert %{"error" => message} = flash
@@ -190,7 +190,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
end
test "does not update email with invalid token", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
- {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/oops")
+ {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/oops")
assert {:live_redirect, %{to: path, flash: flash}} = redirect
assert path == ~p"<%= schema.route_prefix %>/settings"
assert %{"error" => message} = flash
@@ -200,9 +200,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "redirects if <%= schema.singular %> is not logged in", %{token: token} do
conn = build_conn()
- {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm_email/#{token}")
+ {:error, redirect} = live(conn, ~p"<%= schema.route_prefix %>/settings/confirm-email/#{token}")
assert {:redirect, %{to: path, flash: flash}} = redirect
- assert path == ~p"<%= schema.route_prefix %>/log_in"
+ assert path == ~p"<%= schema.route_prefix %>/log-in"
assert %{"error" => message} = flash
assert message == "You must log in to access this page."
end
diff --git a/priv/templates/phx.gen.context/context.ex b/priv/templates/phx.gen.context/context.ex
index 4215d5c5d9..51f2d28d8f 100644
--- a/priv/templates/phx.gen.context/context.ex
+++ b/priv/templates/phx.gen.context/context.ex
@@ -4,5 +4,5 @@ defmodule <%= inspect context.module %> do
"""
import Ecto.Query, warn: false
- alias <%= inspect schema.repo %>
+ alias <%= inspect schema.repo %><%= schema.repo_alias %>
end
diff --git a/priv/templates/phx.gen.embedded/embedded_schema.ex b/priv/templates/phx.gen.embedded/embedded_schema.ex
index fe815b932e..81320e36ac 100644
--- a/priv/templates/phx.gen.embedded/embedded_schema.ex
+++ b/priv/templates/phx.gen.embedded/embedded_schema.ex
@@ -3,7 +3,7 @@ defmodule <%= inspect schema.module %> do
import Ecto.Changeset
alias <%= inspect schema.module %>
- embedded_schema do <%= if !Map.equal?(schema.types, %{}) do %>
+ embedded_schema do <%= if !Enum.empty?(schema.types) do %>
<%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %><% end %>
<%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
<% end %> end
diff --git a/priv/templates/phx.gen.html/controller.ex b/priv/templates/phx.gen.html/controller.ex
index 3dd7aab2b4..de3e013ed9 100644
--- a/priv/templates/phx.gen.html/controller.ex
+++ b/priv/templates/phx.gen.html/controller.ex
@@ -6,7 +6,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
def index(conn, _params) do
<%= schema.plural %> = <%= inspect context.alias %>.list_<%= schema.plural %>()
- render(conn, :index, <%= schema.plural %>: <%= schema.plural %>)
+ render(conn, :index, <%= schema.collection %>: <%= schema.plural %>)
end
def new(conn, _params) do
diff --git a/priv/templates/phx.gen.html/html.ex b/priv/templates/phx.gen.html/html.ex
index 30c96340cd..1bd05b1e70 100644
--- a/priv/templates/phx.gen.html/html.ex
+++ b/priv/templates/phx.gen.html/html.ex
@@ -5,6 +5,9 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
@doc """
Renders a <%= schema.singular %> form.
+
+ The form is defined in the template at
+ <%= schema.singular %>_html/<%= schema.singular %>_form.html.heex
"""
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
diff --git a/priv/templates/phx.gen.html/index.html.heex b/priv/templates/phx.gen.html/index.html.heex
index b6273d48ad..772a71077e 100644
--- a/priv/templates/phx.gen.html/index.html.heex
+++ b/priv/templates/phx.gen.html/index.html.heex
@@ -1,9 +1,11 @@
<.header>
Listing <%= schema.human_plural %>
<:actions>
- <.link href={~p"<%= schema.route_prefix %>/new"}>
- <.button>New <%= schema.human_singular %>
-
+ <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+ <.link href={~p"<%= schema.route_prefix %>/new"}>
+ New <%= schema.human_singular %>
+
+
diff --git a/priv/templates/phx.gen.html/show.html.heex b/priv/templates/phx.gen.html/show.html.heex
index 3442892780..9b46db5678 100644
--- a/priv/templates/phx.gen.html/show.html.heex
+++ b/priv/templates/phx.gen.html/show.html.heex
@@ -2,9 +2,11 @@
<%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
<:subtitle>This is a <%= schema.singular %> record from your database.
<:actions>
- <.link href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
- <.button>Edit <%= schema.singular %>
-
+ <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+ <.link href={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit"}>
+ Edit <%= schema.singular %>
+
+
diff --git a/priv/templates/phx.gen.live/core_components.ex b/priv/templates/phx.gen.live/core_components.ex
index 2fe64b24b3..c604ef5533 100644
--- a/priv/templates/phx.gen.live/core_components.ex
+++ b/priv/templates/phx.gen.live/core_components.ex
@@ -3,8 +3,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
Provides core UI components.
At first glance, this module may seem daunting, but its goal is to provide
- core building blocks for your application, such as modals, tables, and
- forms. The components consist mostly of markup and are well-documented
+ core building blocks for your application, such as tables, forms, and
+ inputs. The components consist mostly of markup and are well-documented
with doc strings and declarative assigns. You may customize and style
them in any way you want, based on your application growth and needs.
@@ -14,80 +14,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
- use Phoenix.Component
+ use Phoenix.Component<%= if @gettext do %>
+ use Gettext, backend: <%= @web_namespace %>.Gettext<% end %>
- alias Phoenix.LiveView.JS<%= if @gettext do %>
- import <%= @web_namespace %>.Gettext<% end %>
-
- @doc """
- Renders a modal.
-
- ## Examples
-
- <.modal id="confirm-modal">
- This is a modal.
-
-
- JS commands may be passed to the `:on_cancel` to configure
- the closing/cancel event, for example:
-
- <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
- This is another modal.
-
-
- """
- attr :id, :string, required: true
- attr :show, :boolean, default: false
- attr :on_cancel, JS, default: %JS{}
- slot :inner_block, required: true
-
- def modal(assigns) do
- ~H"""
-
-
-
-
-
- <.focus_wrap
- id={"#{@id}-container"}
- phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
- phx-key="escape"
- phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
- class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
- >
-
- {gettext("close")}<% else %>"close"<% end %>
- >
- <.icon name="hero-x-mark-solid" class="h-5 w-5" />
-
-
-
- <%%= render_slot(@inner_block) %>
-
-
-
-
-
-
- """
- end
+ alias Phoenix.LiveView.JS
@doc """
Renders flash notices.
@@ -97,7 +27,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!
"""
- attr :id, :string, default: nil, doc: "the optional id of flash container"
+ attr :id, :string, doc: "the optional id of flash container"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :title, :string, default: nil
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
@@ -106,14 +36,16 @@ defmodule <%= @web_namespace %>.CoreComponents do
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
+ assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
+
~H"""
hide("##{@id}")}
role="alert"
class={[
- "fixed top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
+ "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
]}
@@ -125,7 +57,7 @@ defmodule <%= @web_namespace %>.CoreComponents do
<%%= @title %>
<%%= msg %>
-
{gettext("close")}<% else %>"close"<% end %>>
+ >
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
@@ -144,30 +76,31 @@ defmodule <%= @web_namespace %>.CoreComponents do
def flash_group(assigns) do
~H"""
-
- <.flash kind={:info} title="Success!" flash={@flash} />
- <.flash kind={:error} title="Error!" flash={@flash} />
+
+ <.flash kind={:info} title=<%= maybe_heex_attr_gettext.("Success!", @gettext) %> flash={@flash} />
+ <.flash kind={:error} title=<%= maybe_heex_attr_gettext.("Error!", @gettext) %> flash={@flash} />
<.flash
id="client-error"
kind={:error}
- title="We can't find the internet"
+ title=<%= maybe_heex_attr_gettext.("We can't find the internet", @gettext) %>
phx-disconnected={show(".phx-client-error #client-error")}
phx-connected={hide("#client-error")}
hidden
>
- Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+ <%= maybe_eex_gettext.("Attempting to reconnect", @gettext) %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
<.flash
id="server-error"
kind={:error}
- title="Something went wrong!"
+ title=<%= maybe_heex_attr_gettext.("Something went wrong!", @gettext) %>
phx-disconnected={show(".phx-server-error #server-error")}
phx-connected={hide("#server-error")}
hidden
>
- Hang in there while we get back on track
- <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
+ <%= maybe_eex_gettext.("Hang in there while we get back on track", @gettext) %>
+ <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 motion-safe:animate-spin" />
"""
@@ -186,8 +119,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
"""
- attr :for, :any, required: true, doc: "the datastructure for the form"
- attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
+ attr :for, :any, required: true, doc: "the data structure for the form"
+ attr :as, :any, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
@@ -197,8 +130,10 @@ defmodule <%= @web_namespace %>.CoreComponents do
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
+ assigns = assign(assigns, :as, if(assigns[:as], do: %{as: assigns[:as]}, else: %{}))
+
~H"""
- <.form :let={f} for={@for} as={@as} {@rest}>
+ <.form :let={f} for={@for} {@as} {@rest}>
<%%= render_slot(@inner_block, f) %>
@@ -257,7 +192,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
* For live file uploads, see `Phoenix.Component.live_file_input/1`
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
- for more information.
+ for more information. Unsupported types, such as hidden and radio,
+ are best written directly in your templates.
## Examples
@@ -271,8 +207,8 @@ defmodule <%= @web_namespace %>.CoreComponents do
attr :type, :string,
default: "text",
- values: ~w(checkbox color date datetime-local email file hidden month number password
- range radio search select tel text textarea time url week)
+ values: ~w(checkbox color date datetime-local email file month number password
+ range search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
@@ -287,25 +223,27 @@ defmodule <%= @web_namespace %>.CoreComponents do
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)
- slot :inner_block
-
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
+ errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
+
assigns
|> assign(field: nil, id: assigns.id || field.id)
- |> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
+ |> assign(:errors, Enum.map(errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
- def input(%{type: "checkbox", value: value} = assigns) do
+ def input(%{type: "checkbox"} = assigns) do
assigns =
- assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
+ assign_new(assigns, :checked, fn ->
+ Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
+ end)
~H"""
-
+
-
+
.CoreComponents do
def input(%{type: "select"} = assigns) do
~H"""
-
+
<.label for={@id}><%%= @label %>
@@ -343,14 +281,13 @@ defmodule <%= @web_namespace %>.CoreComponents do
def input(%{type: "textarea"} = assigns) do
~H"""
-
+
<.label for={@id}><%%= @label %>
+
-
.CoreComponents do
You can customize the size and colors of the icons by setting
width, height, and background color classes.
- Icons are extracted from your `assets/vendor/heroicons` directory and bundled
- within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
+ Icons are extracted from the `deps/heroicons` directory and bundled within
+ your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-x-mark-solid" />
- <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
+ <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 motion-safe:animate-spin" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
@@ -598,8 +534,9 @@ defmodule <%= @web_namespace %>.CoreComponents do
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
+ time: 300,
transition:
- {"transition-all transform ease-out duration-300",
+ {"transition-all ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
@@ -610,36 +547,11 @@ defmodule <%= @web_namespace %>.CoreComponents do
to: selector,
time: 200,
transition:
- {"transition-all transform ease-in duration-200",
- "opacity-100 translate-y-0 sm:scale-100",
+ {"transition-all ease-in duration-200", "opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
- def show_modal(js \\ %JS{}, id) when is_binary(id) do
- js
- |> JS.show(to: "##{id}")
- |> JS.show(
- to: "##{id}-bg",
- transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
- )
- |> show("##{id}-container")
- |> JS.add_class("overflow-hidden", to: "body")
- |> JS.focus_first(to: "##{id}-content")
- end
-
- def hide_modal(js \\ %JS{}, id) do
- js
- |> JS.hide(
- to: "##{id}-bg",
- transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
- )
- |> hide("##{id}-container")
- |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
- |> JS.remove_class("overflow-hidden", to: "body")
- |> JS.pop_focus()
- end
-
@doc """
Translates an error message using gettext.
"""<%= if @gettext do %>
diff --git a/priv/templates/phx.gen.live/form.ex b/priv/templates/phx.gen.live/form.ex
new file mode 100644
index 0000000000..25627a4e3c
--- /dev/null
+++ b/priv/templates/phx.gen.live/form.ex
@@ -0,0 +1,93 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form do
+ use <%= inspect context.web_module %>, :live_view
+
+ alias <%= inspect context.module %>
+ alias <%= inspect schema.module %>
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <.header>
+ <%%= @page_title %>
+ <:subtitle>Use this form to manage <%= schema.singular %> records in your database.
+
+
+ <.simple_form for={@form} id="<%= schema.singular %>-form" phx-change="validate" phx-submit="save">
+<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 6) %>
+ <:actions>
+ <.button phx-disable-with="Saving...">Save <%= schema.human_singular %>
+
+
+
+ <.back navigate={return_path(@return_to, @<%= schema.singular %>)}>Back
+ """
+ end
+
+ @impl true
+ def mount(params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:return_to, return_to(params["return_to"]))
+ |> apply_action(socket.assigns.live_action, params)}
+ end
+
+ defp return_to("show"), do: "show"
+ defp return_to(_), do: "index"
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+
+ socket
+ |> assign(:page_title, "Edit <%= schema.human_singular %>")
+ |> assign(:<%= schema.singular %>, <%= schema.singular %>)
+ |> assign(:form, to_form(<%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ <%= schema.singular %> = %<%= inspect schema.alias %>{}
+
+ socket
+ |> assign(:page_title, "New <%= schema.human_singular %>")
+ |> assign(:<%= schema.singular %>, <%= schema.singular %>)
+ |> assign(:form, to_form(<%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)))
+ end
+
+ @impl true
+ def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+ changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+ save_<%= schema.singular %>(socket, socket.assigns.live_action, <%= schema.singular %>_params)
+ end
+
+ defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do
+ case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
+ {:ok, <%= schema.singular %>} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "<%= schema.human_singular %> updated successfully")
+ |> push_navigate(to: return_path(socket.assigns.return_to, <%= schema.singular %>))}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do
+ case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
+ {:ok, <%= schema.singular %>} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "<%= schema.human_singular %> created successfully")
+ |> push_navigate(to: return_path(socket.assigns.return_to, <%= schema.singular %>))}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp return_path("index", _<%= schema.singular %>), do: ~p"<%= schema.route_prefix %>"
+ defp return_path("show", <%= schema.singular %>), do: ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"
+end
diff --git a/priv/templates/phx.gen.live/form_component.ex b/priv/templates/phx.gen.live/form_component.ex
deleted file mode 100644
index 52cf6e85b4..0000000000
--- a/priv/templates/phx.gen.live/form_component.ex
+++ /dev/null
@@ -1,90 +0,0 @@
-defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent do
- use <%= inspect context.web_module %>, :live_component
-
- alias <%= inspect context.module %>
-
- @impl true
- def render(assigns) do
- ~H"""
-
- <.header>
- <%%= @title %>
- <:subtitle>Use this form to manage <%= schema.singular %> records in your database.
-
-
- <.simple_form
- for={@form}
- id="<%= schema.singular %>-form"
- phx-target={@myself}
- phx-change="validate"
- phx-submit="save"
- >
-<%= Mix.Tasks.Phx.Gen.Html.indent_inputs(inputs, 8) %>
- <:actions>
- <.button phx-disable-with="Saving...">Save <%= schema.human_singular %>
-
-
-
- """
- end
-
- @impl true
- def update(%{<%= schema.singular %>: <%= schema.singular %>} = assigns, socket) do
- changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)
-
- {:ok,
- socket
- |> assign(assigns)
- |> assign_form(changeset)}
- end
-
- @impl true
- def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
- changeset =
- socket.assigns.<%= schema.singular %>
- |> <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>_params)
- |> Map.put(:action, :validate)
-
- {:noreply, assign_form(socket, changeset)}
- end
-
- def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
- save_<%= schema.singular %>(socket, socket.assigns.action, <%= schema.singular %>_params)
- end
-
- defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do
- case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
- {:ok, <%= schema.singular %>} ->
- notify_parent({:saved, <%= schema.singular %>})
-
- {:noreply,
- socket
- |> put_flash(:info, "<%= schema.human_singular %> updated successfully")
- |> push_patch(to: socket.assigns.patch)}
-
- {:error, %Ecto.Changeset{} = changeset} ->
- {:noreply, assign_form(socket, changeset)}
- end
- end
-
- defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do
- case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
- {:ok, <%= schema.singular %>} ->
- notify_parent({:saved, <%= schema.singular %>})
-
- {:noreply,
- socket
- |> put_flash(:info, "<%= schema.human_singular %> created successfully")
- |> push_patch(to: socket.assigns.patch)}
-
- {:error, %Ecto.Changeset{} = changeset} ->
- {:noreply, assign_form(socket, changeset)}
- end
- end
-
- defp assign_form(socket, %Ecto.Changeset{} = changeset) do
- assign(socket, :form, to_form(changeset))
- end
-
- defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
-end
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
index b4658eecab..ff23665abd 100644
--- a/priv/templates/phx.gen.live/index.ex
+++ b/priv/templates/phx.gen.live/index.ex
@@ -2,39 +2,51 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
use <%= inspect context.web_module %>, :live_view
alias <%= inspect context.module %>
- alias <%= inspect schema.module %>
@impl true
- def mount(_params, _session, socket) do
- {:ok, stream(socket, :<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
+ def render(assigns) do
+ ~H"""
+ <.header>
+ Listing <%= schema.human_plural %>
+ <:actions>
+ <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+ <.link navigate={~p"<%= schema.route_prefix %>/new"}>
+ New <%= schema.human_singular %>
+
+
+
+
+
+ <.table
+ id="<%= schema.plural %>"
+ rows={@streams.<%= schema.collection %>}
+ row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
+ ><%= for {k, _} <- schema.attrs do %>
+ <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= schema.singular %>.<%= k %> %><% end %>
+ <:action :let={{_id, <%= schema.singular %>}}>
+
+ <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show
+
+ <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit
+
+ <:action :let={{id, <%= schema.singular %>}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+ """
end
@impl true
- def handle_params(params, _url, socket) do
- {:noreply, apply_action(socket, socket.assigns.live_action, params)}
- end
-
- defp apply_action(socket, :edit, %{"id" => id}) do
- socket
- |> assign(:page_title, "Edit <%= schema.human_singular %>")
- |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))
- end
-
- defp apply_action(socket, :new, _params) do
- socket
- |> assign(:page_title, "New <%= schema.human_singular %>")
- |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{})
- end
-
- defp apply_action(socket, :index, _params) do
- socket
- |> assign(:page_title, "Listing <%= schema.human_plural %>")
- |> assign(:<%= schema.singular %>, nil)
- end
-
- @impl true
- def handle_info({<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent, {:saved, <%= schema.singular %>}}, socket) do
- {:noreply, stream_insert(socket, :<%= schema.collection %>, <%= schema.singular %>)}
+ def mount(_params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:page_title, "Listing <%= schema.human_plural %>")
+ |> stream(:<%= schema.collection %>, <%= inspect context.alias %>.list_<%= schema.plural %>())}
end
@impl true
diff --git a/priv/templates/phx.gen.live/index.html.heex b/priv/templates/phx.gen.live/index.html.heex
deleted file mode 100644
index 872db4d367..0000000000
--- a/priv/templates/phx.gen.live/index.html.heex
+++ /dev/null
@@ -1,41 +0,0 @@
-<.header>
- Listing <%= schema.human_plural %>
- <:actions>
- <.link patch={~p"<%= schema.route_prefix %>/new"}>
- <.button>New <%= schema.human_singular %>
-
-
-
-
-<.table
- id="<%= schema.plural %>"
- rows={@streams.<%= schema.collection %>}
- row_click={fn {_id, <%= schema.singular %>} -> JS.navigate(~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}") end}
-><%= for {k, _} <- schema.attrs do %>
- <:col :let={{_id, <%= schema.singular %>}} label="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= <%= schema.singular %>.<%= k %> %><% end %>
- <:action :let={{_id, <%= schema.singular %>}}>
-
- <.link navigate={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}"}>Show
-
- <.link patch={~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit"}>Edit
-
- <:action :let={{id, <%= schema.singular %>}}>
- <.link
- phx-click={JS.push("delete", value: %{id: <%= schema.singular %>.id}) |> hide("##{id}")}
- data-confirm="Are you sure?"
- >
- Delete
-
-
-
-
-<.modal :if={@live_action in [:new, :edit]} id="<%= schema.singular %>-modal" show on_cancel={JS.patch(~p"<%= schema.route_prefix %>")}>
- <.live_component
- module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
- id={@<%= schema.singular %>.id || :new}
- title={@page_title}
- action={@live_action}
- <%= schema.singular %>={@<%= schema.singular %>}
- patch={~p"<%= schema.route_prefix %>"}
- />
-
diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs
index 7da5490c7e..7fce16ea06 100644
--- a/priv/templates/phx.gen.live/live_test.exs
+++ b/priv/templates/phx.gen.live/live_test.exs
@@ -26,20 +26,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "saves new <%= schema.singular %>", %{conn: conn} do
{:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
- assert index_live |> element("a", "New <%= schema.human_singular %>") |> render_click() =~
- "New <%= schema.human_singular %>"
+ assert {:ok, form_live, _} =
+ index_live
+ |> element("a", "New <%= schema.human_singular %>")
+ |> render_click()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/new")
- assert_patch(index_live, ~p"<%= schema.route_prefix %>/new")
+ assert render(form_live) =~ "New <%= schema.human_singular %>"
- assert index_live
+ assert form_live
|> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
|> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
- assert index_live
- |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs)
- |> render_submit()
-
- assert_patch(index_live, ~p"<%= schema.route_prefix %>")
+ assert {:ok, index_live, _html} =
+ form_live
+ |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @create_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>")
html = render(index_live)
assert html =~ "<%= schema.human_singular %> created successfully"<%= if schema.string_attr do %>
@@ -49,20 +52,23 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
test "updates <%= schema.singular %> in listing", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
{:ok, index_live, _html} = live(conn, ~p"<%= schema.route_prefix %>")
- assert index_live |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Edit") |> render_click() =~
- "Edit <%= schema.human_singular %>"
+ assert {:ok, form_live, _html} =
+ index_live
+ |> element("#<%= schema.plural %>-#{<%= schema.singular %>.id} a", "Edit")
+ |> render_click()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
- assert_patch(index_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit")
+ assert render(form_live) =~ "Edit <%= schema.human_singular %>"
- assert index_live
+ assert form_live
|> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
|> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
- assert index_live
- |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
- |> render_submit()
-
- assert_patch(index_live, ~p"<%= schema.route_prefix %>")
+ assert {:ok, index_live, _html} =
+ form_live
+ |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>")
html = render(index_live)
assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %>
@@ -87,23 +93,26 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
assert html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
end
- test "updates <%= schema.singular %> within modal", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+ test "updates <%= schema.singular %> and returns to show", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
{:ok, show_live, _html} = live(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
- assert show_live |> element("a", "Edit") |> render_click() =~
- "Edit <%= schema.human_singular %>"
+ assert {:ok, form_live, _} =
+ show_live
+ |> element("a", "Edit")
+ |> render_click()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/edit?return_to=show")
- assert_patch(show_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}/show/edit")
+ assert render(form_live) =~ "Edit <%= schema.human_singular %>"
- assert show_live
+ assert form_live
|> form("#<%= schema.singular %>-form", <%= schema.singular %>: @invalid_attrs)
|> render_change() =~ "<%= Mix.Phoenix.Schema.failed_render_change_message(schema) %>"
- assert show_live
- |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
- |> render_submit()
-
- assert_patch(show_live, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
+ assert {:ok, show_live, _html} =
+ form_live
+ |> form("#<%= schema.singular %>-form", <%= schema.singular %>: @update_attrs)
+ |> render_submit()
+ |> follow_redirect(conn, ~p"<%= schema.route_prefix %>/#{<%= schema.singular %>}")
html = render(show_live)
assert html =~ "<%= schema.human_singular %> updated successfully"<%= if schema.string_attr do %>
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
index b75badea60..140016c74d 100644
--- a/priv/templates/phx.gen.live/show.ex
+++ b/priv/templates/phx.gen.live/show.ex
@@ -3,6 +3,29 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
alias <%= inspect context.module %>
+ @impl true
+ def render(assigns) do
+ ~H"""
+ <.header>
+ <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
+ <:subtitle>This is a <%= schema.singular %> record from your database.
+ <:actions>
+ <.button phx-click={JS.dispatch("click", to: {:inner, "a"})}>
+ <.link navigate={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/edit?return_to=show"}>
+ Edit <%= schema.singular %>
+
+
+
+
+
+ <.list><%= for {k, _} <- schema.attrs do %>
+ <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= @<%= schema.singular %>.<%= k %> %><% end %>
+
+
+ <.back navigate={~p"<%= schema.route_prefix %>"}>Back to <%= schema.plural %>
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
@@ -12,10 +35,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web
def handle_params(%{"id" => id}, _, socket) do
{:noreply,
socket
- |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:page_title, "Show <%= schema.human_singular %>")
|> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))}
end
-
- defp page_title(:show), do: "Show <%= schema.human_singular %>"
- defp page_title(:edit), do: "Edit <%= schema.human_singular %>"
end
diff --git a/priv/templates/phx.gen.live/show.html.heex b/priv/templates/phx.gen.live/show.html.heex
deleted file mode 100644
index f3628a2092..0000000000
--- a/priv/templates/phx.gen.live/show.html.heex
+++ /dev/null
@@ -1,26 +0,0 @@
-<.header>
- <%= schema.human_singular %> <%%= @<%= schema.singular %>.id %>
- <:subtitle>This is a <%= schema.singular %> record from your database.
- <:actions>
- <.link patch={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}/show/edit"} phx-click={JS.push_focus()}>
- <.button>Edit <%= schema.singular %>
-
-
-
-
-<.list><%= for {k, _} <- schema.attrs do %>
- <:item title="<%= Phoenix.Naming.humanize(Atom.to_string(k)) %>"><%%= @<%= schema.singular %>.<%= k %> %><% end %>
-
-
-<.back navigate={~p"<%= schema.route_prefix %>"}>Back to <%= schema.plural %>
-
-<.modal :if={@live_action == :edit} id="<%= schema.singular %>-modal" show on_cancel={JS.patch(~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}")}>
- <.live_component
- module={<%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.FormComponent}
- id={@<%= schema.singular %>.id}
- title={@page_title}
- action={@live_action}
- <%= schema.singular %>={@<%= schema.singular %>}
- patch={~p"<%= schema.route_prefix %>/#{@<%= schema.singular %>}"}
- />
-
diff --git a/priv/templates/phx.gen.release/Dockerfile.eex b/priv/templates/phx.gen.release/Dockerfile.eex
index 8f7c776148..320d453ea8 100644
--- a/priv/templates/phx.gen.release/Dockerfile.eex
+++ b/priv/templates/phx.gen.release/Dockerfile.eex
@@ -18,7 +18,7 @@ ARG DEBIAN_VERSION=<%= debian %>-<%= debian_vsn %>-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
-FROM ${BUILDER_IMAGE} as builder
+FROM ${BUILDER_IMAGE} AS builder
# install build dependencies
RUN apt-get update -y && apt-get install -y build-essential git \
@@ -44,7 +44,9 @@ RUN mkdir config
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile
-
+<%= if assets_dir_exists? do %>
+RUN mix assets.setup
+<% end %>
COPY priv priv
COPY lib lib
@@ -74,9 +76,9 @@ RUN apt-get update -y && \
# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
-ENV LC_ALL en_US.UTF-8
+ENV LANG=en_US.UTF-8
+ENV LANGUAGE=en_US:en
+ENV LC_ALL=en_US.UTF-8
WORKDIR "/app"
RUN chown nobody /app
@@ -89,4 +91,9 @@ COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/<%= otp_app %
USER nobody
+# If using an environment that doesn't automatically reap zombie processes, it is
+# advised to add an init process such as tini via `apt-get install`
+# above and adding an entrypoint. See https://github.com/krallin/tini for details
+# ENTRYPOINT ["/tini", "--"]
+
CMD ["/app/bin/server"]
diff --git a/priv/templates/phx.gen.release/rel/migrate.sh.eex b/priv/templates/phx.gen.release/rel/migrate.sh.eex
index 5e3d114c56..3103c98fcf 100644
--- a/priv/templates/phx.gen.release/rel/migrate.sh.eex
+++ b/priv/templates/phx.gen.release/rel/migrate.sh.eex
@@ -1,3 +1,5 @@
#!/bin/sh
+set -eu
+
cd -P -- "$(dirname -- "$0")"
exec ./<%= otp_app %> eval <%= app_namespace %>.Release.migrate
diff --git a/priv/templates/phx.gen.release/rel/server.sh.eex b/priv/templates/phx.gen.release/rel/server.sh.eex
index 2b35aeef8a..11638f1e9f 100644
--- a/priv/templates/phx.gen.release/rel/server.sh.eex
+++ b/priv/templates/phx.gen.release/rel/server.sh.eex
@@ -1,3 +1,5 @@
#!/bin/sh
+set -eu
+
cd -P -- "$(dirname -- "$0")"
PHX_SERVER=true exec ./<%= otp_app %> start
diff --git a/priv/templates/phx.gen.release/release.ex b/priv/templates/phx.gen.release/release.ex
index cbd7858e72..521d177683 100644
--- a/priv/templates/phx.gen.release/release.ex
+++ b/priv/templates/phx.gen.release/release.ex
@@ -23,6 +23,6 @@ defmodule <%= app_namespace %>.Release do
end
defp load_app do
- Application.load(@app)
+ Application.ensure_loaded(@app)
end
end
diff --git a/priv/templates/phx.gen.schema/migration.exs b/priv/templates/phx.gen.schema/migration.exs
index 34a91963ff..ed6c032fe6 100644
--- a/priv/templates/phx.gen.schema/migration.exs
+++ b/priv/templates/phx.gen.schema/migration.exs
@@ -7,7 +7,7 @@ defmodule <%= inspect schema.repo %>.Migrations.Create<%= Macro.camelize(schema.
<% end %><%= for {k, v} <- schema.attrs do %> add <%= inspect k %>, <%= inspect Mix.Phoenix.Schema.type_for_migration(v) %><%= schema.migration_defaults[k] %>
<% end %><%= for {_, i, _, s} <- schema.assocs do %> add <%= inspect(i) %>, references(<%= inspect(s) %>, on_delete: :nothing<%= if schema.binary_id do %>, type: :binary_id<% end %>)
<% end %>
- timestamps()
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
end
<%= if Enum.any?(schema.indexes) do %><%= for index <- schema.indexes do %>
<%= index %><% end %>
diff --git a/priv/templates/phx.gen.schema/schema.ex b/priv/templates/phx.gen.schema/schema.ex
index fc1d4788ed..94c40751d6 100644
--- a/priv/templates/phx.gen.schema/schema.ex
+++ b/priv/templates/phx.gen.schema/schema.ex
@@ -9,14 +9,14 @@ defmodule <%= inspect schema.module %> do
<%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
<%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
<% end %>
- timestamps()
+ timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
end
@doc false
def changeset(<%= schema.singular %>, attrs) do
<%= schema.singular %>
|> cast(attrs, [<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
- |> validate_required([<%= Enum.map_join(schema.attrs, ", ", &inspect(elem(&1, 0))) %>])
+ |> validate_required([<%= Enum.map_join(Mix.Phoenix.Schema.required_fields(schema), ", ", &inspect(elem(&1, 0))) %>])
<%= for k <- schema.uniques do %> |> unique_constraint(<%= inspect k %>)
<% end %> end
end
diff --git a/priv/templates/phx.gen.socket/socket.ex b/priv/templates/phx.gen.socket/socket.ex
index e44c2d0ea5..772da12e45 100644
--- a/priv/templates/phx.gen.socket/socket.ex
+++ b/priv/templates/phx.gen.socket/socket.ex
@@ -42,7 +42,7 @@ defmodule <%= module %>Socket do
{:ok, socket}
end
- # Socket id's are topics that allow you to identify all sockets for a given user:
+ # Socket IDs are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
#
diff --git a/priv/templates/phx.gen.socket/socket.js b/priv/templates/phx.gen.socket/socket.js
index dd178f483a..80e8453e25 100644
--- a/priv/templates/phx.gen.socket/socket.js
+++ b/priv/templates/phx.gen.socket/socket.js
@@ -32,7 +32,7 @@ let socket = new Socket("/socket", {params: {token: window.userToken}})
// end
//
// Now you need to pass this token to JavaScript. You can do so
-// inside a script tag in "<%= web_prefix %>/templates/layout/app.html.heex":
+// inside a script tag in "<%= web_prefix %>/templates/layout/root.html.heex":
//
//
//
diff --git a/test/mix/tasks/phx.digest.clean_test.exs b/test/mix/tasks/phx.digest.clean_test.exs
index 094fb4d32c..09a3e785b2 100644
--- a/test/mix/tasks/phx.digest.clean_test.exs
+++ b/test/mix/tasks/phx.digest.clean_test.exs
@@ -2,7 +2,7 @@ defmodule Mix.Tasks.Phx.Digest.CleanTest do
use ExUnit.Case
test "fails when the given paths are invalid" do
- Mix.Tasks.Phx.Digest.Clean.run(["--output", "invalid_path"])
+ Mix.Tasks.Phx.Digest.Clean.run(["--output", "invalid_path", "--no-compile"])
assert_received {:mix_shell, :error, ["The output path \"invalid_path\" does not exist"]}
end
@@ -10,11 +10,16 @@ defmodule Mix.Tasks.Phx.Digest.CleanTest do
test "removes old versions", config do
output_path = Path.join("tmp", to_string(config.test))
input_path = "priv/static"
- :ok = File.mkdir_p!(output_path)
- Mix.Tasks.Phx.Digest.Clean.run([input_path, "-o", output_path])
+ try do
+ :ok = File.mkdir_p!(output_path)
- msg = "Clean complete for \"#{output_path}\""
- assert_received {:mix_shell, :info, [^msg]}
+ Mix.Tasks.Phx.Digest.Clean.run([input_path, "-o", output_path, "--no-compile"])
+
+ msg = "Clean complete for \"#{output_path}\""
+ assert_received {:mix_shell, :info, [^msg]}
+ after
+ File.rm_rf!(output_path)
+ end
end
end
diff --git a/test/mix/tasks/phx.digest_test.exs b/test/mix/tasks/phx.digest_test.exs
index a1dbd54742..6593cbff30 100644
--- a/test/mix/tasks/phx.digest_test.exs
+++ b/test/mix/tasks/phx.digest_test.exs
@@ -5,7 +5,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
import MixHelper
test "logs when the path is invalid" do
- Mix.Tasks.Phx.Digest.run(["invalid_path", "--no-deps-check"])
+ Mix.Tasks.Phx.Digest.run(["invalid_path", "--no-compile"])
assert_received {:mix_shell, :error, ["The input path \"invalid_path\" does not exist"]}
end
@@ -13,7 +13,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
test "digests and compress files" do
in_tmp @output_path, fn ->
File.mkdir_p!("priv/static")
- Mix.Tasks.Phx.Digest.run(["priv/static", "-o", @output_path, "--no-deps-check", "--no-compile"])
+ Mix.Tasks.Phx.Digest.run(["priv/static", "-o", @output_path, "--no-compile"])
assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest\""]}
end
end
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
test "digests and compress files without the input path" do
in_tmp @output_path, fn ->
File.mkdir_p!("priv/static")
- Mix.Tasks.Phx.Digest.run(["-o", @output_path, "--no-deps-check", "--no-compile"])
+ Mix.Tasks.Phx.Digest.run(["-o", @output_path, "--no-compile"])
assert_received {:mix_shell, :info, ["Check your digested files at \"mix_phoenix_digest_no_input\""]}
end
end
@@ -31,7 +31,7 @@ defmodule Mix.Tasks.Phx.DigestTest do
test "uses the input path as output path when no output path is given" do
in_tmp @input_path, fn ->
File.mkdir_p!(@input_path)
- Mix.Tasks.Phx.Digest.run([@input_path, "--no-deps-check", "--no-compile"])
+ Mix.Tasks.Phx.Digest.run([@input_path, "--no-compile"])
assert_received {:mix_shell, :info, ["Check your digested files at \"input_path\""]}
end
end
diff --git a/test/mix/tasks/phx.gen.auth/injector_test.exs b/test/mix/tasks/phx.gen.auth/injector_test.exs
index 761493c592..6f43d5d988 100644
--- a/test/mix/tasks/phx.gen.auth/injector_test.exs
+++ b/test/mix/tasks/phx.gen.auth/injector_test.exs
@@ -680,7 +680,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
<.link
- href={~p"/users/log_out"}
+ href={~p"/users/log-out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
@@ -698,7 +698,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
<.link
- href={~p"/users/log_in"}
+ href={~p"/users/log-in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
@@ -774,7 +774,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
\r
\r
<.link\r
- href={~p"/users/log_out"}\r
+ href={~p"/users/log-out"}\r
method="delete"\r
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
>\r
@@ -792,7 +792,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
\r
\r
<.link\r
- href={~p"/users/log_in"}\r
+ href={~p"/users/log-in"}\r
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
>\r
Log in\r
@@ -852,7 +852,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
<.link
- href={~p"/users/log_out"}
+ href={~p"/users/log-out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
@@ -870,7 +870,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
<.link
- href={~p"/users/log_in"}
+ href={~p"/users/log-in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
@@ -932,7 +932,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
\r
\r
<.link\r
- href={~p"/users/log_out"}\r
+ href={~p"/users/log-out"}\r
method="delete"\r
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
>\r
@@ -950,7 +950,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
\r
\r
<.link\r
- href={~p"/users/log_in"}\r
+ href={~p"/users/log-in"}\r
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"\r
>\r
Log in\r
@@ -983,10 +983,10 @@ defmodule Mix.Tasks.Phx.Gen.Auth.InjectorTest do
<%= if @current_user do %>
<%= @current_user.email %>
<.link href={~p"/users/settings"}>Settings
- <.link href={~p"/users/log_out"} method="delete">Log out
+ <.link href={~p"/users/log-out"} method="delete">Log out
<% else %>
<.link href={~p"/users/register"}>Register
- <.link href={~p"/users/log_in"}>Log in
+ <.link href={~p"/users/log-in"}>Log in
<% end %>
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index 7e463a2b8a..773ec9a27c 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -89,9 +89,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -162,12 +161,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
- get "/users/log_in", UserSessionController, :new
- post "/users/log_in", UserSessionController, :create
- get "/users/reset_password", UserResetPasswordController, :new
- post "/users/reset_password", UserResetPasswordController, :create
- get "/users/reset_password/:token", UserResetPasswordController, :edit
- put "/users/reset_password/:token", UserResetPasswordController, :update
+ get "/users/log-in", UserSessionController, :new
+ post "/users/log-in", UserSessionController, :create
+ get "/users/reset-password", UserResetPasswordController, :new
+ post "/users/reset-password", UserResetPasswordController, :create
+ get "/users/reset-password/:token", UserResetPasswordController, :edit
+ put "/users/reset-password/:token", UserResetPasswordController, :update
end
scope "/", MyAppWeb do
@@ -175,13 +174,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
- get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+ get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
end
scope "/", MyAppWeb do
pipe_through [:browser]
- delete "/users/log_out", UserSessionController, :delete
+ delete "/users/log-out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :edit
@@ -195,13 +194,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<.link.*href={~p"/users/settings"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+ ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
assert file =~
~r|<.link.*href={~p"/users/register"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_in"}.*>|s
+ ~r|<.link.*href={~p"/users/log-in"}.*>|s
end)
assert_file("test/support/conn_case.ex", fn file ->
@@ -226,9 +225,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, true}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -251,20 +249,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
assert file =~ ~s|deliver(user.email, "Update email instructions",|
end)
- assert_file("lib/my_app_web/live/user_registration_live.ex")
- assert_file("test/my_app_web/live/user_registration_live_test.exs")
- assert_file("lib/my_app_web/live/user_login_live.ex")
- assert_file("test/my_app_web/live/user_login_live_test.exs")
- assert_file("lib/my_app_web/live/user_reset_password_live.ex")
- assert_file("test/my_app_web/live/user_reset_password_live_test.exs")
- assert_file("lib/my_app_web/live/user_forgot_password_live.ex")
- assert_file("test/my_app_web/live/user_forgot_password_live_test.exs")
- assert_file("lib/my_app_web/live/user_settings_live.ex")
- assert_file("test/my_app_web/live/user_settings_live_test.exs")
- assert_file("lib/my_app_web/live/user_confirmation_live.ex")
- assert_file("test/my_app_web/live/user_confirmation_live_test.exs")
- assert_file("lib/my_app_web/live/user_confirmation_instructions_live.ex")
- assert_file("test/my_app_web/live/user_confirmation_instructions_live_test.exs")
+ assert_file("lib/my_app_web/live/user_live/registration.ex")
+ assert_file("test/my_app_web/live/user_live/registration_test.exs")
+ assert_file("lib/my_app_web/live/user_live/login.ex")
+ assert_file("test/my_app_web/live/user_live/login_test.exs")
+ assert_file("lib/my_app_web/live/user_live/reset_password.ex")
+ assert_file("test/my_app_web/live/user_live/reset_password_test.exs")
+ assert_file("lib/my_app_web/live/user_live/forgot_password.ex")
+ assert_file("test/my_app_web/live/user_live/forgot_password_test.exs")
+ assert_file("lib/my_app_web/live/user_live/settings.ex")
+ assert_file("test/my_app_web/live/user_live/settings_test.exs")
+ assert_file("lib/my_app_web/live/user_live/confirmation.ex")
+ assert_file("test/my_app_web/live/user_live/confirmation_test.exs")
+ assert_file("lib/my_app_web/live/user_live/confirmation_instructions.ex")
+ assert_file("test/my_app_web/live/user_live/confirmation_instructions_test.exs")
assert_file("lib/my_app_web/user_auth.ex")
assert_file("test/my_app_web/user_auth_test.exs")
@@ -292,13 +290,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
live_session :redirect_if_user_is_authenticated,
on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
- live "/users/register", UserRegistrationLive, :new
- live "/users/log_in", UserLoginLive, :new
- live "/users/reset_password", UserForgotPasswordLive, :new
- live "/users/reset_password/:token", UserResetPasswordLive, :edit
+ live "/users/register", UserLive.Registration, :new
+ live "/users/log-in", UserLive.Login, :new
+ live "/users/reset-password", UserLive.ForgotPassword, :new
+ live "/users/reset-password/:token", UserLive.ResetPassword, :edit
end
- post "/users/log_in", UserSessionController, :create
+ post "/users/log-in", UserSessionController, :create
end
scope "/", MyAppWeb do
@@ -306,20 +304,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
live_session :require_authenticated_user,
on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
- live "/users/settings", UserSettingsLive, :edit
- live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
+ live "/users/settings", UserLive.Settings, :edit
+ live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
end
end
scope "/", MyAppWeb do
pipe_through [:browser]
- delete "/users/log_out", UserSessionController, :delete
+ delete "/users/log-out", UserSessionController, :delete
live_session :current_user,
on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
- live "/users/confirm/:token", UserConfirmationLive, :edit
- live "/users/confirm", UserConfirmationInstructionsLive, :new
+ live "/users/confirm/:token", UserLive.Confirmation, :edit
+ live "/users/confirm", UserLive.ConfirmationInstructions, :new
end
end
"""
@@ -330,13 +328,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<\.link.*href={~p"/users/settings"}.*>|s
assert file =~
- ~r|<\.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+ ~r|<\.link.*href={~p"/users/log-out"}.*method="delete".*>|s
assert file =~
~r|<\.link.*href={~p"/users/register"}.*>|s
assert file =~
- ~r|<\.link.*href={~p"/users/log_in"}.*>|s
+ ~r|<\.link.*href={~p"/users/log-in"}.*>|s
end)
assert_file("test/support/conn_case.ex", fn file ->
@@ -359,9 +357,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
test "works with apps generated with --live", config do
in_tmp_phx_project(config.test, ~w(--live), fn ->
Gen.Auth.run(
- ~w(Accounts User users --live),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --live --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_file("lib/my_app_web/components/layouts/root.html.heex", fn file ->
@@ -369,13 +366,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<.link.*href={~p"/users/settings"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+ ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
assert file =~
~r|<.link.*href={~p"/users/register"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_in"}.*>|s
+ ~r|<.link.*href={~p"/users/log-in"}.*>|s
end)
assert_file("lib/my_app_web/router.ex", fn file ->
@@ -390,13 +387,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
live_session :redirect_if_user_is_authenticated,
on_mount: [{MyAppWeb.UserAuth, :redirect_if_user_is_authenticated}] do
- live "/users/register", UserRegistrationLive, :new
- live "/users/log_in", UserLoginLive, :new
- live "/users/reset_password", UserForgotPasswordLive, :new
- live "/users/reset_password/:token", UserResetPasswordLive, :edit
+ live "/users/register", UserLive.Registration, :new
+ live "/users/log-in", UserLive.Login, :new
+ live "/users/reset-password", UserLive.ForgotPassword, :new
+ live "/users/reset-password/:token", UserLive.ResetPassword, :edit
end
- post "/users/log_in", UserSessionController, :create
+ post "/users/log-in", UserSessionController, :create
end
scope "/", MyAppWeb do
@@ -404,20 +401,20 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
live_session :require_authenticated_user,
on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
- live "/users/settings", UserSettingsLive, :edit
- live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
+ live "/users/settings", UserLive.Settings, :edit
+ live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
end
end
scope "/", MyAppWeb do
pipe_through [:browser]
- delete "/users/log_out", UserSessionController, :delete
+ delete "/users/log-out", UserSessionController, :delete
live_session :current_user,
on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
- live "/users/confirm/:token", UserConfirmationLive, :edit
- live "/users/confirm", UserConfirmationInstructionsLive, :new
+ live "/users/confirm/:token", UserLive.Confirmation, :edit
+ live "/users/confirm", UserLive.ConfirmationInstructions, :new
end
end
"""
@@ -428,9 +425,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
test "works with apps generated with --no-live", config do
in_tmp_phx_project(config.test, ~w(--no-live), fn ->
Gen.Auth.run(
- ~w(Accounts User users --no-live),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-live --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_file("lib/my_app_web/components/layouts/root.html.heex", fn file ->
@@ -438,13 +434,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<.link.*href={~p"/users/settings"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_out"}.*method="delete".*>|s
+ ~r|<.link.*href={~p"/users/log-out"}.*method="delete".*>|s
assert file =~
~r|<.link.*href={~p"/users/register"}.*>|s
assert file =~
- ~r|<.link.*href={~p"/users/log_in"}.*>|s
+ ~r|<.link.*href={~p"/users/log-in"}.*>|s
end)
assert_file("lib/my_app_web/router.ex", fn file ->
@@ -459,12 +455,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
- get "/users/log_in", UserSessionController, :new
- post "/users/log_in", UserSessionController, :create
- get "/users/reset_password", UserResetPasswordController, :new
- post "/users/reset_password", UserResetPasswordController, :create
- get "/users/reset_password/:token", UserResetPasswordController, :edit
- put "/users/reset_password/:token", UserResetPasswordController, :update
+ get "/users/log-in", UserSessionController, :new
+ post "/users/log-in", UserSessionController, :create
+ get "/users/reset-password", UserResetPasswordController, :new
+ post "/users/reset-password", UserResetPasswordController, :create
+ get "/users/reset-password/:token", UserResetPasswordController, :edit
+ put "/users/reset-password/:token", UserResetPasswordController, :update
end
scope "/", MyAppWeb do
@@ -472,13 +468,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
- get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+ get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
end
scope "/", MyAppWeb do
pipe_through [:browser]
- delete "/users/log_out", UserSessionController, :delete
+ delete "/users/log-out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :edit
@@ -494,9 +490,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --web warehouse),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --web warehouse --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -531,7 +526,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<\.link.*href={~p"/warehouse/users/register"}.*>|s
assert file =~
- ~r|<\.link.*href={~p"/warehouse/users/log_in"}.*>|s
+ ~r|<\.link.*href={~p"/warehouse/users/log-in"}.*>|s
end)
assert_file(
@@ -553,13 +548,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
~r|<\.link.*href={~p"/warehouse/users/settings"}.*>|s
assert file =~
- ~r|<\.link.*href={~p"/warehouse/users/log_out"}.*method="delete".*>|s
+ ~r|<\.link.*href={~p"/warehouse/users/log-out"}.*method="delete".*>|s
assert file =~
~r|<\.link.*href={~p"/warehouse/users/register"}.*>|s
assert file =~
- ~r|<\.link.*href={~p"/warehouse/users/log_in"}.*>|s
+ ~r|<\.link.*href={~p"/warehouse/users/log-in"}.*>|s
end)
assert_file(
@@ -591,7 +586,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
"lib/my_app_web/controllers/warehouse/user_reset_password_html/edit.html.heex",
fn file ->
assert file =~
- ~S|<.simple_form :let={f} for={@changeset} action={~p"/warehouse/users/reset_password/#{@token}"}>|
+ ~S|<.simple_form :let={f} for={@changeset} action={~p"/warehouse/users/reset-password/#{@token}"}>|
end
)
@@ -599,7 +594,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
"lib/my_app_web/controllers/warehouse/user_reset_password_html/new.html.heex",
fn file ->
assert file =~
- ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/reset_password"}>)
+ ~S(<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/reset-password"}>)
end
)
@@ -620,13 +615,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
assert_file("lib/my_app_web/controllers/warehouse/user_session_html/new.html.heex", fn file ->
assert file =~
- ~S|<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/log_in"}>|
+ ~S|<.simple_form :let={f} for={@conn.params["user"]} as={:user} action={~p"/warehouse/users/log-in"}>|
assert file =~
~S|<.link navigate={~p"/warehouse/users/register"}|
assert file =~
- ~S|<.link href={~p"/warehouse/users/reset_password"}|
+ ~S|<.link href={~p"/warehouse/users/reset-password"}|
end)
assert_file(
@@ -682,12 +677,12 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
- get "/users/log_in", UserSessionController, :new
- post "/users/log_in", UserSessionController, :create
- get "/users/reset_password", UserResetPasswordController, :new
- post "/users/reset_password", UserResetPasswordController, :create
- get "/users/reset_password/:token", UserResetPasswordController, :edit
- put "/users/reset_password/:token", UserResetPasswordController, :update
+ get "/users/log-in", UserSessionController, :new
+ post "/users/log-in", UserSessionController, :create
+ get "/users/reset-password", UserResetPasswordController, :new
+ post "/users/reset-password", UserResetPasswordController, :create
+ get "/users/reset-password/:token", UserResetPasswordController, :edit
+ put "/users/reset-password/:token", UserResetPasswordController, :update
end
scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
@@ -695,13 +690,13 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
- get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
+ get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
end
scope "/warehouse", MyAppWeb.Warehouse, as: :warehouse do
pipe_through [:browser]
- delete "/users/log_out", UserSessionController, :delete
+ delete "/users/log-out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :edit
@@ -723,9 +718,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -777,9 +771,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.MyXQL,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.MyXQL
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -831,9 +824,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.SQLite3,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.SQLite3
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -885,9 +877,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.TDS,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.TDS
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -935,14 +926,43 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
end
end
+ test "allows utc_datetime", config do
+ in_tmp_phx_project(config.test, fn ->
+ send self(), {:mix_shell_input, :yes?, false}
+ with_generator_env(:my_app, [timestamp_type: :utc_datetime], fn ->
+
+ Gen.Auth.run(
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
+ )
+
+ assert [migration] = Path.wildcard("priv/repo/migrations/*_create_users_auth_tables.exs")
+
+ assert_file migration, fn file ->
+ assert file =~ "timestamps(type: :utc_datetime)"
+ assert file =~ "timestamps(type: :utc_datetime, updated_at: false)"
+ end
+
+ assert_file "lib/my_app/accounts/user.ex", fn file ->
+ assert file =~ "field :confirmed_at, :utc_datetime"
+ assert file =~ "timestamps(type: :utc_datetime)"
+ assert file =~ "now = DateTime.utc_now() |> DateTime.truncate(:second)"
+ end
+
+ assert_file "lib/my_app/accounts/user_token.ex", fn file ->
+ assert file =~ "timestamps(type: :utc_datetime, updated_at: false)"
+ end
+ end)
+ end)
+ end
+
test "supports --binary-id option", config do
in_tmp_phx_project(config.test, fn ->
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --binary-id),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --binary-id --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -973,9 +993,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --hashing-lib bcrypt),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --hashing-lib bcrypt --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -999,9 +1018,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --hashing-lib pbkdf2),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --hashing-lib pbkdf2 --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1025,15 +1043,14 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --hashing-lib argon2),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --hashing-lib argon2 --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
assert_file("mix.exs", fn file ->
- assert file =~ ~s|{:argon2_elixir, "~> 3.0"}|
+ assert file =~ ~s|{:argon2_elixir, "~> 4.0"}|
end)
assert_file("config/test.exs", fn file ->
@@ -1054,9 +1071,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users --table my_users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --table my_users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1086,9 +1102,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1145,9 +1160,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1213,9 +1227,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
with_generator_env(:my_app_web, [context_app: false], fn ->
assert_raise Mix.Error, ~r/no context_app configured/, fn ->
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
end
end)
@@ -1232,9 +1245,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1265,9 +1277,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1301,9 +1312,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1333,9 +1343,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1371,7 +1380,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
<.link
- href={~p"/users/log_out"}
+ href={~p"/users/log-out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
@@ -1389,7 +1398,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
<.link
- href={~p"/users/log_in"}
+ href={~p"/users/log-in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
@@ -1410,9 +1419,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts User users),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts User users --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
@@ -1436,7 +1444,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
<.link
- href={~p"/users/log_out"}
+ href={~p"/users/log-out"}
method="delete"
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
@@ -1454,7 +1462,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
<.link
- href={~p"/users/log_in"}
+ href={~p"/users/log-in"}
class="text-[0.8125rem] leading-6 text-zinc-900 font-semibold hover:text-zinc-700"
>
Log in
@@ -1478,9 +1486,8 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
send self(), {:mix_shell_input, :yes?, false}
Gen.Auth.run(
- ~w(Accounts Admin admins),
- ecto_adapter: Ecto.Adapters.Postgres,
- validate_dependencies?: false
+ ~w(Accounts Admin admins --no-compile),
+ ecto_adapter: Ecto.Adapters.Postgres
)
assert_received {:mix_shell, :yes?, [@liveview_option_message]}
diff --git a/test/mix/tasks/phx.gen.cert_test.exs b/test/mix/tasks/phx.gen.cert_test.exs
index 06d5a30c90..abc4483ca8 100644
--- a/test/mix/tasks/phx.gen.cert_test.exs
+++ b/test/mix/tasks/phx.gen.cert_test.exs
@@ -56,7 +56,7 @@ defmodule Mix.Tasks.Phx.CertTest do
# We don't actually verify the server cert contents, we just check that
# the client and server are able to complete the TLS handshake
- assert {:ok, client} = :ssl.connect('localhost', port, [verify: :verify_none], @timeout)
+ assert {:ok, client} = :ssl.connect(~c"localhost", port, [verify: :verify_none], @timeout)
:ssl.close(client)
:ssl.close(server)
end)
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index 1e17d58cb4..f13c3421c8 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -1,4 +1,4 @@
-Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
+Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
defmodule Phoenix.DupContext do
end
@@ -15,64 +15,77 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
end
test "new context", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
schema = Schema.new("Blog.Post", "posts", [], [])
context = Context.new("Blog", schema, [])
assert %Context{
- alias: Blog,
- base_module: Phoenix,
- basename: "blog",
- module: Phoenix.Blog,
- web_module: PhoenixWeb,
- schema: %Mix.Phoenix.Schema{
- alias: Post,
- human_plural: "Posts",
- human_singular: "Post",
- module: Phoenix.Blog.Post,
- plural: "posts",
- singular: "post"
- }} = context
+ alias: Blog,
+ base_module: Phoenix,
+ basename: "blog",
+ module: Phoenix.Blog,
+ web_module: PhoenixWeb,
+ schema: %Mix.Phoenix.Schema{
+ alias: Post,
+ human_plural: "Posts",
+ human_singular: "Post",
+ module: Phoenix.Blog.Post,
+ plural: "posts",
+ singular: "post"
+ }
+ } = context
assert String.ends_with?(context.dir, "lib/phoenix/blog")
assert String.ends_with?(context.file, "lib/phoenix/blog.ex")
assert String.ends_with?(context.test_file, "test/phoenix/blog_test.exs")
- assert String.ends_with?(context.test_fixtures_file, "test/support/fixtures/blog_fixtures.ex")
+
+ assert String.ends_with?(
+ context.test_fixtures_file,
+ "test/support/fixtures/blog_fixtures.ex"
+ )
+
assert String.ends_with?(context.schema.file, "lib/phoenix/blog/post.ex")
- end
+ end)
end
test "new nested context", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
schema = Schema.new("Site.Blog.Post", "posts", [], [])
context = Context.new("Site.Blog", schema, [])
assert %Context{
- alias: Blog,
- base_module: Phoenix,
- basename: "blog",
- module: Phoenix.Site.Blog,
- web_module: PhoenixWeb,
- schema: %Mix.Phoenix.Schema{
- alias: Post,
- human_plural: "Posts",
- human_singular: "Post",
- module: Phoenix.Site.Blog.Post,
- plural: "posts",
- singular: "post"
- }} = context
+ alias: Blog,
+ base_module: Phoenix,
+ basename: "blog",
+ module: Phoenix.Site.Blog,
+ web_module: PhoenixWeb,
+ schema: %Mix.Phoenix.Schema{
+ alias: Post,
+ human_plural: "Posts",
+ human_singular: "Post",
+ module: Phoenix.Site.Blog.Post,
+ plural: "posts",
+ singular: "post"
+ }
+ } = context
assert String.ends_with?(context.dir, "lib/phoenix/site/blog")
assert String.ends_with?(context.file, "lib/phoenix/site/blog.ex")
assert String.ends_with?(context.test_file, "test/phoenix/site/blog_test.exs")
- assert String.ends_with?(context.test_fixtures_file, "test/support/fixtures/site/blog_fixtures.ex")
+
+ assert String.ends_with?(
+ context.test_fixtures_file,
+ "test/support/fixtures/site/blog_fixtures.ex"
+ )
+
assert String.ends_with?(context.schema.file, "lib/phoenix/site/blog/post.ex")
- end
+ end)
end
test "new existing context", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
File.mkdir_p!("lib/phoenix/blog")
+
File.write!("lib/phoenix/blog.ex", """
defmodule Phoenix.Blog do
end
@@ -85,23 +98,27 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
refute Context.pre_existing_test_fixtures?(context)
File.mkdir_p!("test/phoenix/blog")
+
File.write!(context.test_file, """
defmodule Phoenix.BlogTest do
end
""")
+
assert Context.pre_existing_tests?(context)
File.mkdir_p!("test/support/fixtures")
+
File.write!(context.test_fixtures_file, """
defmodule Phoenix.BlogFixtures do
end
""")
+
assert Context.pre_existing_test_fixtures?(context)
- end
+ end)
end
test "invalid mix arguments", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
assert_raise Mix.Error, ~r/Expected the context, "blog", to be a valid module name/, fn ->
Gen.Context.run(~w(blog Post posts title:string))
end
@@ -114,13 +131,17 @@ 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 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/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))
@@ -129,90 +150,95 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
Gen.Context.run(~w(Blog Post))
end
- end
+ end)
end
test "generates context and handles existing contexts", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
Gen.Context.run(~w(Blog Post posts slug:unique secret:redact title:string))
- assert_file "lib/phoenix/blog/post.ex", fn file ->
+ assert_file("lib/phoenix/blog/post.ex", fn file ->
assert file =~ "field :title, :string"
assert file =~ "field :secret, :string, redact: true"
- end
+ end)
- assert_file "lib/phoenix/blog.ex", fn file ->
+ assert_file("lib/phoenix/blog.ex", fn file ->
assert file =~ "def get_post!"
assert file =~ "def list_posts"
assert file =~ "def create_post"
assert file =~ "def update_post"
assert file =~ "def delete_post"
assert file =~ "def change_post"
- end
+ end)
- assert_file "test/phoenix/blog_test.exs", fn file ->
+ assert_file("test/phoenix/blog_test.exs", fn file ->
assert file =~ "use Phoenix.DataCase"
assert file =~ "describe \"posts\" do"
assert file =~ "import Phoenix.BlogFixtures"
- end
+ end)
- assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+ assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
assert file =~ "defmodule Phoenix.BlogFixtures do"
assert file =~ "def post_fixture(attrs \\\\ %{})"
assert file =~ "title: \"some title\""
- end
+ end)
assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
- assert_file path, fn file ->
+
+ assert_file(path, fn file ->
assert file =~ "create table(:posts)"
assert file =~ "add :title, :string"
assert file =~ "add :secret, :string"
assert file =~ "create unique_index(:posts, [:slug])"
- end
-
+ end)
- send self(), {:mix_shell_input, :yes?, true}
+ send(self(), {:mix_shell_input, :yes?, true})
Gen.Context.run(~w(Blog Comment comments title:string))
- assert_received {:mix_shell, :info, ["You are generating into an existing context" <> notice]}
- assert notice =~ "Phoenix.Blog context currently has 6 functions and 1 file in its directory"
+ assert_received {:mix_shell, :info,
+ ["You are generating into an existing context" <> notice]}
+
+ assert notice =~
+ "Phoenix.Blog context currently has 6 functions and 1 file in its directory"
+
assert_received {:mix_shell, :yes?, ["Would you like to proceed?"]}
- assert_file "lib/phoenix/blog/comment.ex", fn file ->
+ assert_file("lib/phoenix/blog/comment.ex", fn file ->
assert file =~ "field :title, :string"
- end
+ end)
- assert_file "test/phoenix/blog_test.exs", fn file ->
+ assert_file("test/phoenix/blog_test.exs", fn file ->
assert file =~ "use Phoenix.DataCase"
assert file =~ "describe \"comments\" do"
assert file =~ "import Phoenix.BlogFixtures"
- end
+ end)
- assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+ assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
assert file =~ "defmodule Phoenix.BlogFixtures do"
assert file =~ "def comment_fixture(attrs \\\\ %{})"
assert file =~ "title: \"some title\""
- end
+ end)
assert [path] = Path.wildcard("priv/repo/migrations/*_create_comments.exs")
- assert_file path, fn file ->
+
+ assert_file(path, fn file ->
assert file =~ "create table(:comments)"
assert file =~ "add :title, :string"
- end
+ end)
- assert_file "lib/phoenix/blog.ex", fn file ->
+ assert_file("lib/phoenix/blog.ex", fn file ->
assert file =~ "def get_comment!"
assert file =~ "def list_comments"
assert file =~ "def create_comment"
assert file =~ "def update_comment"
assert file =~ "def delete_comment"
assert file =~ "def change_comment"
- end
- end
+ end)
+ end)
end
test "generates with unique fields", config do
- in_tmp_project config.test, fn ->
+ in_tmp_project(config.test, fn ->
Gen.Context.run(~w(Blog Post posts
slug:string:unique
subject:unique
@@ -224,57 +250,70 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
published?:boolean
))
- assert_received {:mix_shell, :info, ["""
+ assert_received {:mix_shell, :info,
+ [
+ """
- Some of the generated database columns are unique. Please provide
- unique implementations for the following fixture function(s) in
- test/support/fixtures/blog_fixtures.ex:
+ Some of the generated database columns are unique. Please provide
+ unique implementations for the following fixture function(s) in
+ test/support/fixtures/blog_fixtures.ex:
- def unique_post_price do
- raise "implement the logic to generate a unique post price"
- end
+ def unique_post_price do
+ raise "implement the logic to generate a unique post price"
+ end
- def unique_post_published_at do
- raise "implement the logic to generate a unique post published_at"
- end
- """]}
+ def unique_post_published_at do
+ raise "implement the logic to generate a unique post published_at"
+ end
+ """
+ ]}
- assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+ assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
assert file =~ ~S|def unique_post_order, do: System.unique_integer([:positive])|
- assert file =~ ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
- assert file =~ ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
- assert file =~ ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
+
+ assert file =~
+ ~S|def unique_post_slug, do: "some slug#{System.unique_integer([:positive])}"|
+
+ assert file =~
+ ~S|def unique_post_body, do: "some body#{System.unique_integer([:positive])}"|
+
+ assert file =~
+ ~S|def unique_post_subject, do: "some subject#{System.unique_integer([:positive])}"|
+
refute file =~ ~S|def unique_post_author|
+
assert file =~ """
- def unique_post_price do
- raise "implement the logic to generate a unique post price"
- end
- """
+ def unique_post_price do
+ raise "implement the logic to generate a unique post price"
+ end
+ """
assert file =~ """
- body: unique_post_body(),
- order: unique_post_order(),
- price: unique_post_price(),
- published?: true,
- published_at: unique_post_published_at(),
- slug: unique_post_slug(),
- subject: unique_post_subject()
- """
- end
+ body: unique_post_body(),
+ order: unique_post_order(),
+ price: unique_post_price(),
+ published?: true,
+ published_at: unique_post_published_at(),
+ slug: unique_post_slug(),
+ subject: unique_post_subject()
+ """
+ end)
assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
- assert_file path, fn file ->
+
+ assert_file(path, fn file ->
assert file =~ "create table(:posts)"
assert file =~ "create unique_index(:posts, [:order])"
assert file =~ "create unique_index(:posts, [:price])"
assert file =~ "create unique_index(:posts, [:slug])"
assert file =~ "create unique_index(:posts, [:subject])"
- end
- end
+ end)
+ end)
end
- test "does not prompt on unimplemented functions with only string, text and integer unique fields", config do
- in_tmp_project config.test, fn ->
+ test "does not prompt on unimplemented functions with only string, text and integer unique fields",
+ config do
+ in_tmp_project(config.test, fn ->
Gen.Context.run(~w(Blog Post posts
slug:string:unique
subject:unique
@@ -282,57 +321,60 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
order:integer:unique
))
- refute_received {:mix_shell, :info, ["\nSome of the generated database columns are unique." <> _]}
- end
+ refute_received {:mix_shell, :info,
+ ["\nSome of the generated database columns are unique." <> _]}
+ end)
end
- test "generates into existing context without prompt with --merge-with-existing-context", config do
- in_tmp_project config.test, fn ->
+ test "generates into existing context without prompt with --merge-with-existing-context",
+ config do
+ in_tmp_project(config.test, fn ->
Gen.Context.run(~w(Blog Post posts title))
- assert_file "lib/phoenix/blog.ex", fn file ->
+ assert_file("lib/phoenix/blog.ex", fn file ->
assert file =~ "def get_post!"
assert file =~ "def list_posts"
assert file =~ "def create_post"
assert file =~ "def update_post"
assert file =~ "def delete_post"
assert file =~ "def change_post"
- end
+ end)
Gen.Context.run(~w(Blog Comment comments message:string --merge-with-existing-context))
- refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+ refute_received {:mix_shell, :info,
+ ["You are generating into an existing context" <> _notice]}
- assert_file "lib/phoenix/blog.ex", fn file ->
+ assert_file("lib/phoenix/blog.ex", fn file ->
assert file =~ "def get_comment!"
assert file =~ "def list_comments"
assert file =~ "def create_comment"
assert file =~ "def update_comment"
assert file =~ "def delete_comment"
assert file =~ "def change_comment"
- end
- end
+ end)
+ end)
end
test "when more than 50 attributes are given", config do
- in_tmp_project config.test, fn ->
- long_attribute_list = Enum.map_join(0..55, " ", &("attribute#{&1}:string"))
+ in_tmp_project(config.test, fn ->
+ long_attribute_list = Enum.map_join(0..55, " ", &"attribute#{&1}:string")
Gen.Context.run(~w(Blog Post posts title #{long_attribute_list}))
- assert_file "test/phoenix/blog_test.exs", fn file ->
+ assert_file("test/phoenix/blog_test.exs", fn file ->
refute file =~ "...}"
- end
- end
+ end)
+ end)
end
+ test "generates context with no schema and repo option", config do
+ in_tmp_project(config.test, fn ->
+ Gen.Context.run(~w(Blog Post posts title:string --no-schema --repo=Foo.RepoX))
- test "generates context with no schema", config do
- in_tmp_project config.test, fn ->
- Gen.Context.run(~w(Blog Post posts title:string --no-schema))
-
- refute_file "lib/phoenix/blog/post.ex"
+ refute_file("lib/phoenix/blog/post.ex")
- assert_file "lib/phoenix/blog.ex", fn file ->
+ assert_file("lib/phoenix/blog.ex", fn file ->
+ assert file =~ "alias Foo.RepoX, as: Repo"
assert file =~ "def get_post!"
assert file =~ "def list_posts"
assert file =~ "def create_post"
@@ -340,22 +382,22 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
assert file =~ "def delete_post"
assert file =~ "def change_post"
assert file =~ "raise \"TODO\""
- end
+ end)
- assert_file "test/phoenix/blog_test.exs", fn file ->
+ assert_file("test/phoenix/blog_test.exs", fn file ->
assert file =~ "use Phoenix.DataCase"
assert file =~ "describe \"posts\" do"
assert file =~ "import Phoenix.BlogFixtures"
- end
+ end)
- assert_file "test/support/fixtures/blog_fixtures.ex", fn file ->
+ assert_file("test/support/fixtures/blog_fixtures.ex", fn file ->
assert file =~ "defmodule Phoenix.BlogFixtures do"
assert file =~ "def post_fixture(attrs \\\\ %{})"
assert file =~ "title: \"some title\""
- end
+ end)
assert Path.wildcard("priv/repo/migrations/*_create_posts.exs") == []
- end
+ end)
end
test "generates context with enum", config do
diff --git a/test/mix/tasks/phx.gen.embedded_test.exs b/test/mix/tasks/phx.gen.embedded_test.exs
index df47f5702a..1bc02c67b4 100644
--- a/test/mix/tasks/phx.gen.embedded_test.exs
+++ b/test/mix/tasks/phx.gen.embedded_test.exs
@@ -26,7 +26,7 @@ defmodule Mix.Tasks.Phx.Gen.EmbeddedTest do
human_plural: "Nil",
human_singular: "Post",
attrs: [title: :string],
- types: %{title: :string},
+ types: [title: :string],
embedded?: true,
defaults: %{title: ""}
} = schema
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index ab3395586a..450e0599b7 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -44,7 +44,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
datetime = %{DateTime.utc_now() | second: 0, microsecond: {0, 6}}
in_tmp_project(config.test, fn ->
- Gen.Html.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
+ Gen.Html.run(~w(Blog Post posts title content:text slug:unique votes:integer cost:decimal
tags:array:text popular:boolean drafted_at:datetime
status:enum:unpublished:published:deleted
published_at:utc_datetime
@@ -54,6 +54,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
alarm:time
alarm_usec:time_usec
secret:uuid:redact announcement_date:date alarm:time
+ metadata:map
weight:float user_id:references:users))
assert_file("lib/phoenix/blog/post.ex")
@@ -109,6 +110,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
assert_file(path, fn file ->
assert file =~ "create table(:posts)"
assert file =~ "add :title, :string"
+ assert file =~ "add :content, :text"
assert file =~ "add :status, :string"
assert file =~ "create unique_index(:posts, [:slug])"
end)
@@ -150,6 +152,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
assert_file("lib/phoenix_web/controllers/post_html/post_form.html.heex", fn file ->
assert file =~ ~S(<.simple_form :let={f} for={@changeset} action={@action}>)
assert file =~ ~s(<.input field={f[:title]} type="text")
+ assert file =~ ~s(<.input field={f[:content]} type="textarea")
assert file =~ ~s(<.input field={f[:votes]} type="number")
assert file =~ ~s(<.input field={f[:cost]} type="number" label="Cost" step="any")
@@ -167,6 +170,7 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
assert file =~ ~s(<.input field={f[:announcement_date]} type="date")
assert file =~ ~s(<.input field={f[:alarm]} type="time")
assert file =~ ~s(<.input field={f[:secret]} type="text" label="Secret" />)
+ refute file =~ ~s(field={f[:metadata]})
assert file =~ """
<.input
@@ -324,6 +328,15 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
end)
end
+ test "with a matching plural and singular term", config do
+ in_tmp_project(config.test, fn ->
+ Gen.Html.run(~w(Tracker Series series value:integer))
+ assert_file("lib/phoenix_web/controllers/series_controller.ex", fn file ->
+ assert file =~ "render(conn, :index, series_collection: series)"
+ end)
+ end)
+ end
+
test "with --no-context no warning is emitted when context exists", config do
in_tmp_project(config.test, fn ->
Gen.Html.run(~w(Blog Post posts title:string))
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index 61067dbf6d..32fb2112f2 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -113,7 +113,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
[
"""
- Add the resource to your :api scope in lib/phoenix_web/router.ex:
+ Add the resource to the "/api" scope in lib/phoenix_web/router.ex:
resources "/posts", PostController, except: [:new, :edit]
"""
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index bd8e73132f..dd5cb47564 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -55,7 +55,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
test "generates live resource and handles existing contexts", config do
in_tmp_live_project config.test, fn ->
- Gen.Live.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
+ Gen.Live.run(~w(Blog Post posts title content:text slug:unique votes:integer cost:decimal
tags:array:text popular:boolean drafted_at:datetime
status:enum:unpublished:published:deleted
published_at:utc_datetime
@@ -65,6 +65,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
alarm:time
alarm_usec:time_usec
secret:uuid:redact announcement_date:date alarm:time
+ metadata:map
weight:float user_id:references:users))
assert_file "lib/phoenix/blog/post.ex"
@@ -79,35 +80,37 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ "defmodule PhoenixWeb.PostLive.Show"
end
- assert_file "lib/phoenix_web/live/post_live/form_component.ex", fn file ->
- assert file =~ "defmodule PhoenixWeb.PostLive.FormComponent"
+ assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
+ assert file =~ "defmodule PhoenixWeb.PostLive.Form"
end
assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
assert_file path, fn file ->
assert file =~ "create table(:posts)"
assert file =~ "add :title, :string"
+ assert file =~ "add :content, :text"
assert file =~ "create unique_index(:posts, [:slug])"
end
- assert_file "lib/phoenix_web/live/post_live/index.html.heex", fn file ->
- assert file =~ ~S|~p"/posts"|
+ assert_file "lib/phoenix_web/live/post_live/index.ex", fn file ->
+ assert file =~ ~S|~p"/posts/#{post}"|
end
- assert_file "lib/phoenix_web/live/post_live/show.html.heex", fn file ->
+ assert_file "lib/phoenix_web/live/post_live/show.ex", fn file ->
assert file =~ ~S|~p"/posts"|
end
- assert_file "lib/phoenix_web/live/post_live/form_component.ex", fn file ->
+ assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
assert file =~ ~s(<.simple_form)
assert file =~ ~s(<.input field={@form[:title]} type="text")
+ assert file =~ ~s(<.input field={@form[:content]} type="textarea")
assert file =~ ~s(<.input field={@form[:votes]} type="number")
assert file =~ ~s(<.input field={@form[:cost]} type="number" label="Cost" step="any")
assert file =~ """
- <.input
- field={@form[:tags]}
- type="select"
- multiple
+ <.input
+ field={@form[:tags]}
+ type="select"
+ multiple
"""
assert file =~ ~s(<.input field={@form[:popular]} type="checkbox")
assert file =~ ~s(<.input field={@form[:drafted_at]} type="datetime-local")
@@ -116,10 +119,11 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ ~s(<.input field={@form[:announcement_date]} type="date")
assert file =~ ~s(<.input field={@form[:alarm]} type="time")
assert file =~ ~s(<.input field={@form[:secret]} type="text" label="Secret" />)
+ refute file =~ ~s(
- assert file =~ "defmodule PhoenixWeb.CommentLive.FormComponent"
+ assert_file "lib/phoenix_web/live/comment_live/form.ex", fn file ->
+ assert file =~ "defmodule PhoenixWeb.CommentLive.Form"
end
assert_receive {:mix_shell, :info, ["""
@@ -166,11 +170,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
Add the live routes to your browser scope in lib/phoenix_web/router.ex:
live "/comments", CommentLive.Index, :index
- live "/comments/new", CommentLive.Index, :new
- live "/comments/:id/edit", CommentLive.Index, :edit
-
+ live "/comments/new", CommentLive.Form, :new
live "/comments/:id", CommentLive.Show, :show
- live "/comments/:id/show/edit", CommentLive.Show, :edit
+ live "/comments/:id/edit", CommentLive.Form, :edit
"""]}
assert_receive({:mix_shell, :info, ["""
@@ -226,8 +228,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Show"
end
- assert_file "lib/phoenix_web/live/blog/post_live/form_component.ex", fn file ->
- assert file =~ "defmodule PhoenixWeb.Blog.PostLive.FormComponent"
+ assert_file "lib/phoenix_web/live/blog/post_live/form.ex", fn file ->
+ assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Form"
end
assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
@@ -236,24 +238,21 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ "add :title, :string"
end
- assert_file "lib/phoenix_web/live/blog/post_live/index.html.heex", fn file ->
- assert file =~ ~S|~p"/blog/posts"|
+ assert_file "lib/phoenix_web/live/blog/post_live/index.ex", fn file ->
assert file =~ ~S|~p"/blog/posts/#{post}/edit"|
assert file =~ ~S|~p"/blog/posts/new"|
assert file =~ ~S|~p"/blog/posts/#{post}"|
end
- assert_file "lib/phoenix_web/live/blog/post_live/show.html.heex", fn file ->
+ assert_file "lib/phoenix_web/live/blog/post_live/show.ex", fn file ->
assert file =~ ~S|~p"/blog/posts"|
- assert file =~ ~S|~p"/blog/posts/#{@post}"|
- assert file =~ ~S|~p"/blog/posts/#{@post}/show/edit"|
+ assert file =~ ~S|~p"/blog/posts/#{@post}/edit?return_to=show"|
end
assert_file "test/phoenix_web/live/blog/post_live_test.exs", fn file ->
assert file =~ ~S|~p"/blog/posts"|
assert file =~ ~S|~p"/blog/posts/new"|
- assert file =~ ~S|~p"/blog/posts/#{post}"|
- assert file =~ ~S|~p"/blog/posts/#{post}/show/edit"|
+ assert file =~ ~S|~p"/blog/posts/#{post}/edit"|
end
assert_receive {:mix_shell, :info, ["""
@@ -265,11 +264,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
...
live "/posts", PostLive.Index, :index
- live "/posts/new", PostLive.Index, :new
- live "/posts/:id/edit", PostLive.Index, :edit
-
+ live "/posts/new", PostLive.Form, :new
live "/posts/:id", PostLive.Show, :show
- live "/posts/:id/show/edit", PostLive.Show, :edit
+ live "/posts/:id/edit", PostLive.Form, :edit
end
"""]}
end
@@ -285,10 +282,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert_file "lib/phoenix_web/live/post_live/index.ex"
assert_file "lib/phoenix_web/live/post_live/show.ex"
- assert_file "lib/phoenix_web/live/post_live/form_component.ex"
+ assert_file "lib/phoenix_web/live/post_live/form.ex"
- assert_file "lib/phoenix_web/live/post_live/index.html.heex"
- assert_file "lib/phoenix_web/live/post_live/show.html.heex"
assert_file "test/phoenix_web/live/post_live_test.exs"
end
end
@@ -303,10 +298,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert_file "lib/phoenix_web/live/post_live/index.ex"
assert_file "lib/phoenix_web/live/post_live/show.ex"
- assert_file "lib/phoenix_web/live/post_live/form_component.ex"
+ assert_file "lib/phoenix_web/live/post_live/form.ex"
- assert_file "lib/phoenix_web/live/post_live/index.html.heex"
- assert_file "lib/phoenix_web/live/post_live/show.html.heex"
assert_file "test/phoenix_web/live/post_live_test.exs"
end
end
@@ -323,10 +316,8 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert_file "lib/phoenix_web/live/comment_live/index.ex"
assert_file "lib/phoenix_web/live/comment_live/show.ex"
- assert_file "lib/phoenix_web/live/comment_live/form_component.ex"
+ assert_file "lib/phoenix_web/live/comment_live/form.ex"
- assert_file "lib/phoenix_web/live/comment_live/index.html.heex"
- assert_file "lib/phoenix_web/live/comment_live/show.html.heex"
assert_file "test/phoenix_web/live/comment_live_test.exs"
end
end
@@ -339,17 +330,16 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert_file "lib/phoenix/tracker/series.ex"
assert_file "lib/phoenix_web/live/series_live/index.ex", fn file ->
- assert file =~ "stream(socket, :series_collection, Tracker.list_series())"
+ assert file =~ "|> stream(:series_collection, Tracker.list_series())"
end
assert_file "lib/phoenix_web/live/series_live/show.ex"
- assert_file "lib/phoenix_web/live/series_live/form_component.ex"
+ assert_file "lib/phoenix_web/live/series_live/form.ex"
- assert_file "lib/phoenix_web/live/series_live/index.html.heex", fn file ->
+ assert_file "lib/phoenix_web/live/series_live/index.ex", fn file ->
assert file =~ "@streams.series_collection"
end
- assert_file "lib/phoenix_web/live/series_live/show.html.heex"
assert_file "test/phoenix_web/live/series_live_test.exs"
end
end
@@ -389,9 +379,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ "use PhoenixWeb, :live_view"
end
- assert_file "lib/phoenix_web/live/user_live/form_component.ex", fn file ->
- assert file =~ "defmodule PhoenixWeb.UserLive.FormComponent"
- assert file =~ "use PhoenixWeb, :live_component"
+ assert_file "lib/phoenix_web/live/user_live/form.ex", fn file ->
+ assert file =~ "defmodule PhoenixWeb.UserLive.Form"
+ assert file =~ "use PhoenixWeb, :live_view"
end
assert_file "test/phoenix_web/live/user_live_test.exs", fn file ->
@@ -432,9 +422,9 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
assert file =~ "use Phoenix, :live_view"
end
- assert_file "lib/phoenix/live/user_live/form_component.ex", fn file ->
- assert file =~ "defmodule Phoenix.UserLive.FormComponent"
- assert file =~ "use Phoenix, :live_component"
+ assert_file "lib/phoenix/live/user_live/form.ex", fn file ->
+ assert file =~ "defmodule Phoenix.UserLive.Form"
+ assert file =~ "use Phoenix, :live_view"
end
assert_file "test/phoenix/live/user_live_test.exs", fn file ->
diff --git a/test/mix/tasks/phx.gen.release_test.exs b/test/mix/tasks/phx.gen.release_test.exs
index 30386eba21..9ea1319f0e 100644
--- a/test/mix/tasks/phx.gen.release_test.exs
+++ b/test/mix/tasks/phx.gen.release_test.exs
@@ -123,8 +123,9 @@ defmodule Mix.Tasks.Phx.Gen.ReleaseTest do
Gen.Release.run(["--docker"])
assert_file("Dockerfile", fn file ->
+ assert file =~ ~S|RUN mix assets.setup|
assert file =~ ~S|COPY assets assets|
- assert file =~ ~S|mix assets.deploy|
+ assert file =~ ~S|RUN mix assets.deploy|
end)
end)
end
@@ -134,8 +135,9 @@ defmodule Mix.Tasks.Phx.Gen.ReleaseTest do
Gen.Release.run(["--docker"])
assert_file("Dockerfile", fn file ->
+ refute file =~ ~S|RUN mix assets.setup|
refute file =~ ~S|COPY assets assets|
- refute file =~ ~S|mix assets.deploy|
+ refute file =~ ~S|RUN mix assets.deploy|
end)
end)
end
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index caab17f49b..e7f62cd9df 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -16,7 +16,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
test "build" do
in_tmp_project "build", fn ->
- schema = Gen.Schema.build(~w(Blog.Post posts title:string), [])
+ schema = Gen.Schema.build(~w(Blog.Post posts title:string tags:map), [])
assert %Schema{
alias: Post,
@@ -28,10 +28,11 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
singular: "post",
human_plural: "Posts",
human_singular: "Post",
- attrs: [title: :string],
- types: %{title: :string},
+ attrs: [title: :string, tags: :map],
+ types: [title: :string, tags: :map],
+ optionals: [:tags],
route_helper: "post",
- defaults: %{title: ""},
+ defaults: %{title: "", tags: ""},
} = schema
assert String.ends_with?(schema.file, "lib/phoenix/blog/post.ex")
end
@@ -52,7 +53,7 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
human_plural: "Posts",
human_singular: "Post",
attrs: [title: :string],
- types: %{title: :string},
+ types: [title: :string],
route_helper: "api_v1_post",
defaults: %{title: ""},
} = schema
@@ -112,6 +113,50 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
end
end
+ test "allows a custom repo", config do
+ in_tmp_project config.test, fn ->
+ Gen.Schema.run(~w(Blog.Post blog_posts title:string --repo MyApp.CustomRepo))
+
+ assert [migration] = Path.wildcard("priv/custom_repo/migrations/*_create_blog_posts.exs")
+ assert_file migration, fn file ->
+ assert file =~ "defmodule MyApp.CustomRepo.Migrations.CreateBlogPosts do"
+ end
+ end
+ end
+
+ test "allows a custom migration dir", config do
+ in_tmp_project config.test, fn ->
+ Gen.Schema.run(~w(Blog.Post blog_posts title:string --migration-dir priv/custom_dir))
+
+ assert [migration] = Path.wildcard("priv/custom_dir/*_create_blog_posts.exs")
+ assert_file migration, fn file ->
+ assert file =~ "defmodule Phoenix.Repo.Migrations.CreateBlogPosts do"
+ end
+ end
+ end
+
+ test "custom migration_dir takes precedence over custom repo name", config do
+ in_tmp_project config.test, fn ->
+ Gen.Schema.run(~w(Blog.Post blog_posts title:string \
+ --repo MyApp.CustomRepo --migration-dir priv/custom_dir))
+
+ assert [migration] = Path.wildcard("priv/custom_dir/*_create_blog_posts.exs")
+ assert_file migration, fn file ->
+ assert file =~ "defmodule MyApp.CustomRepo.Migrations.CreateBlogPosts do"
+ end
+ end
+ end
+
+ test "does not add maps to the required list", config do
+ in_tmp_project config.test, fn ->
+ Gen.Schema.run(~w(Blog.Post blog_posts title:string tags:map published_at:naive_datetime))
+ assert_file "lib/phoenix/blog/post.ex", fn file ->
+ assert file =~ "cast(attrs, [:title, :tags, :published_at]"
+ assert file =~ "validate_required([:title, :published_at]"
+ end
+ end
+ end
+
test "generates nested schema", config do
in_tmp_project config.test, fn ->
Gen.Schema.run(~w(Blog.Admin.User users name:string))
@@ -301,6 +346,22 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
end
end
end
+
+ in_tmp_project "uses defaults from generators configuration (:utc_datetime)", fn ->
+ with_generator_env [timestamp_type: :utc_datetime], fn ->
+ Gen.Schema.run(~w(Blog.Post posts))
+
+ assert [migration] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+
+ assert_file migration, fn file ->
+ assert file =~ "timestamps(type: :utc_datetime)"
+ end
+
+ assert_file "lib/phoenix/blog/post.ex", fn file ->
+ assert file =~ "timestamps(type: :utc_datetime)"
+ end
+ end
+ end
end
test "generates migrations with a custom migration module", config do
diff --git a/test/mix/tasks/phx.routes_test.exs b/test/mix/tasks/phx.routes_test.exs
index eb1e05605e..e044fc8686 100644
--- a/test/mix/tasks/phx.routes_test.exs
+++ b/test/mix/tasks/phx.routes_test.exs
@@ -1,8 +1,12 @@
-Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
+Code.require_file("../../../installer/test/mix_helper.exs", __DIR__)
defmodule PageController do
def init(opts), do: opts
def call(conn, _opts), do: conn
+
+ defmodule Live do
+ def init(opts), do: opts
+ end
end
defmodule PhoenixTestWeb.Router do
@@ -17,45 +21,47 @@ end
defmodule PhoenixTestLiveWeb.Router do
use Phoenix.Router
- get "/", PageController, :index, metadata: %{log_module: PageLive.Index}
+ get "/", PageController, :index, metadata: %{mfa: {PageController.Live, :init, 1}}
end
defmodule Mix.Tasks.Phx.RoutesTest do
use ExUnit.Case, async: true
test "format routes for specific router" do
- Mix.Tasks.Phx.Routes.run(["PhoenixTestWeb.Router"])
+ Mix.Tasks.Phx.Routes.run(["PhoenixTestWeb.Router", "--no-compile"])
assert_received {:mix_shell, :info, [routes]}
assert routes =~ "page_path GET / PageController :index"
end
test "prints error when explicit router cannot be found" do
- assert_raise Mix.Error, "the provided router, Foo.UnknownBar.CantFindBaz, does not exist", fn ->
- Mix.Tasks.Phx.Routes.run(["Foo.UnknownBar.CantFindBaz"])
- end
+ assert_raise Mix.Error,
+ "the provided router, Foo.UnknownBar.CantFindBaz, does not exist",
+ fn ->
+ Mix.Tasks.Phx.Routes.run(["Foo.UnknownBar.CantFindBaz", "--no-compile"])
+ end
end
test "prints error when implicit router cannot be found" do
assert_raise Mix.Error, ~r/no router found at FooWeb.Router or Foo.Router/, fn ->
- Mix.Tasks.Phx.Routes.run([], Foo)
+ Mix.Tasks.Phx.Routes.run(["--no-compile"], Foo)
end
end
test "implicit router detection for web namespace" do
- Mix.Tasks.Phx.Routes.run([], PhoenixTest)
+ Mix.Tasks.Phx.Routes.run(["--no-compile"], PhoenixTest)
assert_received {:mix_shell, :info, [routes]}
assert routes =~ "page_path GET / PageController :index"
end
test "implicit router detection fallback for old namespace" do
- Mix.Tasks.Phx.Routes.run([], PhoenixTestOld)
+ Mix.Tasks.Phx.Routes.run(["--no-compile"], PhoenixTestOld)
assert_received {:mix_shell, :info, [routes]}
assert routes =~ "page_path GET /old PageController :index"
end
- test "overrides module name for route with :log_module metadata" do
- Mix.Tasks.Phx.Routes.run(["PhoenixTestLiveWeb.Router"])
+ test "overrides module name for route with :mfa metadata" do
+ Mix.Tasks.Phx.Routes.run(["PhoenixTestLiveWeb.Router", "--no-compile"])
assert_received {:mix_shell, :info, [routes]}
- assert routes =~ "page_path GET / PageLive.Index :index"
+ assert routes =~ "page_path GET / PageController.Live :index"
end
end
diff --git a/test/phoenix/code_reloader_test.exs b/test/phoenix/code_reloader_test.exs
index 70b5d0cfe7..36f2911e23 100644
--- a/test/phoenix/code_reloader_test.exs
+++ b/test/phoenix/code_reloader_test.exs
@@ -7,7 +7,7 @@ defmodule Phoenix.CodeReloaderTest do
def config(:reloadable_apps), do: nil
end
- def reload(_) do
+ def reload(_, _) do
{:error, "oops"}
end
@@ -38,20 +38,26 @@ defmodule Phoenix.CodeReloaderTest do
:erlang.trace(pid, true, [:receive])
opts = Phoenix.CodeReloader.init([])
- conn = conn(:get, "/")
- |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
- |> Phoenix.CodeReloader.call(opts)
+
+ conn =
+ conn(:get, "/")
+ |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
+ |> Phoenix.CodeReloader.call(opts)
+
assert conn.state == :unset
assert_receive {:trace, ^pid, :receive, {_, _, {:reload!, Endpoint, _}}}
end
test "renders compilation error on failure" do
- opts = Phoenix.CodeReloader.init(reloader: &__MODULE__.reload/1)
- conn = conn(:get, "/")
- |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
- |> Phoenix.CodeReloader.call(opts)
- assert conn.state == :sent
+ opts = Phoenix.CodeReloader.init(reloader: &__MODULE__.reload/2)
+
+ conn =
+ conn(:get, "/")
+ |> Plug.Conn.put_private(:phoenix_endpoint, Endpoint)
+ |> Phoenix.CodeReloader.call(opts)
+
+ assert conn.state == :sent
assert conn.status == 500
assert conn.resp_body =~ "oops"
assert conn.resp_body =~ "CompileError"
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 64db87e558..4b2057f09c 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -530,13 +530,35 @@ defmodule Phoenix.Controller.ControllerTest do
conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "hello world.json")
assert conn.status == 200
assert get_resp_header(conn, "content-disposition") ==
- ["attachment; filename=\"hello+world.json\""]
+ ["attachment; filename=\"hello%20world.json\"; filename*=utf-8''hello%20world.json"]
assert get_resp_header(conn, "content-type") ==
["application/json"]
assert conn.resp_body ==
"world"
end
+ test "sends file for download for filename with unreserved characters" do
+ conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "hello, world.json")
+ assert conn.status == 200
+ assert get_resp_header(conn, "content-disposition") ==
+ ["attachment; filename=\"hello%2C%20world.json\"; filename*=utf-8''hello%2C%20world.json"]
+ assert get_resp_header(conn, "content-type") ==
+ ["application/json"]
+ assert conn.resp_body ==
+ "world"
+ end
+
+ test "sends file supports UTF-8" do
+ conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "测 试.txt")
+ assert conn.status == 200
+ assert get_resp_header(conn, "content-disposition") ==
+ ["attachment; filename=\"%E6%B5%8B%20%E8%AF%95.txt\"; filename*=utf-8''%E6%B5%8B%20%E8%AF%95.txt"]
+ assert get_resp_header(conn, "content-type") ==
+ ["text/plain"]
+ assert conn.resp_body ==
+ "world"
+ end
+
test "sends file for download with custom :filename and :encode false" do
conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "dev's hello world.json", encode: false)
assert conn.status == 200
@@ -583,10 +605,10 @@ defmodule Phoenix.Controller.ControllerTest do
end
test "sends binary for download with :filename" do
- conn = send_download(conn(:get, "/"), {:binary, "world"}, filename: "hello world.json")
+ conn = send_download(conn(:get, "/"), {:binary, "world"}, filename: "hello.json")
assert conn.status == 200
assert get_resp_header(conn, "content-disposition") ==
- ["attachment; filename=\"hello+world.json\""]
+ ["attachment; filename=\"hello.json\""]
assert get_resp_header(conn, "content-type") ==
["application/json"]
assert conn.resp_body ==
@@ -777,7 +799,7 @@ defmodule Phoenix.Controller.ControllerTest do
test "current_path/2 allows custom nested query params" do
conn = build_conn_for_path("/")
- assert current_path(conn, %{foo: %{bar: [:baz], baz: :qux}}) == "/?foo[bar][]=baz&foo[baz]=qux"
+ assert current_path(conn, [foo: [bar: [:baz], baz: :qux]]) == "/?foo[bar][]=baz&foo[baz]=qux"
end
test "current_url/1 with root path includes trailing slash" do
diff --git a/test/phoenix/controller/render_test.exs b/test/phoenix/controller/render_test.exs
index 8f9399f708..a9d065a4b9 100644
--- a/test/phoenix/controller/render_test.exs
+++ b/test/phoenix/controller/render_test.exs
@@ -176,6 +176,7 @@ defmodule Phoenix.Controller.RenderTest do
setup context do
:telemetry.attach_many(context.test, @render_events, &__MODULE__.message_pid/4, self())
+ on_exit(fn -> :telemetry.detach(context.test) end)
end
def message_pid(event, measures, metadata, test_pid) do
diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs
index b50f1ab0a6..708e264c21 100644
--- a/test/phoenix/digester_test.exs
+++ b/test/phoenix/digester_test.exs
@@ -13,6 +13,7 @@ defmodule Phoenix.DigesterTest do
setup do
File.rm_rf!(@output_path)
+ on_exit(fn -> File.rm_rf!(@output_path) end)
:ok
end
@@ -90,6 +91,8 @@ defmodule Phoenix.DigesterTest do
assert json["digests"][key]["sha512"] ==
"93pY5dBa8nHHi0Zfj75O/vXCBXb+UvEVCyU7Yd3pzOJ7o1wkYBWbvs3pVXhBChEmo8MDANT11vsggo2+bnYqoQ=="
+ after
+ File.rm_rf!("tmp/digest")
end
test "excludes compiled files" do
@@ -133,6 +136,8 @@ defmodule Phoenix.DigesterTest do
assert_in_delta json["digests"]["foo-1198fd3c7ecf0e8f4a33a6e4fc5ae168.css"]["mtime"],
now(),
2
+ after
+ File.rm_rf!("tmp/digest")
end
test "excludes files that no longer exist from cache manifest" do
@@ -150,6 +155,8 @@ defmodule Phoenix.DigesterTest do
json = Path.join(input_path, "cache_manifest.json") |> json_read!()
assert json["digests"] == %{}
+ after
+ File.rm_rf!("tmp/digest")
end
test "digests and compress nested files" do
@@ -172,38 +179,46 @@ defmodule Phoenix.DigesterTest do
input_path = Path.join("tmp", "phoenix_digest_twice")
input_file = Path.join(input_path, "file.js")
- File.rm_rf!(input_path)
- File.mkdir_p!(input_path)
- File.mkdir_p!(@output_path)
+ try do
+ File.rm_rf!(input_path)
+ File.mkdir_p!(input_path)
+ File.mkdir_p!(@output_path)
- File.write!(input_file, "console.log('test');")
- assert :ok = Phoenix.Digester.compile(input_path, @output_path, true)
+ File.write!(input_file, "console.log('test');")
+ assert :ok = Phoenix.Digester.compile(input_path, @output_path, true)
- json1 = Path.join(@output_path, "cache_manifest.json") |> json_read!()
- assert Enum.count(json1["digests"]) == 1
+ json1 = Path.join(@output_path, "cache_manifest.json") |> json_read!()
+ assert Enum.count(json1["digests"]) == 1
- File.write!(input_file, "console.log('test2');")
- assert :ok = Phoenix.Digester.compile(input_path, @output_path, true)
+ File.write!(input_file, "console.log('test2');")
+ assert :ok = Phoenix.Digester.compile(input_path, @output_path, true)
- json2 = Path.join(@output_path, "cache_manifest.json") |> json_read!()
- assert Enum.count(json2["digests"]) == 2
+ json2 = Path.join(@output_path, "cache_manifest.json") |> json_read!()
+ assert Enum.count(json2["digests"]) == 2
+ after
+ File.rm_rf!(input_path)
+ end
end
test "doesn't duplicate files when digesting and compressing twice" do
input_path = Path.join("tmp", "phoenix_digest_twice")
input_file = Path.join(input_path, "file.js")
- File.rm_rf!(input_path)
- File.mkdir_p!(input_path)
- File.write!(input_file, "console.log('test');")
-
- assert :ok = Phoenix.Digester.compile(input_path, input_path, true)
- assert :ok = Phoenix.Digester.compile(input_path, input_path, true)
-
- output_files = assets_files(input_path)
- refute "file.js.gz.gz" in output_files
- refute "cache_manifest.json.gz" in output_files
- refute Enum.any?(output_files, &(&1 =~ ~r/file-#{@hash_regex}.[\w|\d]*.[-#{@hash_regex}/))
+ try do
+ File.rm_rf!(input_path)
+ File.mkdir_p!(input_path)
+ File.write!(input_file, "console.log('test');")
+
+ assert :ok = Phoenix.Digester.compile(input_path, input_path, true)
+ assert :ok = Phoenix.Digester.compile(input_path, input_path, true)
+
+ output_files = assets_files(input_path)
+ refute "file.js.gz.gz" in output_files
+ refute "cache_manifest.json.gz" in output_files
+ refute Enum.any?(output_files, &(&1 =~ ~r/file-#{@hash_regex}.[\w|\d]*.[-#{@hash_regex}/))
+ after
+ File.rm_rf!(input_path)
+ end
end
test "digests only absolute and relative asset paths found within stylesheets with vsn" do
diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs
index beb946a994..5d8a6579fa 100644
--- a/test/phoenix/endpoint/endpoint_test.exs
+++ b/test/phoenix/endpoint/endpoint_test.exs
@@ -7,13 +7,17 @@ defmodule Phoenix.Endpoint.EndpointTest do
use ExUnit.Case, async: true
use RouterHelper
- @config [url: [host: {:system, "ENDPOINT_TEST_HOST"}, path: "/api"],
- static_url: [host: "static.example.com"],
- server: false, http: [port: 80], https: [port: 443],
- force_ssl: [subdomains: true],
- cache_manifest_skip_vsn: false,
- cache_static_manifest: "../../../../test/fixtures/digest/compile/cache_manifest.json",
- pubsub_server: :endpoint_pub]
+ @config [
+ url: [host: {:system, "ENDPOINT_TEST_HOST"}, path: "/api"],
+ static_url: [host: "static.example.com"],
+ server: false,
+ http: [port: 80],
+ https: [port: 443],
+ force_ssl: [subdomains: true],
+ cache_manifest_skip_vsn: false,
+ cache_static_manifest: "../../../../test/fixtures/digest/compile/cache_manifest.json",
+ pubsub_server: :endpoint_pub
+ ]
Application.put_env(:phoenix, __MODULE__.Endpoint, @config)
@@ -39,24 +43,24 @@ defmodule Phoenix.Endpoint.EndpointTest do
end
setup_all do
- ExUnit.CaptureLog.capture_log(fn -> start_supervised! Endpoint end)
- start_supervised! {Phoenix.PubSub, name: :endpoint_pub}
- on_exit fn -> Application.delete_env(:phoenix, :serve_endpoints) end
+ ExUnit.CaptureLog.capture_log(fn -> start_supervised!(Endpoint) end)
+ start_supervised!({Phoenix.PubSub, name: :endpoint_pub})
+ on_exit(fn -> Application.delete_env(:phoenix, :serve_endpoints) end)
:ok
end
test "defines child_spec/1" do
assert Endpoint.child_spec([]) == %{
- id: Endpoint,
- start: {Endpoint, :start_link, [[]]},
- type: :supervisor
- }
+ id: Endpoint,
+ start: {Endpoint, :start_link, [[]]},
+ type: :supervisor
+ }
end
test "warns if there is no configuration for an endpoint" do
assert ExUnit.CaptureLog.capture_log(fn ->
- NoConfigEndpoint.start_link()
- end) =~ "no configuration"
+ NoConfigEndpoint.start_link()
+ end) =~ "no configuration"
end
test "has reloadable configuration" do
@@ -75,10 +79,13 @@ defmodule Phoenix.Endpoint.EndpointTest do
assert Endpoint.config_change([{Endpoint, config}], []) == :ok
assert Endpoint.config(:endpoint_id) == endpoint_id
+
assert Enum.sort(Endpoint.config(:url)) ==
- [host: {:system, "ENDPOINT_TEST_HOST"}, path: "/api", port: 1234]
+ [host: {:system, "ENDPOINT_TEST_HOST"}, path: "/api", port: 1234]
+
assert Enum.sort(Endpoint.config(:static_url)) ==
- [host: "static.example.com", port: 456]
+ [host: "static.example.com", port: 456]
+
assert Endpoint.url() == "https://example.com:1234"
assert Endpoint.path("/") == "/api/"
assert Endpoint.static_url() == "https://static.example.com:456"
@@ -136,7 +143,7 @@ defmodule Phoenix.Endpoint.EndpointTest do
conn = conn(:get, "https://example.com/")
assert Endpoint.call(conn, []).script_name == ~w"api"
- conn = put_in conn.script_name, ~w(foo)
+ conn = put_in(conn.script_name, ~w(foo))
assert Endpoint.call(conn, []).script_name == ~w"api"
end
@@ -149,19 +156,25 @@ defmodule Phoenix.Endpoint.EndpointTest do
test "sends hsts on https requests on force_ssl" do
conn = Endpoint.call(conn(:get, "https://example.com/"), [])
+
assert get_resp_header(conn, "strict-transport-security") ==
- ["max-age=31536000; includeSubDomains"]
+ ["max-age=31536000; includeSubDomains"]
end
test "warms up caches on load and config change" do
assert Endpoint.config_change([{Endpoint, @config}], []) == :ok
+
assert Endpoint.config(:cache_static_manifest_latest) ==
%{"foo.css" => "foo-d978852bea6530fcd197b5445ed008fd.css"}
assert Endpoint.static_path("/foo.css") == "/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d"
# Trigger a config change and the cache should be warmed up again
- config = put_in(@config[:cache_static_manifest], "../../../../test/fixtures/digest/compile/cache_manifest_upgrade.json")
+ config =
+ put_in(
+ @config[:cache_static_manifest],
+ "../../../../test/fixtures/digest/compile/cache_manifest_upgrade.json"
+ )
assert Endpoint.config_change([{Endpoint, config}], []) == :ok
assert Endpoint.config(:cache_static_manifest_latest) == %{"foo.css" => "foo-ghijkl.css"}
@@ -190,74 +203,116 @@ defmodule Phoenix.Endpoint.EndpointTest do
"/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d#info#me"
end
- @tag :capture_log
- test "invokes init/2 callback" do
- defmodule InitEndpoint do
- use Phoenix.Endpoint, otp_app: :phoenix
-
- def init(:supervisor, opts) do
- send opts[:parent], {self(), :sample}
- {:ok, opts}
- end
- end
-
- {:ok, pid} = InitEndpoint.start_link(parent: self())
- assert_receive {^pid, :sample}
- end
-
@tag :capture_log
test "uses url configuration for static path" do
Application.put_env(:phoenix, __MODULE__.UrlEndpoint, url: [path: "/api"])
+
defmodule UrlEndpoint do
use Phoenix.Endpoint, otp_app: :phoenix
end
+
UrlEndpoint.start_link()
assert UrlEndpoint.path("/phoenix.png") =~ "/api/phoenix.png"
assert UrlEndpoint.static_path("/phoenix.png") =~ "/api/phoenix.png"
+ after
+ :code.purge(__MODULE__.UrlEndpoint)
+ :code.delete(__MODULE__.UrlEndpoint)
end
@tag :capture_log
test "uses static_url configuration for static path" do
Application.put_env(:phoenix, __MODULE__.StaticEndpoint, static_url: [path: "/static"])
+
defmodule StaticEndpoint do
use Phoenix.Endpoint, otp_app: :phoenix
end
+
StaticEndpoint.start_link()
assert StaticEndpoint.path("/phoenix.png") =~ "/phoenix.png"
assert StaticEndpoint.static_path("/phoenix.png") =~ "/static/phoenix.png"
+ after
+ :code.purge(__MODULE__.StaticEndpoint)
+ :code.delete(__MODULE__.StaticEndpoint)
+ end
+
+ @tag :capture_log
+ test "can find the running address and port for an endpoint" do
+ Application.put_env(:phoenix, __MODULE__.AddressEndpoint,
+ http: [ip: {127, 0, 0, 1}, port: 0],
+ server: true
+ )
+
+ defmodule AddressEndpoint do
+ use Phoenix.Endpoint, otp_app: :phoenix
+ end
+
+ AddressEndpoint.start_link()
+ assert {:ok, {{127, 0, 0, 1}, port}} = AddressEndpoint.server_info(:http)
+ assert is_integer(port)
+ after
+ :code.purge(__MODULE__.AddressEndpoint)
+ :code.delete(__MODULE__.AddressEndpoint)
end
test "injects pubsub broadcast with configured server" do
Endpoint.subscribe("sometopic")
- some = spawn fn -> :ok end
+ some = spawn(fn -> :ok end)
Endpoint.broadcast_from(some, "sometopic", "event1", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event1", payload: %{key: :val}, topic: "sometopic"}
+ event: "event1",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
Endpoint.broadcast_from!(some, "sometopic", "event2", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event2", payload: %{key: :val}, topic: "sometopic"}
+ event: "event2",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
Endpoint.broadcast("sometopic", "event3", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event3", payload: %{key: :val}, topic: "sometopic"}
+ event: "event3",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
Endpoint.broadcast!("sometopic", "event4", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event4", payload: %{key: :val}, topic: "sometopic"}
+ event: "event4",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
Endpoint.local_broadcast_from(some, "sometopic", "event1", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event1", payload: %{key: :val}, topic: "sometopic"}
+ event: "event1",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
Endpoint.local_broadcast("sometopic", "event3", %{key: :val})
+
assert_receive %Phoenix.Socket.Broadcast{
- event: "event3", payload: %{key: :val}, topic: "sometopic"}
+ event: "event3",
+ payload: %{key: :val},
+ topic: "sometopic"
+ }
end
test "loads cache manifest from specified application" do
- config = put_in(@config[:cache_static_manifest], {:phoenix, "../../../../test/fixtures/digest/compile/cache_manifest.json"})
+ config =
+ put_in(
+ @config[:cache_static_manifest],
+ {:phoenix, "../../../../test/fixtures/digest/compile/cache_manifest.json"}
+ )
assert Endpoint.config_change([{Endpoint, config}], []) == :ok
assert Endpoint.static_path("/foo.css") == "/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d"
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index f4d3c71e66..3b2b958db6 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -42,7 +42,6 @@ defmodule Phoenix.Endpoint.SupervisorTest do
end
defmodule ServerEndpoint do
- def init(:supervisor, config), do: {:ok, config}
def __sockets__(), do: []
end
@@ -92,42 +91,45 @@ defmodule Phoenix.Endpoint.SupervisorTest do
end
import ExUnit.CaptureLog
+
test "logs info if :http or :https configuration is set but not :server when running in release" do
- Logger.configure(level: :info)
# simulate running inside release
System.put_env("RELEASE_NAME", "phoenix-test")
- Application.put_env(:phoenix, ServerEndpoint, [server: false, http: [], https: []])
+ Application.put_env(:phoenix, ServerEndpoint, server: false, http: [], https: [])
+
assert capture_log(fn ->
- {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
- end) =~ "Configuration :server"
+ {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
+ end) =~ "Configuration :server"
+
+ Application.put_env(:phoenix, ServerEndpoint, server: false, http: [])
- Application.put_env(:phoenix, ServerEndpoint, [server: false, http: []])
assert capture_log(fn ->
- {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
- end) =~ "Configuration :server"
+ {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
+ end) =~ "Configuration :server"
+
+ Application.put_env(:phoenix, ServerEndpoint, server: false, https: [])
- Application.put_env(:phoenix, ServerEndpoint, [server: false, https: []])
assert capture_log(fn ->
- {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
- end) =~ "Configuration :server"
+ {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
+ end) =~ "Configuration :server"
+
+ Application.put_env(:phoenix, ServerEndpoint, server: false)
- Application.put_env(:phoenix, ServerEndpoint, [server: false])
refute capture_log(fn ->
- {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
- end) =~ "Configuration :server"
+ {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
+ end) =~ "Configuration :server"
+
+ Application.put_env(:phoenix, ServerEndpoint, server: true)
- Application.put_env(:phoenix, ServerEndpoint, [server: true])
refute capture_log(fn ->
- {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
- end) =~ "Configuration :server"
+ {:ok, {_, _children}} = Supervisor.init({:phoenix, ServerEndpoint, []})
+ end) =~ "Configuration :server"
Application.delete_env(:phoenix, ServerEndpoint)
- Logger.configure(level: :warning)
end
describe "watchers" do
defmodule WatchersEndpoint do
- def init(:supervisor, config), do: {:ok, config}
def __sockets__(), do: []
end
@@ -143,6 +145,16 @@ defmodule Phoenix.Endpoint.SupervisorTest do
end)
end
+ test "init/1 doesn't start watchers when `:server` config is true and `:watchers` is false" do
+ Application.put_env(:phoenix, WatchersEndpoint, server: true, watchers: false)
+ {:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
+
+ refute Enum.any?(children, fn
+ %{start: {Phoenix.Endpoint.Watcher, :start_link, _config}} -> true
+ _ -> false
+ end)
+ end
+
test "init/1 doesn't start watchers when `:server` config is false" do
Application.put_env(:phoenix, WatchersEndpoint, server: false, watchers: @watchers)
{:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
@@ -168,4 +180,44 @@ defmodule Phoenix.Endpoint.SupervisorTest do
end)
end
end
+
+ describe "origin & CSRF checks config" do
+ defmodule TestSocket do
+ @behaviour Phoenix.Socket.Transport
+ def child_spec(_), do: :ignore
+ def connect(_), do: {:ok, []}
+ def init(state), do: {:ok, state}
+ def handle_in(_, state), do: {:ok, state}
+ def handle_info(_, state), do: {:ok, state}
+ def terminate(_, _), do: :ok
+ end
+
+ defmodule SocketEndpoint do
+ use Phoenix.Endpoint, otp_app: :phoenix
+
+ socket "/ws", TestSocket, websocket: [check_csrf: false, check_origin: false]
+ end
+
+ Application.put_env(:phoenix, SocketEndpoint, [])
+
+ test "fails when CSRF and origin checks both disabled in transport" do
+ assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn ->
+ Supervisor.init({:phoenix, SocketEndpoint, []})
+ end
+ end
+
+ defmodule SocketEndpointOriginCheckDisabled do
+ use Phoenix.Endpoint, otp_app: :phoenix
+
+ socket "/ws", TestSocket, websocket: [check_csrf: false]
+ end
+
+ Application.put_env(:phoenix, SocketEndpointOriginCheckDisabled, check_origin: false)
+
+ test "fails when origin is disabled in endpoint config and CSRF disabled in transport" do
+ assert_raise ArgumentError, ~r/one of :check_origin and :check_csrf must be set/, fn ->
+ Supervisor.init({:phoenix, SocketEndpointOriginCheckDisabled, []})
+ end
+ end
+ end
end
diff --git a/test/phoenix/integration/websocket_socket_test.exs b/test/phoenix/integration/websocket_socket_test.exs
index 5bc4c5599d..edffb39400 100644
--- a/test/phoenix/integration/websocket_socket_test.exs
+++ b/test/phoenix/integration/websocket_socket_test.exs
@@ -1,10 +1,11 @@
Code.require_file("../../support/websocket_client.exs", __DIR__)
+Code.require_file("../../support/http_client.exs", __DIR__)
defmodule Phoenix.Integration.WebSocketTest do
use ExUnit.Case, async: true
import ExUnit.CaptureLog
- alias Phoenix.Integration.WebsocketClient
+ alias Phoenix.Integration.{HTTPClient, WebsocketClient}
alias __MODULE__.Endpoint
@moduletag :capture_log
@@ -104,6 +105,14 @@ defmodule Phoenix.Integration.WebSocketTest do
:ok
end
+ test "handles invalid upgrade requests" do
+ capture_log(fn ->
+ path = String.replace_prefix(@path, "ws", "http")
+ assert {:ok, %{body: body, status: 400}} = HTTPClient.request(:get, path, %{})
+ assert body =~ "'connection' header must contain 'upgrade'"
+ end)
+ end
+
test "refuses unallowed origins" do
capture_log(fn ->
headers = [{"origin", "https://example.com"}]
diff --git a/test/phoenix/logger_test.exs b/test/phoenix/logger_test.exs
index 2de2fef67e..9928761407 100644
--- a/test/phoenix/logger_test.exs
+++ b/test/phoenix/logger_test.exs
@@ -5,63 +5,75 @@ defmodule Phoenix.LoggerTest do
describe "filter_values/2 with discard strategy" do
test "in top level map" do
values = %{"foo" => "bar", "password" => "should_not_show"}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{"foo" => "bar", "password" => "[FILTERED]"}
+ %{"foo" => "bar", "password" => "[FILTERED]"}
end
test "when a map has secret key" do
values = %{"foo" => "bar", "map" => %{"password" => "should_not_show"}}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{"foo" => "bar", "map" => %{"password" => "[FILTERED]"}}
+ %{"foo" => "bar", "map" => %{"password" => "[FILTERED]"}}
end
test "when a list has a map with secret" do
values = %{"foo" => "bar", "list" => [%{"password" => "should_not_show"}]}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{"foo" => "bar", "list" => [%{"password" => "[FILTERED]"}]}
+ %{"foo" => "bar", "list" => [%{"password" => "[FILTERED]"}]}
end
test "does not filter structs" do
values = %{"foo" => "bar", "file" => %Plug.Upload{}}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{"foo" => "bar", "file" => %Plug.Upload{}}
+ %{"foo" => "bar", "file" => %Plug.Upload{}}
values = %{"foo" => "bar", "file" => %{__struct__: "s"}}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{"foo" => "bar", "file" => %{:__struct__ => "s"}}
+ %{"foo" => "bar", "file" => %{:__struct__ => "s"}}
end
test "does not fail on atomic keys" do
values = %{:foo => "bar", "password" => "should_not_show"}
+
assert Phoenix.Logger.filter_values(values, ["password"]) ==
- %{:foo => "bar", "password" => "[FILTERED]"}
+ %{:foo => "bar", "password" => "[FILTERED]"}
end
end
describe "filter_values/2 with keep strategy" do
test "discards values not specified in params" do
values = %{"foo" => "bar", "password" => "abc123", "file" => %Plug.Upload{}}
+
assert Phoenix.Logger.filter_values(values, {:keep, []}) ==
- %{"foo" => "[FILTERED]", "password" => "[FILTERED]", "file" => "[FILTERED]"}
+ %{"foo" => "[FILTERED]", "password" => "[FILTERED]", "file" => "[FILTERED]"}
end
test "keeps values that are specified in params" do
values = %{"foo" => "bar", "password" => "abc123", "file" => %Plug.Upload{}}
+
assert Phoenix.Logger.filter_values(values, {:keep, ["foo", "file"]}) ==
- %{"foo" => "bar", "password" => "[FILTERED]", "file" => %Plug.Upload{}}
+ %{"foo" => "bar", "password" => "[FILTERED]", "file" => %Plug.Upload{}}
end
test "keeps all values under keys that are kept" do
values = %{"foo" => %{"bar" => 1, "baz" => 2}}
+
assert Phoenix.Logger.filter_values(values, {:keep, ["foo"]}) ==
- %{"foo" => %{"bar" => 1, "baz" => 2}}
+ %{"foo" => %{"bar" => 1, "baz" => 2}}
end
test "only filters leaf values" do
values = %{"foo" => %{"bar" => 1, "baz" => 2}, "ids" => [1, 2]}
+
assert Phoenix.Logger.filter_values(values, {:keep, []}) ==
- %{"foo" => %{"bar" => "[FILTERED]", "baz" => "[FILTERED]"},
- "ids" => ["[FILTERED]", "[FILTERED]"]}
+ %{
+ "foo" => %{"bar" => "[FILTERED]", "baz" => "[FILTERED]"},
+ "ids" => ["[FILTERED]", "[FILTERED]"]
+ }
end
end
@@ -84,32 +96,27 @@ defmodule Phoenix.LoggerTest do
)
assert ExUnit.CaptureLog.capture_log(fn ->
- Plug.Telemetry.call(conn(:get, "/"), opts)
- end) =~ "[debug] GET /"
+ Plug.Telemetry.call(conn(:get, "/"), opts)
+ end) =~ "[debug] GET /"
assert ExUnit.CaptureLog.capture_log(fn ->
- Plug.Telemetry.call(conn(:get, "/warn"), opts)
- end) =~ ~r"\[warn(ing)?\] ?GET /warn"
+ Plug.Telemetry.call(conn(:get, "/warn"), opts)
+ end) =~ ~r"\[warn(ing)?\] ?GET /warn"
assert ExUnit.CaptureLog.capture_log(fn ->
- Plug.Telemetry.call(conn(:get, "/error/404"), opts)
- end) =~ "[error] GET /error/404"
+ Plug.Telemetry.call(conn(:get, "/error/404"), opts)
+ end) =~ "[error] GET /error/404"
assert ExUnit.CaptureLog.capture_log(fn ->
- Plug.Telemetry.call(conn(:get, "/any"), opts)
- end) =~ "[info] GET /any"
+ Plug.Telemetry.call(conn(:get, "/any"), opts)
+ end) =~ "[info] GET /any"
end
test "invokes log level from Plug.Telemetry" do
assert ExUnit.CaptureLog.capture_log(fn ->
- opts = Plug.Telemetry.init(event_prefix: [:phoenix, :endpoint])
- Plug.Telemetry.call(conn(:get, "/"), opts)
- end) =~ "[info] GET /"
-
- assert ExUnit.CaptureLog.capture_log(fn ->
- opts = Plug.Telemetry.init(event_prefix: [:phoenix, :endpoint], log: false)
- Plug.Telemetry.call(conn(:get, "/"), opts)
- end) == ""
+ opts = Plug.Telemetry.init(event_prefix: [:phoenix, :endpoint], log: :error)
+ Plug.Telemetry.call(conn(:get, "/"), opts)
+ end) =~ "[error] GET /"
end
end
end
diff --git a/test/phoenix/param_test.exs b/test/phoenix/param_test.exs
index 8b392cec46..58209105dc 100644
--- a/test/phoenix/param_test.exs
+++ b/test/phoenix/param_test.exs
@@ -32,6 +32,9 @@ defmodule Phoenix.ParamTest do
end
assert to_param(struct(Foo, id: 1)) == "1"
assert to_param(struct(Foo, id: "foo")) == "foo"
+ after
+ :code.purge(__MODULE__.Foo)
+ :code.delete(__MODULE__.Foo)
end
test "to_param for derivable structs without id" do
@@ -55,5 +58,10 @@ defmodule Phoenix.ParamTest do
assert_raise ArgumentError, msg, fn ->
to_param(struct(Bar, uuid: nil))
end
+ after
+ :code.purge(Module.concat(Phoenix.Param, __MODULE__.Bar))
+ :code.delete(Module.concat(Phoenix.Param, __MODULE__.Bar))
+ :code.purge(__MODULE__.Bar)
+ :code.delete(__MODULE__.Bar)
end
end
diff --git a/test/phoenix/router/routing_test.exs b/test/phoenix/router/routing_test.exs
index 8933d8782e..fa015fc8c5 100644
--- a/test/phoenix/router/routing_test.exs
+++ b/test/phoenix/router/routing_test.exs
@@ -67,7 +67,7 @@ defmodule Phoenix.Router.RoutingTest do
get "/no_log", SomePlug, [], log: false
get "/fun_log", SomePlug, [], log: {LogLevel, :log_level, []}
- get "/override-plug-name", SomePlug, :action, metadata: %{log_module: PlugOverride}
+ get "/override-plug-name", SomePlug, :action, metadata: %{mfa: {LogLevel, :log_level, 1}}
get "/users/:user_id/files/:id", UserController, :image
scope "/halt-plug" do
@@ -283,7 +283,7 @@ defmodule Phoenix.Router.RoutingTest do
test "overrides plug name that processes the route when set in metadata" do
assert capture_log(fn -> call(Router, :get, "/override-plug-name") end) =~
- "Processing with PlugOverride"
+ "Processing with Phoenix.Router.RoutingTest.LogLevel.log_level/1"
end
test "logs custom level when log is set to a 1-arity function" do
@@ -323,6 +323,7 @@ defmodule Phoenix.Router.RoutingTest do
end,
nil
)
+ on_exit(fn -> :telemetry.detach(test_name) end)
end
test "phoenix.router_dispatch.start and .stop are emitted on success" do
diff --git a/test/phoenix/router/scope_test.exs b/test/phoenix/router/scope_test.exs
index d3180813fa..54bb472693 100644
--- a/test/phoenix/router/scope_test.exs
+++ b/test/phoenix/router/scope_test.exs
@@ -12,7 +12,10 @@ defmodule Phoenix.Router.ScopedRoutingTest do
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 multi_host(conn, _params), do: text(conn, "multi_host request from #{conn.host}")
- def other_subdomain(conn, _params), do: text(conn, "other_subdomain request from #{conn.host}")
+
+ def other_subdomain(conn, _params),
+ do: text(conn, "other_subdomain request from #{conn.host}")
+
def proxy(conn, _) do
{controller, action} = conn.private.proxy_to
controller.call(conn, controller.init(action))
@@ -24,6 +27,11 @@ defmodule Phoenix.Router.ScopedRoutingTest do
def call(conn, _opts), do: conn
end
+ defmodule :erlang_like do
+ def init(action), do: action
+ def call(conn, action), do: Plug.Conn.send_resp(conn, 200, "Erlang like #{action}")
+ end
+
defmodule Router do
use Phoenix.Router
@@ -38,6 +46,7 @@ defmodule Phoenix.Router.ScopedRoutingTest do
end
scope "/admin" do
+ get "/erlang/like", :erlang_like, :action, as: :erlang_like
get "/users/:id", Api.V1.UserController, :show
end
@@ -60,7 +69,7 @@ defmodule Phoenix.Router.ScopedRoutingTest do
scope "/scoped", alias: false do
get "/noalias", Api.V1.UserController, :proxy,
- private: %{proxy_to: {scoped_alias(__MODULE__, Api.V1.UserController), :show}}
+ private: %{proxy_to: {scoped_alias(__MODULE__, Api.V1.UserController), :show}}
end
end
end
@@ -120,6 +129,12 @@ defmodule Phoenix.Router.ScopedRoutingTest do
assert conn.params["id"] == "13"
end
+ test "single scope with Erlang like route" do
+ conn = call(Router, :get, "/admin/erlang/like")
+ assert conn.status == 200
+ assert conn.resp_body == "Erlang like action"
+ end
+
test "double scope for single routes" do
conn = call(Router, :get, "/api/v1/users/1")
assert conn.status == 200
@@ -194,15 +209,17 @@ defmodule Phoenix.Router.ScopedRoutingTest do
end
test "bad host raises" do
- assert_raise ArgumentError, "expected router scope :host to be compile-time string or list of strings, got: nil", fn ->
- defmodule BadRouter do
- use Phoenix.Router
-
- scope "/admin", host: ["foo.", nil] do
- get "/users/:id", Api.V1.UserController, :baz_host
- end
- end
- end
+ assert_raise ArgumentError,
+ "expected router scope :host to be compile-time string or list of strings, got: nil",
+ fn ->
+ defmodule BadRouter do
+ use Phoenix.Router
+
+ scope "/admin", host: ["foo.", nil] do
+ get "/users/:id", Api.V1.UserController, :baz_host
+ end
+ end
+ end
end
test "private data in scopes" do
@@ -245,6 +262,7 @@ defmodule Phoenix.Router.ScopedRoutingTest do
defmodule SomeRouter do
use Phoenix.Router, otp_app: :phoenix
get "/foo", Router, []
+
scope "/another" do
resources :bar, Router, []
end
@@ -268,6 +286,7 @@ defmodule Phoenix.Router.ScopedRoutingTest do
assert_raise ArgumentError, ~r/`static` is a reserved route prefix/, fn ->
defmodule ErrorRouter do
use Phoenix.Router
+
scope "/" do
get "/", StaticController, :index
end
@@ -277,6 +296,7 @@ defmodule Phoenix.Router.ScopedRoutingTest do
assert_raise ArgumentError, ~r/`static` is a reserved route prefix/, fn ->
defmodule ErrorRouter do
use Phoenix.Router
+
scope "/" do
get "/", Api.V1.UserController, :show, as: :static
end
diff --git a/test/phoenix/socket/socket_test.exs b/test/phoenix/socket/socket_test.exs
index fb72c5dd20..d089a8de29 100644
--- a/test/phoenix/socket/socket_test.exs
+++ b/test/phoenix/socket/socket_test.exs
@@ -81,4 +81,49 @@ defmodule Phoenix.SocketTest do
assert socket.assigns[:abc] == :def
end
end
+
+ describe "drainer_spec/1" do
+ defmodule Endpoint do
+ use Phoenix.Endpoint, otp_app: :phoenix
+ end
+
+ defmodule DrainerSpecSocket do
+ use Phoenix.Socket
+
+ def id(_), do: "123"
+
+ def dynamic_drainer_config do
+ [
+ batch_size: 200,
+ batch_interval: 2_000,
+ shutdown: 20_000
+ ]
+ end
+ end
+
+ test "loads static drainer config" do
+ drainer_spec = [
+ batch_size: 100,
+ batch_interval: 1_000,
+ shutdown: 10_000
+ ]
+
+ assert DrainerSpecSocket.drainer_spec(drainer: drainer_spec, endpoint: Endpoint) ==
+ {Phoenix.Socket.PoolDrainer, {Endpoint, DrainerSpecSocket, drainer_spec}}
+ end
+
+ test "loads dynamic drainer config" do
+ drainer_spec = DrainerSpecSocket.dynamic_drainer_config()
+
+ assert DrainerSpecSocket.drainer_spec(
+ drainer: {DrainerSpecSocket, :dynamic_drainer_config, []},
+ endpoint: Endpoint
+ ) ==
+ {Phoenix.Socket.PoolDrainer, {Endpoint, DrainerSpecSocket, drainer_spec}}
+ end
+
+ test "returns ignore if drainer is set to false" do
+ assert DrainerSpecSocket.drainer_spec(drainer: false, endpoint: Endpoint) == :ignore
+ end
+ end
end
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index 2a3e3df9f7..05700342dd 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -276,7 +276,7 @@ defmodule Phoenix.Socket.TransportTest do
end
end
- describe "connect_info/3" do
+ describe "connect_info/4" do
defp load_connect_info(connect_info) do
[connect_info: connect_info] = Transport.load_config(connect_info: connect_info)
connect_info
@@ -330,5 +330,31 @@ defmodule Phoenix.Socket.TransportTest do
|> Transport.connect_info(Endpoint, connect_info)
end
+ test "loads the session when CSRF is disabled despite CSRF token not being provided" do
+ conn = conn(:get, "https://foo.com/") |> Endpoint.call([])
+ session_cookie = conn.cookies["_hello_key"]
+
+ connect_info = load_connect_info(session: {Endpoint, :session_config, []})
+
+ assert %{session: %{"from_session" => "123"}} =
+ conn(:get, "https://foo.com/")
+ |> put_req_cookie("_hello_key", session_cookie)
+ |> fetch_query_params()
+ |> Transport.connect_info(Endpoint, connect_info, check_csrf: false)
+ end
+
+ test "doesn't load session when an invalid CSRF token is provided" do
+ conn = conn(:get, "https://foo.com/") |> Endpoint.call([])
+ invalid_csrf_token = "some invalid CSRF token"
+ session_cookie = conn.cookies["_hello_key"]
+
+ connect_info = load_connect_info(session: {Endpoint, :session_config, []})
+
+ assert %{session: nil} =
+ conn(:get, "https://foo.com/", _csrf_token: invalid_csrf_token)
+ |> put_req_cookie("_hello_key", session_cookie)
+ |> fetch_query_params()
+ |> Transport.connect_info(Endpoint, connect_info)
+ end
end
end
diff --git a/test/phoenix/socket/v1_json_serializer_test.exs b/test/phoenix/socket/v1_json_serializer_test.exs
index 7f0cfc9eb7..62652d3f26 100644
--- a/test/phoenix/socket/v1_json_serializer_test.exs
+++ b/test/phoenix/socket/v1_json_serializer_test.exs
@@ -7,8 +7,6 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
@serializer V1.JSONSerializer
@v1_msg_json "{\"event\":\"e\",\"payload\":\"m\",\"ref\":null,\"topic\":\"t\"}"
@v1_bad_json "[null,null,\"t\",\"e\",{\"m\":1}]"
- @v1_reply_json "{\"event\":\"phx_reply\",\"payload\":{\"response\":null,\"status\":null},\"ref\":\"null\",\"topic\":\"t\"}"
- @v1_fastlane_json "{\"event\":\"e\",\"payload\":\"m\",\"ref\":null,\"topic\":\"t\"}"
def encode!(serializer, msg) do
{:socket_push, :text, encoded} = serializer.encode!(msg)
@@ -24,12 +22,26 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
test "encode!/1 encodes `Phoenix.Socket.Message` as JSON" do
msg = %Message{topic: "t", event: "e", payload: "m"}
- assert encode!(@serializer, msg) == @v1_msg_json
+ encoded = encode!(@serializer, msg)
+
+ assert Jason.decode!(encoded) == %{
+ "event" => "e",
+ "payload" => "m",
+ "ref" => nil,
+ "topic" => "t"
+ }
end
test "encode!/1 encodes `Phoenix.Socket.Reply` as JSON" do
msg = %Reply{topic: "t", ref: "null"}
- assert encode!(@serializer, msg) == @v1_reply_json
+ encoded = encode!(@serializer, msg)
+
+ assert Jason.decode!(encoded) == %{
+ "event" => "phx_reply",
+ "payload" => %{"response" => nil, "status" => nil},
+ "ref" => "null",
+ "topic" => "t"
+ }
end
test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
@@ -47,6 +59,13 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
test "fastlane!/1 encodes a broadcast into a message as JSON" do
msg = %Broadcast{topic: "t", event: "e", payload: "m"}
- assert fastlane!(@serializer, msg) == @v1_fastlane_json
+ encoded = fastlane!(@serializer, msg)
+
+ assert Jason.decode!(encoded) == %{
+ "event" => "e",
+ "payload" => "m",
+ "ref" => nil,
+ "topic" => "t"
+ }
end
end
diff --git a/test/phoenix/socket/v2_json_serializer_test.exs b/test/phoenix/socket/v2_json_serializer_test.exs
index 4723eb727a..c534ff47d3 100644
--- a/test/phoenix/socket/v2_json_serializer_test.exs
+++ b/test/phoenix/socket/v2_json_serializer_test.exs
@@ -4,7 +4,6 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
@serializer V2.JSONSerializer
@v2_fastlane_json "[null,null,\"t\",\"e\",{\"m\":1}]"
- @v2_reply_json "[null,null,\"t\",\"phx_reply\",{\"response\":{\"m\":1},\"status\":null}]"
@v2_msg_json "[null,null,\"t\",\"e\",{\"m\":1}]"
@client_push <<
@@ -101,7 +100,15 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
test "encode!/1 encodes `Phoenix.Socket.Reply` as JSON" do
msg = %Reply{topic: "t", payload: %{m: 1}}
- assert encode!(@serializer, msg) == @v2_reply_json
+ encoded = encode!(@serializer, msg)
+
+ assert Jason.decode!(encoded) == [
+ nil,
+ nil,
+ "t",
+ "phx_reply",
+ %{"response" => %{"m" => 1}, "status" => nil}
+ ]
end
test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
@@ -225,7 +232,6 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
payload: {:binary, <<101, 102, 103>>}
})
end
-
end
end
diff --git a/test/phoenix/test/channel_test.exs b/test/phoenix/test/channel_test.exs
index fd7078a0fa..de69baf311 100644
--- a/test/phoenix/test/channel_test.exs
+++ b/test/phoenix/test/channel_test.exs
@@ -290,6 +290,12 @@ defmodule Phoenix.Test.ChannelTest do
assert_reply ref, :ok, %{"resp" => "foo"}
end
+ test "works with list data structures" do
+ {:ok, _, socket} = join(socket(UserSocket), Channel, "foo:ok")
+ ref = push(socket, "reply", %{req: [%{bar: "baz"}, %{bar: "foo"}]})
+ assert_reply ref, :ok, %{"resp" => [%{"bar" => "baz"}, %{"bar" => "foo"}]}
+ end
+
test "receives async replies" do
{:ok, _, socket} = join(socket(UserSocket), Channel, "foo:ok")
diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs
index 1041794e13..b6e1e1c30c 100644
--- a/test/phoenix/test/conn_test.exs
+++ b/test/phoenix/test/conn_test.exs
@@ -180,6 +180,14 @@ defmodule Phoenix.Test.ConnTest do
assert conn.host == "localhost"
end
+ test "remote_ip is persisted" do
+ conn =
+ %Plug.Conn{build_conn(:get, "http://localhost/", nil) | remote_ip: {192, 168, 0, 1}}
+ |> recycle()
+
+ assert conn.remote_ip == {192, 168, 0, 1}
+ end
+
test "cookies are persisted" do
conn =
build_conn()
diff --git a/test/phoenix/token_test.exs b/test/phoenix/token_test.exs
index fbb81b5a28..cbc708baf7 100644
--- a/test/phoenix/token_test.exs
+++ b/test/phoenix/token_test.exs
@@ -90,12 +90,6 @@ defmodule Phoenix.TokenTest do
assert signed1 != signed2
end
- test "passes key_length options to key generator" do
- signed1 = Token.sign(conn(), "id", 1, signed_at: 0, key_length: 16)
- signed2 = Token.sign(conn(), "id", 1, signed_at: 0, key_length: 32)
- assert signed1 != signed2
- end
-
test "key defaults" do
signed1 = Token.sign(conn(), "id", 1, signed_at: 0)
@@ -185,12 +179,6 @@ defmodule Phoenix.TokenTest do
signed2 = Token.encrypt(conn(), "secret", 1, signed_at: 0, key_digest: :sha512)
assert signed1 != signed2
end
-
- test "passes key_length options to key generator" do
- signed1 = Token.encrypt(conn(), "secret", 1, signed_at: 0, key_length: 16)
- signed2 = Token.encrypt(conn(), "secret", 1, signed_at: 0, key_length: 32)
- assert signed1 != signed2
- end
end
defp socket() do
diff --git a/test/phoenix/verified_routes_test.exs b/test/phoenix/verified_routes_test.exs
index 67ac0fb27a..52723f2ae1 100644
--- a/test/phoenix/verified_routes_test.exs
+++ b/test/phoenix/verified_routes_test.exs
@@ -187,6 +187,9 @@ defmodule Phoenix.VerifiedRoutesTest do
end)
assert warnings == ""
+ after
+ :code.purge(__MODULE__.Hash)
+ :code.delete(__MODULE__.Hash)
end
test "unverified_path" do
@@ -210,6 +213,9 @@ defmodule Phoenix.VerifiedRoutesTest do
def test, do: ~p"/posts/1"foo
end
end
+ after
+ :code.purge(__MODULE__.LeftOver)
+ :code.delete(__MODULE__.LeftOver)
end
test "~p raises on dynamic interpolation" do
@@ -221,6 +227,9 @@ defmodule Phoenix.VerifiedRoutesTest do
def test, do: ~p"/posts/#{1}#{2}"
end
end
+ after
+ :code.purge(__MODULE__.DynamicDynamic)
+ :code.delete(__MODULE__.DynamicDynamic)
end
test "~p raises when not prefixed by /" do
@@ -235,6 +244,9 @@ defmodule Phoenix.VerifiedRoutesTest do
def test, do: ~p"posts/1"
end
end
+ after
+ :code.purge(__MODULE__.SigilPPrefix)
+ :code.delete(__MODULE__.SigilPPrefix)
end
test "path arities" do
@@ -262,6 +274,9 @@ defmodule Phoenix.VerifiedRoutesTest do
def test, do: path(%URI{}, "/posts/1")
end
end
+ after
+ :code.purge(__MODULE__.MissingPathPrefix)
+ :code.delete(__MODULE__.MissingPathPrefix)
end
test "url raises when non ~p is passed" do
@@ -271,6 +286,9 @@ defmodule Phoenix.VerifiedRoutesTest do
def test, do: url("/posts/1")
end
end
+ after
+ :code.purge(__MODULE__.MissingURLPrefix)
+ :code.delete(__MODULE__.MissingURLPrefix)
end
test "static_integrity" do
@@ -334,6 +352,9 @@ defmodule Phoenix.VerifiedRoutesTest do
end
end
end
+ after
+ :code.purge(__MODULE__.InvalidQuery)
+ :code.delete(__MODULE__.InvalidQuery)
end
test "~p with complex ids" do
@@ -496,7 +517,14 @@ defmodule Phoenix.VerifiedRoutesTest do
warnings = String.replace(warnings, ~r/(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]/, "")
assert warnings =~
- "warning: no route path for Phoenix.VerifiedRoutesTest.Router matches \"/router_forward/warn\"\n test/phoenix/verified_routes_test.exs:#{line}: Phoenix.VerifiedRoutesTest.Forwards.test/0\n\n"
+ "warning: no route path for Phoenix.VerifiedRoutesTest.Router matches \"/router_forward/warn\""
+
+ assert warnings =~
+ ~r"test/phoenix/verified_routes_test.exs:#{line}:(\d+:)? Phoenix.VerifiedRoutesTest.Forwards.test/0"
+
+ after
+ :code.purge(__MODULE__.Forwards)
+ :code.delete(__MODULE__.Forwards)
end
test "~p warns on unmatched path" do
@@ -521,6 +549,9 @@ defmodule Phoenix.VerifiedRoutesTest do
assert warnings =~
~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/unknown/#{123}"|
+ after
+ :code.purge(__MODULE__.Unmatched)
+ :code.delete(__MODULE__.Unmatched)
end
test "~p warns on warn_on_verify: true route" do
@@ -535,19 +566,27 @@ defmodule Phoenix.VerifiedRoutesTest do
assert warnings =~
~s|no route path for Phoenix.VerifiedRoutesTest.Router matches "/should-warn/foobar"|
+ after
+ :code.purge(__MODULE__.VerifyFalse)
+ :code.delete(__MODULE__.VerifyFalse)
end
test "~p does not warn if route without warn_on_verify: true matches first" do
warnings =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
defmodule VerifyFalseTrueMatchesFirst do
- use Phoenix.VerifiedRoutes, endpoint: unquote(@endpoint), router: CatchAllWarningRouter
+ use Phoenix.VerifiedRoutes,
+ endpoint: unquote(@endpoint),
+ router: CatchAllWarningRouter
def test, do: ~p"/"
end
end)
assert warnings == ""
+ after
+ :code.purge(__MODULE__.VerifyFalseTrueMatchesFirst)
+ :code.delete(__MODULE__.VerifyFalseTrueMatchesFirst)
end
end
end
diff --git a/test/support/router_helper.exs b/test/support/router_helper.exs
index d306b31374..9cbf4be231 100644
--- a/test/support/router_helper.exs
+++ b/test/support/router_helper.exs
@@ -11,7 +11,8 @@ defmodule RouterHelper do
defmacro __using__(_) do
quote do
- use Plug.Test
+ import Plug.Test
+ import Plug.Conn
import RouterHelper
end
end
diff --git a/test/test_helper.exs b/test/test_helper.exs
index 3202405518..ae30db4b73 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -26,7 +26,7 @@ assert_timeout = String.to_integer(
)
excludes =
- if Version.match?(System.version(), "~> 1.14") do
+ if Version.match?(System.version(), "~> 1.15") do
[]
else
[:mix_phx_new]