solid_objects 0.6.0 → 0.7.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 +13 -0
- data/app/assets/javascripts/solid_objects/component_batch_refresh.js +148 -0
- data/app/controllers/solid_objects/components_controller.rb +84 -0
- data/app/helpers/solid_objects/actor_helper.rb +10 -1
- data/config/routes.rb +1 -0
- data/docs/realtime.md +74 -0
- data/lib/solid_objects/actor_view.rb +14 -7
- data/lib/solid_objects/component_registration.rb +23 -4
- data/lib/solid_objects/component_subscriptions.rb +16 -10
- data/lib/solid_objects/component_token.rb +19 -2
- data/lib/solid_objects/turbo_stream_renderer.rb +12 -0
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/controllers/solid_objects/components_controller.rbs +11 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +7 -4
- data/sig/generated/lib/solid_objects/component_registration.rbs +11 -4
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +3 -0
- data/sig/generated/lib/solid_objects/component_token.rbs +7 -2
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0614e42e260b81ab323d292435cd4f518a4f0b26a21865e5f63c9768db362231
|
|
4
|
+
data.tar.gz: a8c7d4bc22e431b89ab07058c0ad35ca82a44eef08d30b0a547b28c0dd76f82e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 07caec47a1c3452bb3f3b6fc6f950f60f9c68277ebb558630d112fcc7bfb9e3591f893e8bdaefea4329680cde6d6a86c36c0d9b6531d6ac889507abfda939920
|
|
7
|
+
data.tar.gz: 23d756629a2b8212d06077569b9d0c35552c6ee416c95851ee78357ecfe1abf672e0a973d9715f0b66942ecebf8e1c007f21d33bde3bfb7e79af30f323a809bf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- Add `batch:` to reactive components. Components sharing a batch in one actor
|
|
6
|
+
scope collapse into a single browser request per revision instead of one
|
|
7
|
+
request per component. The new `GET /solid_objects/components/batch` endpoint
|
|
8
|
+
returns HTML frames inside a documented JSON envelope, so Turbo morph and ERB
|
|
9
|
+
rendering are unchanged while the contract stays machine readable. Duplicate
|
|
10
|
+
notifications for the same batch and revision coalesce in the browser,
|
|
11
|
+
unchanged components are never requested, and stale frames cannot overwrite a
|
|
12
|
+
newer target. Components without `batch:` behave exactly as before.
|
|
13
|
+
- Add a JavaScript test suite for the browser modules, run in CI with Node's
|
|
14
|
+
test runner and jsdom.
|
|
15
|
+
|
|
3
16
|
## 0.6.0 - 2026-08-09
|
|
4
17
|
|
|
5
18
|
- Add `broadcast_payload`, an actor DSL for sending one personalized JSON state
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const pendingBatches = new Map()
|
|
2
|
+
const activeBatches = new Map()
|
|
3
|
+
|
|
4
|
+
class SolidObjectsBatchRefreshElement extends HTMLElement {
|
|
5
|
+
connectedCallback() {
|
|
6
|
+
if (this.dataset.started === "true") return
|
|
7
|
+
|
|
8
|
+
this.dataset.started = "true"
|
|
9
|
+
this.enqueue()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Several observables changing in one commit produce several notifications
|
|
13
|
+
// for the same batch and revision. Merging them in a microtask turns those
|
|
14
|
+
// into a single request.
|
|
15
|
+
enqueue() {
|
|
16
|
+
const batch = this.dataset.batch
|
|
17
|
+
const revision = this.dataset.revision
|
|
18
|
+
const source = this.dataset.source
|
|
19
|
+
const scope = this.closest("[id]")?.id
|
|
20
|
+
if (!batch || !revision || !source || !scope) return this.remove()
|
|
21
|
+
|
|
22
|
+
// Two actor scopes may reuse a batch name on one page. Keying by scope
|
|
23
|
+
// keeps their requests from merging or cancelling each other.
|
|
24
|
+
const group = `${scope}:${batch}`
|
|
25
|
+
const key = `${group}:${revision}`
|
|
26
|
+
const pending = pendingBatches.get(key)
|
|
27
|
+
if (pending) {
|
|
28
|
+
pending.sources.add(source)
|
|
29
|
+
this.remove()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const merged = { sources: new Set([ source ]) }
|
|
34
|
+
pendingBatches.set(key, merged)
|
|
35
|
+
queueMicrotask(() => {
|
|
36
|
+
pendingBatches.delete(key)
|
|
37
|
+
requestBatch(group, batch, merged.sources)
|
|
38
|
+
})
|
|
39
|
+
this.remove()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function requestBatch(group, batch, sources) {
|
|
44
|
+
const previous = activeBatches.get(group)
|
|
45
|
+
previous?.abort()
|
|
46
|
+
|
|
47
|
+
const controller = new AbortController()
|
|
48
|
+
activeBatches.set(group, controller)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const url = mergedUrl(sources)
|
|
52
|
+
if (!url) return
|
|
53
|
+
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
credentials: "same-origin",
|
|
56
|
+
headers: { Accept: "application/json" },
|
|
57
|
+
redirect: "error",
|
|
58
|
+
signal: controller.signal
|
|
59
|
+
})
|
|
60
|
+
if (!response.ok) return dispatchBatchError(batch, `http_${response.status}`)
|
|
61
|
+
|
|
62
|
+
const body = await response.json()
|
|
63
|
+
if (!Array.isArray(body?.frames)) {
|
|
64
|
+
return dispatchBatchError(batch, "invalid_response")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
body.frames.forEach(applyFrame)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.name !== "AbortError") dispatchBatchError(batch, "request_failed")
|
|
70
|
+
} finally {
|
|
71
|
+
if (activeBatches.get(group) === controller) activeBatches.delete(group)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Every notification for one batch and revision carries the same endpoint and
|
|
76
|
+
// differs only by which components changed, so the union of their tokens is the
|
|
77
|
+
// complete set to render.
|
|
78
|
+
function mergedUrl(sources) {
|
|
79
|
+
const urls = [ ...sources ].map((source) => new URL(source, window.location.href))
|
|
80
|
+
const first = urls[0]
|
|
81
|
+
if (!first || first.origin !== window.location.origin) return
|
|
82
|
+
|
|
83
|
+
const tokens = new Set()
|
|
84
|
+
urls.forEach((url) => {
|
|
85
|
+
url.searchParams.getAll("tokens[]").forEach((token) => tokens.add(token))
|
|
86
|
+
})
|
|
87
|
+
first.searchParams.delete("tokens[]")
|
|
88
|
+
tokens.forEach((token) => first.searchParams.append("tokens[]", token))
|
|
89
|
+
return first
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyFrame(frame) {
|
|
93
|
+
const target = document.getElementById(frame?.target)
|
|
94
|
+
if (!target || !frame.html) return
|
|
95
|
+
if (!newerRevision(frame.revision, target.dataset.solidObjectsRevision)) return
|
|
96
|
+
|
|
97
|
+
const parsed = new DOMParser().parseFromString(frame.html, "text/html")
|
|
98
|
+
const replacement = parsed.getElementById(frame.target)
|
|
99
|
+
if (!replacement) return
|
|
100
|
+
|
|
101
|
+
const stream = document.createElement("turbo-stream")
|
|
102
|
+
stream.setAttribute("action", "replace")
|
|
103
|
+
if (frame.refresh_method === "morph") stream.setAttribute("method", "morph")
|
|
104
|
+
stream.setAttribute("target", frame.target)
|
|
105
|
+
|
|
106
|
+
const template = document.createElement("template")
|
|
107
|
+
template.content.append(document.importNode(replacement, true))
|
|
108
|
+
stream.append(template)
|
|
109
|
+
document.documentElement.append(stream)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function newerRevision(candidate, current) {
|
|
113
|
+
const candidateRevision = parseRevision(candidate)
|
|
114
|
+
const currentRevision = parseRevision(current)
|
|
115
|
+
if (!candidateRevision || !currentRevision) return false
|
|
116
|
+
|
|
117
|
+
return candidateRevision[0] > currentRevision[0] ||
|
|
118
|
+
(candidateRevision[0] === currentRevision[0] &&
|
|
119
|
+
candidateRevision[1] > currentRevision[1])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseRevision(revision) {
|
|
123
|
+
if (!revision) return
|
|
124
|
+
|
|
125
|
+
const values = String(revision).split(":").map(Number)
|
|
126
|
+
if (
|
|
127
|
+
values.length !== 2 ||
|
|
128
|
+
values.some((value) => !Number.isSafeInteger(value) || value < 0)
|
|
129
|
+
) return
|
|
130
|
+
|
|
131
|
+
return values
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function dispatchBatchError(batch, reason) {
|
|
135
|
+
document.dispatchEvent(
|
|
136
|
+
new CustomEvent("solid-objects:batch-refresh-error", {
|
|
137
|
+
bubbles: true,
|
|
138
|
+
detail: { batch, reason }
|
|
139
|
+
})
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!customElements.get("solid-objects-batch-refresh")) {
|
|
144
|
+
customElements.define(
|
|
145
|
+
"solid-objects-batch-refresh",
|
|
146
|
+
SolidObjectsBatchRefreshElement
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -6,13 +6,97 @@ module SolidObjects
|
|
|
6
6
|
class ComponentsController < ActionController::Base
|
|
7
7
|
protect_from_forgery with: :exception
|
|
8
8
|
|
|
9
|
+
BATCH_LIMIT = 50
|
|
10
|
+
|
|
9
11
|
# @rbs () -> void
|
|
10
12
|
def show
|
|
11
13
|
SolidObjects.instrument(:"component.refreshed") { |payload| refresh(payload) }
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
# @rbs () -> void
|
|
17
|
+
def batch
|
|
18
|
+
SolidObjects.instrument(:"component.batch_refreshed") { |payload| refresh_batch(payload) }
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
private
|
|
15
22
|
|
|
23
|
+
# @rbs (Hash[Symbol, untyped]) -> void
|
|
24
|
+
def refresh_batch(payload)
|
|
25
|
+
tokens = Array(params.require(:tokens))
|
|
26
|
+
raise ActionController::ParameterMissing, :tokens if tokens.empty?
|
|
27
|
+
raise ArgumentError if tokens.length > BATCH_LIMIT
|
|
28
|
+
|
|
29
|
+
registrations = tokens.map { |token| ComponentRegistration.from_token(token) }
|
|
30
|
+
validate_single_batch!(registrations)
|
|
31
|
+
requested_revision = requested_revision_key
|
|
32
|
+
snapshot = ActorSnapshot.new(registrations.first.reference)
|
|
33
|
+
payload.merge!(
|
|
34
|
+
actor_type: snapshot.reference.actor_type,
|
|
35
|
+
actor_id: snapshot.reference.actor_id,
|
|
36
|
+
batch: registrations.first.batch,
|
|
37
|
+
components: registrations.map(&:component_name),
|
|
38
|
+
instance_id: snapshot.instance_id,
|
|
39
|
+
revision: snapshot.revision
|
|
40
|
+
)
|
|
41
|
+
if newer_than_snapshot?(requested_revision, snapshot)
|
|
42
|
+
payload[:outcome] = "conflict"
|
|
43
|
+
return head :conflict
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
authorization_context = SolidObjects
|
|
47
|
+
.configuration
|
|
48
|
+
.component_authorization_context
|
|
49
|
+
.call(controller: self)
|
|
50
|
+
frames = registrations.map do |registration|
|
|
51
|
+
rendered = ComponentRenderer.new(
|
|
52
|
+
snapshot:,
|
|
53
|
+
registration:,
|
|
54
|
+
view_context: component_view_context,
|
|
55
|
+
authorization_context:
|
|
56
|
+
).call
|
|
57
|
+
{
|
|
58
|
+
"target" => registration.dom_id,
|
|
59
|
+
"revision" => "#{snapshot.instance_id}:#{snapshot.revision}",
|
|
60
|
+
"refresh_method" => registration.refresh_method,
|
|
61
|
+
"html" => component_frame(registration, snapshot, rendered)
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
response.headers["Cache-Control"] = "private, no-store"
|
|
65
|
+
payload[:outcome] = "rendered"
|
|
66
|
+
render json: {
|
|
67
|
+
"actor_type" => snapshot.reference.actor_type,
|
|
68
|
+
"actor_id" => snapshot.reference.actor_id,
|
|
69
|
+
"batch" => registrations.first.batch,
|
|
70
|
+
"instance_id" => snapshot.instance_id,
|
|
71
|
+
"revision" => snapshot.revision,
|
|
72
|
+
"frames" => frames
|
|
73
|
+
}
|
|
74
|
+
rescue Unauthorized
|
|
75
|
+
payload[:outcome] = "unauthorized"
|
|
76
|
+
head :forbidden
|
|
77
|
+
rescue UnknownComponent
|
|
78
|
+
payload[:outcome] = "unknown_component"
|
|
79
|
+
head :not_found
|
|
80
|
+
rescue ActionController::ParameterMissing,
|
|
81
|
+
ArgumentError,
|
|
82
|
+
InvalidComponentToken
|
|
83
|
+
payload[:outcome] = "invalid_token"
|
|
84
|
+
head :bad_request
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @rbs (Array[ComponentRegistration]) -> void
|
|
88
|
+
def validate_single_batch!(registrations)
|
|
89
|
+
first = registrations.first
|
|
90
|
+
raise ArgumentError unless first.batch
|
|
91
|
+
return if registrations.all? do |registration|
|
|
92
|
+
registration.batch == first.batch &&
|
|
93
|
+
registration.reference.actor_type == first.reference.actor_type &&
|
|
94
|
+
registration.reference.actor_id == first.reference.actor_id
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
raise ArgumentError
|
|
98
|
+
end
|
|
99
|
+
|
|
16
100
|
# @rbs (Hash[Symbol, untyped]) -> void
|
|
17
101
|
def refresh(payload)
|
|
18
102
|
registration = ComponentRegistration.from_token(
|
|
@@ -32,6 +32,13 @@ module SolidObjects
|
|
|
32
32
|
data: { turbo_track: "reload" }
|
|
33
33
|
)
|
|
34
34
|
end
|
|
35
|
+
batch_client = if actor.batched_components?
|
|
36
|
+
javascript_include_tag(
|
|
37
|
+
"solid_objects/component_batch_refresh",
|
|
38
|
+
type: "module",
|
|
39
|
+
data: { turbo_track: "reload" }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
35
42
|
payload_client = if payload_names
|
|
36
43
|
javascript_include_tag(
|
|
37
44
|
"solid_objects/state_payload",
|
|
@@ -42,7 +49,9 @@ module SolidObjects
|
|
|
42
49
|
|
|
43
50
|
content_tag(
|
|
44
51
|
:div,
|
|
45
|
-
safe_join(
|
|
52
|
+
safe_join(
|
|
53
|
+
[ refresh_client, batch_client, payload_client, subscription, content ].compact
|
|
54
|
+
),
|
|
46
55
|
id: DomIdentity.scope(reference)
|
|
47
56
|
)
|
|
48
57
|
end
|
data/config/routes.rb
CHANGED
data/docs/realtime.md
CHANGED
|
@@ -110,6 +110,80 @@ applications discover the namespaced engine asset. Applications created with
|
|
|
110
110
|
explicitly serve the module. Turbo's normal morph rules still apply; use
|
|
111
111
|
`data-turbo-permanent` for elements that must never be changed.
|
|
112
112
|
|
|
113
|
+
## Batched component refreshes
|
|
114
|
+
|
|
115
|
+
A component refresh costs one browser request. When one actor mutation changes
|
|
116
|
+
several components, the page pays one request per component. Adding `batch:`
|
|
117
|
+
groups them so a revision costs one request no matter how many components in the
|
|
118
|
+
group changed:
|
|
119
|
+
|
|
120
|
+
```erb
|
|
121
|
+
<%= actor.component :player, key: 1,
|
|
122
|
+
observes: :player_one, batch: :playmat, refresh: :morph %>
|
|
123
|
+
|
|
124
|
+
<%= actor.component :player_controls, key: 1,
|
|
125
|
+
observes: :player_one_controls, batch: :playmat, refresh: :morph %>
|
|
126
|
+
|
|
127
|
+
<%= actor.component :library_search, key: 1,
|
|
128
|
+
observes: :library, batch: :playmat, refresh: :morph %>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Before, one mutation touching all three observables produced three requests:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
commit -> 3 invalidations -> 3 refresh elements -> 3 GET /solid_objects/components
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
After, the three notifications coalesce in the browser into one request:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
commit -> 3 invalidations -> 1 GET /solid_objects/components/batch -> 3 frames
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### The batch endpoint
|
|
144
|
+
|
|
145
|
+
`GET /solid_objects/components/batch` takes the signed `tokens[]` of the
|
|
146
|
+
components to render plus the `instance_id` and `revision` the browser holds. It
|
|
147
|
+
returns HTML frames inside a JSON envelope:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"actor_type": "playmat_room",
|
|
152
|
+
"actor_id": "table-1",
|
|
153
|
+
"batch": "playmat",
|
|
154
|
+
"instance_id": 12,
|
|
155
|
+
"revision": 48,
|
|
156
|
+
"frames": [
|
|
157
|
+
{
|
|
158
|
+
"target": "solid-objects-component-...",
|
|
159
|
+
"revision": "12:48",
|
|
160
|
+
"refresh_method": "morph",
|
|
161
|
+
"html": "<turbo-frame id=\"...\" data-solid-objects-revision=\"12:48\">...</turbo-frame>"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Why frames inside JSON rather than one HTML document or pure JSON state.** HTML
|
|
168
|
+
alone would force the browser to pick frames out of an undocumented document.
|
|
169
|
+
Pure JSON would mean a second renderer and would give up ERB and Turbo morph. A
|
|
170
|
+
JSON envelope of frame descriptors keeps `ComponentRenderer` and Turbo exactly as
|
|
171
|
+
they are while giving the client a documented contract with per-frame revisions.
|
|
172
|
+
|
|
173
|
+
### What the protocol guarantees
|
|
174
|
+
|
|
175
|
+
Only components whose dependencies changed are requested; the rest are never
|
|
176
|
+
named in the batch. Duplicate notifications for the same batch and revision merge
|
|
177
|
+
into one request, and a superseded request for the same batch is aborted. Each
|
|
178
|
+
frame carries its own revision and cannot overwrite a target that already holds a
|
|
179
|
+
newer one. Authorization is unchanged: every component in the batch passes the
|
|
180
|
+
same `authorize_query` boundary an individual refresh uses, and the batch name is
|
|
181
|
+
signed into the component token, so a browser cannot invent or widen a group. A
|
|
182
|
+
batch mixing actors or groups is rejected.
|
|
183
|
+
|
|
184
|
+
Components without `batch:` keep issuing their own request, and a scope can mix
|
|
185
|
+
batched and unbatched components freely.
|
|
186
|
+
|
|
113
187
|
## Personalized state payloads
|
|
114
188
|
|
|
115
189
|
Reactive ERB components cost one browser request per changed component. When a
|
|
@@ -37,18 +37,19 @@ module SolidObjects
|
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
40
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol, ?batch: untyped) -> untyped
|
|
41
41
|
def component(
|
|
42
42
|
name,
|
|
43
43
|
observes: nil,
|
|
44
44
|
partial: nil,
|
|
45
45
|
key: nil,
|
|
46
46
|
locals: {},
|
|
47
|
-
refresh: :replace
|
|
47
|
+
refresh: :replace,
|
|
48
|
+
batch: nil
|
|
48
49
|
)
|
|
49
50
|
component_name = normalized_component_name(name)
|
|
50
51
|
unless observes
|
|
51
|
-
validate_static_options!(key:, locals:, refresh:)
|
|
52
|
+
validate_static_options!(key:, locals:, refresh:, batch:)
|
|
52
53
|
return static_component(component_name, partial:)
|
|
53
54
|
end
|
|
54
55
|
if partial
|
|
@@ -67,7 +68,8 @@ module SolidObjects
|
|
|
67
68
|
locals:,
|
|
68
69
|
refresh_method: refresh,
|
|
69
70
|
snapshot:,
|
|
70
|
-
refresh_path
|
|
71
|
+
refresh_path:,
|
|
72
|
+
batch: batch&.to_s
|
|
71
73
|
)
|
|
72
74
|
ensure_unique_component!(registration)
|
|
73
75
|
rendered = ComponentRenderer.new(
|
|
@@ -98,6 +100,11 @@ module SolidObjects
|
|
|
98
100
|
observable_names.dup
|
|
99
101
|
end
|
|
100
102
|
|
|
103
|
+
# @rbs () -> bool
|
|
104
|
+
def batched_components?
|
|
105
|
+
component_registrations.any?(&:batch)
|
|
106
|
+
end
|
|
107
|
+
|
|
101
108
|
# @rbs () -> bool
|
|
102
109
|
def morph_components?
|
|
103
110
|
component_registrations.any?(&:morph?)
|
|
@@ -212,9 +219,9 @@ module SolidObjects
|
|
|
212
219
|
"#{registration.component_key.inspect} is already rendered in this solid_object scope"
|
|
213
220
|
end
|
|
214
221
|
|
|
215
|
-
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
216
|
-
def validate_static_options!(key:, locals:, refresh:)
|
|
217
|
-
return if key.nil? && locals.empty? && refresh.to_s == "replace"
|
|
222
|
+
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol, batch: untyped) -> void
|
|
223
|
+
def validate_static_options!(key:, locals:, refresh:, batch:)
|
|
224
|
+
return if key.nil? && locals.empty? && refresh.to_s == "replace" && batch.nil?
|
|
218
225
|
|
|
219
226
|
raise ArgumentError,
|
|
220
227
|
"key, locals, and refresh require an observable component"
|
|
@@ -7,6 +7,7 @@ module SolidObjects
|
|
|
7
7
|
# @rbs @reference: Reference
|
|
8
8
|
# @rbs @component_name: String
|
|
9
9
|
# @rbs @component_key: String | Integer?
|
|
10
|
+
# @rbs @batch: String?
|
|
10
11
|
# @rbs @dependencies: Array[String]
|
|
11
12
|
# @rbs @locals: Hash[String, untyped]
|
|
12
13
|
# @rbs @refresh_method: String
|
|
@@ -18,6 +19,7 @@ module SolidObjects
|
|
|
18
19
|
attr_reader :reference,
|
|
19
20
|
:component_name,
|
|
20
21
|
:component_key,
|
|
22
|
+
:batch,
|
|
21
23
|
:dependencies,
|
|
22
24
|
:locals,
|
|
23
25
|
:refresh_method,
|
|
@@ -26,7 +28,7 @@ module SolidObjects
|
|
|
26
28
|
:refresh_path,
|
|
27
29
|
:token
|
|
28
30
|
|
|
29
|
-
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
31
|
+
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String, ?batch: String?) -> void
|
|
30
32
|
def initialize(
|
|
31
33
|
reference:,
|
|
32
34
|
component_name:,
|
|
@@ -37,9 +39,11 @@ module SolidObjects
|
|
|
37
39
|
instance_id:,
|
|
38
40
|
revision:,
|
|
39
41
|
refresh_path:,
|
|
40
|
-
token
|
|
42
|
+
token:,
|
|
43
|
+
batch: nil
|
|
41
44
|
)
|
|
42
45
|
@reference = reference
|
|
46
|
+
@batch = batch
|
|
43
47
|
@component_name = component_name
|
|
44
48
|
@component_key = component_key
|
|
45
49
|
@dependencies = dependencies.freeze
|
|
@@ -52,7 +56,7 @@ module SolidObjects
|
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
class << self
|
|
55
|
-
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
59
|
+
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String, ?batch: String?) -> ComponentRegistration
|
|
56
60
|
def issue(
|
|
57
61
|
reference:,
|
|
58
62
|
component_name:,
|
|
@@ -61,7 +65,8 @@ module SolidObjects
|
|
|
61
65
|
locals:,
|
|
62
66
|
refresh_method:,
|
|
63
67
|
snapshot:,
|
|
64
|
-
refresh_path
|
|
68
|
+
refresh_path:,
|
|
69
|
+
batch: nil
|
|
65
70
|
)
|
|
66
71
|
token = ComponentToken.generate(
|
|
67
72
|
reference:,
|
|
@@ -70,6 +75,7 @@ module SolidObjects
|
|
|
70
75
|
dependencies:,
|
|
71
76
|
locals:,
|
|
72
77
|
refresh_method:,
|
|
78
|
+
batch:,
|
|
73
79
|
instance_id: snapshot.instance_id,
|
|
74
80
|
revision: snapshot.revision,
|
|
75
81
|
refresh_path:
|
|
@@ -99,6 +105,7 @@ module SolidObjects
|
|
|
99
105
|
reference:,
|
|
100
106
|
component_name: payload.fetch("component_name"),
|
|
101
107
|
component_key: payload["component_key"],
|
|
108
|
+
batch: payload["batch"],
|
|
102
109
|
dependencies:,
|
|
103
110
|
locals: payload.fetch("locals"),
|
|
104
111
|
refresh_method: payload.fetch("refresh_method"),
|
|
@@ -149,6 +156,18 @@ module SolidObjects
|
|
|
149
156
|
locals.merge("component_key" => component_key).freeze
|
|
150
157
|
end
|
|
151
158
|
|
|
159
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
160
|
+
def batch_refresh_url(registrations, instance_id, revision)
|
|
161
|
+
query = URI.encode_www_form(
|
|
162
|
+
[
|
|
163
|
+
[ "instance_id", instance_id ],
|
|
164
|
+
[ "revision", revision ],
|
|
165
|
+
*registrations.map { |registration| [ "tokens[]", registration.token ] }
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
"#{refresh_path}/batch?#{query}"
|
|
169
|
+
end
|
|
170
|
+
|
|
152
171
|
# @rbs (Integer, Integer) -> String
|
|
153
172
|
def refresh_url(instance_id, revision)
|
|
154
173
|
query = URI.encode_www_form(
|
|
@@ -48,16 +48,17 @@ module SolidObjects
|
|
|
48
48
|
observable_name = invalidation.fetch("observable_name")
|
|
49
49
|
instance_id = invalidation.fetch("instance_id")
|
|
50
50
|
revision = invalidation.fetch("revision")
|
|
51
|
-
registrations.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
registration.dom_id,
|
|
55
|
-
instance_id,
|
|
56
|
-
revision
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
refresh(registration, instance_id, revision)
|
|
51
|
+
changed = registrations.select do |registration|
|
|
52
|
+
registration.dependencies.include?(observable_name) &&
|
|
53
|
+
newer_revision?(registration.dom_id, instance_id, revision)
|
|
60
54
|
end
|
|
55
|
+
batched, individual = changed.partition(&:batch)
|
|
56
|
+
streams = individual.map { |registration| refresh(registration, instance_id, revision) }
|
|
57
|
+
batched.group_by(&:batch).each_value do |group|
|
|
58
|
+
group.each { |registration| record_revision(registration, instance_id, revision) }
|
|
59
|
+
streams << TurboStreamRenderer.batch_refresh(group, instance_id, revision)
|
|
60
|
+
end
|
|
61
|
+
streams
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# @rbs (ActorSnapshot) -> Array[String]
|
|
@@ -90,9 +91,14 @@ module SolidObjects
|
|
|
90
91
|
|
|
91
92
|
attr_reader :registrations, :revisions
|
|
92
93
|
|
|
94
|
+
# @rbs (ComponentRegistration, Integer, Integer) -> void
|
|
95
|
+
def record_revision(registration, instance_id, revision)
|
|
96
|
+
revisions[registration.dom_id] = [ instance_id, revision ]
|
|
97
|
+
end
|
|
98
|
+
|
|
93
99
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
94
100
|
def refresh(registration, instance_id, revision)
|
|
95
|
-
|
|
101
|
+
record_revision(registration, instance_id, revision)
|
|
96
102
|
TurboStreamRenderer.component_refresh(
|
|
97
103
|
registration,
|
|
98
104
|
instance_id,
|
|
@@ -9,6 +9,7 @@ module SolidObjects
|
|
|
9
9
|
MAXIMUM_DEPENDENCIES = 50
|
|
10
10
|
MAXIMUM_LOCALS = 50
|
|
11
11
|
MAXIMUM_COMPONENT_KEY_BYTES = 512
|
|
12
|
+
MAXIMUM_BATCH_BYTES = 64
|
|
12
13
|
REFRESH_METHODS = %w[replace morph].freeze
|
|
13
14
|
RESERVED_LOCALS = %w[actor authorization_context component_key].freeze
|
|
14
15
|
RUBY_KEYWORDS = %w[
|
|
@@ -19,7 +20,7 @@ module SolidObjects
|
|
|
19
20
|
|
|
20
21
|
module_function
|
|
21
22
|
|
|
22
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
23
|
+
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol, ?batch: untyped) -> String
|
|
23
24
|
def generate(
|
|
24
25
|
reference:,
|
|
25
26
|
component_name:,
|
|
@@ -29,13 +30,15 @@ module SolidObjects
|
|
|
29
30
|
refresh_path:,
|
|
30
31
|
component_key: nil,
|
|
31
32
|
locals: {},
|
|
32
|
-
refresh_method: "replace"
|
|
33
|
+
refresh_method: "replace",
|
|
34
|
+
batch: nil
|
|
33
35
|
)
|
|
34
36
|
payload = {
|
|
35
37
|
"actor_type" => reference.actor_type,
|
|
36
38
|
"actor_id" => reference.actor_id,
|
|
37
39
|
"component_name" => component_name,
|
|
38
40
|
"component_key" => Serialization.dump(component_key),
|
|
41
|
+
"batch" => batch&.to_s,
|
|
39
42
|
"dependencies" => dependencies,
|
|
40
43
|
"locals" => Serialization.dump(locals),
|
|
41
44
|
"refresh_method" => refresh_method.to_s,
|
|
@@ -80,6 +83,7 @@ module SolidObjects
|
|
|
80
83
|
|
|
81
84
|
validate_component_name!(payload.fetch("component_name"))
|
|
82
85
|
validate_component_key!(payload["component_key"])
|
|
86
|
+
validate_batch!(payload["batch"])
|
|
83
87
|
validate_dependencies!(payload.fetch("dependencies"))
|
|
84
88
|
validate_locals!(payload.fetch("locals"))
|
|
85
89
|
validate_refresh_method!(payload.fetch("refresh_method"))
|
|
@@ -97,6 +101,7 @@ module SolidObjects
|
|
|
97
101
|
return unless payload.is_a?(Hash)
|
|
98
102
|
|
|
99
103
|
payload["component_key"] = nil unless payload.key?("component_key")
|
|
104
|
+
payload["batch"] = nil unless payload.key?("batch")
|
|
100
105
|
payload["locals"] = {} unless payload.key?("locals")
|
|
101
106
|
payload["refresh_method"] = "replace" unless payload.key?("refresh_method")
|
|
102
107
|
end
|
|
@@ -124,6 +129,18 @@ module SolidObjects
|
|
|
124
129
|
end
|
|
125
130
|
private_class_method :validate_component_key!
|
|
126
131
|
|
|
132
|
+
# @rbs (untyped) -> void
|
|
133
|
+
def validate_batch!(batch)
|
|
134
|
+
return if batch.nil?
|
|
135
|
+
return if batch.is_a?(String) &&
|
|
136
|
+
batch.bytesize.positive? &&
|
|
137
|
+
batch.bytesize <= MAXIMUM_BATCH_BYTES &&
|
|
138
|
+
batch.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
139
|
+
|
|
140
|
+
raise InvalidComponentToken, "invalid actor component batch"
|
|
141
|
+
end
|
|
142
|
+
private_class_method :validate_batch!
|
|
143
|
+
|
|
127
144
|
# @rbs (Array[untyped]) -> void
|
|
128
145
|
def validate_dependencies!(dependencies)
|
|
129
146
|
valid = dependencies.any? &&
|
|
@@ -54,6 +54,18 @@ module SolidObjects
|
|
|
54
54
|
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
58
|
+
def batch_refresh(registrations, instance_id, revision)
|
|
59
|
+
first = registrations.first
|
|
60
|
+
scope = DomIdentity.scope(first.reference)
|
|
61
|
+
batch = ERB::Util.html_escape(first.batch)
|
|
62
|
+
source = ERB::Util.html_escape(
|
|
63
|
+
first.batch_refresh_url(registrations, instance_id, revision)
|
|
64
|
+
)
|
|
65
|
+
targets = ERB::Util.html_escape(registrations.map(&:dom_id).join(" "))
|
|
66
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-batch-refresh data-batch="#{batch}" data-revision="#{instance_id}:#{revision}" data-targets="#{targets}" data-source="#{source}"></solid-objects-batch-refresh></template></turbo-stream>)
|
|
67
|
+
end
|
|
68
|
+
|
|
57
69
|
# @rbs (Hash[String, untyped]) -> String
|
|
58
70
|
def state_payload(payload)
|
|
59
71
|
reference = Reference.new(
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class ComponentsController < ActionController::Base
|
|
5
|
+
BATCH_LIMIT: ::Integer
|
|
6
|
+
|
|
5
7
|
# @rbs () -> void
|
|
6
8
|
def show: () -> void
|
|
7
9
|
|
|
10
|
+
# @rbs () -> void
|
|
11
|
+
def batch: () -> void
|
|
12
|
+
|
|
8
13
|
private
|
|
9
14
|
|
|
15
|
+
# @rbs (Hash[Symbol, untyped]) -> void
|
|
16
|
+
def refresh_batch: (Hash[Symbol, untyped]) -> void
|
|
17
|
+
|
|
18
|
+
# @rbs (Array[ComponentRegistration]) -> void
|
|
19
|
+
def validate_single_batch!: (Array[ComponentRegistration]) -> void
|
|
20
|
+
|
|
10
21
|
# @rbs (Hash[Symbol, untyped]) -> void
|
|
11
22
|
def refresh: (Hash[Symbol, untyped]) -> void
|
|
12
23
|
|
|
@@ -22,8 +22,8 @@ module SolidObjects
|
|
|
22
22
|
# @rbs (Symbol | String) -> untyped
|
|
23
23
|
def value: (Symbol | String) -> untyped
|
|
24
24
|
|
|
25
|
-
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
26
|
-
def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
25
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol, ?batch: untyped) -> untyped
|
|
26
|
+
def component: (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol, ?batch: untyped) -> untyped
|
|
27
27
|
|
|
28
28
|
# @rbs () -> Array[String]
|
|
29
29
|
def component_tokens: () -> Array[String]
|
|
@@ -31,6 +31,9 @@ module SolidObjects
|
|
|
31
31
|
# @rbs () -> Array[String]
|
|
32
32
|
def scalar_observable_names: () -> Array[String]
|
|
33
33
|
|
|
34
|
+
# @rbs () -> bool
|
|
35
|
+
def batched_components?: () -> bool
|
|
36
|
+
|
|
34
37
|
# @rbs () -> bool
|
|
35
38
|
def morph_components?: () -> bool
|
|
36
39
|
|
|
@@ -79,8 +82,8 @@ module SolidObjects
|
|
|
79
82
|
# @rbs (ComponentRegistration) -> void
|
|
80
83
|
def ensure_unique_component!: (ComponentRegistration) -> void
|
|
81
84
|
|
|
82
|
-
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
83
|
-
def validate_static_options!: (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
85
|
+
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol, batch: untyped) -> void
|
|
86
|
+
def validate_static_options!: (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol, batch: untyped) -> void
|
|
84
87
|
|
|
85
88
|
# @rbs () -> Proc | ComponentPathResolver
|
|
86
89
|
def component_path_resolver: () -> Proc
|
|
@@ -16,6 +16,8 @@ module SolidObjects
|
|
|
16
16
|
|
|
17
17
|
@dependencies: Array[String]
|
|
18
18
|
|
|
19
|
+
@batch: String?
|
|
20
|
+
|
|
19
21
|
@component_key: String | Integer?
|
|
20
22
|
|
|
21
23
|
@component_name: String
|
|
@@ -28,6 +30,8 @@ module SolidObjects
|
|
|
28
30
|
|
|
29
31
|
attr_reader component_key: untyped
|
|
30
32
|
|
|
33
|
+
attr_reader batch: untyped
|
|
34
|
+
|
|
31
35
|
attr_reader dependencies: untyped
|
|
32
36
|
|
|
33
37
|
attr_reader locals: untyped
|
|
@@ -42,11 +46,11 @@ module SolidObjects
|
|
|
42
46
|
|
|
43
47
|
attr_reader token: untyped
|
|
44
48
|
|
|
45
|
-
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
46
|
-
def initialize: (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
49
|
+
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String, ?batch: String?) -> void
|
|
50
|
+
def initialize: (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String, ?batch: String?) -> void
|
|
47
51
|
|
|
48
|
-
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
49
|
-
def self.issue: (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
52
|
+
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String, ?batch: String?) -> ComponentRegistration
|
|
53
|
+
def self.issue: (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String, ?batch: String?) -> ComponentRegistration
|
|
50
54
|
|
|
51
55
|
# @rbs (String) -> ComponentRegistration
|
|
52
56
|
def self.from_token: (String) -> ComponentRegistration
|
|
@@ -69,6 +73,9 @@ module SolidObjects
|
|
|
69
73
|
# @rbs () -> Hash[String, untyped]
|
|
70
74
|
def authorization_arguments: () -> Hash[String, untyped]
|
|
71
75
|
|
|
76
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
77
|
+
def batch_refresh_url: (Array[ComponentRegistration], Integer, Integer) -> String
|
|
78
|
+
|
|
72
79
|
# @rbs (Integer, Integer) -> String
|
|
73
80
|
def refresh_url: (Integer, Integer) -> String
|
|
74
81
|
end
|
|
@@ -31,6 +31,9 @@ module SolidObjects
|
|
|
31
31
|
|
|
32
32
|
attr_reader revisions: untyped
|
|
33
33
|
|
|
34
|
+
# @rbs (ComponentRegistration, Integer, Integer) -> void
|
|
35
|
+
def record_revision: (ComponentRegistration, Integer, Integer) -> void
|
|
36
|
+
|
|
34
37
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
35
38
|
def refresh: (ComponentRegistration, Integer, Integer) -> String
|
|
36
39
|
|
|
@@ -12,14 +12,16 @@ module SolidObjects
|
|
|
12
12
|
|
|
13
13
|
MAXIMUM_COMPONENT_KEY_BYTES: ::Integer
|
|
14
14
|
|
|
15
|
+
MAXIMUM_BATCH_BYTES: ::Integer
|
|
16
|
+
|
|
15
17
|
REFRESH_METHODS: untyped
|
|
16
18
|
|
|
17
19
|
RESERVED_LOCALS: untyped
|
|
18
20
|
|
|
19
21
|
RUBY_KEYWORDS: untyped
|
|
20
22
|
|
|
21
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
22
|
-
def self?.generate: (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
23
|
+
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol, ?batch: untyped) -> String
|
|
24
|
+
def self?.generate: (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol, ?batch: untyped) -> String
|
|
23
25
|
|
|
24
26
|
# @rbs (String) -> Hash[String, untyped]
|
|
25
27
|
def self?.verify: (String) -> Hash[String, untyped]
|
|
@@ -36,6 +38,9 @@ module SolidObjects
|
|
|
36
38
|
# @rbs (untyped) -> void
|
|
37
39
|
def self?.validate_component_key!: (untyped) -> void
|
|
38
40
|
|
|
41
|
+
# @rbs (untyped) -> void
|
|
42
|
+
def self?.validate_batch!: (untyped) -> void
|
|
43
|
+
|
|
39
44
|
# @rbs (Array[untyped]) -> void
|
|
40
45
|
def self?.validate_dependencies!: (Array[untyped]) -> void
|
|
41
46
|
|
|
@@ -15,6 +15,9 @@ module SolidObjects
|
|
|
15
15
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
16
16
|
def self?.component_refresh: (ComponentRegistration, Integer, Integer) -> String
|
|
17
17
|
|
|
18
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
19
|
+
def self?.batch_refresh: (Array[ComponentRegistration], Integer, Integer) -> String
|
|
20
|
+
|
|
18
21
|
# @rbs (Hash[String, untyped]) -> String
|
|
19
22
|
def self?.state_payload: (Hash[String, untyped]) -> String
|
|
20
23
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
@@ -262,6 +262,7 @@ files:
|
|
|
262
262
|
- MIT-LICENSE
|
|
263
263
|
- README.md
|
|
264
264
|
- Rakefile
|
|
265
|
+
- app/assets/javascripts/solid_objects/component_batch_refresh.js
|
|
265
266
|
- app/assets/javascripts/solid_objects/component_refresh.js
|
|
266
267
|
- app/assets/javascripts/solid_objects/state_payload.js
|
|
267
268
|
- app/controllers/solid_objects/application_controller.rb
|