solid_objects 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +54 -7
  4. data/app/assets/javascripts/solid_objects/component_refresh.js +124 -0
  5. data/app/controllers/solid_objects/components_controller.rb +31 -9
  6. data/app/helpers/solid_objects/actor_helper.rb +8 -1
  7. data/docs/adr/0009-realtime-updates.md +18 -1
  8. data/docs/architecture.md +34 -18
  9. data/docs/authorization.md +24 -0
  10. data/docs/correctness.md +27 -5
  11. data/docs/operations.md +20 -2
  12. data/docs/realtime.md +78 -13
  13. data/docs/roadmap.md +6 -5
  14. data/examples/application/app/views/chat_rooms/show.html.erb +3 -1
  15. data/lib/solid_objects/actor_view.rb +40 -13
  16. data/lib/solid_objects/component_registration.rb +61 -16
  17. data/lib/solid_objects/component_renderer.rb +14 -17
  18. data/lib/solid_objects/component_subscriptions.rb +7 -7
  19. data/lib/solid_objects/component_token.rb +70 -2
  20. data/lib/solid_objects/database_adapters/sqlite.rb +43 -5
  21. data/lib/solid_objects/doctor.rb +46 -8
  22. data/lib/solid_objects/dom_identity.rb +12 -3
  23. data/lib/solid_objects/engine.rb +7 -0
  24. data/lib/solid_objects/synchronous_invocation.rb +16 -1
  25. data/lib/solid_objects/turbo_stream_renderer.rb +14 -5
  26. data/lib/solid_objects/version.rb +1 -1
  27. data/sig/generated/controllers/solid_objects/components_controller.rbs +6 -0
  28. data/sig/generated/lib/solid_objects/actor_view.rbs +10 -4
  29. data/sig/generated/lib/solid_objects/component_registration.rbs +34 -10
  30. data/sig/generated/lib/solid_objects/component_renderer.rbs +4 -8
  31. data/sig/generated/lib/solid_objects/component_token.rbs +24 -2
  32. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +9 -0
  33. data/sig/generated/lib/solid_objects/doctor.rbs +12 -0
  34. data/sig/generated/lib/solid_objects/dom_identity.rbs +5 -2
  35. data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +8 -0
  36. data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +3 -0
  37. metadata +3 -2
data/docs/realtime.md CHANGED
@@ -28,11 +28,37 @@ never influence partial resolution. The older
28
28
  `actor.component(:summary, partial: "server/chosen/path")` form remains
29
29
  available for initial-only static rendering.
30
30
 
31
- The partial receives exactly two component locals:
31
+ The partial receives these built-in component locals:
32
32
 
33
33
  - `actor`, which exposes the declared observables as deeply frozen ordinary
34
34
  Ruby values plus `actor_id` and `reference`; and
35
35
  - `authorization_context`, the context for this initial render or refresh.
36
+ - `component_key`, the signed string or integer key, or `nil` for an unkeyed
37
+ component.
38
+
39
+ Applications can declare additional JSON-compatible locals. They are
40
+ normalized, signed into the component token, deeply frozen, and supplied on
41
+ both the initial render and every refresh:
42
+
43
+ ```erb
44
+ <% @players.each do |player| %>
45
+ <%= actor.component :player,
46
+ key: player.id,
47
+ observes: %i[players life_totals],
48
+ locals: { player_id: player.id } %>
49
+ <% end %>
50
+ ```
51
+
52
+ This resolves every instance to the same `_player.html.erb` partial while
53
+ giving each one a distinct opaque DOM target. The `(component name, key)` pair
54
+ must be unique within one `solid_object` scope. An unkeyed component retains
55
+ the existing target and uniqueness behavior.
56
+
57
+ Local names must be valid Ruby local identifiers. `actor`,
58
+ `authorization_context`, and `component_key` are reserved. Local values must
59
+ use the same safe JSON value set as actor messages. Tokens are limited to
60
+ 16 KiB and one subscription accepts at most 50 components, so locals should be
61
+ small identifiers or rendering options rather than copied actor state.
36
62
 
37
63
  `actor.state` is unavailable in reactive components. Reading an observable not
38
64
  listed in `observes:` raises `UnknownComponentDependency`. This keeps
@@ -51,6 +77,39 @@ Arrays, hashes, loops, conditionals, nested markup, and host helper output are
51
77
  normal ERB. Escaping remains Action View's responsibility; Solid Objects never
52
78
  marks actor strings as HTML safe.
53
79
 
80
+ ## Replace and morph refreshes
81
+
82
+ Reactive components use `refresh: :replace` by default. The existing path
83
+ replaces the target with a Turbo Frame whose signed URL performs the authorized
84
+ request-time render.
85
+
86
+ Use `refresh: :morph` when preserving unchanged DOM nodes matters:
87
+
88
+ ```erb
89
+ <%= actor.component :battlefield,
90
+ key: player.id,
91
+ observes: %i[battlefields zone_counts],
92
+ locals: { player_id: player.id },
93
+ refresh: :morph %>
94
+ ```
95
+
96
+ Morph invalidations append a short-lived gem-owned browser element to the
97
+ actor scope. It fetches the same signed component endpoint with normal
98
+ same-origin cookies, aborts an older request for the same keyed target, and
99
+ converts the authorized response into Turbo's scoped
100
+ `replace method="morph"`. Before applying it, the browser compares the
101
+ response's `(instance_id, state_revision)` with the current target. An older
102
+ response cannot overwrite newer HTML.
103
+
104
+ The engine exposes the `solid_objects/component_refresh` module through the
105
+ host asset pipeline and `solid_object` includes it only when the scope contains
106
+ a morph component. No host Stimulus controller, custom channel, custom stream
107
+ action, or polling loop is required. Default Propshaft and Sprockets
108
+ applications discover the namespaced engine asset. Applications created with
109
+ `--skip-asset-pipeline` should keep the default replace strategy unless they
110
+ explicitly serve the module. Turbo's normal morph rules still apply; use
111
+ `data-turbo-permanent` for elements that must never be changed.
112
+
54
113
  ## Authorization
55
114
 
56
115
  The HTML contains a signed actor identity token. Signing prevents modification;
@@ -61,9 +120,10 @@ approval.
61
120
  Initial scalar and component reads call `authorize_query` with the context
62
121
  passed to `solid_object`. The refresh controller resolves a new request context
63
122
  through `component_authorization_context`, then calls `authorize_query` again
64
- for the component name and every declared dependency. The default resolver
65
- supplies the engine controller; applications commonly resolve it to
66
- `Current.user`:
123
+ for the component name and every declared dependency. Keyed registrations pass
124
+ their `component_key` plus all declared locals as `arguments` at both
125
+ boundaries. The default resolver supplies the engine controller; applications
126
+ commonly resolve it to `Current.user`:
67
127
 
68
128
  ```ruby
69
129
  configuration.component_authorization_context = ->(controller:) { Current.user }
@@ -77,8 +137,10 @@ The three contexts are intentionally different:
77
137
  | Action Cable subscription | The authenticated Cable connection |
78
138
  | Component refresh | Value returned by `component_authorization_context` for the engine controller request |
79
139
 
80
- Do not substitute a signed token for any of them. Never authorize solely from
81
- actor ID, token possession, stream name, component name, or DOM ID.
140
+ Do not substitute a signed token for any of them. Keys and locals are visible
141
+ to the browser and signed for integrity, not encrypted or authorized. Never
142
+ authorize solely from actor ID, token possession, stream name, component name,
143
+ component key, locals, or DOM ID.
82
144
 
83
145
  ## Broadcast durability
84
146
 
@@ -96,10 +158,11 @@ authorized browser requests affected components through the engine endpoint
96
158
  with its normal cookies. Responses are `private, no-store`.
97
159
 
98
160
  Several changed dependencies from one message sequence produce one logical
99
- refresh for a component. An unrelated observable does not refresh it. If a
100
- newer invalidation arrives while a Turbo Frame request is in flight, the new
101
- frame replaces the old frame element; the detached older response has no
102
- current target.
161
+ refresh for each keyed component registration. An unrelated observable does
162
+ not refresh it. If a newer invalidation arrives while a replace request is in
163
+ flight, the new frame replaces the old frame element; the detached older
164
+ response has no current target. Morph requests are coalesced per target with an
165
+ `AbortController` and perform a final client-side revision comparison.
103
166
 
104
167
  If Cable delivery is lost, reconnecting `ActorChannel` transmits replacements
105
168
  from current actor state. It compares the signed component revision with the
@@ -111,8 +174,8 @@ truth.
111
174
 
112
175
  The component endpoint rejects a requested revision newer than the committed
113
176
  snapshot. This is a final server-side guard; browser safety primarily comes
114
- from monotonic channel filtering and replacing the entire Turbo Frame
115
- generation.
177
+ from monotonic channel filtering plus replace-frame detachment or morph
178
+ response revision fencing.
116
179
 
117
180
  ## Cost model
118
181
 
@@ -120,7 +183,9 @@ The durable row cost is unchanged: one broadcast row per changed observable,
120
183
  containing its JSON value and the message/instance references needed to derive
121
184
  invalidation metadata. No rendered document is stored. Each affected component
122
185
  adds one authorized GET and one partial render per non-coalesced state
123
- revision. Scalar observables remain the cheaper path for one text value.
186
+ revision. A repeated keyed component adds one GET and render per key. Signed
187
+ locals increase page and Cable subscription bytes but do not create durable
188
+ rows. Scalar observables remain the cheaper path for one text value.
124
189
 
125
190
  ## Deployment
126
191
 
data/docs/roadmap.md CHANGED
@@ -15,8 +15,8 @@
15
15
  - Transactional effects with success/failure actor messages
16
16
  - Actor-to-actor asynchronous outbox delivery
17
17
  - One-shot and recurring reminders with `:latest` or `:all` catch-up
18
- - Durable observable invalidations, scalar Turbo replacement, and authorized
19
- request-time ERB component refresh
18
+ - Durable observable invalidations, scalar Turbo replacement, keyed ERB
19
+ components, signed component locals, and authorized replace or morph refresh
20
20
  - Reconciliation read APIs
21
21
  - Installation doctor, authorization reference, fit guide, and legacy-state
22
22
  migration cookbook
@@ -36,9 +36,10 @@
36
36
  role or run periodic maintenance automatically.
37
37
  - Wake-up strategy: in-process signaling plus durable polling and injection are
38
38
  implemented; PostgreSQL `LISTEN/NOTIFY` and optional Redis adapters are not.
39
- - Realtime: scalar and dependency-driven ERB component replacement,
40
- personalized refresh authorization, revision fencing, coalescing, and
41
- reconnect convergence are implemented; Turbo append actions are not.
39
+ - Realtime: scalar and dependency-driven keyed ERB component replacement or
40
+ morphing, personalized refresh authorization, revision fencing, coalescing,
41
+ and reconnect convergence are implemented; application-directed Turbo
42
+ append intents are not.
42
43
  - Backpressure: mailbox/payload/state/result caps and fair yields exist;
43
44
  distributed per-actor rate limits and global admission control do not.
44
45
  - Administration: actor and dead-letter views plus policy hooks exist; richer
@@ -4,5 +4,7 @@
4
4
  <%= room.presence %>
5
5
  </p>
6
6
 
7
- <%= room.component :messages, observes: :recent_messages %>
7
+ <%= room.component :messages,
8
+ observes: :recent_messages,
9
+ refresh: :morph %>
8
10
  <% end %>
@@ -37,10 +37,20 @@ module SolidObjects
37
37
  )
38
38
  end
39
39
 
40
- # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
41
- def component(name, observes: nil, partial: nil)
40
+ # @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
41
+ def component(
42
+ name,
43
+ observes: nil,
44
+ partial: nil,
45
+ key: nil,
46
+ locals: {},
47
+ refresh: :replace
48
+ )
42
49
  component_name = normalized_component_name(name)
43
- return static_component(component_name, partial:) unless observes
50
+ unless observes
51
+ validate_static_options!(key:, locals:, refresh:)
52
+ return static_component(component_name, partial:)
53
+ end
44
54
  if partial
45
55
  raise ArgumentError,
46
56
  "reactive components resolve partials by actor and component name"
@@ -48,19 +58,21 @@ module SolidObjects
48
58
 
49
59
  dependencies = normalized_dependencies(observes)
50
60
  validate_dependencies!(dependencies)
51
- ensure_unique_component!(component_name)
52
61
  refresh_path = component_path_resolver.call(view_context:)
53
62
  registration = ComponentRegistration.issue(
54
63
  reference:,
55
64
  component_name:,
65
+ component_key: key,
56
66
  dependencies:,
67
+ locals:,
68
+ refresh_method: refresh,
57
69
  snapshot:,
58
70
  refresh_path:
59
71
  )
72
+ ensure_unique_component!(registration)
60
73
  rendered = ComponentRenderer.new(
61
74
  snapshot:,
62
- component_name:,
63
- dependencies:,
75
+ registration:,
64
76
  view_context:,
65
77
  authorization_context:
66
78
  ).call
@@ -68,9 +80,10 @@ module SolidObjects
68
80
  view_context.content_tag(
69
81
  :"turbo-frame",
70
82
  rendered,
71
- id: DomIdentity.component(reference, component_name),
83
+ id: registration.dom_id,
72
84
  data: {
73
- solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}"
85
+ solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}",
86
+ solid_objects_refresh: registration.refresh_method
74
87
  }
75
88
  )
76
89
  end
@@ -85,6 +98,11 @@ module SolidObjects
85
98
  observable_names.dup
86
99
  end
87
100
 
101
+ # @rbs () -> bool
102
+ def morph_components?
103
+ component_registrations.any?(&:morph?)
104
+ end
105
+
88
106
  # @rbs () -> State
89
107
  def state
90
108
  snapshot.actor.state
@@ -183,14 +201,23 @@ module SolidObjects
183
201
  "unknown observable dependency #{unknown.inspect} for #{reference.actor_type}"
184
202
  end
185
203
 
186
- # @rbs (String) -> void
187
- def ensure_unique_component!(component_name)
188
- return unless component_registrations.any? do |registration|
189
- registration.component_name == component_name
204
+ # @rbs (ComponentRegistration) -> void
205
+ def ensure_unique_component!(registration)
206
+ return unless component_registrations.any? do |existing_registration|
207
+ existing_registration.dom_id == registration.dom_id
190
208
  end
191
209
 
192
210
  raise ArgumentError,
193
- "component #{component_name.inspect} is already rendered in this solid_object scope"
211
+ "component #{registration.component_name.inspect} with key " \
212
+ "#{registration.component_key.inspect} is already rendered in this solid_object scope"
213
+ end
214
+
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"
218
+
219
+ raise ArgumentError,
220
+ "key, locals, and refresh require an observable component"
194
221
  end
195
222
 
196
223
  # @rbs () -> Proc | ComponentPathResolver
@@ -6,7 +6,10 @@ module SolidObjects
6
6
  class ComponentRegistration
7
7
  # @rbs @reference: Reference
8
8
  # @rbs @component_name: String
9
+ # @rbs @component_key: String | Integer?
9
10
  # @rbs @dependencies: Array[String]
11
+ # @rbs @locals: Hash[String, untyped]
12
+ # @rbs @refresh_method: String
10
13
  # @rbs @instance_id: Integer
11
14
  # @rbs @revision: Integer
12
15
  # @rbs @refresh_path: String
@@ -14,17 +17,23 @@ module SolidObjects
14
17
 
15
18
  attr_reader :reference,
16
19
  :component_name,
20
+ :component_key,
17
21
  :dependencies,
22
+ :locals,
23
+ :refresh_method,
18
24
  :instance_id,
19
25
  :revision,
20
26
  :refresh_path,
21
27
  :token
22
28
 
23
- # @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
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
24
30
  def initialize(
25
31
  reference:,
26
32
  component_name:,
33
+ component_key:,
27
34
  dependencies:,
35
+ locals:,
36
+ refresh_method:,
28
37
  instance_id:,
29
38
  revision:,
30
39
  refresh_path:,
@@ -32,7 +41,10 @@ module SolidObjects
32
41
  )
33
42
  @reference = reference
34
43
  @component_name = component_name
44
+ @component_key = component_key
35
45
  @dependencies = dependencies.freeze
46
+ @locals = Serialization.readonly_copy(locals)
47
+ @refresh_method = refresh_method
36
48
  @instance_id = instance_id
37
49
  @revision = revision
38
50
  @refresh_path = refresh_path
@@ -40,30 +52,43 @@ module SolidObjects
40
52
  end
41
53
 
42
54
  class << self
43
- # @rbs (reference: Reference, component_name: String, dependencies: Array[String], snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
44
- def issue(reference:, component_name:, dependencies:, snapshot:, refresh_path:)
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
56
+ def issue(
57
+ reference:,
58
+ component_name:,
59
+ component_key:,
60
+ dependencies:,
61
+ locals:,
62
+ refresh_method:,
63
+ snapshot:,
64
+ refresh_path:
65
+ )
45
66
  token = ComponentToken.generate(
46
67
  reference:,
47
68
  component_name:,
69
+ component_key:,
48
70
  dependencies:,
71
+ locals:,
72
+ refresh_method:,
49
73
  instance_id: snapshot.instance_id,
50
74
  revision: snapshot.revision,
51
75
  refresh_path:
52
76
  )
53
- new(
54
- reference:,
55
- component_name:,
56
- dependencies:,
57
- instance_id: snapshot.instance_id,
58
- revision: snapshot.revision,
59
- refresh_path:,
60
- token:
61
- )
77
+ build(ComponentToken.verify(token), token:)
62
78
  end
63
79
 
64
80
  # @rbs (String) -> ComponentRegistration
65
81
  def from_token(token)
66
82
  payload = ComponentToken.verify(token)
83
+ build(payload, token:)
84
+ rescue UnknownActorType, UnknownComponentDependency => error
85
+ raise InvalidComponentToken, error.message
86
+ end
87
+
88
+ private
89
+
90
+ # @rbs (Hash[String, untyped], token: String) -> ComponentRegistration
91
+ def build(payload, token:)
67
92
  reference = Reference.new(
68
93
  actor_type: payload.fetch("actor_type"),
69
94
  actor_id: payload.fetch("actor_id")
@@ -73,18 +98,17 @@ module SolidObjects
73
98
  new(
74
99
  reference:,
75
100
  component_name: payload.fetch("component_name"),
101
+ component_key: payload["component_key"],
76
102
  dependencies:,
103
+ locals: payload.fetch("locals"),
104
+ refresh_method: payload.fetch("refresh_method"),
77
105
  instance_id: payload.fetch("instance_id"),
78
106
  revision: payload.fetch("revision"),
79
107
  refresh_path: payload.fetch("refresh_path"),
80
108
  token:
81
109
  )
82
- rescue UnknownActorType, UnknownComponentDependency => error
83
- raise InvalidComponentToken, error.message
84
110
  end
85
111
 
86
- private
87
-
88
112
  # @rbs (Reference, Array[String]) -> void
89
113
  def validate_dependencies!(reference, dependencies)
90
114
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
@@ -104,6 +128,27 @@ module SolidObjects
104
128
  [ instance_id, revision ]
105
129
  end
106
130
 
131
+ # @rbs () -> String
132
+ def dom_id
133
+ DomIdentity.component(
134
+ reference,
135
+ component_name,
136
+ key: component_key
137
+ )
138
+ end
139
+
140
+ # @rbs () -> bool
141
+ def morph?
142
+ refresh_method == "morph"
143
+ end
144
+
145
+ # @rbs () -> Hash[String, untyped]
146
+ def authorization_arguments
147
+ return locals unless component_key
148
+
149
+ locals.merge("component_key" => component_key).freeze
150
+ end
151
+
107
152
  # @rbs (Integer, Integer) -> String
108
153
  def refresh_url(instance_id, revision)
109
154
  query = URI.encode_www_form(
@@ -3,46 +3,44 @@
3
3
  module SolidObjects
4
4
  class ComponentRenderer
5
5
  # @rbs @snapshot: ActorSnapshot
6
- # @rbs @component_name: String
7
- # @rbs @dependencies: Array[String]
6
+ # @rbs @registration: ComponentRegistration
8
7
  # @rbs @view_context: untyped
9
8
  # @rbs @authorization_context: untyped
10
9
 
11
- # @rbs (snapshot: ActorSnapshot, component_name: String, dependencies: Array[String], view_context: untyped, authorization_context: untyped) -> void
10
+ # @rbs (snapshot: ActorSnapshot, registration: ComponentRegistration, view_context: untyped, authorization_context: untyped) -> void
12
11
  def initialize(
13
12
  snapshot:,
14
- component_name:,
15
- dependencies:,
13
+ registration:,
16
14
  view_context:,
17
15
  authorization_context:
18
16
  )
19
17
  @snapshot = snapshot
20
- @component_name = component_name
21
- @dependencies = dependencies
18
+ @registration = registration
22
19
  @view_context = view_context
23
20
  @authorization_context = authorization_context
24
21
  end
25
22
 
26
23
  # @rbs () -> untyped
27
24
  def call
28
- [ component_name, *dependencies ].uniq.each do |authorization_name|
25
+ [ registration.component_name, *registration.dependencies ].uniq.each do |authorization_name|
29
26
  authorize_read!(authorization_name)
30
27
  end
31
28
  actor = ComponentView.new(
32
29
  snapshot:,
33
- dependencies:,
30
+ dependencies: registration.dependencies,
34
31
  authorization_context:
35
32
  )
36
33
  view_context.render(
37
34
  partial: default_partial,
38
- locals: {
35
+ locals: registration.locals.transform_keys(&:to_sym).merge(
39
36
  actor:,
40
- authorization_context:
41
- }
37
+ authorization_context:,
38
+ component_key: registration.component_key
39
+ )
42
40
  )
43
41
  rescue ActionView::MissingTemplate
44
42
  raise UnknownComponent,
45
- "unknown component #{component_name.inspect} for #{snapshot.reference.actor_type}"
43
+ "unknown component #{registration.component_name.inspect} for #{snapshot.reference.actor_type}"
46
44
  rescue ActionView::Template::Error => error
47
45
  raise error.cause if error.cause.is_a?(SolidObjects::Error)
48
46
 
@@ -52,8 +50,7 @@ module SolidObjects
52
50
  private
53
51
 
54
52
  attr_reader :snapshot,
55
- :component_name,
56
- :dependencies,
53
+ :registration,
57
54
  :view_context,
58
55
  :authorization_context
59
56
 
@@ -63,7 +60,7 @@ module SolidObjects
63
60
  actor_type: snapshot.reference.actor_type,
64
61
  actor_id: snapshot.reference.actor_id,
65
62
  message_name: observable_name,
66
- arguments: {},
63
+ arguments: registration.authorization_arguments,
67
64
  authorization_context:
68
65
  )
69
66
  return if authorized
@@ -76,7 +73,7 @@ module SolidObjects
76
73
  actor_name = snapshot.actor_class.name
77
74
  raise InvalidActor, "anonymous actors cannot render reactive components" unless actor_name
78
75
 
79
- "actors/#{actor_name.underscore}/#{component_name}"
76
+ "actors/#{actor_name.underscore}/#{registration.component_name}"
80
77
  end
81
78
  end
82
79
  end
@@ -28,7 +28,7 @@ module SolidObjects
28
28
  validate_identity!(registration, reference)
29
29
  end
30
30
  end
31
- if registrations.map(&:component_name).uniq.length != registrations.length
31
+ if registrations.map(&:dom_id).uniq.length != registrations.length
32
32
  raise InvalidComponentToken, "duplicate actor component registration"
33
33
  end
34
34
 
@@ -39,7 +39,7 @@ module SolidObjects
39
39
  def initialize(registrations)
40
40
  @registrations = registrations
41
41
  @revisions = registrations.to_h do |registration|
42
- [ registration.component_name, registration.revision_key ]
42
+ [ registration.dom_id, registration.revision_key ]
43
43
  end
44
44
  end
45
45
 
@@ -51,7 +51,7 @@ module SolidObjects
51
51
  registrations.filter_map do |registration|
52
52
  next unless registration.dependencies.include?(observable_name)
53
53
  next unless newer_revision?(
54
- registration.component_name,
54
+ registration.dom_id,
55
55
  instance_id,
56
56
  revision
57
57
  )
@@ -64,7 +64,7 @@ module SolidObjects
64
64
  def reconnect_refreshes(snapshot)
65
65
  registrations.filter_map do |registration|
66
66
  next unless newer_revision?(
67
- registration.component_name,
67
+ registration.dom_id,
68
68
  snapshot.instance_id,
69
69
  snapshot.revision
70
70
  )
@@ -92,7 +92,7 @@ module SolidObjects
92
92
 
93
93
  # @rbs (ComponentRegistration, Integer, Integer) -> String
94
94
  def refresh(registration, instance_id, revision)
95
- revisions[registration.component_name] = [ instance_id, revision ]
95
+ revisions[registration.dom_id] = [ instance_id, revision ]
96
96
  TurboStreamRenderer.component_refresh(
97
97
  registration,
98
98
  instance_id,
@@ -101,8 +101,8 @@ module SolidObjects
101
101
  end
102
102
 
103
103
  # @rbs (String, Integer, Integer) -> bool
104
- def newer_revision?(component_name, instance_id, revision)
105
- current = revisions.fetch(component_name)
104
+ def newer_revision?(dom_id, instance_id, revision)
105
+ current = revisions.fetch(dom_id)
106
106
  (current <=> [ instance_id, revision ]) == -1
107
107
  end
108
108
  end