@@ -34,7 +34,7 @@ This is a port of the programming model, not Cloudflare's edge runtime or
3434platform. Read the conceptual overview at [ solidobjects.dev] ( https://solidobjects.dev/ )
3535and the exact Rails guarantees in [ Correctness and delivery semantics] ( docs/correctness.md ) .
3636
37- Version 0.1 is an early release. Its correctness core is implemented and tested,
37+ Version 0.2 is an early release. Its correctness core is implemented and tested,
3838but the project does not yet claim production readiness. See
3939[ Status] ( #status ) and the [ roadmap] ( docs/roadmap.md ) .
4040
@@ -45,7 +45,7 @@ but the project does not yet claim production readiness. See
4545- [ Installation] ( #installation )
4646- [ Defining an actor] ( #defining-an-actor )
4747- [ Actor identity] ( #actor-identity )
48- - [ Messages and queries ] ( #messages-and-queries )
48+ - [ Invoking an object ] ( #invoking-an-object )
4949- [ Effects] ( #effects )
5050- [ Reminders] ( #reminders )
5151- [ Destroying an object] ( #destroying-an-object )
@@ -171,7 +171,7 @@ refresh from current actor state.
171171` cart.component(:summary) ` supports initial rendering of
172172` actors/shopping_cart/_summary ` . Durable live component replacement and
173173Turbo append actions are roadmap work; observable replacement is the live path
174- implemented in 0.1 .
174+ implemented in 0.2 .
175175
176176Reactive views require ` turbo-rails ` and a working Action Cable adapter in the
177177host application. They are optional; the actor runtime itself does not depend
206206
207207Knowledge of an actor ID or signed stream token is never authorization.
208208
209- Start the runtime:
209+ Start the runtime for asynchronous messages, effects, reminders, and
210+ broadcasts:
210211
211212``` bash
212213bundle exec solid_objects start
@@ -251,18 +252,20 @@ declared on the actor are durable message handlers. They can use `items`,
251252` self.checkout_status = "pending" ` , or the lower-level ` state ` object. Declare
252253helper methods as private or protected so they are not exposed as messages.
253254
254- Attributes also become ordered read queries on a reference. Declared messages
255- become asynchronous methods :
255+ Attributes also become ordered read queries on a reference. Public actor
256+ methods and attribute readers are synchronous caller-assisted invocations :
256257
257258``` ruby
258259cart = ShoppingCart .ref(" alice" )
259- message = cart.add_item(product_id: " shirt-123" , quantity: 2 )
260+ cart.add_item(product_id: " shirt-123" , quantity: 2 )
260261items = cart.items
261262```
262263
263- ` message ` is a ` SolidObjects::MessageReference ` . ` items ` is a deeply frozen
264- JSON snapshot; mutating it cannot bypass the actor mailbox. State changes must
265- go through public actor message methods.
264+ Use ` cart.async(:add_item, product_id: "shirt-123", quantity: 2) ` to enqueue
265+ without waiting; that call returns a ` SolidObjects::MessageReference ` . ` items `
266+ is a deeply frozen JSON snapshot, so mutating it cannot bypass the actor
267+ mailbox. State changes must go through public actor methods or explicit
268+ ` async ` .
266269
267270State, arguments, results, effects, and reminder arguments accept
268271JSON-compatible values. Solid Objects never deserializes Ruby ` Marshal ` data.
310313Actor types resolve only through the explicit registry. Solid Objects never
311314constantizes a type supplied by a client.
312315
313- ## Messages and queries
316+ ## Invoking an object
314317
315318As with a Durable Object stub, declared actor operations are available directly
316319on a reference:
@@ -325,24 +328,27 @@ class Counter < SolidObjects::Actor
325328end
326329
327330counter = Counter .ref(" global" )
328- message = counter.increment(amount: 5 )
331+ value = counter.increment(amount: 5 )
329332value = counter.value
330333```
331334
332- Public actor methods are messages and become asynchronous syntax over ` tell ` ,
333- returning a durable ` MessageReference ` . Declared queries and attribute readers
334- are synchronous syntax over ` ask ` . Unlike Cloudflare RPC, the initial ` ask `
335- implementation waits by polling the durable database row.
336- The ` message(:name) { ... } ` DSL remains available for dynamic definitions.
337- The explicit ` tell ` and ` ask ` forms remain available for dynamic operation
338- names or names that collide with Ruby or reference methods.
335+ Like RPC on a Durable Object stub, a direct call is synchronous from the
336+ caller's perspective. Solid Objects first durably enqueues the invocation, then
337+ executes that actor locally when its fenced activation is available. It returns
338+ the committed, deeply frozen result. Earlier mailbox entries still run first,
339+ and a remote worker may win the activation without changing the result
340+ semantics.
339341
340- ### ` tell `
342+ The ` message(:name) { ... } ` and ` query(:name) { ... } ` DSLs remain available
343+ for dynamic definitions.
341344
342- ` tell ` durably enqueues work and immediately returns a message reference:
345+ ### ` async `
346+
347+ Use ` async ` for durable fire-and-forget work. It returns a
348+ ` MessageReference ` immediately and leaves execution to the worker fleet:
343349
344350``` ruby
345- message = order.tell (
351+ message = order.async (
346352 :submit ,
347353 idempotency_key: " submit-order-123"
348354)
@@ -351,23 +357,44 @@ message = order.tell(
351357Use ` available_at: ` to spread bulk work or delay one message:
352358
353359``` ruby
354- order.tell (:evaluate , available_at: 10 .minutes.from_now)
360+ order.async (:evaluate , available_at: 10 .minutes.from_now)
355361```
356362
357- ### ` ask `
363+ ### ` sync `
358364
359- ` ask ` durably enqueues a query or message and waits for its result:
365+ Use explicit ` sync ` when the operation name is dynamic or collides with a
366+ reference method:
360367
361368``` ruby
362- status = order.ask (:status , timeout: 5 .seconds)
369+ status = order.sync (:status , timeout: 5 .seconds)
363370```
364371
365- The initial cross-process implementation polls the durable message row. It is
366- intended for background callers and control paths, not latency-sensitive HTTP
367- request handling. A timeout does not cancel the actor message.
372+ Direct calls and ` sync ` use the same caller-assisted execution path. A healthy
373+ actor normally needs no worker round trip, making this path suitable for HTTP
374+ and MCP request/response boundaries when the handler itself fits the
375+ application's latency budget. If another process owns the activation, the
376+ caller waits for the durable result using wake-up hints with bounded database
377+ polling as the fallback. A timeout never cancels the durable invocation.
378+
379+ Actor code cannot use direct calls or ` sync ` on another actor; synchronous
380+ actor-to-actor waits can deadlock in cycles. Use ` async ` or ` send_to ` and a
381+ result message.
382+
383+ ### Domain rejection
384+
385+ Reject invalid input without retrying or creating a dead letter:
386+
387+ ``` ruby
388+ def submit (response: )
389+ reject :validation_failed , " Response is not valid" unless valid?(response)
390+
391+ self .response = response
392+ end
393+ ```
368394
369- Actor code cannot call ` ask ` ; synchronous actor-to-actor waits can deadlock in
370- cycles. Use ` tell ` or ` send_to ` and a result message.
395+ The caller receives ` SolidObjects::Rejected ` with a stable code, message, and
396+ JSON-compatible details. The rejected message remains durable for audit, actor
397+ state is rolled back, and no later mailbox turn is blocked.
371398
372399### Redelivery
373400
@@ -445,7 +472,7 @@ enqueue each one.
445472
446473Self-scheduling actors should also have a low-frequency application reconciler.
447474It may read ` SolidObjects::Instance.states_for ` , ` .without_pending_work ` , and
448- ` .orphaned ` , but every repair must go through ` tell ` . Never bulk-update actor
475+ ` .orphaned ` , but every repair must go through ` async ` . Never bulk-update actor
449476state around the lease and fencing checks.
450477
451478## Destroying an object
@@ -514,7 +541,7 @@ Important defaults:
514541| Setting | Default |
515542| --- | ---: |
516543| ` polling_interval ` | 0.1 seconds |
517- | ` ask_polling_interval ` | 0.05 seconds |
544+ | ` sync_polling_interval ` | 0.05 seconds |
518545| ` lease_duration ` | 30 seconds |
519546| ` lease_renewal_interval ` | 10 seconds |
520547| ` idle_deactivation_timeout ` | 30 seconds |
@@ -633,7 +660,7 @@ Solid Objects does not promise:
633660- global order across actors;
634661- distributed transactions;
635662- bounded end-to-end latency;
636- - cancellation when an ` ask ` caller times out; or
663+ - cancellation when a synchronous caller times out; or
637664- that a lease prevents stale Ruby code from continuing to run.
638665
639666The fencing generation prevents stale code from committing.
@@ -700,15 +727,18 @@ See the [development guide](docs/development.md) and
700727
701728## Status
702729
703- Implemented and tested in 0.1 :
730+ Implemented and tested in 0.2 :
704731
705732- Rails engine, install generator, migrations, and ` solid_objects ` executable;
706733- actor registry, references, JSON state, and state migrations;
734+ - direct synchronous actor RPC, explicit ` sync ` , and durable ` async ` ;
707735- durable message history plus ready and claimed membership tables;
708736- concurrent sequence allocation and actor creation;
709- - activation leases, renewal, fencing generations, and stale-write rejection;
737+ - activation leases, per-activation tokens, fencing generations, and
738+ stale-write rejection;
710739- bounded activation passes, idle activation cache, and hot-actor fairness;
711- - retries, strict poison ordering, dead letters, and retry tooling;
740+ - retries, terminal domain rejection, strict poison ordering, dead letters,
741+ and retry tooling;
712742- transactional effects and asynchronous actor-to-actor messages;
713743- one-shot and recurring per-actor reminders;
714744- authorized actor destruction with fenced stale-write rejection and cascading
0 commit comments