live_cable 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/app/assets/javascript/subscriptions.js +43 -3
- data/app/channels/live_channel.rb +5 -0
- data/lib/live_cable/component/broadcasting.rb +3 -1
- data/lib/live_cable/component/identification.rb +0 -4
- data/lib/live_cable/component/lifecycle.rb +0 -1
- data/lib/live_cable/component/streaming.rb +0 -7
- data/lib/live_cable/connection.rb +0 -6
- data/lib/live_cable/testing/test_channel.rb +10 -1
- data/lib/live_cable/testing/test_component.rb +3 -14
- data/lib/live_cable/version.rb +1 -1
- metadata +1 -2
- data/lib/live_cable/connection/channel_management.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9328496395172b7689b680c9fc54f222f26da2cc2d5a1cd25683fbcbdbe3c2bf
|
|
4
|
+
data.tar.gz: c824da0a8a7cc10f3ca84a2965d264d88b879944b108d57e37599b0865b07b9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed7c3db367dea001f0d50cfd0d05dd5c7ba837800438924e944c7a92041dd0b252293682792b17bb8730934a659ba2839a1c6ad283469fdacc00bfa4771f4a79
|
|
7
|
+
data.tar.gz: a97bb8a0096edb1a4daa016b59051aef62a1d93eb1e8635d4a5462b5b6444ae5bdf28ae997b141f0e3ff0dfafde71fdcb08d98d322921c4e527e8e2c3ed416b6
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,42 @@ The Ruby gem (`live_cable`) and the npm package (`@isometriks/live_cable`) are
|
|
|
6
6
|
released together and share a single version number. Entries below note which
|
|
7
7
|
side of the pair a change affects when it isn't both.
|
|
8
8
|
|
|
9
|
+
## 0.2.1 - 2026-08-13
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- `Component#channel_name` and `Connection#channel_name`. A component no longer
|
|
14
|
+
subscribes to a stream of its own now that payloads are written straight to its
|
|
15
|
+
channel, so the name identified a stream nothing published to or read from.
|
|
16
|
+
Subscribing to external streams with `stream_from` is unaffected (gem).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **The opening payload from `LiveChannel#subscribed` could be dropped.**
|
|
21
|
+
Payloads were published through the pubsub adapter, but ActionCable registers
|
|
22
|
+
`stream_from` asynchronously — so the initial render could be published before
|
|
23
|
+
the subscription it targets existed, and pubsub delivers only to subscribers
|
|
24
|
+
present at that moment, with no buffering or retry. The component was left
|
|
25
|
+
showing `data-live-status-value="disconnected"`, with no cached render on the
|
|
26
|
+
client for a Turbo reattach to replay, until some later action happened to
|
|
27
|
+
produce a refresh. When a subscribe render raised, the `_error` payload was
|
|
28
|
+
lost outright and the failure never surfaced in the browser at all. A
|
|
29
|
+
component's stream belongs to exactly one connection, so payloads are now
|
|
30
|
+
written straight to that connection rather than published, which removes the
|
|
31
|
+
race along with a pubsub round trip (gem).
|
|
32
|
+
- **Components not reattaching after a Turbo Drive navigation.** Navigating to a
|
|
33
|
+
page containing a component that is already subscribed deliberately keeps the
|
|
34
|
+
existing subscription, so `LiveChannel#subscribed` does not run again and the
|
|
35
|
+
server sends nothing. The newly rendered element was left with its
|
|
36
|
+
server-rendered status of `disconnected` and never received the component's
|
|
37
|
+
current state. A reconnecting controller now re-syncs the subscription's status
|
|
38
|
+
and replays the last render into the new element (npm).
|
|
39
|
+
- Building DOM from a server render used iterator helpers
|
|
40
|
+
(`childNodes.values().find(...)`), an ES2025 feature, which raised
|
|
41
|
+
`TypeError: ... .find is not a function` on runtimes that do not implement them
|
|
42
|
+
— Node 20 and earlier, and browsers older than Chrome 122 / Firefox 131 /
|
|
43
|
+
Safari 18.4. Rewritten with `Array.from` (npm).
|
|
44
|
+
|
|
9
45
|
## 0.2.0
|
|
10
46
|
|
|
11
47
|
The gem and npm package versions are realigned in this release. The npm package
|
|
@@ -34,8 +34,7 @@ function createDOMFromHTML(html) {
|
|
|
34
34
|
template.innerHTML = html
|
|
35
35
|
|
|
36
36
|
// Find a node that isn't a comment
|
|
37
|
-
const node = template.content.childNodes
|
|
38
|
-
.values()
|
|
37
|
+
const node = Array.from(template.content.childNodes)
|
|
39
38
|
.find(n => n.nodeName !== '#comment')
|
|
40
39
|
|
|
41
40
|
if (node) {
|
|
@@ -230,6 +229,15 @@ class ComponentState {
|
|
|
230
229
|
return this.#element
|
|
231
230
|
}
|
|
232
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Whether a render has been received and cached, and can therefore be
|
|
234
|
+
* replayed into a freshly attached element.
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
get hasRender() {
|
|
238
|
+
return this.#lastTemplate !== null && Boolean(this.#partsByTemplate[this.#lastTemplate])
|
|
239
|
+
}
|
|
240
|
+
|
|
233
241
|
/**
|
|
234
242
|
* Create a DOM element from stored state.
|
|
235
243
|
* @param {Object} refresh - Optional refresh data to update state
|
|
@@ -345,12 +353,44 @@ class Subscription {
|
|
|
345
353
|
|
|
346
354
|
/**
|
|
347
355
|
* Update the controller reference.
|
|
348
|
-
* Called when a Stimulus controller reconnects to an existing subscription
|
|
356
|
+
* Called when a Stimulus controller reconnects to an existing subscription —
|
|
357
|
+
* most importantly after a Turbo Drive navigation to a page that contains the
|
|
358
|
+
* same component, where prune() deliberately keeps the subscription alive.
|
|
359
|
+
*
|
|
360
|
+
* In that case the ActionCable subscription is never recreated, so
|
|
361
|
+
* LiveChannel#subscribed does not run again and the server sends nothing.
|
|
362
|
+
* Without re-syncing here, the newly rendered element keeps the
|
|
363
|
+
* server-rendered status of "disconnected" forever and never receives the
|
|
364
|
+
* component's current state.
|
|
349
365
|
*
|
|
350
366
|
* @param {Object} controller - Stimulus controller instance
|
|
351
367
|
*/
|
|
352
368
|
set controller(controller) {
|
|
369
|
+
const previous = this.#controller
|
|
370
|
+
|
|
353
371
|
this.#controller = controller
|
|
372
|
+
this.#componentState.element = controller.element
|
|
373
|
+
|
|
374
|
+
if (previous && previous !== controller) {
|
|
375
|
+
this.#reattach()
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Bring a freshly connected controller up to date with state this
|
|
381
|
+
* subscription already holds.
|
|
382
|
+
* @private
|
|
383
|
+
*/
|
|
384
|
+
#reattach() {
|
|
385
|
+
if (this.#currentStatus) {
|
|
386
|
+
this.#controller.statusValue = this.#currentStatus
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Replay the last render into the new element. The server-side component
|
|
390
|
+
// is the same instance, so the cached parts are its current state.
|
|
391
|
+
if (this.#componentState.hasRender) {
|
|
392
|
+
this.#handleRefresh(null)
|
|
393
|
+
}
|
|
354
394
|
}
|
|
355
395
|
|
|
356
396
|
/**
|
|
@@ -41,6 +41,11 @@ class LiveChannel < ActionCable::Channel::Base
|
|
|
41
41
|
@component = nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Exposes #transmit, which ActionCable keeps private to the channel.
|
|
45
|
+
def broadcast(data)
|
|
46
|
+
transmit(data)
|
|
47
|
+
end
|
|
48
|
+
|
|
44
49
|
private
|
|
45
50
|
|
|
46
51
|
# @return [LiveCable::Component, nil]
|
|
@@ -5,8 +5,10 @@ module LiveCable
|
|
|
5
5
|
module Broadcasting
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
|
+
# Nil until the component's own controller subscribes; children rendered
|
|
9
|
+
# by a parent have no channel to deliver to yet.
|
|
8
10
|
def broadcast(data)
|
|
9
|
-
|
|
11
|
+
channel&.broadcast(data)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def broadcast_subscribe
|
|
@@ -7,14 +7,7 @@ module LiveCable
|
|
|
7
7
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
|
-
def start_stream
|
|
11
|
-
channel.stream_from(channel_name)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
10
|
def stop_stream
|
|
15
|
-
channel.stop_stream_from(channel_name)
|
|
16
|
-
|
|
17
|
-
# Stop all additional streams started via stream_from
|
|
18
11
|
additional_streams.each do |stream_name|
|
|
19
12
|
channel.stop_stream_from(stream_name)
|
|
20
13
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module LiveCable
|
|
4
4
|
class Connection
|
|
5
5
|
include ComponentManagement
|
|
6
|
-
include ChannelManagement
|
|
7
6
|
include StateManagement
|
|
8
7
|
include Messaging
|
|
9
8
|
include Broadcasting
|
|
@@ -13,17 +12,12 @@ module LiveCable
|
|
|
13
12
|
|
|
14
13
|
def initialize(request)
|
|
15
14
|
@request = request
|
|
16
|
-
@session_id = SecureRandom.uuid
|
|
17
15
|
@containers = Hash.new { |hash, key| hash[key] = Container.new }
|
|
18
16
|
@components = {}
|
|
19
|
-
@channels = {}
|
|
20
17
|
end
|
|
21
18
|
|
|
22
19
|
private
|
|
23
20
|
|
|
24
|
-
# @return [String]
|
|
25
|
-
attr_reader :session_id
|
|
26
|
-
|
|
27
21
|
# @return [ActionDispatch::Request]
|
|
28
22
|
attr_reader :request
|
|
29
23
|
|
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
module LiveCable
|
|
4
4
|
module Testing
|
|
5
5
|
# Stand-in for an ActionCable channel. Records streams started via
|
|
6
|
-
# stream_from so tests can trigger their callbacks with receive_stream
|
|
6
|
+
# stream_from so tests can trigger their callbacks with receive_stream,
|
|
7
|
+
# and payloads sent by components.
|
|
7
8
|
class TestChannel
|
|
8
9
|
# @return [Hash<String, Hash>] Stream name => { coder:, callback: }
|
|
9
10
|
attr_reader :streams
|
|
10
11
|
|
|
12
|
+
# @return [Array<Hash>] Payloads sent by components through this channel
|
|
13
|
+
attr_reader :transmissions
|
|
14
|
+
|
|
11
15
|
# @return [LiveCable::Testing::TestCableConnection]
|
|
12
16
|
attr_reader :connection
|
|
13
17
|
|
|
14
18
|
def initialize(identifiers = {})
|
|
15
19
|
@streams = {}
|
|
20
|
+
@transmissions = []
|
|
16
21
|
@connection = TestCableConnection.new(identifiers)
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
def broadcast(data)
|
|
25
|
+
@transmissions << data
|
|
26
|
+
end
|
|
27
|
+
|
|
19
28
|
def stream_from(name, coder: nil, &block)
|
|
20
29
|
@streams[name] = { coder:, callback: block }
|
|
21
30
|
end
|
|
@@ -18,9 +18,6 @@ module LiveCable
|
|
|
18
18
|
super(component)
|
|
19
19
|
@connection = connection
|
|
20
20
|
@channel = channel
|
|
21
|
-
@broadcasts = []
|
|
22
|
-
|
|
23
|
-
capture_broadcasts(component)
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
# @return [LiveCable::Component] The underlying component instance
|
|
@@ -74,15 +71,15 @@ module LiveCable
|
|
|
74
71
|
# (e.g. :_refresh, :_ack, :_error, :_status)
|
|
75
72
|
# @return [Array<Hash>]
|
|
76
73
|
def broadcasts(key = nil)
|
|
77
|
-
return
|
|
74
|
+
return channel.transmissions.dup unless key
|
|
78
75
|
|
|
79
|
-
|
|
76
|
+
channel.transmissions.select { |broadcast| broadcast.key?(key) }
|
|
80
77
|
end
|
|
81
78
|
|
|
82
79
|
# Forget previously captured broadcasts. Useful after mounting, to
|
|
83
80
|
# assert on the effects of a single action.
|
|
84
81
|
def clear_broadcasts
|
|
85
|
-
|
|
82
|
+
channel.transmissions.clear
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
# All events the component has dispatched via dispatch_event, in
|
|
@@ -134,14 +131,6 @@ module LiveCable
|
|
|
134
131
|
def receive_message(message)
|
|
135
132
|
connection.receive(component, { 'messages' => [message] })
|
|
136
133
|
end
|
|
137
|
-
|
|
138
|
-
def capture_broadcasts(component)
|
|
139
|
-
captured = @broadcasts
|
|
140
|
-
|
|
141
|
-
component.define_singleton_method(:broadcast) do |data|
|
|
142
|
-
captured << data
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
134
|
end
|
|
146
135
|
end
|
|
147
136
|
end
|
data/lib/live_cable/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: live_cable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Craig Blanchette
|
|
@@ -145,7 +145,6 @@ files:
|
|
|
145
145
|
- lib/live_cable/configuration.rb
|
|
146
146
|
- lib/live_cable/connection.rb
|
|
147
147
|
- lib/live_cable/connection/broadcasting.rb
|
|
148
|
-
- lib/live_cable/connection/channel_management.rb
|
|
149
148
|
- lib/live_cable/connection/component_management.rb
|
|
150
149
|
- lib/live_cable/connection/error_handling.rb
|
|
151
150
|
- lib/live_cable/connection/messaging.rb
|