live_cable 0.3.0 → 0.4.0
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 +567 -0
- data/README.md +14 -2
- data/app/assets/javascript/controllers/live_controller.js +51 -7
- data/app/assets/javascript/live_cable.js +2 -0
- data/app/assets/javascript/loading.js +58 -6
- data/app/assets/javascript/subscriptions.js +292 -22
- data/app/channels/live_channel.rb +51 -29
- data/app/helpers/live_cable_helper.rb +31 -2
- data/lib/live_cable/action_cable_connection.rb +4 -1
- data/lib/live_cable/component/broadcasting.rb +37 -6
- data/lib/live_cable/component/dispatching.rb +36 -0
- data/lib/live_cable/component/lifecycle.rb +1 -1
- data/lib/live_cable/component/reactive_variables.rb +33 -5
- data/lib/live_cable/component/rendering.rb +30 -14
- data/lib/live_cable/component/streaming.rb +35 -4
- data/lib/live_cable/component.rb +4 -3
- data/lib/live_cable/connection/broadcasting.rb +15 -3
- data/lib/live_cable/connection/component_management.rb +12 -8
- data/lib/live_cable/connection/error_handling.rb +28 -7
- data/lib/live_cable/connection/messaging.rb +40 -52
- data/lib/live_cable/connection/state_management.rb +13 -16
- data/lib/live_cable/connection.rb +33 -0
- data/lib/live_cable/container.rb +17 -11
- data/lib/live_cable/defaults_signer.rb +95 -0
- data/lib/live_cable/delegation/array.rb +27 -1
- data/lib/live_cable/delegation/hash.rb +55 -1
- data/lib/live_cable/delegation/methods.rb +25 -1
- data/lib/live_cable/delegation/model.rb +12 -0
- data/lib/live_cable/delegator.rb +54 -6
- data/lib/live_cable/dispatch.rb +54 -0
- data/lib/live_cable/engine.rb +3 -1
- data/lib/live_cable/forbidden.rb +7 -0
- data/lib/live_cable/identity_digest.rb +27 -0
- data/lib/live_cable/model_observer.rb +37 -0
- data/lib/live_cable/observer_tracking.rb +10 -1
- data/lib/live_cable/predicate_builder.rb +13 -0
- data/lib/live_cable/render_context.rb +82 -8
- data/lib/live_cable/rendering/controller_renderer.rb +26 -0
- data/lib/live_cable/rendering/dependency_visitor.rb +68 -24
- data/lib/live_cable/rendering/handler.rb +1 -1
- data/lib/live_cable/rendering/method_analyzer.rb +18 -4
- data/lib/live_cable/rendering/method_collector.rb +13 -5
- data/lib/live_cable/rendering/partial.rb +13 -1
- data/lib/live_cable/rendering/partial_renderer.rb +59 -7
- data/lib/live_cable/rendering/render_result.rb +5 -0
- data/lib/live_cable/rendering/renderer.rb +18 -14
- data/lib/live_cable/rendering/view_buffer.rb +21 -0
- data/lib/live_cable/testing/render_state.rb +5 -3
- data/lib/live_cable/testing/test_channel.rb +6 -0
- data/lib/live_cable/testing/test_component.rb +3 -3
- data/lib/live_cable/testing.rb +9 -5
- data/lib/live_cable/version.rb +1 -1
- data/lib/live_cable.rb +32 -3
- metadata +9 -1
|
@@ -4,7 +4,9 @@ import LoadingState from "@isometriks/live_cable/loading"
|
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static values = {
|
|
7
|
-
|
|
7
|
+
// Opaque signed blob produced by the server; forwarded verbatim to the
|
|
8
|
+
// channel on subscribe, where it is verified and decoded.
|
|
9
|
+
defaults: String,
|
|
8
10
|
status: String,
|
|
9
11
|
component: String,
|
|
10
12
|
actions: Array,
|
|
@@ -24,7 +26,7 @@ export default class extends Controller {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
connect() {
|
|
27
|
-
this.#loading = new LoadingState(this.element)
|
|
29
|
+
this.#loading = new LoadingState(this.element, { onStalled: () => this.#stalled() })
|
|
28
30
|
this.element.addEventListener("call", this.#callActionCallback)
|
|
29
31
|
|
|
30
32
|
this.#subscription = SubscriptionManager.subscribe(
|
|
@@ -34,6 +36,12 @@ export default class extends Controller {
|
|
|
34
36
|
this
|
|
35
37
|
)
|
|
36
38
|
|
|
39
|
+
// The subscription outlives a controller, so messages held from before a
|
|
40
|
+
// disconnect need this controller's watchdog
|
|
41
|
+
for (let i = this.#subscription.pendingCount; i > 0; i--) {
|
|
42
|
+
this.#loading.start()
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
// Create callbacks for each action or form
|
|
38
46
|
this.actionsValue.forEach((action) => {
|
|
39
47
|
this[`action_$${action}`] = ({ params, currentTarget }) => {
|
|
@@ -48,6 +56,9 @@ export default class extends Controller {
|
|
|
48
56
|
|
|
49
57
|
disconnect() {
|
|
50
58
|
this.element.removeEventListener("call", this.#callActionCallback)
|
|
59
|
+
// A new controller brings its own loading state; don't leave this one's
|
|
60
|
+
// watchdog to fire on an element that has gone
|
|
61
|
+
this.#loading?.reset()
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
sendCall(action, params = {}, trigger = null) {
|
|
@@ -57,11 +68,12 @@ export default class extends Controller {
|
|
|
57
68
|
)
|
|
58
69
|
}
|
|
59
70
|
|
|
60
|
-
// Called by the subscription when the server answers a message
|
|
61
|
-
//
|
|
62
|
-
// state once all in-flight messages have been answered
|
|
71
|
+
// Called by the subscription when the server answers a message (its
|
|
72
|
+
// reply, an ack, or an error). Restores any live-loading / live-disable-with
|
|
73
|
+
// state once all in-flight messages have been answered, and returns true
|
|
74
|
+
// when it did.
|
|
63
75
|
finishLoading() {
|
|
64
|
-
this.#loading?.finish()
|
|
76
|
+
return this.#loading?.finish() ?? false
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
// Whether any message is still awaiting a server response
|
|
@@ -69,11 +81,31 @@ export default class extends Controller {
|
|
|
69
81
|
return this.#loading?.active ?? false
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
// How many messages are still awaiting a server response
|
|
85
|
+
get inFlight() {
|
|
86
|
+
return this.#loading?.inFlight ?? 0
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
// Called by the subscription when the component is being torn down.
|
|
73
90
|
resetLoading() {
|
|
74
91
|
this.#loading?.reset()
|
|
75
92
|
}
|
|
76
93
|
|
|
94
|
+
// The loading state gave up waiting for a reply. Anything still held for a
|
|
95
|
+
// reconnect is dropped with it, so a message the page has said didn't go
|
|
96
|
+
// through can't go through later - the user would click again, and it
|
|
97
|
+
// would run twice. A message already sent can't be recalled; the event
|
|
98
|
+
// says how many were dropped so the page can word its message.
|
|
99
|
+
#stalled() {
|
|
100
|
+
const discarded = this.#subscription?.discardPending() ?? 0
|
|
101
|
+
|
|
102
|
+
this.statusValue = 'stalled'
|
|
103
|
+
this.element.dispatchEvent(new CustomEvent('live:stalled', {
|
|
104
|
+
bubbles: true,
|
|
105
|
+
detail: { discarded },
|
|
106
|
+
}))
|
|
107
|
+
}
|
|
108
|
+
|
|
77
109
|
#callMessage(params, action) {
|
|
78
110
|
return {
|
|
79
111
|
_action: action,
|
|
@@ -115,8 +147,20 @@ export default class extends Controller {
|
|
|
115
147
|
return {
|
|
116
148
|
_action: '_reactive',
|
|
117
149
|
name: target.name,
|
|
118
|
-
value: target
|
|
150
|
+
value: this.#reactiveValue(target),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#reactiveValue(target) {
|
|
155
|
+
if (target.type === 'checkbox') {
|
|
156
|
+
return target.checked
|
|
119
157
|
}
|
|
158
|
+
|
|
159
|
+
if (target.type === 'select-multiple') {
|
|
160
|
+
return Array.from(target.selectedOptions, (option) => option.value)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return target.value
|
|
120
164
|
}
|
|
121
165
|
|
|
122
166
|
#form(action, { currentTarget, params }) {
|
|
@@ -4,8 +4,10 @@ import DOM from '@isometriks/live_cable/dom'
|
|
|
4
4
|
|
|
5
5
|
const observer = new LiveObserver()
|
|
6
6
|
observer.start()
|
|
7
|
+
SubscriptionManager.syncIdentity()
|
|
7
8
|
|
|
8
9
|
document.addEventListener('turbo:before-render', (event) => {
|
|
10
|
+
SubscriptionManager.syncIdentity()
|
|
9
11
|
SubscriptionManager.prune(event.detail.newBody)
|
|
10
12
|
})
|
|
11
13
|
|
|
@@ -12,12 +12,26 @@
|
|
|
12
12
|
* message is in flight. If the attribute has a value, the element's label
|
|
13
13
|
* (textContent, or value for inputs) is swapped for it.
|
|
14
14
|
*
|
|
15
|
-
* The state is cleared when the server
|
|
16
|
-
* or an ack (sent when
|
|
17
|
-
* Multiple in-flight messages are counted; the DOM is
|
|
18
|
-
* all of them have been answered.
|
|
15
|
+
* The state is cleared when the server replies with the component's own
|
|
16
|
+
* re-render, an error, or an ack (sent when the action didn't re-render the
|
|
17
|
+
* component by itself). Multiple in-flight messages are counted; the DOM is
|
|
18
|
+
* only restored once all of them have been answered.
|
|
19
|
+
*
|
|
20
|
+
* If no reply comes for `LoadingState.timeout` milliseconds while a message
|
|
21
|
+
* is in flight, the state gives up: it resets and calls `onStalled`. That is
|
|
22
|
+
* the backstop for a reply that is never coming - the server stopped while
|
|
23
|
+
* running the message, say - which would otherwise leave a button disabled
|
|
24
|
+
* until the page is reloaded.
|
|
19
25
|
*/
|
|
20
26
|
export default class LoadingState {
|
|
27
|
+
/**
|
|
28
|
+
* Milliseconds to wait for any reply before giving up. Long enough to
|
|
29
|
+
* cover a reconnect after a deploy, since messages held for one count too.
|
|
30
|
+
* Set it before components connect to change it everywhere.
|
|
31
|
+
* @type {number}
|
|
32
|
+
*/
|
|
33
|
+
static timeout = 30_000
|
|
34
|
+
|
|
21
35
|
/** @type {HTMLElement} */
|
|
22
36
|
#root
|
|
23
37
|
/** @type {number} */
|
|
@@ -26,12 +40,20 @@ export default class LoadingState {
|
|
|
26
40
|
#markedElements = new Set()
|
|
27
41
|
/** @type {Map<Element, Object>} - Original state of disabled elements */
|
|
28
42
|
#disabledElements = new Map()
|
|
43
|
+
/** @type {Function|null} */
|
|
44
|
+
#onStalled
|
|
45
|
+
/** @type {number|null} */
|
|
46
|
+
#timer = null
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
49
|
* @param {HTMLElement} root - The component's root element
|
|
50
|
+
* @param {Object} options
|
|
51
|
+
* @param {Function} [options.onStalled] - Called after the state gives up
|
|
52
|
+
* waiting and resets itself
|
|
32
53
|
*/
|
|
33
|
-
constructor(root) {
|
|
54
|
+
constructor(root, { onStalled = null } = {}) {
|
|
34
55
|
this.#root = root
|
|
56
|
+
this.#onStalled = onStalled
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
/**
|
|
@@ -42,6 +64,14 @@ export default class LoadingState {
|
|
|
42
64
|
return this.#inFlight > 0
|
|
43
65
|
}
|
|
44
66
|
|
|
67
|
+
/**
|
|
68
|
+
* How many messages are awaiting a server response.
|
|
69
|
+
* @returns {number}
|
|
70
|
+
*/
|
|
71
|
+
get inFlight() {
|
|
72
|
+
return this.#inFlight
|
|
73
|
+
}
|
|
74
|
+
|
|
45
75
|
/**
|
|
46
76
|
* Mark the component as loading.
|
|
47
77
|
* Called right before a message is sent to the server.
|
|
@@ -52,7 +82,9 @@ export default class LoadingState {
|
|
|
52
82
|
* elements. Disabled for reactive inputs so typing doesn't lose focus.
|
|
53
83
|
*/
|
|
54
84
|
start(trigger = null, { disable = true } = {}) {
|
|
55
|
-
this.#inFlight++
|
|
85
|
+
if (this.#inFlight++ === 0) {
|
|
86
|
+
this.#arm()
|
|
87
|
+
}
|
|
56
88
|
|
|
57
89
|
this.#mark(this.#root)
|
|
58
90
|
|
|
@@ -79,9 +111,12 @@ export default class LoadingState {
|
|
|
79
111
|
this.#inFlight--
|
|
80
112
|
|
|
81
113
|
if (this.#inFlight > 0) {
|
|
114
|
+
// Still waiting on others, but the server is evidently answering
|
|
115
|
+
this.#arm()
|
|
82
116
|
return false
|
|
83
117
|
}
|
|
84
118
|
|
|
119
|
+
this.#disarm()
|
|
85
120
|
this.#restore()
|
|
86
121
|
return true
|
|
87
122
|
}
|
|
@@ -92,9 +127,26 @@ export default class LoadingState {
|
|
|
92
127
|
*/
|
|
93
128
|
reset() {
|
|
94
129
|
this.#inFlight = 0
|
|
130
|
+
this.#disarm()
|
|
95
131
|
this.#restore()
|
|
96
132
|
}
|
|
97
133
|
|
|
134
|
+
#arm() {
|
|
135
|
+
this.#disarm()
|
|
136
|
+
this.#timer = setTimeout(() => this.#stall(), LoadingState.timeout)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#disarm() {
|
|
140
|
+
clearTimeout(this.#timer)
|
|
141
|
+
this.#timer = null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#stall() {
|
|
145
|
+
this.#timer = null
|
|
146
|
+
this.reset()
|
|
147
|
+
this.#onStalled?.()
|
|
148
|
+
}
|
|
149
|
+
|
|
98
150
|
#mark(element) {
|
|
99
151
|
element.setAttribute('live-loading', '')
|
|
100
152
|
this.#markedElements.add(element)
|