Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disconnect loaded Frame Element while rendering #454

Merged
merged 1 commit into from
Nov 18, 2021

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Nov 18, 2021

Closes #453


While loading a matching <turbo-frame> element from a response body,
the FrameController "activates" it by invoking
FrameElement.connectedCallback() after importing it into the document.

During that activation, several event listeners are attached, including
some on the document.

Unfortunately, that FrameElement.connectedCallback() invocation is
never mirrored with a FrameElement.disconnectedCallback() invocation,
so the attached listeners are never detached, causing an accumulation.

This commit invokes the disconnectedCallback() after loaded element's
contents are extracted into the browser's document.

@@ -29,6 +29,7 @@ export class FrameRenderer extends Renderer<FrameElement> {
if (sourceRange) {
sourceRange.selectNodeContents(frameElement)
this.currentElement.appendChild(sourceRange.extractContents())
frameElement.disconnectedCallback()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure of a way to add a test to guard against a regression.

The getEventListeners() function used to uncover this leak is defined in the console's suite of tools, and isn't available to the document.

I'm open to ideas!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe attach a listener that increments a global you can check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the code within the connectedCallback() and disconnectedCallback() fire any events directly. It's also worth noting that we're manually invoking these methods within Turbo itself, so they're kind of "synthetic" connections, which makes a MutationObserver similarly unpredictable, since the underlying element itself might not be attached to the browser's document.

@seanpdoyle
Copy link
Contributor Author

@dhh this feels like an important fix to ship as part of 7.1.0-rc2.

Closes [hotwired#453]

---

While loading a matching `<turbo-frame>` element from a response body,
the `FrameController` "activates" it by invoking
`FrameElement.connectedCallback()` after importing it into the document.

During that activation, several event listeners are attached, including
some on the `document`.

Unfortunately, that `FrameElement.connectedCallback()` invocation is
never mirrored with a `FrameElement.disconnectedCallback()` invocation,
so the attached listeners are never detached, causing an accumulation.

This commit invokes the `disconnectedCallback()` after loaded element's
contents are extracted into the browser's document.

[hotwired#453]: hotwired#453
@seanpdoyle seanpdoyle force-pushed the frame-leaking-event-listeners branch from 666429f to da43934 Compare November 18, 2021 15:56
@dhh dhh merged commit ce984f4 into hotwired:main Nov 18, 2021
@seanpdoyle seanpdoyle deleted the frame-leaking-event-listeners branch November 18, 2021 19:07
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 18, 2021
Add test coverage to [hotwired#454][]. While the original fix was
important to ship, it was unclear how to guard against re-introducing
the leaks.

This commit introduces the `withoutChangingEventListenersCount()` test
helper to wrap a block within the test harness. While executing within
the block, the driven page's `document.addEventListener` and
`document.removeEventListener` functions are replaced with decorated
functions that increment and decrement a counter. At the start of the
block, capture and return how many times `addEventListener` has been
invoked without a matching `removeEventListener`. At the end, capture
and return the differential.

[hotwired#454]: hotwired#454
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 18, 2021
The fix introduced in [hotwired#454][] resolved the issue, but
introduced a matching `disconnectedCallback()` far-off in the
`FrameRenderer`.

This commit moves that call to occur _immediately_ after it's paired
`element.connectedCallback()`.

[hotwired#454]: hotwired#454
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 18, 2021
Add test coverage to [hotwired#454][]. While the original fix was
important to ship, it was unclear how to guard against re-introducing
the leaks.

This commit introduces the `withoutChangingEventListenersCount()` test
helper to wrap a block within the test harness. While executing within
the block, the driven page's `document.addEventListener` and
`document.removeEventListener` functions are replaced with decorated
functions that increment and decrement a counter. At the start of the
block, capture and return how many times `addEventListener` has been
invoked without a matching `removeEventListener`. At the end, capture
and return the differential.

[hotwired#454]: hotwired#454
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 18, 2021
The fix introduced in [hotwired#454][] resolved the issue, but
introduced a matching `disconnectedCallback()` far-off in the
`FrameRenderer`.

This commit moves that call to occur _immediately_ after it's paired
`element.connectedCallback()`.

[hotwired#454]: hotwired#454
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 18, 2021
The fix introduced in [hotwired#454][] resolved the issue, but
introduced a matching `disconnectedCallback()` far-off in the
`FrameRenderer`.

This commit moves that call to occur _immediately_ after it's paired
`element.connectedCallback()`.

[hotwired#454]: hotwired#454
dhh added a commit that referenced this pull request Nov 19, 2021
* Add test coverage for event listener leaking

Add test coverage to [#454][]. While the original fix was
important to ship, it was unclear how to guard against re-introducing
the leaks.

This commit introduces the `withoutChangingEventListenersCount()` test
helper to wrap a block within the test harness. While executing within
the block, the driven page's `document.addEventListener` and
`document.removeEventListener` functions are replaced with decorated
functions that increment and decrement a counter. At the start of the
block, capture and return how many times `addEventListener` has been
invoked without a matching `removeEventListener`. At the end, capture
and return the differential.

[#454]: #454

* Move the `disconnectedCallback()` call

The fix introduced in [#454][] resolved the issue, but
introduced a matching `disconnectedCallback()` far-off in the
`FrameRenderer`.

This commit moves that call to occur _immediately_ after it's paired
`element.connectedCallback()`.

[#454]: #454

Co-authored-by: David Heinemeier Hansson <david@basecamp.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

turbo:click and turbo:before-visit event handlers accumulating on document
2 participants