From 666b4d4f79bd9faca42ac0e680abf1be16ebcf7a Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Fri, 18 Sep 2026 11:11:53 +1200 Subject: [PATCH 1/3] fix(serializer): make list encoding linear instead of quadratic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `encode_data/5` for a list built the result with `acc ++ [encoded_data]` inside `Enum.map_reduce/3`, copying the accumulator on every element. Serializing a collection was therefore O(n²) in the number of resources and dominated response time for large index endpoints: 77k resources took ~9.6s in the append alone (~25-30s end to end in CoreGateway with links), which is what pushed `GET /api/save/:id/relationships/assets` past Maxwell's 60s timeout for large Edit style trainings (Sentry MAXWELL-7A7T). Map each element and `Enum.unzip/1` the `{to_include, encoded}` pairs instead. Same output, same order. resources before after 10,000 0.15s 0.01s 40,000 2.69s 0.07s 77,011 9.61s 0.17s The existing list test serialized three identical items and so could not catch an ordering regression; it now serializes 50 distinct posts and asserts the order of `data` and of `included`, plus dedup of a shared comment author. Co-Authored-By: Claude Opus 5 --- lib/jsonapi/serializer.ex | 7 ++--- test/jsonapi/serializer_test.exs | 48 ++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/jsonapi/serializer.ex b/lib/jsonapi/serializer.ex index c76065e4..3324c9f2 100644 --- a/lib/jsonapi/serializer.ex +++ b/lib/jsonapi/serializer.ex @@ -50,10 +50,9 @@ defmodule JSONAPI.Serializer do def encode_data(_view, nil, _conn, _query_includes, _options), do: {[], nil} def encode_data(view, data, conn, query_includes, options) when is_list(data) do - Enum.map_reduce(data, [], fn d, acc -> - {to_include, encoded_data} = encode_data(view, d, conn, query_includes, options) - {to_include, acc ++ [encoded_data]} - end) + data + |> Enum.map(&encode_data(view, &1, conn, query_includes, options)) + |> Enum.unzip() end def encode_data(view, data, conn, query_includes, options) do diff --git a/test/jsonapi/serializer_test.exs b/test/jsonapi/serializer_test.exs index c6de3131..4a8a5e39 100644 --- a/test/jsonapi/serializer_test.exs +++ b/test/jsonapi/serializer_test.exs @@ -216,39 +216,45 @@ defmodule JSONAPI.SerializerTest do assert Enum.count(encoded[:included]) == 4 end - test "serialize handles a list" do - data = %{ - id: 1, - text: "Hello", - body: "Hello world", - author: %{id: 2, username: "jason"}, - best_comments: [ - %{id: 5, text: "greatest comment ever", user: %{id: 4, username: "jack"}}, - %{id: 6, text: "not so great", user: %{id: 2, username: "jason"}} - ] - } - - data_list = [data, data, data] + test "serialize handles a list, preserving order and deduplicating includes" do + data_list = + Enum.map(1..50, fn i -> + %{ + id: i, + text: "post #{i}", + body: "body #{i}", + author: %{id: 100 + i, username: "u#{i}"}, + best_comments: [ + %{id: 200 + i, text: "comment #{i}", user: %{id: 4, username: "jack"}} + ] + } + end) conn = Plug.Conn.fetch_query_params(%Plug.Conn{}) encoded = Serializer.serialize(PostView, data_list, conn) - assert Enum.count(encoded[:data]) == 3 + assert Enum.count(encoded[:data]) == 50 - Enum.each(encoded[:data], fn enc -> + Enum.zip(encoded[:data], data_list) + |> Enum.each(fn {enc, data} -> assert enc[:id] == PostView.id(data) assert enc[:type] == PostView.type() - - attributes = enc[:attributes] - assert attributes[:text] == data[:text] - assert attributes[:body] == data[:body] - + assert enc[:attributes][:text] == data[:text] + assert enc[:attributes][:body] == data[:body] assert enc[:links][:self] == PostView.url_for(data, conn) assert map_size(enc[:relationships]) == 2 end) - assert Enum.count(encoded[:included]) == 4 + # 50 authors + 50 comments + the one shared comment author (included once). + assert Enum.count(encoded[:included]) == 101 + + included_ids = fn type -> + encoded[:included] |> Enum.filter(&(&1[:type] == type)) |> Enum.map(& &1[:id]) + end + + assert included_ids.("comment") == Enum.map(1..50, &"#{200 + &1}") + assert included_ids.("user") == ["101", "4" | Enum.map(2..50, &"#{100 + &1}")] end test "serialize handles an empty relationship" do From b5971b45e3e07695498c57a6f036134f361b54fe Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Fri, 18 Sep 2026 11:11:54 +1200 Subject: [PATCH 2/3] test(serializer): property tests for collection serialization Adds stream_data (test only) and three properties pinning the list clause of `encode_data/5` to the single-resource clause, for arbitrary lists of resources with nested includes: - `data` equals each element serialized on its own, in order - `included` equals `flatten_included/1` over each element's `included` - splitting the list into chunks and concatenating gives the same result The id space is deliberately small, and users/comments are drawn from fixed pools, so lists routinely contain repeated resources and structurally identical includes and the dedup path is exercised. Reversing the list in the fix makes all three fail within a few runs. Capped at 25 runs each. The module is not async: the views read process-global Application env that other test modules mutate. Co-Authored-By: Claude Opus 5 --- mix.exs | 3 +- mix.lock | 1 + test/jsonapi/serializer_property_test.exs | 120 ++++++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 test/jsonapi/serializer_property_test.exs diff --git a/mix.exs b/mix.exs index 88afc18e..f8e3121c 100644 --- a/mix.exs +++ b/mix.exs @@ -53,7 +53,8 @@ defmodule JSONAPI.Mixfile do {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:phoenix, "~> 1.3", only: :test}, {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, - {:git_ops, "~> 2.2", only: ~w[dev test]a} + {:git_ops, "~> 2.2", only: ~w[dev test]a}, + {:stream_data, "~> 1.1", only: :test} ] end diff --git a/mix.lock b/mix.lock index 8ae95dea..25520365 100644 --- a/mix.lock +++ b/mix.lock @@ -21,6 +21,7 @@ "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"}, + "stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, diff --git a/test/jsonapi/serializer_property_test.exs b/test/jsonapi/serializer_property_test.exs new file mode 100644 index 00000000..8185d449 --- /dev/null +++ b/test/jsonapi/serializer_property_test.exs @@ -0,0 +1,120 @@ +defmodule JSONAPI.SerializerPropertyTest do + @moduledoc """ + Properties of `JSONAPI.Serializer.serialize/5` for collections. + + The list clause of `encode_data/5` must behave exactly like serializing + each element on its own and concatenating: same resources, same order, + same `included`. These pin that down for arbitrary lists, including the + degenerate cases (empty list, duplicate resources, duplicate includes) + that a hand-written example test tends to miss. + """ + + # Not async: the views read process-global Application env + # (:remove_links, :field_transformation, :namespace, ...) that other test + # modules mutate with put_env/delete_env. + use ExUnit.Case, async: false + use ExUnitProperties + + alias JSONAPI.Serializer + + @max_runs 25 + + defmodule UserView do + use JSONAPI.View, type: "user" + + def fields, do: [:username] + end + + defmodule CommentView do + use JSONAPI.View, type: "comment" + + def fields, do: [:text] + def relationships, do: [user: {JSONAPI.SerializerPropertyTest.UserView, :include}] + end + + defmodule PostView do + use JSONAPI.View, type: "post" + + def fields, do: [:title, :body] + def meta(data, _conn), do: %{title_length: String.length(data.title)} + + def relationships do + [ + author: {JSONAPI.SerializerPropertyTest.UserView, :include}, + comments: {JSONAPI.SerializerPropertyTest.CommentView, :include} + ] + end + end + + # Small id space on purpose so that lists routinely contain repeated + # resources; `included` must still be deduplicated in first-seen order. + defp id, do: integer(1..20) + + # Users and comments are drawn from small fixed pools rather than generated + # with random attributes, so that structurally identical includes recur and + # the dedup path in `flatten_included/1` is actually exercised. + @users for i <- 1..5, do: %{id: i, username: "user#{i}"} + @comments for i <- 1..8, u <- @users, do: %{id: i, text: "comment #{i}", user: u} + + defp user, do: member_of(@users) + defp comment, do: member_of(@comments) + + defp post do + gen all( + id <- id(), + title <- string(:printable, max_length: 20), + body <- string(:printable, max_length: 40), + author <- one_of([constant(nil), user()]), + comments <- list_of(comment(), max_length: 4) + ) do + %{id: id, title: title, body: body, author: author, comments: comments} + end + end + + defp conn, do: Plug.Conn.fetch_query_params(%Plug.Conn{host: "example.com"}) + + defp expected_included(posts, conn) do + posts + |> Enum.map(&Serializer.serialize(PostView, &1, conn)[:included]) + |> Serializer.flatten_included() + end + + property "serializing a list yields each element's resource, in order" do + check all(posts <- list_of(post(), max_length: 30), max_runs: @max_runs) do + conn = conn() + encoded = Serializer.serialize(PostView, posts, conn) + + expected = Enum.map(posts, &Serializer.serialize(PostView, &1, conn)[:data]) + + assert encoded[:data] == expected + end + end + + property "included for a list is the deduplicated, in-order union of each element's included" do + check all(posts <- list_of(post(), max_length: 30), max_runs: @max_runs) do + conn = conn() + encoded = Serializer.serialize(PostView, posts, conn) + + assert encoded[:included] == expected_included(posts, conn) + end + end + + property "serializing a list is invariant to how it is split into chunks" do + check all( + posts <- list_of(post(), min_length: 1, max_length: 30), + chunk <- integer(1..30), + max_runs: @max_runs + ) do + conn = conn() + whole = Serializer.serialize(PostView, posts, conn) + + chunked = + Enum.map(Enum.chunk_every(posts, chunk), &Serializer.serialize(PostView, &1, conn)) + + assert whole[:data] == Enum.flat_map(chunked, & &1[:data]) + + assert whole[:included] == + chunked |> Enum.map(& &1[:included]) |> Serializer.flatten_included() + end + end +end From dd5b0af65ce6c87784bea857836e1825105e057b Mon Sep 17 00:00:00 2001 From: Paul Oliver Date: Fri, 18 Sep 2026 10:18:34 +1200 Subject: [PATCH 3/3] chore(deps): clear hex.audit advisories in the dev/test lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mix hex.audit` (the CI `audit` job, which `is_releasable` depends on) has not run against master since 2025-01 and now fails on advisories published since. None of the flagged packages are runtime deps of a consumer — plug is a library requirement (`~> 1.10`, unchanged) that the consuming app pins itself, and phoenix/earmark are dev/test only — so this only touches the lockfile: - plug 1.16.1 -> 1.17.4 (EEF-CVE-2026-54892, -8468, -56814, -56813) Kept on the 1.17 line: plug 1.19+ needs Elixir >= 1.15 and CI runs 1.14 (1.20.3 fails to compile there). Same line CoreGateway runs. - phoenix 1.7.18 -> 1.7.24 (EEF-CVE-2026-32689, -56811, -56812) - earmark removed. It was an orphan `only: :dev` dep: nothing in lib/ or mix.exs references Earmark, and ex_doc uses earmark_parser. It is retired upstream and carries EEF-CVE-2026-48591. `mix hex.audit`: "No retired or security advisory packages found", so no allowlist is needed. Tests, credo and the formatter are unchanged. Co-Authored-By: Claude Opus 5 --- mix.exs | 1 - mix.lock | 17 ++++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/mix.exs b/mix.exs index f8e3121c..c280762e 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,6 @@ defmodule JSONAPI.Mixfile do {:plug, "~> 1.10"}, {:jason, "~> 1.0", optional: true}, {:ex_doc, "~> 0.20", only: ~w[dev test]a}, - {:earmark, ">= 0.0.0", only: :dev}, {:credo, "~> 1.4", only: [:dev, :test], runtime: false}, {:phoenix, "~> 1.3", only: :test}, {:dialyxir, "~> 1.1.0", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 25520365..3e08aba1 100644 --- a/mix.lock +++ b/mix.lock @@ -3,26 +3,25 @@ "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, "credo": {:hex, :credo, "1.7.11", "d3e805f7ddf6c9c854fd36f089649d7cf6ba74c42bc3795d587814e3c9847102", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "56826b4306843253a66e47ae45e98e7d284ee1f95d53d1612bb483f88a8cf219"}, "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, - "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, "earmark_parser": {:hex, :earmark_parser, "1.4.42", "f23d856f41919f17cd06a493923a722d87a2d684f143a1e663c04a2b93100682", [:mix], [], "hexpm", "6915b6ca369b5f7346636a2f41c6a6d78b5af419d61a611079189233358b8b8b"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [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", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, "git_ops": {:hex, :git_ops, "2.6.3", "38c6e381b8281b86e2911fa39bea4eab2d171c86d7428786566891efb73b68c3", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a81cb6c6a2a026a4d48cb9a2e1dfca203f9283a3a70aa0c7bc171970c44f23f8"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix": {:hex, :phoenix, "1.7.24", "4cb76aed6d3f03878893769020e97c4394ee95b62b2b2d6313c20f66d7d37baa", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "a283a9d91517116166244fdd8ed8b405c142774dc39b4a4047d179cecca9c09f"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.3.0", "03916bfbc31a5121945b3cfffe5aec647a5c97fe1dc172a319b94428562359c9", [:mix], [], "hexpm", "eec7be6e9cf02e2551d389b558402d6c637cd3973796326e7ba4bb03c6b2e91d"}, "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"}, + "plug": {:hex, :plug, "1.17.4", "5b9972f4ea7e71b9d235a91eadb2b2079941ef2b4547f9ee6a87c68d50ddca4a", [: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", "39645c7e8cf5bd812d9df702d8c954e86e42c98791a49ea296c6433f69579a94"}, + "plug_crypto": {:hex, :plug_crypto, "2.2.0", "144014737daaf485407f5ed77daeaad74d651b216a28c87543f8cc7043f8efc8", [:mix], [], "hexpm", "83a95744ab1c75876542b6fab135fcc176280e0f301a111c1f757fddcec95d2c"}, "stream_data": {:hex, :stream_data, "1.4.0", "026f929db613aabea6208012ae9b8970d3fd5f88b3bdf26831bc536f98c42036", [:mix], [], "hexpm", "2b0ee3a340dcce1c8cf6302a763ee757d1e01c54d6e16d9069062509d68b1dc9"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [: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", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [: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", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, }