Skip to content

Commit 65bdbc4

Browse files
authored
deduplicate stream inserts (#3599)
Also adjusts stream_insert to prepend the insert instead of expensively appending it. Fixes #2689. Closes #3596. Closes #3598.
1 parent e22d22e commit 65bdbc4

File tree

2 files changed

+32
-5
lines changed

2 files changed

+32
-5
lines changed

lib/phoenix_live_view/live_stream.ex

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ defmodule Phoenix.LiveView.LiveStream do
2020
"stream :dom_id must return a function which accepts each item, got: #{inspect(dom_id)}"
2121
end
2222

23-
items_list = for item <- items, do: {dom_id.(item), -1, item, opts[:limit]}
23+
# We need to go through the items one time to map them into the proper insert tuple format.
24+
# Conveniently, we reverse the list in this pass, which we need to in order to be consistent
25+
# with manually calling stream_insert multiple times, as stream_insert prepends.
26+
items_list =
27+
for item <- items, reduce: [] do
28+
items -> [{dom_id.(item), -1, item, opts[:limit]} | items]
29+
end
2430

2531
%LiveStream{
2632
ref: ref,
@@ -61,16 +67,30 @@ defmodule Phoenix.LiveView.LiveStream do
6167
def insert_item(%LiveStream{} = stream, item, at, limit) do
6268
item_id = stream.dom_id.(item)
6369

64-
%{stream | inserts: stream.inserts ++ [{item_id, at, item, limit}]}
70+
%{stream | inserts: [{item_id, at, item, limit} | stream.inserts]}
6571
end
6672

6773
defimpl Enumerable, for: LiveStream do
6874
def count(%LiveStream{inserts: inserts}), do: {:ok, length(inserts)}
6975

7076
def member?(%LiveStream{}, _item), do: raise(RuntimeError, "not implemented")
7177

72-
def reduce(%LiveStream{inserts: inserts} = stream, acc, fun) do
78+
def reduce(%LiveStream{} = stream, acc, fun) do
7379
if stream.consumable? do
80+
# the inserts are stored in reverse insert order, so we need to reverse them
81+
# before rendering; we also remove duplicates to only use the most recent
82+
# inserts, which, as the items are reversed, are first
83+
{inserts, _} =
84+
for {id, _, _, _} = insert <- stream.inserts, reduce: {[], MapSet.new()} do
85+
{inserts, ids} ->
86+
if MapSet.member?(ids, id) do
87+
# skip duplicates
88+
{inserts, ids}
89+
else
90+
{[insert | inserts], MapSet.put(ids, id)}
91+
end
92+
end
93+
7494
do_reduce(inserts, acc, fun)
7595
else
7696
raise ArgumentError, """

test/phoenix_live_view/live_stream_test.exs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ defmodule Phoenix.LiveView.LiveStreamTest do
1313

1414
test "default dom_id" do
1515
stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], [])
16-
assert stream.inserts == [{"users-1", -1, %{id: 1}, nil}, {"users-2", -1, %{id: 2}, nil}]
16+
assert stream.inserts == [{"users-2", -1, %{id: 2}, nil}, {"users-1", -1, %{id: 1}, nil}]
1717
end
1818

1919
test "custom dom_id" do
2020
stream = LiveStream.new(:users, 0, [%{name: "u1"}, %{name: "u2"}], dom_id: &"u-#{&1.name}")
21-
assert stream.inserts == [{"u-u1", -1, %{name: "u1"}, nil}, {"u-u2", -1, %{name: "u2"}, nil}]
21+
assert stream.inserts == [{"u-u2", -1, %{name: "u2"}, nil}, {"u-u1", -1, %{name: "u1"}, nil}]
2222
end
2323

2424
test "default dom_id without struct or map with :id" do
@@ -28,4 +28,11 @@ defmodule Phoenix.LiveView.LiveStreamTest do
2828
LiveStream.new(:users, 0, [%{user_id: 1}, %{user_id: 2}], [])
2929
end
3030
end
31+
32+
test "inserts are deduplicated (last insert wins)" do
33+
assert stream = LiveStream.new(:users, 0, [%{id: 1}, %{id: 2}], [])
34+
stream = LiveStream.insert_item(stream, %{id: 2, updated: true}, -1, nil)
35+
stream = %{stream | consumable?: true}
36+
assert Enum.to_list(stream) == [{"users-1", %{id: 1}}, {"users-2", %{id: 2, updated: true}}]
37+
end
3138
end

0 commit comments

Comments
 (0)