@@ -74,6 +74,7 @@ tested, but the project does not yet claim production readiness. See
7474- [ Cloudflare Durable Objects for Rails] ( #cloudflare-durable-objects-for-rails )
7575- [ Reactive ERB] ( #reactive-erb )
7676- [ Installation] ( #installation )
77+ - [ Upgrading] ( #upgrading )
7778- [ Worker requirements] ( #worker-requirements )
7879- [ Defining an actor] ( #defining-an-actor )
7980- [ Actor identity] ( #actor-identity )
@@ -173,42 +174,105 @@ rolled-back state change cannot leak into the page.
173174Define an observable:
174175
175176``` ruby
176- class ShoppingCart < SolidObjects ::Actor
177- attribute :items , default: -> { [] }
177+ class ChatRoom < SolidObjects ::Actor
178+ attribute :recent_messages , default: -> { [] }
179+ attribute :status , default: " open"
178180
179- observable :items_count do
180- items.sum { | item | item.fetch( " quantity " ) }
181+ observable :message_count do
182+ recent_messages.length
181183 end
184+
185+ observable :recent_messages
186+ observable :status
182187end
183188```
184189
185- Render it:
190+ Scalar observables remain stable ` <span> ` targets:
191+
192+ ``` erb
193+ <%= solid_object @room, authorization_context: current_user do |room| %>
194+ Messages: <%= room.message_count %>
195+ <% end %>
196+ ```
197+
198+ Reactive components rerender a host ERB partial when one of their explicit
199+ dependencies changes:
186200
187201``` erb
188- <%= solid_object current_cart do |cart| %>
189- Cart items: <%= cart.items_count %>
202+ <%= solid_object @room, authorization_context: current_user do |room| %>
203+ <%= room.component :messages, observes: :recent_messages %>
204+ <%= room.component :presence, observes: %i[recent_messages status] %>
190205<% end %>
191206```
192207
193- That template provides initial server rendering, a stable opaque DOM target,
194- and live Turbo replacements after committed actor turns. One ` solid_object `
195- block makes one Action Cable subscription for all values inside it, and Action
196- Cable multiplexes subscriptions over the browser's WebSocket.
208+ ` room.component(:messages) ` resolves only
209+ ` actors/chat_room/_messages ` . Its partial receives ` actor ` and
210+ ` authorization_context ` locals:
211+
212+ ``` erb
213+ <ul>
214+ <% actor.recent_messages.each do |message| %>
215+ <li><%= message.fetch("body") %></li>
216+ <% end %>
217+ </ul>
218+ ```
219+
220+ Declared observables are deeply frozen ordinary Ruby values inside a
221+ component. Arrays support loops, hashes support ordinary lookup, conditionals
222+ work normally, and ERB still escapes user strings. A reactive component cannot
223+ read ` actor.state ` , access an undeclared observable, or choose a dynamic
224+ partial path.
225+
226+ That template provides initial server rendering, stable opaque DOM targets,
227+ and live updates after committed actor turns. One ` solid_object ` block makes
228+ one Action Cable subscription for all scalar values and components inside it,
229+ and Action Cable multiplexes subscriptions over the browser's WebSocket.
197230
198231No client-side state store, custom Stimulus controller, channel class, manual
199232broadcast, or one-WebSocket-per-value setup is required. Signed stream tokens
200- protect integrity, an application policy authorizes every subscription,
201- broadcasts are delivered from a durable outbox, and reconnecting clients
202- refresh from current actor state.
233+ protect integrity, not access. Initial rendering authorizes with the
234+ ` authorization_context ` passed to ` solid_object ` ; Cable authorizes with its
235+ connection; every component refresh authorizes again with a request-specific
236+ context:
203237
204- ` cart.component(:summary) ` supports initial rendering of
205- ` actors/shopping_cart/_summary ` . Durable live component replacement and
206- Turbo append actions are roadmap work; observable replacement is the live path
207- implemented today.
238+ ``` ruby
239+ SolidObjects .configure do |configuration |
240+ configuration.component_authorization_context = -> (controller: ) { Current .user }
241+ end
242+ ```
243+
244+ The durable outbox stores one row per changed observable, never personalized
245+ HTML. Cable sends invalidation metadata over the shared actor stream, then a
246+ Turbo Frame requests the component with normal cookies. Only scalar targets
247+ that the server rendered into this ` solid_object ` scope are signed into its
248+ stream token and receive value payloads; component-only dependencies do not
249+ send their values to the browser. The endpoint renders the latest committed
250+ snapshot, returns ` private, no-store ` , and reauthorizes the component name plus
251+ every declared dependency. Two viewers can therefore receive different HTML
252+ for the same actor without sharing either projection.
253+
254+ Reconnect compares the component's signed initial revision with the latest
255+ actor incarnation and state revision, then refreshes stale components. Cable
256+ coalesces several dependency changes from one actor turn into one component
257+ refresh and ignores older out-of-order invalidations. A newer invalidation
258+ replaces an in-flight frame, so its detached older response cannot overwrite
259+ newer state.
260+
261+ Reactive components add no HTML to durable rows, but each affected component
262+ causes an authorized HTTP render. One actor turn still inserts one broadcast
263+ row per changed observable; several dependencies from that turn coalesce at
264+ the subscriber. Keep components bounded, declare only necessary dependencies,
265+ and use scalar observables for inexpensive single-value replacement.
208266
209267Reactive views require ` turbo-rails ` and a working Action Cable adapter in the
210- host application. They are optional; the actor runtime itself does not depend
211- on Turbo.
268+ host application. The Solid Objects engine must be mounted so its signed
269+ component endpoint is reachable. Reactive views are optional; the actor
270+ runtime itself does not depend on Turbo.
271+
272+ ``` ruby
273+ # config/routes.rb
274+ mount SolidObjects ::Engine => " /solid_objects"
275+ ```
212276
213277## Installation
214278
@@ -274,6 +338,50 @@ can generate the gem RBI with:
274338bundle exec tapioca gem solid_objects
275339```
276340
341+ ## Upgrading
342+
343+ Review [ CHANGELOG.md] ( CHANGELOG.md ) for compatibility and deployment-order
344+ notes, then update the gem:
345+
346+ ``` bash
347+ bundle update solid_objects
348+ ```
349+
350+ If the ` Gemfile ` pins an exact version, update that constraint first and run
351+ ` bundle install ` . Commit both ` Gemfile.lock ` and the copied Solid Objects
352+ migrations.
353+
354+ Copy only migrations that the newer gem has added, migrate, and verify the
355+ installation:
356+
357+ ``` bash
358+ bin/rails solid_objects:install:migrations
359+ bin/rails db:migrate
360+ bin/rails solid_objects:doctor
361+ ```
362+
363+ The migration task skips engine migrations already present in the application
364+ and gives new migrations host-specific timestamps. Inspect the resulting
365+ ` db/migrate/*.solid_objects.rb ` files before applying them. Do not rerun
366+ ` generate solid_objects:install ` during an upgrade because that also attempts
367+ to regenerate the application initializer.
368+
369+ When Solid Objects uses a separate database configuration named ` actors ` , copy
370+ and run migrations through that database's configured migration path:
371+
372+ ``` bash
373+ DATABASE=actors bin/rails solid_objects:install:migrations
374+ bin/rails db:migrate:actors
375+ bin/rails solid_objects:doctor
376+ ```
377+
378+ For production, back up the actor database and run new migrations before
379+ starting application or Solid Objects worker processes that require the new
380+ schema. Restart the web and Solid Objects worker fleet after the bundle and
381+ schema are current. For releases that change actor state versions, also follow
382+ the [ state migration and rolling-deployment guide] ( docs/state-migrations.md ) ;
383+ Rails schema migrations and actor state migrations are separate concerns.
384+
277385## Worker requirements
278386
279387Synchronous actors can be adopted without adding a long-running process. Start
@@ -290,7 +398,7 @@ the runtime when the feature introduces asynchronous delivery or outboxes:
290398| ` emit ` without an actor callback | Effect worker |
291399| ` emit ` with success or failure callback | Effect worker and actor worker |
292400| Actor-to-actor ` async ` or ` send_to ` | Effect worker and actor worker |
293- | Observable Turbo updates | Broadcast worker, Action Cable, and the actor execution path |
401+ | Scalar or component Turbo updates | Broadcast worker, Action Cable, and the actor execution path |
294402| Initial ` solid_object ` server render | No Solid Objects worker; normal Rails rendering |
295403
296404One command starts every Solid Objects role:
@@ -929,7 +1037,7 @@ See the [development guide](docs/development.md) and
9291037
9301038## Status
9311039
932- Implemented and tested in 0.3 :
1040+ Implemented and tested in 0.4 :
9331041
9341042- Rails engine, install generator, migrations, and ` solid_objects ` executable;
9351043- actor registry, references, JSON state, and state migrations;
@@ -947,7 +1055,8 @@ Implemented and tested in 0.3:
9471055- one-shot and recurring per-actor reminders;
9481056- authorized actor destruction with fenced stale-write rejection and cascading
9491057 durable-work cleanup;
950- - durable observable broadcasts and authorized Action Cable refresh;
1058+ - durable observable invalidations, scalar Turbo replacement, and authorized
1059+ request-time ERB component refresh;
9511060- process registration, heartbeats, caller shutdown, cleanup, and bounded
9521061 message/process retention plus opt-in actor-instance expiration;
9531062- an opt-in Minitest helper for actor-state isolation and deterministic async
@@ -961,8 +1070,8 @@ Partially implemented:
9611070 run periodic maintenance automatically;
9621071- cross-process wake-up uses polling; PostgreSQL notifications and optional
9631072 Redis acceleration are not implemented;
964- - live observable replacement works , while live component replacement and
965- Turbo append actions remain future work;
1073+ - live observable and component replacement work , while Turbo append actions
1074+ remain future work;
9661075- local admission limits exist, but distributed rate limits and global
9671076 admission control do not; and
9681077- administration views and pruning commands exist, but scheduled maintenance
0 commit comments