Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

bensheldon
Copy link

@bensheldon bensheldon commented Sep 12, 2025

When the turbo-cable-stream-source element is disconnected from the DOM, wait a tick wait until the Turbo Render operation finishes to allow any potential DOM re-connect to occur. This prevents unnecessary channel unsubscribe/subscribe cycles when disconnected and then immediately reconnected with data-turbo-permanent and Turbo Drive.

This is possible because all of the Turbo Permanent / bardo.js operations are synchronous.

Related issues:

disconnectedCallback() {
async disconnectedCallback() {
this.disconnecting = true;
await new Promise(resolve => setTimeout(resolve, 0))
Copy link
Contributor

Choose a reason for hiding this comment

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

Turbo relies on a variety of "timing" utilities internally, I wonder if any of them could be accessed here:

export function nextRepaint() {
  if (document.visibilityState === "hidden") {
    return nextEventLoopTick()
  } else {
    return nextAnimationFrame()
  }
}

export function nextAnimationFrame() {
  return new Promise((resolve) => requestAnimationFrame(() => resolve()))
}

export function nextEventLoopTick() {
  return new Promise((resolve) => setTimeout(() => resolve(), 0))
}

export function nextMicrotask() {
  return Promise.resolve()
}

nextEventLoopTick is the obvious candidate, though I wonder if any of the others would be more appropriate.

Copy link
Author

Choose a reason for hiding this comment

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

👍🏻 I'll take a look at what I can import.

Copy link
Author

Choose a reason for hiding this comment

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

It looks like none of the timing utilities are exported by Turbo. In the latest revision, I copied a named method called nextEventLoopTick. Not my favorite, but open to feedback (for example, I could add another file with like util.js).

@seanpdoyle
Copy link
Contributor

this entire class of problem could be sidestepped by moving permanent elements into a holding area in the dom, then moving them into the new page rather than removing and adding them (hotwired/turbo#305 (comment))

This is a really interesting idea! The introduction of Element.moveBefore and the connectedMoveCallback could be extremely valuable (for both morphing and [data-turbo-permanent]).

@bensheldon
Copy link
Author

I'll look at that test failure. Looks like it's relevant.

I appreciate the interest in beforeMove. The CanIUse on it looks pretty bleak right now unfortunately: https://caniuse.com/mdn-api_customelementregistry_define_connectedmovecallback_lifecycle_callback

@bensheldon bensheldon force-pushed the permanent-compat-cable-stream-source branch from d56108d to b6faa7c Compare September 13, 2025 15:31
@bensheldon
Copy link
Author

@seanpdoyle I pushed up a fix for the failing test. I didn't notice that attributeChangedCallback was calling connectedCallback + disconnectedCallback; I extracted the subscribe/unsubscribe behavior separate from the newly introduced delay.

@bensheldon
Copy link
Author

Thinking aloud, I'm wondering how we can make the lifecycle more resilient. Waiting for the nextTick works currently, but is dependent on all of the turbo permanent dom operations being synchronous, which maybe isn't a good future-forward expectation.

I think we could potentially track using turbo render events. As the order of events seems to be:

  1. turbo:before-render
  2. start turbo permanent operation
  3. element is disconnected from dom
  4. element is re-connected to dom
  5. finish turbo permanent operation
  6. turbo:render

I'll take a swing at that, and now that I'm a bit more familiar with the test suite, I'll see if I can lock those in too.

This turned out to be more fun and interesting than expected 😉

@bensheldon bensheldon force-pushed the permanent-compat-cable-stream-source branch from b6faa7c to 37b8b64 Compare September 13, 2025 22:13
When the element is disconnected from the DOM, wait a until after Turbo Render to allow any potential DOM re-connect to occur. Prevents unnecessary unsubscribe/subscribe cycles when preserved with data-turbo-permanent.
@bensheldon bensheldon force-pushed the permanent-compat-cable-stream-source branch from 37b8b64 to 2cb3434 Compare September 13, 2025 23:17
@bensheldon
Copy link
Author

@seanpdoyle I just pushed up that change to hook into turbo:before-render and turbo:render instead of simply waiting a tick. The test I added was helpful.

Comment on lines +131 to +144
let removedOnce = false;
el.reconnectedObserver = new MutationObserver((mutations) => {
mutations.forEach((m) => {
const isConnected = el.hasAttribute("connected");
if (m.oldValue === "" && !isConnected) {
removedOnce = true;
el.setAttribute("disconnected", "");
}
if (removedOnce && m.oldValue == null && isConnected) {
el.setAttribute("reconnected", "");
}
});
});
el.reconnectedObserver.observe(el, { attributes: true, attributeOldValue: true, attributeFilter: ["connected"] });
Copy link
Author

@bensheldon bensheldon Sep 15, 2025

Choose a reason for hiding this comment

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

Just noting this observer would be unnecessary if there was something like this: #498 (comment)

...as we could more simply add a listener to the page for a subscription-connected/subscription-disconnected event rather than have to set up a mutation observer.

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.

2 participants