Skip to content

Allow turbo-cable-stream-source to be compatible with data-turbo-permanent - #756

Open
bensheldon wants to merge 1 commit into
hotwired:mainfrom
bensheldon:permanent-compat-cable-stream-source
Open

Allow turbo-cable-stream-source to be compatible with data-turbo-permanent#756
bensheldon wants to merge 1 commit into
hotwired:mainfrom
bensheldon:permanent-compat-cable-stream-source

Conversation

@bensheldon

@bensheldon bensheldon commented Sep 12, 2025

Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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"] });

@bensheldon bensheldon Sep 15, 2025

Copy link
Copy Markdown
Author

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.

@brunoprietog

Copy link
Copy Markdown
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]).

That's very interesting and would address the root cause of the problem. Currently, permanent elements in Turbo are not truly permanent, as they are detached and attached to the dom anyway. We can't use moveBefore, but we have Idiomorph for that, which uses it when available.

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.

3 participants