You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I'm working on an app that uses Turbo 8 through version 2.0.0-beta.2 of turbo-rails.
I've noticed that if a page contains multiple <turbo-cable-stream-source> elements, all with the same signed-stream-name, via multiple calls to turbo_stream_from(foo), refreshes on foo are duplicated, leading to request multiplication for every refresh event.
For example, with 5 copies of the same <turbo-cable-stream-source>, there are 5 subscription requests (shown highlighted below in green), and 1 subscription confirmation (shown below, not highlighted):
(Relatedly, rails/rails#44652 seems to imply that there would be constantly repeated subscription attempts for the N-1 subscriptions? But I don't see that behavior.)
Then, when there is a refresh from an update! on the relevant model (configured with broadcasts_refreshes), the server sends a single websockets message for "<turbo-stream action=\"refresh\"></turbo-stream>" (not 5 of them), and the client initiates 5 requests, 4 of which are canceled:
Although the requests are canceled, the requests may still result in server work, which cumulatively results in a lot of wasteful work, particularly if the endpoints are expensive.
It seems surprising to me that N copies of <turbo-cable-stream-source> all with the same signed-stream-name leads to multiple requests, particularly given that they're auto-cancelled.
Moreover, working around the duplication problem feels like it introduces a lot of complexity: it becomes necessary to track which pages have which subscriptions and for what purpose.
For example, consider a simple model that uses Turbo 8:
In my case, the abc and xyz partials are relatively complex and may appear in different contexts across different pages. The intention is to make sure that using either of these partials results in Turbo-powered updates, hence the embedded turbo_stream_from patient directly into the partials themselves.
However, if a page happens to use bothabc and xyz partials, Turbo refresh requests are doubled. With N partials, there are effectively N copies of the subscription.
To avoid that duplication, a developer has to track which pages use abc, or xyz, or both, and then make sure that the relevant turbo_stream_from subscriptions are set up only once, at level of abstraction higher than the partial.
I only just recently started using Turbo, but this behavior feels like it's a bug. It's also interesting that the client is apparently happy with only 1 subscription confirmation from the server, yet that single subscription confirmation still results in N requests. Perhaps that points to the cause of the problem.
The text was updated successfully, but these errors were encountered:
The idea is to deduplicate consumer.subscriptions.subscriptions by simply dropping the N-1 duplicates subscriptions it contains for each stream name. forget() seems to work just fine (from Action Cable), even though it doesn't notify the server of an unsubscribe.
From the above screenshot of websocket messages, the server in my case only confirms 1 of the N subscription requests, and doesn't send N copies of a given event, but the client processes refresh requests as if all N are connected, leading to N requests. So there doesn't seem to be a need to let the server know the subscription is being removed.
import{cableasActionCable}from"@hotwired/turbo-rails"asyncfunctiondeduplicateSubscriptions(){constconsumer=awaitActionCable.getConsumer();constsubscriptionManager=consumer.subscriptions;constsubscriptions=subscriptionManager.subscriptions;constuniqueSubscriptions=[];// Unique-ify the list of subscriptions based on `identifier`.subscriptions.forEach((subscription)=>{constidentifier=subscription.identifier;constmatchingSubscription=uniqueSubscriptions.find((s)=>{returns.identifier===identifier;});if(!matchingSubscription){uniqueSubscriptions.push(subscription);}});subscriptions.forEach((subscription)=>{if(!uniqueSubscriptions.includes(subscription)){subscriptionManager.forget(subscription);}});};constdeduplicatingObserver=newMutationObserver((mutationList,_observer)=>{mutationList.forEach((mutation)=>{if(mutation.target.tagName==="TURBO-CABLE-STREAM-SOURCE"){// `return` so we only dedupe once.returndeduplicateSubscriptions();}});});deduplicatingObserver.observe(document.body,{attributes: true,childList: true,subtree: true});
Since Object.watch and similar observer APIs are deprecated, I couldn't find a simpler way to deduplicate consumer.subscriptions.subscriptions whenever it's modified. It's a little hacky, but <turbo-cable-stream-source> changes seem to map 1-to-1 to the issue here. More than open to other ideas. It's maybe worth mentioning that the workaround does leave the connected attribute on all N elements, which may be misleading.
I'm working on an app that uses Turbo 8 through version 2.0.0-beta.2 of turbo-rails.
I've noticed that if a page contains multiple
<turbo-cable-stream-source>
elements, all with the samesigned-stream-name
, via multiple calls toturbo_stream_from(foo)
, refreshes onfoo
are duplicated, leading to request multiplication for every refresh event.For example, with 5 copies of the same
<turbo-cable-stream-source>
, there are 5 subscription requests (shown highlighted below in green), and 1 subscription confirmation (shown below, not highlighted):(Relatedly, rails/rails#44652 seems to imply that there would be constantly repeated subscription attempts for the N-1 subscriptions? But I don't see that behavior.)
Then, when there is a refresh from an
update!
on the relevant model (configured withbroadcasts_refreshes
), the server sends a single websockets message for"<turbo-stream action=\"refresh\"></turbo-stream>"
(not 5 of them), and the client initiates 5 requests, 4 of which are canceled:Although the requests are canceled, the requests may still result in server work, which cumulatively results in a lot of wasteful work, particularly if the endpoints are expensive.
It seems surprising to me that N copies of
<turbo-cable-stream-source>
all with the samesigned-stream-name
leads to multiple requests, particularly given that they're auto-cancelled.Moreover, working around the duplication problem feels like it introduces a lot of complexity: it becomes necessary to track which pages have which subscriptions and for what purpose.
For example, consider a simple model that uses Turbo 8:
Then suppose there are some related partials:
In my case, the
abc
andxyz
partials are relatively complex and may appear in different contexts across different pages. The intention is to make sure that using either of these partials results in Turbo-powered updates, hence the embeddedturbo_stream_from patient
directly into the partials themselves.However, if a page happens to use both
abc
andxyz
partials, Turbo refresh requests are doubled. With N partials, there are effectively N copies of the subscription.To avoid that duplication, a developer has to track which pages use
abc
, orxyz
, or both, and then make sure that the relevantturbo_stream_from
subscriptions are set up only once, at level of abstraction higher than the partial.I only just recently started using Turbo, but this behavior feels like it's a bug. It's also interesting that the client is apparently happy with only 1 subscription confirmation from the server, yet that single subscription confirmation still results in N requests. Perhaps that points to the cause of the problem.
The text was updated successfully, but these errors were encountered: