Skip to content

Commit 2cb3434

Browse files
committed
Allow turbo-cable-stream-source to be compatible with turbo-permanent
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.
1 parent 30cd8fc commit 2cb3434

File tree

8 files changed

+135
-22
lines changed

8 files changed

+135
-22
lines changed

app/assets/javascripts/turbo.js

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5386,25 +5386,44 @@ function walk(obj) {
53865386

53875387
class TurboCableStreamSourceElement extends HTMLElement {
53885388
static observedAttributes=[ "channel", "signed-stream-name" ];
5389+
constructor() {
5390+
super();
5391+
this.beforeTurboRender = this.beforeTurboRender.bind(this);
5392+
this.afterTurboRender = this.afterTurboRender.bind(this);
5393+
}
53895394
async connectedCallback() {
5395+
document.addEventListener("turbo:before-render", this.beforeTurboRender);
5396+
document.addEventListener("turbo:render", this.afterTurboRender);
5397+
if (!this.withinTurboRender) {
5398+
await this.subscribe();
5399+
}
5400+
}
5401+
async disconnectedCallback() {
5402+
document.removeEventListener("turbo:before-render", this.beforeTurboRender);
5403+
document.removeEventListener("turbo:render", this.afterTurboRender);
5404+
if (!this.withinTurboRender) {
5405+
this.unsubscribe();
5406+
}
5407+
}
5408+
async attributeChangedCallback() {
5409+
if (this.subscription) {
5410+
this.unsubscribe();
5411+
await this.subscribe();
5412+
}
5413+
}
5414+
async subscribe() {
53905415
connectStreamSource(this);
53915416
this.subscription = await subscribeTo(this.channel, {
53925417
received: this.dispatchMessageEvent.bind(this),
53935418
connected: this.subscriptionConnected.bind(this),
53945419
disconnected: this.subscriptionDisconnected.bind(this)
53955420
});
53965421
}
5397-
disconnectedCallback() {
5422+
unsubscribe() {
53985423
disconnectStreamSource(this);
53995424
if (this.subscription) this.subscription.unsubscribe();
54005425
this.subscriptionDisconnected();
54015426
}
5402-
attributeChangedCallback() {
5403-
if (this.subscription) {
5404-
this.disconnectedCallback();
5405-
this.connectedCallback();
5406-
}
5407-
}
54085427
dispatchMessageEvent(data) {
54095428
const event = new MessageEvent("message", {
54105429
data: data
@@ -5417,6 +5436,15 @@ class TurboCableStreamSourceElement extends HTMLElement {
54175436
subscriptionDisconnected() {
54185437
this.removeAttribute("connected");
54195438
}
5439+
beforeTurboRender() {
5440+
this.withinTurboRender = true;
5441+
}
5442+
afterTurboRender() {
5443+
if (this.withinTurboRender && !this.isConnected) {
5444+
this.unsubscribe();
5445+
}
5446+
this.withinTurboRender = false;
5447+
}
54205448
get channel() {
54215449
const channel = this.getAttribute("channel");
54225450
const signed_stream_name = this.getAttribute("signed-stream-name");

app/assets/javascripts/turbo.min.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/assets/javascripts/turbo.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/javascript/turbo/cable_stream_source_element.js

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
1-
import { connectStreamSource, disconnectStreamSource } from "@hotwired/turbo"
2-
import { subscribeTo } from "./cable"
1+
import {connectStreamSource, disconnectStreamSource} from "@hotwired/turbo"
2+
import {subscribeTo} from "./cable"
33
import snakeize from "./snakeize"
44

55
class TurboCableStreamSourceElement extends HTMLElement {
66
static observedAttributes = ["channel", "signed-stream-name"]
77

8+
constructor() {
9+
super()
10+
this.beforeTurboRender = this.beforeTurboRender.bind(this)
11+
this.afterTurboRender = this.afterTurboRender.bind(this)
12+
}
13+
814
async connectedCallback() {
15+
document.addEventListener("turbo:before-render", this.beforeTurboRender)
16+
document.addEventListener("turbo:render", this.afterTurboRender)
17+
18+
if (!this.withinTurboRender) {
19+
await this.subscribe()
20+
}
21+
}
22+
23+
async disconnectedCallback() {
24+
document.removeEventListener("turbo:before-render", this.beforeTurboRender)
25+
document.removeEventListener("turbo:render", this.afterTurboRender)
26+
27+
if (!this.withinTurboRender) {
28+
this.unsubscribe()
29+
}
30+
}
31+
32+
async attributeChangedCallback() {
33+
if (this.subscription) {
34+
this.unsubscribe()
35+
await this.subscribe()
36+
}
37+
}
38+
39+
async subscribe() {
940
connectStreamSource(this)
1041
this.subscription = await subscribeTo(this.channel, {
1142
received: this.dispatchMessageEvent.bind(this),
@@ -14,19 +45,12 @@ class TurboCableStreamSourceElement extends HTMLElement {
1445
})
1546
}
1647

17-
disconnectedCallback() {
48+
unsubscribe() {
1849
disconnectStreamSource(this)
1950
if (this.subscription) this.subscription.unsubscribe()
2051
this.subscriptionDisconnected()
2152
}
2253

23-
attributeChangedCallback() {
24-
if (this.subscription) {
25-
this.disconnectedCallback()
26-
this.connectedCallback()
27-
}
28-
}
29-
3054
dispatchMessageEvent(data) {
3155
const event = new MessageEvent("message", { data })
3256
return this.dispatchEvent(event)
@@ -40,10 +64,21 @@ class TurboCableStreamSourceElement extends HTMLElement {
4064
this.removeAttribute("connected")
4165
}
4266

67+
beforeTurboRender() {
68+
this.withinTurboRender = true;
69+
}
70+
71+
afterTurboRender() {
72+
if (this.withinTurboRender && !this.isConnected) {
73+
this.unsubscribe();
74+
}
75+
this.withinTurboRender = false;
76+
}
77+
4378
get channel() {
4479
const channel = this.getAttribute("channel")
4580
const signed_stream_name = this.getAttribute("signed-stream-name")
46-
return { channel, signed_stream_name, ...snakeize({ ...this.dataset }) }
81+
return {channel, signed_stream_name, ...snakeize({...this.dataset})}
4782
}
4883
}
4984

test/dummy/app/controllers/messages_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ def index
1111
@messages = Message.all
1212
end
1313

14+
def permanent
15+
@messages = Message.all
16+
end
17+
1418
def section
1519
end
1620

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<% navigations = params.fetch(:navigations, 0).to_i %>
2+
3+
<h1>Permanent Messages</h1>
4+
5+
<p>Navigations: <%= navigations %></p>
6+
<%= link_to "Navigate", permanent_messages_path(navigations: navigations + 1) %>
7+
8+
<div id="messages-wrapper" data-turbo-permanent>
9+
<%= turbo_stream_from "messages" %>
10+
<div id="messages">
11+
</div>
12+
</div>

test/dummy/config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
resources :messages do
66
collection do
77
get :section
8+
get :permanent
89
end
910
end
1011
resources :trays

test/system/broadcasts_test.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,39 @@ class BroadcastsTest < ApplicationSystemTestCase
120120
assert_no_text original.content
121121
end
122122

123+
test "the turbo-cable-stream-source does not unsubscribe+resubscribe within turbo-permanent" do
124+
visit permanent_messages_path
125+
assert_selector "turbo-cable-stream-source[connected]"
126+
127+
cable_stream_source = find("turbo-cable-stream-source")
128+
cable_stream_source.execute_script <<~JS
129+
const el = this;
130+
131+
let removedOnce = false;
132+
el.reconnectedObserver = new MutationObserver((mutations) => {
133+
mutations.forEach((m) => {
134+
const isConnected = el.hasAttribute("connected");
135+
if (m.oldValue === "" && !isConnected) {
136+
removedOnce = true;
137+
el.setAttribute("disconnected", "");
138+
}
139+
if (removedOnce && m.oldValue == null && isConnected) {
140+
el.setAttribute("reconnected", "");
141+
}
142+
});
143+
});
144+
el.reconnectedObserver.observe(el, { attributes: true, attributeOldValue: true, attributeFilter: ["connected"] });
145+
JS
146+
147+
assert_text "Navigations: 0"
148+
click_link "Navigate"
149+
assert_text "Navigations: 1"
150+
151+
assert_selector "turbo-cable-stream-source[connected]"
152+
assert_no_selector "turbo-cable-stream-source[disconnected]"
153+
assert_no_selector "turbo-cable-stream-source[reconnected]"
154+
end
155+
123156
private
124157

125158
def reconnect_cable_stream_source(from:, to:)

0 commit comments

Comments
 (0)