solid_objects 0.5.1 → 0.6.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 +24 -0
- data/app/assets/javascripts/solid_objects/state_payload.js +69 -0
- data/app/helpers/solid_objects/actor_helper.rb +13 -4
- data/benchmark/support.rb +2 -0
- data/docs/benchmarks.md +7 -0
- data/docs/realtime.md +87 -0
- data/lib/solid_objects/actor.rb +7 -0
- data/lib/solid_objects/actor_channel.rb +55 -4
- data/lib/solid_objects/actor_definition.rb +19 -0
- data/lib/solid_objects/database_adapter.rb +28 -5
- data/lib/solid_objects/database_adapters/sqlite.rb +5 -5
- data/lib/solid_objects/errors.rb +6 -0
- data/lib/solid_objects/executor.rb +22 -7
- data/lib/solid_objects/payload_broadcast.rb +67 -0
- data/lib/solid_objects/stream_token.rb +19 -12
- data/lib/solid_objects/turbo_stream_renderer.rb +22 -5
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +1 -0
- data/sig/generated/helpers/solid_objects/actor_helper.rbs +2 -2
- data/sig/generated/lib/solid_objects/actor.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +11 -0
- data/sig/generated/lib/solid_objects/actor_definition.rbs +7 -0
- data/sig/generated/lib/solid_objects/database_adapter.rbs +10 -0
- data/sig/generated/lib/solid_objects/errors.rbs +6 -0
- data/sig/generated/lib/solid_objects/executor.rbs +7 -4
- data/sig/generated/lib/solid_objects/payload_broadcast.rbs +35 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +5 -2
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6996e5e1ea8fa47b59b66cb6ed0f72e28d1673c2da5697e5507873db55f0b7ff
|
|
4
|
+
data.tar.gz: edb728dd6427ca4a1486dc791816b7a3ab8d3bdf822992c27d3800ab925c120e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 888370023e0d24e18eda279862a01a31b8583fccc5913fae571fa76d4001de1f408b22e7c3242c23975f01aeb293c47bd9314d94d98177e99c353fb78796caaa
|
|
7
|
+
data.tar.gz: 130982ddf19508a6268ae7356ac6249cd8efcc10cc29dd5c13e5cd865322540deabae688e511825a3a92bdc6bfe6314cf9f003d3ec2f4d273d968668438a1588
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- Add `broadcast_payload`, an actor DSL for sending one personalized JSON state
|
|
6
|
+
payload over the actor stream a page already has open. The block runs once per
|
|
7
|
+
subscriber with that subscriber's authorization context, so private state
|
|
8
|
+
never crosses sessions. Payloads carry actor identity and the monotonic state
|
|
9
|
+
revision, and both the channel and the browser drop stale revisions. Subscribe
|
|
10
|
+
with `solid_object room, payloads: :playmat_state` and handle the
|
|
11
|
+
`solid-objects:payload` DOM event. ERB component refreshes remain the default
|
|
12
|
+
and are unchanged. A mutation that changes payload state without changing a
|
|
13
|
+
declared observable still invalidates subscribers, through a revision-only
|
|
14
|
+
broadcast that carries no observable value to the browser.
|
|
15
|
+
|
|
16
|
+
## 0.5.2 - 2026-08-09
|
|
17
|
+
|
|
18
|
+
- Read the database clock once per transaction instead of once per step, and
|
|
19
|
+
resolve the SQLite busy wait from configuration instead of querying the
|
|
20
|
+
connection for it. A synchronous call now issues 49 database queries instead
|
|
21
|
+
of 66, which matters most on PostgreSQL and MySQL where every query is a
|
|
22
|
+
network round trip.
|
|
23
|
+
- Apply every migration in the benchmark harness. It applied only the initial
|
|
24
|
+
migration, so the `state_revision` column added in 0.4.0 was missing, every
|
|
25
|
+
message failed at commit, and the synchronous benchmarks timed out.
|
|
26
|
+
|
|
3
27
|
## 0.5.1 - 2026-08-07
|
|
4
28
|
|
|
5
29
|
- Restore the SQLite busy wait that a synchronous invocation suspends for its
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const deliveredRevisions = new Map()
|
|
2
|
+
|
|
3
|
+
class SolidObjectsPayloadElement extends HTMLElement {
|
|
4
|
+
connectedCallback() {
|
|
5
|
+
if (this.dataset.started === "true") return
|
|
6
|
+
|
|
7
|
+
this.dataset.started = "true"
|
|
8
|
+
this.deliver()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
deliver() {
|
|
12
|
+
try {
|
|
13
|
+
const name = this.dataset.name
|
|
14
|
+
const revision = revisionFor(this)
|
|
15
|
+
const scope = this.closest("[id]")
|
|
16
|
+
if (!name || !revision || !scope) return
|
|
17
|
+
|
|
18
|
+
const key = `${scope.id}:${name}`
|
|
19
|
+
if (!newerRevision(revision, deliveredRevisions.get(key))) return
|
|
20
|
+
|
|
21
|
+
const payload = JSON.parse(this.textContent)
|
|
22
|
+
deliveredRevisions.set(key, revision)
|
|
23
|
+
scope.dispatchEvent(
|
|
24
|
+
new CustomEvent("solid-objects:payload", {
|
|
25
|
+
bubbles: true,
|
|
26
|
+
detail: {
|
|
27
|
+
name,
|
|
28
|
+
instanceId: revision[0],
|
|
29
|
+
revision: revision[1],
|
|
30
|
+
payload
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
} catch {
|
|
35
|
+
this.dispatchEvent(
|
|
36
|
+
new CustomEvent("solid-objects:payload-error", {
|
|
37
|
+
bubbles: true,
|
|
38
|
+
detail: { reason: "invalid_payload" }
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
} finally {
|
|
42
|
+
this.remove()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function newerRevision(candidate, current) {
|
|
48
|
+
if (!current) return true
|
|
49
|
+
|
|
50
|
+
return candidate[0] > current[0] ||
|
|
51
|
+
(candidate[0] === current[0] && candidate[1] > current[1])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function revisionFor(element) {
|
|
55
|
+
const revision = element.dataset.revision
|
|
56
|
+
if (!revision) return
|
|
57
|
+
|
|
58
|
+
const values = revision.split(":").map(Number)
|
|
59
|
+
if (
|
|
60
|
+
values.length !== 2 ||
|
|
61
|
+
values.some((value) => !Number.isSafeInteger(value) || value < 0)
|
|
62
|
+
) return
|
|
63
|
+
|
|
64
|
+
return values
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!customElements.get("solid-objects-payload")) {
|
|
68
|
+
customElements.define("solid-objects-payload", SolidObjectsPayloadElement)
|
|
69
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
module ActorHelper
|
|
5
|
-
# @rbs (Reference, ?authorization_context: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
-
def solid_object(reference, authorization_context: self, &block)
|
|
5
|
+
# @rbs (Reference, ?authorization_context: untyped, ?payloads: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
+
def solid_object(reference, authorization_context: self, payloads: nil, &block)
|
|
7
|
+
payload_names = Array(payloads).map(&:to_s).uniq.presence
|
|
7
8
|
actor = ActorView.new(
|
|
8
9
|
reference:,
|
|
9
10
|
view_context: self,
|
|
@@ -13,7 +14,8 @@ module SolidObjects
|
|
|
13
14
|
subscription_data = {
|
|
14
15
|
token: StreamToken.generate(
|
|
15
16
|
reference,
|
|
16
|
-
observables: actor.scalar_observable_names
|
|
17
|
+
observables: actor.scalar_observable_names,
|
|
18
|
+
payloads: payload_names
|
|
17
19
|
)
|
|
18
20
|
}
|
|
19
21
|
if actor.component_tokens.any?
|
|
@@ -30,10 +32,17 @@ module SolidObjects
|
|
|
30
32
|
data: { turbo_track: "reload" }
|
|
31
33
|
)
|
|
32
34
|
end
|
|
35
|
+
payload_client = if payload_names
|
|
36
|
+
javascript_include_tag(
|
|
37
|
+
"solid_objects/state_payload",
|
|
38
|
+
type: "module",
|
|
39
|
+
data: { turbo_track: "reload" }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
33
42
|
|
|
34
43
|
content_tag(
|
|
35
44
|
:div,
|
|
36
|
-
safe_join([ refresh_client, subscription, content ].compact),
|
|
45
|
+
safe_join([ refresh_client, payload_client, subscription, content ].compact),
|
|
37
46
|
id: DomIdentity.scope(reference)
|
|
38
47
|
)
|
|
39
48
|
end
|
data/benchmark/support.rb
CHANGED
|
@@ -228,7 +228,9 @@ module SolidObjectsBenchmark
|
|
|
228
228
|
# @rbs () -> void
|
|
229
229
|
def migrate
|
|
230
230
|
require_relative "../db/migrate/20260805000000_create_solid_objects_tables"
|
|
231
|
+
require_relative "../db/migrate/20260806000000_add_state_revision_to_solid_objects_instances"
|
|
231
232
|
CreateSolidObjectsTables.new.migrate(:up)
|
|
233
|
+
AddStateRevisionToSolidObjectsInstances.new.migrate(:up)
|
|
232
234
|
end
|
|
233
235
|
|
|
234
236
|
# @rbs () -> void
|
data/docs/benchmarks.md
CHANGED
|
@@ -41,6 +41,13 @@ scenario used four worker threads.
|
|
|
41
41
|
| Synchronous latency | p50 1.8 ms, p95 25.6 ms, p99 156.2 ms |
|
|
42
42
|
| Activation reuse | 98.0%, four activations for 200 messages |
|
|
43
43
|
| Queries for one message turn | 29 |
|
|
44
|
+
| Queries for one synchronous call | 49 |
|
|
45
|
+
|
|
46
|
+
A synchronous call costs far more queries than a worker turn because the caller
|
|
47
|
+
also registers or heartbeats its caller process, claims the activation, and
|
|
48
|
+
observes the result. Query count, not query time, dominates synchronous latency
|
|
49
|
+
on a networked database: measured locally against SQLite, database time is
|
|
50
|
+
roughly 5% of a call and the remaining 95% is Ruby.
|
|
44
51
|
|
|
45
52
|
The difference between the SQLite development result and the MySQL adoption
|
|
46
53
|
result is why Solid Objects does not publish one latency promise. Network
|
data/docs/realtime.md
CHANGED
|
@@ -110,6 +110,93 @@ 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
|
+
## Personalized state payloads
|
|
114
|
+
|
|
115
|
+
Reactive ERB components cost one browser request per changed component. When a
|
|
116
|
+
single actor mutation changes several components, an application pays several
|
|
117
|
+
round trips for one logical update. A payload broadcast collapses that into one
|
|
118
|
+
message on the stream the page already has open.
|
|
119
|
+
|
|
120
|
+
Declare the payload on the actor. The block receives the actor and the
|
|
121
|
+
subscriber's authorization context, and it runs **once per subscriber**, so two
|
|
122
|
+
sessions watching the same actor never see each other's private state:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
class PlaymatRoom < SolidObjects::Actor
|
|
126
|
+
actor_type "playmat_room"
|
|
127
|
+
|
|
128
|
+
attribute :hands, default: -> { {} }
|
|
129
|
+
attribute :turn, default: 1
|
|
130
|
+
|
|
131
|
+
observable :turn
|
|
132
|
+
|
|
133
|
+
broadcast_payload :playmat_state do |room, authorization_context|
|
|
134
|
+
{
|
|
135
|
+
"turn" => room.turn,
|
|
136
|
+
"hand" => room.hands.fetch(authorization_context.session_id, [])
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Subscribe the scope to it:
|
|
143
|
+
|
|
144
|
+
```erb
|
|
145
|
+
<%= solid_object room, payloads: :playmat_state do |actor| %>
|
|
146
|
+
<div data-playmat></div>
|
|
147
|
+
<% end %>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Handle it with any JavaScript. The gem dispatches a DOM event and requires no
|
|
151
|
+
framework:
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
document.addEventListener("solid-objects:payload", (event) => {
|
|
155
|
+
const { name, revision, payload } = event.detail
|
|
156
|
+
if (name !== "playmat_state") return
|
|
157
|
+
|
|
158
|
+
renderPlaymat(payload)
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### What the protocol guarantees
|
|
163
|
+
|
|
164
|
+
The payload travels as a Turbo Stream element on the existing actor stream, so
|
|
165
|
+
applications do not run a second WebSocket system. Each message carries the
|
|
166
|
+
actor identity plus the `instance_id` and monotonic `state_revision` that fence
|
|
167
|
+
component refreshes, and both the channel and the browser drop a payload that
|
|
168
|
+
is not newer than the last one delivered for that scope and name. A reconnecting
|
|
169
|
+
client receives the current payload on subscribe.
|
|
170
|
+
|
|
171
|
+
Authorization is the same `authorize_query` boundary that components use, called
|
|
172
|
+
with the payload name and the subscriber's Cable connection. A subscriber that
|
|
173
|
+
fails the check is skipped rather than served a partial payload, and the payload
|
|
174
|
+
name is signed into the stream token, so a browser cannot ask for a payload the
|
|
175
|
+
server did not offer.
|
|
176
|
+
|
|
177
|
+
Payload blocks read committed actor state through the same snapshot components
|
|
178
|
+
use. They cannot write application records, and the return value must be a JSON
|
|
179
|
+
object or array so the wire format stays inspectable.
|
|
180
|
+
|
|
181
|
+
### mtg-playmat before and after
|
|
182
|
+
|
|
183
|
+
Before, one mutation that touched three observables produced three refresh
|
|
184
|
+
elements and three HTTP requests:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
commit -> 3 Action Cable messages -> 3 GET /solid_objects/components -> 3 renders
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
After, the same mutation delivers one personalized payload and the page renders
|
|
191
|
+
once:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
commit -> 1 Action Cable message -> 0 HTTP requests -> 1 render
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Components remain the default. An actor with no `broadcast_payload` and a scope
|
|
198
|
+
with no `payloads:` option behave exactly as before.
|
|
199
|
+
|
|
113
200
|
## Authorization
|
|
114
201
|
|
|
115
202
|
The HTML contains a signed actor identity token. Signing prevents modification;
|
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -57,6 +57,13 @@ module SolidObjects
|
|
|
57
57
|
definition.add_observable(name, block)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
# @rbs (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
|
|
61
|
+
def broadcast_payload(name, &block)
|
|
62
|
+
raise InvalidActor, "payload broadcasts require a block" unless block
|
|
63
|
+
|
|
64
|
+
definition.add_payload_broadcast(name, block)
|
|
65
|
+
end
|
|
66
|
+
|
|
60
67
|
# @rbs (?Integer) -> Integer
|
|
61
68
|
def state_version(version = nil)
|
|
62
69
|
definition.set_state_version(version) if version
|
|
@@ -19,7 +19,9 @@ module SolidObjects
|
|
|
19
19
|
|
|
20
20
|
@reference = Reference.new(actor_type:, actor_id:)
|
|
21
21
|
@scalar_observables = identity["observables"]
|
|
22
|
+
@payload_names = identity["payloads"]
|
|
22
23
|
validate_scalar_observables!
|
|
24
|
+
validate_payload_names!
|
|
23
25
|
@component_subscriptions = ComponentSubscriptions.parse(
|
|
24
26
|
params["components"],
|
|
25
27
|
reference:
|
|
@@ -36,6 +38,7 @@ module SolidObjects
|
|
|
36
38
|
)
|
|
37
39
|
end
|
|
38
40
|
refresh_outdated_components(snapshot)
|
|
41
|
+
transmit_state_payloads(snapshot)
|
|
39
42
|
rescue KeyError,
|
|
40
43
|
JSON::ParserError,
|
|
41
44
|
InvalidStreamToken,
|
|
@@ -46,14 +49,20 @@ module SolidObjects
|
|
|
46
49
|
|
|
47
50
|
private
|
|
48
51
|
|
|
49
|
-
attr_reader :reference,
|
|
52
|
+
attr_reader :reference,
|
|
53
|
+
:component_subscriptions,
|
|
54
|
+
:scalar_observables,
|
|
55
|
+
:payload_names
|
|
50
56
|
|
|
51
57
|
# @rbs (String) -> void
|
|
52
58
|
def receive_broadcast(stream)
|
|
53
59
|
invalidation = TurboStreamRenderer.invalidation(stream)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
revision_only = invalidation &&
|
|
61
|
+
invalidation.fetch("observable_name") == PayloadBroadcast::REVISION_OBSERVABLE
|
|
62
|
+
if !revision_only &&
|
|
63
|
+
(!invalidation ||
|
|
64
|
+
scalar_observables.nil? ||
|
|
65
|
+
scalar_observables.include?(invalidation.fetch("observable_name")))
|
|
57
66
|
transmit stream
|
|
58
67
|
end
|
|
59
68
|
return unless invalidation
|
|
@@ -61,6 +70,48 @@ module SolidObjects
|
|
|
61
70
|
component_subscriptions
|
|
62
71
|
.refreshes_for(invalidation)
|
|
63
72
|
.each { |refresh| transmit refresh }
|
|
73
|
+
transmit_state_payloads(ActorSnapshot.new(reference))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs (ActorSnapshot) -> void
|
|
77
|
+
def transmit_state_payloads(snapshot)
|
|
78
|
+
return if payload_names.nil? || payload_names.empty?
|
|
79
|
+
return unless newer_payload_revision?(snapshot)
|
|
80
|
+
|
|
81
|
+
payload_names.each do |name|
|
|
82
|
+
payload = PayloadBroadcast.new(
|
|
83
|
+
snapshot:,
|
|
84
|
+
name:,
|
|
85
|
+
authorization_context: connection
|
|
86
|
+
).call
|
|
87
|
+
transmit TurboStreamRenderer.state_payload(payload)
|
|
88
|
+
rescue Unauthorized
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
@payload_revision = [ snapshot.instance_id, snapshot.revision ]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @rbs (ActorSnapshot) -> bool
|
|
95
|
+
def newer_payload_revision?(snapshot)
|
|
96
|
+
current = @payload_revision
|
|
97
|
+
return true unless current
|
|
98
|
+
|
|
99
|
+
(current <=> [ snapshot.instance_id, snapshot.revision ]) == -1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @rbs () -> void
|
|
103
|
+
def validate_payload_names!
|
|
104
|
+
return unless payload_names
|
|
105
|
+
|
|
106
|
+
broadcasts = SolidObjects
|
|
107
|
+
.registry
|
|
108
|
+
.fetch(reference.actor_type)
|
|
109
|
+
.definition
|
|
110
|
+
.payload_broadcasts
|
|
111
|
+
unknown = payload_names.find { |name| !broadcasts.key?(name.to_sym) }
|
|
112
|
+
return unless unknown
|
|
113
|
+
|
|
114
|
+
raise InvalidStreamToken, "unknown payload broadcast #{unknown.inspect}"
|
|
64
115
|
end
|
|
65
116
|
|
|
66
117
|
# @rbs (ActorSnapshot) -> void
|
|
@@ -9,6 +9,7 @@ module SolidObjects
|
|
|
9
9
|
# @rbs @messages: Hash[Symbol, Handler]
|
|
10
10
|
# @rbs @queries: Hash[Symbol, Handler]
|
|
11
11
|
# @rbs @observables: Hash[Symbol, Handler]
|
|
12
|
+
# @rbs @payload_broadcasts: Hash[Symbol, Handler]
|
|
12
13
|
# @rbs @state_version: Integer
|
|
13
14
|
# @rbs @state_migrations: Array[StateMigration]
|
|
14
15
|
# @rbs @activation_hooks: Array[Proc]
|
|
@@ -20,6 +21,7 @@ module SolidObjects
|
|
|
20
21
|
:messages,
|
|
21
22
|
:queries,
|
|
22
23
|
:observables,
|
|
24
|
+
:payload_broadcasts,
|
|
23
25
|
:state_version,
|
|
24
26
|
:state_migrations,
|
|
25
27
|
:activation_hooks,
|
|
@@ -31,6 +33,7 @@ module SolidObjects
|
|
|
31
33
|
@messages = {}
|
|
32
34
|
@queries = {}
|
|
33
35
|
@observables = {}
|
|
36
|
+
@payload_broadcasts = {}
|
|
34
37
|
@state_version = 1
|
|
35
38
|
@state_migrations = []
|
|
36
39
|
@activation_hooks = []
|
|
@@ -83,6 +86,21 @@ module SolidObjects
|
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
# @rbs (Symbol | String, Proc) -> Handler
|
|
90
|
+
def add_payload_broadcast(name, block)
|
|
91
|
+
payload_name = name.to_sym
|
|
92
|
+
if payload_broadcasts.key?(payload_name)
|
|
93
|
+
raise InvalidActor, "#{payload_name.inspect} payload broadcast is already defined"
|
|
94
|
+
end
|
|
95
|
+
unless payload_name.to_s.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
96
|
+
raise InvalidActor, "payload broadcast names may contain only letters, digits, and underscores"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Handler.new(name: payload_name, block:).tap do |handler|
|
|
100
|
+
payload_broadcasts[payload_name] = handler
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
# @rbs (Class) -> ActorDefinition
|
|
87
105
|
def synchronize_instance_messages(actor_class)
|
|
88
106
|
names = actor_message_method_names(actor_class)
|
|
@@ -149,6 +167,7 @@ module SolidObjects
|
|
|
149
167
|
copy.instance_variable_set(:@messages, messages.dup)
|
|
150
168
|
copy.instance_variable_set(:@queries, queries.dup)
|
|
151
169
|
copy.instance_variable_set(:@observables, observables.dup)
|
|
170
|
+
copy.instance_variable_set(:@payload_broadcasts, payload_broadcasts.dup)
|
|
152
171
|
copy.instance_variable_set(:@state_version, state_version)
|
|
153
172
|
copy.instance_variable_set(:@state_migrations, state_migrations.dup)
|
|
154
173
|
copy.instance_variable_set(:@activation_hooks, activation_hooks.dup)
|
|
@@ -4,6 +4,9 @@ require "time"
|
|
|
4
4
|
|
|
5
5
|
module SolidObjects
|
|
6
6
|
class DatabaseAdapter
|
|
7
|
+
TRANSACTION_CLOCK = :solid_objects_transaction_clock
|
|
8
|
+
TRANSACTION_CLOCK_SCOPE = :solid_objects_transaction_clock_scope
|
|
9
|
+
|
|
7
10
|
class << self
|
|
8
11
|
# @rbs (untyped) -> DatabaseAdapter
|
|
9
12
|
def for(connection)
|
|
@@ -46,10 +49,9 @@ module SolidObjects
|
|
|
46
49
|
|
|
47
50
|
# @rbs () -> Time
|
|
48
51
|
def database_now
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
value.is_a?(Time) ? value.utc : Time.parse("#{value} UTC").utc
|
|
52
|
+
return read_database_now unless ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK_SCOPE]
|
|
53
|
+
|
|
54
|
+
ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK] ||= read_database_now
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# @rbs () { () -> untyped } -> untyped
|
|
@@ -70,7 +72,7 @@ module SolidObjects
|
|
|
70
72
|
with_transaction_deadline(connection) do
|
|
71
73
|
connection.transaction(requires_new: true) do
|
|
72
74
|
configure_transaction_deadline(connection)
|
|
73
|
-
block.call
|
|
75
|
+
with_transaction_clock { block.call }
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
end
|
|
@@ -91,6 +93,27 @@ module SolidObjects
|
|
|
91
93
|
|
|
92
94
|
attr_reader :connection_pool, :fixed_connection
|
|
93
95
|
|
|
96
|
+
# @rbs () { () -> untyped } -> untyped
|
|
97
|
+
def with_transaction_clock
|
|
98
|
+
return yield if ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK_SCOPE]
|
|
99
|
+
|
|
100
|
+
ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK_SCOPE] = true
|
|
101
|
+
begin
|
|
102
|
+
yield
|
|
103
|
+
ensure
|
|
104
|
+
ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK_SCOPE] = false
|
|
105
|
+
ActiveSupport::IsolatedExecutionState[TRANSACTION_CLOCK] = nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @rbs () -> Time
|
|
110
|
+
def read_database_now
|
|
111
|
+
value = with_connection do |connection|
|
|
112
|
+
connection.select_value("SELECT #{current_time_expression}")
|
|
113
|
+
end
|
|
114
|
+
value.is_a?(Time) ? value.utc : Time.parse("#{value} UTC").utc
|
|
115
|
+
end
|
|
116
|
+
|
|
94
117
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
95
118
|
def with_transaction_deadline(_connection)
|
|
96
119
|
yield
|
|
@@ -80,13 +80,13 @@ module SolidObjects
|
|
|
80
80
|
|
|
81
81
|
# @rbs (untyped) -> Hash[Symbol, untyped]?
|
|
82
82
|
def restorable_busy_wait(connection)
|
|
83
|
-
pragma_timeout = connection.select_value("PRAGMA busy_timeout").to_i
|
|
84
|
-
return { pragma_timeout: } if pragma_timeout.positive?
|
|
85
|
-
|
|
86
83
|
handler_timeout = configured_busy_handler_timeout(connection)
|
|
87
|
-
return
|
|
84
|
+
return { handler_timeout: } if handler_timeout
|
|
85
|
+
|
|
86
|
+
pragma_timeout = connection.select_value("PRAGMA busy_timeout").to_i
|
|
87
|
+
return nil unless pragma_timeout.positive?
|
|
88
88
|
|
|
89
|
-
{ pragma_timeout
|
|
89
|
+
{ pragma_timeout: }
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# @rbs (untyped, Hash[Symbol, untyped]) -> void
|
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -27,7 +27,7 @@ module SolidObjects
|
|
|
27
27
|
result = invoke_actor(message_context)
|
|
28
28
|
ensure_query_did_not_mutate_state!(state_before)
|
|
29
29
|
observable_changes = changed_observables(observables_before, actor.observable_values)
|
|
30
|
-
complete(result, observable_changes)
|
|
30
|
+
complete(result, observable_changes, state_changed: actor.state.to_h != state_before)
|
|
31
31
|
true
|
|
32
32
|
rescue LostActivation
|
|
33
33
|
raise
|
|
@@ -75,8 +75,8 @@ module SolidObjects
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# @rbs (untyped, Hash[String, untyped]) -> void
|
|
79
|
-
def complete(result, observable_changes)
|
|
78
|
+
# @rbs (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
79
|
+
def complete(result, observable_changes, state_changed:)
|
|
80
80
|
serialized_state = Serialization.dump(
|
|
81
81
|
actor.state.to_h,
|
|
82
82
|
max_bytes: SolidObjects.configuration.max_state_bytes
|
|
@@ -111,7 +111,12 @@ module SolidObjects
|
|
|
111
111
|
enqueued_effects.concat(
|
|
112
112
|
enqueue_actor_messages(locked_message, instance, outbound_message_intents)
|
|
113
113
|
)
|
|
114
|
-
enqueue_broadcasts(
|
|
114
|
+
enqueue_broadcasts(
|
|
115
|
+
locked_message,
|
|
116
|
+
instance,
|
|
117
|
+
observable_changes,
|
|
118
|
+
state_changed:
|
|
119
|
+
)
|
|
115
120
|
claimed_message.destroy!
|
|
116
121
|
end
|
|
117
122
|
|
|
@@ -249,9 +254,14 @@ module SolidObjects
|
|
|
249
254
|
end
|
|
250
255
|
end
|
|
251
256
|
|
|
252
|
-
# @rbs (Message, Instance, Hash[String, untyped]) -> void
|
|
253
|
-
def enqueue_broadcasts(locked_message, instance, observable_changes)
|
|
254
|
-
|
|
257
|
+
# @rbs (Message, Instance, Hash[String, untyped], state_changed: bool) -> void
|
|
258
|
+
def enqueue_broadcasts(locked_message, instance, observable_changes, state_changed:)
|
|
259
|
+
broadcasts = observable_changes
|
|
260
|
+
if broadcasts.empty? && state_changed && payload_broadcasts?
|
|
261
|
+
broadcasts = { PayloadBroadcast::REVISION_OBSERVABLE => {} }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
broadcasts.each do |observable_name, value|
|
|
255
265
|
Broadcast.create!(
|
|
256
266
|
message: locked_message,
|
|
257
267
|
instance:,
|
|
@@ -266,6 +276,11 @@ module SolidObjects
|
|
|
266
276
|
end
|
|
267
277
|
end
|
|
268
278
|
|
|
279
|
+
# @rbs () -> bool
|
|
280
|
+
def payload_broadcasts?
|
|
281
|
+
actor.class.definition.payload_broadcasts.any?
|
|
282
|
+
end
|
|
283
|
+
|
|
269
284
|
# @rbs (Exception) -> void
|
|
270
285
|
def fail_message(error)
|
|
271
286
|
error_details = serialized_error(error)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class PayloadBroadcast
|
|
5
|
+
MAXIMUM_PAYLOAD_BYTES = 1_048_576
|
|
6
|
+
REVISION_OBSERVABLE = "solid_objects.revision"
|
|
7
|
+
|
|
8
|
+
# @rbs @snapshot: ActorSnapshot
|
|
9
|
+
# @rbs @name: String
|
|
10
|
+
# @rbs @authorization_context: untyped
|
|
11
|
+
|
|
12
|
+
attr_reader :name
|
|
13
|
+
|
|
14
|
+
# @rbs (snapshot: ActorSnapshot, name: String, authorization_context: untyped) -> void
|
|
15
|
+
def initialize(snapshot:, name:, authorization_context:)
|
|
16
|
+
@snapshot = snapshot
|
|
17
|
+
@name = name
|
|
18
|
+
@authorization_context = authorization_context
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs () -> Hash[String, untyped]
|
|
22
|
+
def call
|
|
23
|
+
handler = snapshot.actor_class.definition.payload_broadcasts[name.to_sym]
|
|
24
|
+
raise UnknownPayloadBroadcast, "unknown payload broadcast #{name.inspect}" unless handler
|
|
25
|
+
|
|
26
|
+
authorize!
|
|
27
|
+
{
|
|
28
|
+
"actor_type" => snapshot.reference.actor_type,
|
|
29
|
+
"actor_id" => snapshot.reference.actor_id,
|
|
30
|
+
"name" => name,
|
|
31
|
+
"instance_id" => snapshot.instance_id,
|
|
32
|
+
"revision" => snapshot.revision,
|
|
33
|
+
"payload" => rendered_payload(handler)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :snapshot, :authorization_context
|
|
40
|
+
|
|
41
|
+
# @rbs (ActorDefinition::Handler) -> untyped
|
|
42
|
+
def rendered_payload(handler)
|
|
43
|
+
payload = Serialization.dump(
|
|
44
|
+
handler.block.call(snapshot.actor, authorization_context),
|
|
45
|
+
max_bytes: MAXIMUM_PAYLOAD_BYTES
|
|
46
|
+
)
|
|
47
|
+
return payload if payload.is_a?(Hash) || payload.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
raise InvalidPayloadBroadcast,
|
|
50
|
+
"payload broadcast #{name.inspect} must return a JSON object or array"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs () -> void
|
|
54
|
+
def authorize!
|
|
55
|
+
authorized = SolidObjects.configuration.authorize_query.call(
|
|
56
|
+
actor_type: snapshot.reference.actor_type,
|
|
57
|
+
actor_id: snapshot.reference.actor_id,
|
|
58
|
+
message_name: name,
|
|
59
|
+
arguments: {},
|
|
60
|
+
authorization_context:
|
|
61
|
+
)
|
|
62
|
+
return if authorized
|
|
63
|
+
|
|
64
|
+
raise Unauthorized, "actor payload broadcast is not authorized"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -9,13 +9,14 @@ module SolidObjects
|
|
|
9
9
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
-
# @rbs (Reference, ?observables: Array[String]?) -> String
|
|
13
|
-
def generate(reference, observables: nil)
|
|
12
|
+
# @rbs (Reference, ?observables: Array[String]?, ?payloads: Array[String]?) -> String
|
|
13
|
+
def generate(reference, observables: nil, payloads: nil)
|
|
14
14
|
identity = {
|
|
15
15
|
"actor_type" => reference.actor_type,
|
|
16
16
|
"actor_id" => reference.actor_id
|
|
17
17
|
}
|
|
18
18
|
identity["observables"] = observables if observables
|
|
19
|
+
identity["payloads"] = payloads if payloads
|
|
19
20
|
validate_identity!(identity)
|
|
20
21
|
verifier.generate(identity, purpose: PURPOSE)
|
|
21
22
|
end
|
|
@@ -36,21 +37,27 @@ module SolidObjects
|
|
|
36
37
|
raise InvalidStreamToken, "invalid actor stream token"
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
validate_names!(identity["observables"], "observables")
|
|
41
|
+
validate_names!(identity["payloads"], "payloads")
|
|
42
|
+
identity
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs (untyped, String) -> void
|
|
46
|
+
def validate_names!(names, label)
|
|
47
|
+
return unless names
|
|
41
48
|
|
|
42
|
-
valid =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
observable.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
49
|
+
valid = names.is_a?(Array) &&
|
|
50
|
+
names.length <= MAXIMUM_OBSERVABLES &&
|
|
51
|
+
names.uniq.length == names.length &&
|
|
52
|
+
names.all? do |name|
|
|
53
|
+
name.is_a?(String) && name.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
48
54
|
end
|
|
49
|
-
return
|
|
55
|
+
return if valid
|
|
50
56
|
|
|
51
|
-
raise InvalidStreamToken, "invalid actor stream
|
|
57
|
+
raise InvalidStreamToken, "invalid actor stream #{label}"
|
|
52
58
|
end
|
|
53
59
|
private_class_method :validate_identity!
|
|
60
|
+
private_class_method :validate_names!
|
|
54
61
|
|
|
55
62
|
# @rbs () -> ActiveSupport::MessageVerifier
|
|
56
63
|
def verifier
|
|
@@ -16,11 +16,15 @@ module SolidObjects
|
|
|
16
16
|
actor_type: broadcast.instance.actor_type,
|
|
17
17
|
actor_id: broadcast.instance.actor_id
|
|
18
18
|
)
|
|
19
|
-
stream =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
stream = if broadcast.observable_name == PayloadBroadcast::REVISION_OBSERVABLE
|
|
20
|
+
""
|
|
21
|
+
else
|
|
22
|
+
observable_value(
|
|
23
|
+
reference,
|
|
24
|
+
broadcast.observable_name,
|
|
25
|
+
broadcast.value
|
|
26
|
+
)
|
|
27
|
+
end
|
|
24
28
|
metadata = Base64.urlsafe_encode64(
|
|
25
29
|
JSON.generate(
|
|
26
30
|
"instance_id" => broadcast.instance_id,
|
|
@@ -50,6 +54,19 @@ module SolidObjects
|
|
|
50
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>)
|
|
51
55
|
end
|
|
52
56
|
|
|
57
|
+
# @rbs (Hash[String, untyped]) -> String
|
|
58
|
+
def state_payload(payload)
|
|
59
|
+
reference = Reference.new(
|
|
60
|
+
actor_type: payload.fetch("actor_type"),
|
|
61
|
+
actor_id: payload.fetch("actor_id")
|
|
62
|
+
)
|
|
63
|
+
scope = DomIdentity.scope(reference)
|
|
64
|
+
name = ERB::Util.html_escape(payload.fetch("name"))
|
|
65
|
+
revision = "#{payload.fetch("instance_id")}:#{payload.fetch("revision")}"
|
|
66
|
+
body = ERB::Util.html_escape(JSON.generate(payload.fetch("payload")))
|
|
67
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-payload data-name="#{name}" data-revision="#{revision}">#{body}</solid-objects-payload></template></turbo-stream>)
|
|
68
|
+
end
|
|
69
|
+
|
|
53
70
|
# @rbs (String) -> Hash[String, untyped]?
|
|
54
71
|
def invalidation(stream)
|
|
55
72
|
encoded = stream[INVALIDATION_PATTERN, 1]
|
data/lib/solid_objects.rb
CHANGED
|
@@ -40,6 +40,7 @@ require "solid_objects/component_registration"
|
|
|
40
40
|
require "solid_objects/component_subscriptions"
|
|
41
41
|
require "solid_objects/component_view"
|
|
42
42
|
require "solid_objects/component_renderer"
|
|
43
|
+
require "solid_objects/payload_broadcast"
|
|
43
44
|
require "solid_objects/state_snapshot"
|
|
44
45
|
require "solid_objects/actor_view"
|
|
45
46
|
require "solid_objects/actor_channel"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
module ActorHelper
|
|
5
|
-
# @rbs (Reference, ?authorization_context: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
-
def solid_object: (Reference, ?authorization_context: untyped) { (ActorView) -> untyped } -> untyped
|
|
5
|
+
# @rbs (Reference, ?authorization_context: untyped, ?payloads: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
+
def solid_object: (Reference, ?authorization_context: untyped, ?payloads: untyped) { (ActorView) -> untyped } -> untyped
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -90,6 +90,9 @@ module SolidObjects
|
|
|
90
90
|
# @rbs (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
|
|
91
91
|
def self.observable: (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
|
|
92
92
|
|
|
93
|
+
# @rbs (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
|
|
94
|
+
def self.broadcast_payload: (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
|
|
95
|
+
|
|
93
96
|
# @rbs (?Integer) -> Integer
|
|
94
97
|
def self.state_version: (?Integer) -> Integer
|
|
95
98
|
|
|
@@ -13,9 +13,20 @@ module SolidObjects
|
|
|
13
13
|
|
|
14
14
|
attr_reader scalar_observables: untyped
|
|
15
15
|
|
|
16
|
+
attr_reader payload_names: untyped
|
|
17
|
+
|
|
16
18
|
# @rbs (String) -> void
|
|
17
19
|
def receive_broadcast: (String) -> void
|
|
18
20
|
|
|
21
|
+
# @rbs (ActorSnapshot) -> void
|
|
22
|
+
def transmit_state_payloads: (ActorSnapshot) -> void
|
|
23
|
+
|
|
24
|
+
# @rbs (ActorSnapshot) -> bool
|
|
25
|
+
def newer_payload_revision?: (ActorSnapshot) -> bool
|
|
26
|
+
|
|
27
|
+
# @rbs () -> void
|
|
28
|
+
def validate_payload_names!: () -> void
|
|
29
|
+
|
|
19
30
|
# @rbs (ActorSnapshot) -> void
|
|
20
31
|
def refresh_outdated_components: (ActorSnapshot) -> void
|
|
21
32
|
|
|
@@ -42,6 +42,8 @@ module SolidObjects
|
|
|
42
42
|
|
|
43
43
|
@state_version: Integer
|
|
44
44
|
|
|
45
|
+
@payload_broadcasts: Hash[Symbol, Handler]
|
|
46
|
+
|
|
45
47
|
@observables: Hash[Symbol, Handler]
|
|
46
48
|
|
|
47
49
|
@queries: Hash[Symbol, Handler]
|
|
@@ -58,6 +60,8 @@ module SolidObjects
|
|
|
58
60
|
|
|
59
61
|
attr_reader observables: untyped
|
|
60
62
|
|
|
63
|
+
attr_reader payload_broadcasts: untyped
|
|
64
|
+
|
|
61
65
|
attr_reader state_version: untyped
|
|
62
66
|
|
|
63
67
|
attr_reader state_migrations: untyped
|
|
@@ -81,6 +85,9 @@ module SolidObjects
|
|
|
81
85
|
# @rbs (Symbol | String, Proc?) -> Handler
|
|
82
86
|
def add_observable: (Symbol | String, Proc?) -> Handler
|
|
83
87
|
|
|
88
|
+
# @rbs (Symbol | String, Proc) -> Handler
|
|
89
|
+
def add_payload_broadcast: (Symbol | String, Proc) -> Handler
|
|
90
|
+
|
|
84
91
|
# @rbs (Class) -> ActorDefinition
|
|
85
92
|
def synchronize_instance_messages: (Class) -> ActorDefinition
|
|
86
93
|
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class DatabaseAdapter
|
|
5
|
+
TRANSACTION_CLOCK: ::Symbol
|
|
6
|
+
|
|
7
|
+
TRANSACTION_CLOCK_SCOPE: ::Symbol
|
|
8
|
+
|
|
5
9
|
# @rbs (untyped) -> DatabaseAdapter
|
|
6
10
|
def self.for: (untyped) -> DatabaseAdapter
|
|
7
11
|
|
|
@@ -42,6 +46,12 @@ module SolidObjects
|
|
|
42
46
|
|
|
43
47
|
attr_reader fixed_connection: untyped
|
|
44
48
|
|
|
49
|
+
# @rbs () { () -> untyped } -> untyped
|
|
50
|
+
def with_transaction_clock: () { () -> untyped } -> untyped
|
|
51
|
+
|
|
52
|
+
# @rbs () -> Time
|
|
53
|
+
def read_database_now: () -> Time
|
|
54
|
+
|
|
45
55
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
46
56
|
def with_transaction_deadline: (untyped) { () -> untyped } -> untyped
|
|
47
57
|
|
|
@@ -30,8 +30,8 @@ module SolidObjects
|
|
|
30
30
|
# @rbs (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
|
|
31
31
|
def changed_observables: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
|
|
32
32
|
|
|
33
|
-
# @rbs (untyped, Hash[String, untyped]) -> void
|
|
34
|
-
def complete: (untyped, Hash[String, untyped]) -> void
|
|
33
|
+
# @rbs (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
34
|
+
def complete: (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
35
35
|
|
|
36
36
|
# @rbs (Array[Actor::CommitActionIntent]) -> void
|
|
37
37
|
def execute_commit_actions: (Array[Actor::CommitActionIntent]) -> void
|
|
@@ -51,8 +51,11 @@ module SolidObjects
|
|
|
51
51
|
# @rbs (Message, Instance, Array[Actor::OutboundMessageIntent]) -> Array[Effect]
|
|
52
52
|
def enqueue_actor_messages: (Message, Instance, Array[Actor::OutboundMessageIntent]) -> Array[Effect]
|
|
53
53
|
|
|
54
|
-
# @rbs (Message, Instance, Hash[String, untyped]) -> void
|
|
55
|
-
def enqueue_broadcasts: (Message, Instance, Hash[String, untyped]) -> void
|
|
54
|
+
# @rbs (Message, Instance, Hash[String, untyped], state_changed: bool) -> void
|
|
55
|
+
def enqueue_broadcasts: (Message, Instance, Hash[String, untyped], state_changed: bool) -> void
|
|
56
|
+
|
|
57
|
+
# @rbs () -> bool
|
|
58
|
+
def payload_broadcasts?: () -> bool
|
|
56
59
|
|
|
57
60
|
# @rbs (Exception) -> void
|
|
58
61
|
def fail_message: (Exception) -> void
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/payload_broadcast.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class PayloadBroadcast
|
|
5
|
+
MAXIMUM_PAYLOAD_BYTES: ::Integer
|
|
6
|
+
|
|
7
|
+
REVISION_OBSERVABLE: ::String
|
|
8
|
+
|
|
9
|
+
@snapshot: ActorSnapshot
|
|
10
|
+
|
|
11
|
+
@name: String
|
|
12
|
+
|
|
13
|
+
@authorization_context: untyped
|
|
14
|
+
|
|
15
|
+
attr_reader name: untyped
|
|
16
|
+
|
|
17
|
+
# @rbs (snapshot: ActorSnapshot, name: String, authorization_context: untyped) -> void
|
|
18
|
+
def initialize: (snapshot: ActorSnapshot, name: String, authorization_context: untyped) -> void
|
|
19
|
+
|
|
20
|
+
# @rbs () -> Hash[String, untyped]
|
|
21
|
+
def call: () -> Hash[String, untyped]
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader snapshot: untyped
|
|
26
|
+
|
|
27
|
+
attr_reader authorization_context: untyped
|
|
28
|
+
|
|
29
|
+
# @rbs (ActorDefinition::Handler) -> untyped
|
|
30
|
+
def rendered_payload: (ActorDefinition::Handler) -> untyped
|
|
31
|
+
|
|
32
|
+
# @rbs () -> void
|
|
33
|
+
def authorize!: () -> void
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -6,8 +6,8 @@ module SolidObjects
|
|
|
6
6
|
|
|
7
7
|
MAXIMUM_OBSERVABLES: ::Integer
|
|
8
8
|
|
|
9
|
-
# @rbs (Reference, ?observables: Array[String]?) -> String
|
|
10
|
-
def self?.generate: (Reference, ?observables: Array[String]?) -> String
|
|
9
|
+
# @rbs (Reference, ?observables: Array[String]?, ?payloads: Array[String]?) -> String
|
|
10
|
+
def self?.generate: (Reference, ?observables: Array[String]?, ?payloads: Array[String]?) -> String
|
|
11
11
|
|
|
12
12
|
# @rbs (String) -> Hash[String, untyped]
|
|
13
13
|
def self?.verify: (String) -> Hash[String, untyped]
|
|
@@ -15,6 +15,9 @@ module SolidObjects
|
|
|
15
15
|
# @rbs (Hash[String, untyped]) -> Hash[String, untyped]
|
|
16
16
|
def self?.validate_identity!: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
17
17
|
|
|
18
|
+
# @rbs (untyped, String) -> void
|
|
19
|
+
def self?.validate_names!: (untyped, String) -> void
|
|
20
|
+
|
|
18
21
|
# @rbs () -> ActiveSupport::MessageVerifier
|
|
19
22
|
def self?.verifier: () -> ActiveSupport::MessageVerifier
|
|
20
23
|
|
|
@@ -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 (Hash[String, untyped]) -> String
|
|
19
|
+
def self?.state_payload: (Hash[String, untyped]) -> String
|
|
20
|
+
|
|
18
21
|
# @rbs (String) -> Hash[String, untyped]?
|
|
19
22
|
def self?.invalidation: (String) -> Hash[String, untyped]?
|
|
20
23
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -263,6 +263,7 @@ files:
|
|
|
263
263
|
- README.md
|
|
264
264
|
- Rakefile
|
|
265
265
|
- app/assets/javascripts/solid_objects/component_refresh.js
|
|
266
|
+
- app/assets/javascripts/solid_objects/state_payload.js
|
|
266
267
|
- app/controllers/solid_objects/application_controller.rb
|
|
267
268
|
- app/controllers/solid_objects/components_controller.rb
|
|
268
269
|
- app/controllers/solid_objects/dead_letters_controller.rb
|
|
@@ -382,6 +383,7 @@ files:
|
|
|
382
383
|
- lib/solid_objects/mailbox.rb
|
|
383
384
|
- lib/solid_objects/message_pruner.rb
|
|
384
385
|
- lib/solid_objects/message_reference.rb
|
|
386
|
+
- lib/solid_objects/payload_broadcast.rb
|
|
385
387
|
- lib/solid_objects/process_pruner.rb
|
|
386
388
|
- lib/solid_objects/process_registry.rb
|
|
387
389
|
- lib/solid_objects/reference.rb
|
|
@@ -452,6 +454,7 @@ files:
|
|
|
452
454
|
- sig/generated/lib/solid_objects/mailbox.rbs
|
|
453
455
|
- sig/generated/lib/solid_objects/message_pruner.rbs
|
|
454
456
|
- sig/generated/lib/solid_objects/message_reference.rbs
|
|
457
|
+
- sig/generated/lib/solid_objects/payload_broadcast.rbs
|
|
455
458
|
- sig/generated/lib/solid_objects/process_pruner.rbs
|
|
456
459
|
- sig/generated/lib/solid_objects/process_registry.rbs
|
|
457
460
|
- sig/generated/lib/solid_objects/reference.rbs
|