Skip to content

Commit 3369bef

Browse files
authored
Fix pending link update race condition (#3710)
applyPendingDiffs had a race condition where updates were silently dropped if when trying to apply them there was already a new pending link. We fix this by skipping applyPendingDiffs when a link is still pending. Bug originally reported by @Gazler: https://gist.github.com/Gazler/80dac42d3134ff55219853f1cb105e78
1 parent 331eb53 commit 3369bef

File tree

4 files changed

+79
-0
lines changed

4 files changed

+79
-0
lines changed

assets/js/phoenix_live_view/view.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,12 @@ export default class View {
754754
}
755755

756756
applyPendingUpdates(){
757+
// prevent race conditions where we might still be pending a new
758+
// navigation after applying the current one;
759+
// if we call update and a pendingDiff is not applied, it would
760+
// be silently dropped otherwise, as update would push it back to
761+
// pendingDiffs, but we clear it immediately after
762+
if(this.liveSocket.hasPendingLink() && this.root.isMain()){ return }
757763
this.pendingDiffs.forEach(({diff, events}) => this.update(diff, events))
758764
this.pendingDiffs = []
759765
this.eachChild(child => child.applyPendingUpdates())
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
defmodule Phoenix.LiveViewTest.E2E.Issue3709Live do
2+
# https://github.com/phoenixframework/phoenix_live_view/issues/3709
3+
use Phoenix.LiveView
4+
5+
defmodule SomeComponent do
6+
use Phoenix.LiveComponent
7+
8+
def render(assigns) do
9+
~H"""
10+
<div>
11+
Hello
12+
</div>
13+
"""
14+
end
15+
end
16+
17+
def mount(_params, _session, socket) do
18+
{:ok, assign(socket, id: nil)}
19+
end
20+
21+
def handle_params(params, _, socket) do
22+
{:noreply, assign(socket, :id, params["id"])}
23+
end
24+
25+
def render(assigns) do
26+
~H"""
27+
<ul>
28+
<li :for={i <- 1..10}>
29+
<.link patch={"/issues/3709/#{i}"}>Link {i}</.link>
30+
</li>
31+
</ul>
32+
<div>
33+
<.live_component module={SomeComponent} id={"user-#{@id}"} /> id: {@id}
34+
<div>
35+
Click the button, then click any link.
36+
<button onclick="document.querySelectorAll('li a').forEach((x) => x.click())">
37+
Break Stuff
38+
</button>
39+
</div>
40+
</div>
41+
"""
42+
end
43+
end

test/e2e/test_helper.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ defmodule Phoenix.LiveViewTest.E2E.Router do
163163
live "/3656", Issue3656Live
164164
live "/3658", Issue3658Live
165165
live "/3684", Issue3684Live
166+
live "/3709", Issue3709Live
167+
live "/3709/:id", Issue3709Live
166168
end
167169
end
168170

test/e2e/tests/issues/3709.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const {test, expect} = require("../../test-fixtures")
2+
const {syncLV} = require("../../utils")
3+
4+
// https://github.com/phoenixframework/phoenix_live_view/issues/3709
5+
test("pendingDiffs don't race with navigation", async ({page}) => {
6+
const logs = []
7+
page.on("console", (e) => logs.push(e.text()))
8+
const errors = []
9+
page.on("pageerror", (err) => errors.push(err))
10+
11+
await page.goto("/issues/3709/1")
12+
await syncLV(page)
13+
await expect(page.locator("body")).toContainText("id: 1")
14+
15+
await page.getByRole("button", {name: "Break Stuff"}).click()
16+
await syncLV(page)
17+
18+
expect(logs).not.toEqual(expect.arrayContaining([expect.stringMatching("Cannot read properties of undefined (reading 's')")]))
19+
20+
await page.getByRole("link", {name: "Link 5"}).click()
21+
await syncLV(page)
22+
await expect(page.locator("body")).toContainText("id: 5")
23+
24+
expect(logs).not.toEqual(expect.arrayContaining([expect.stringMatching("Cannot set properties of undefined (setting 'newRender')")]))
25+
26+
// no uncaught exceptions
27+
expect(errors).toEqual([])
28+
})

0 commit comments

Comments
 (0)