Repository navigation
RFP: Actor#transmit and register_transmit, the staging side of the transmit family #48
Copy link
Copy link
Closed
Description
Summary
Add the staging side of the transmit family to the Ruby gem, so a Rails
actor can replay its operations onto another runtime the same way a
solid-objects-js actor does with this.transmit().increment({ amount }):
class Counter < SolidObjects::Actor
message def increment(amount: 1)
self.count += amount
transmit.increment(amount:)
count
end
end
SolidObjects.register_transmit do |envelope|
DeliverToUpstream.call(envelope) # raise while offline; the outbox retries
endThis is the counterpart of #47 (SolidObjects::Transmission.receive, the
ingest). #47 gives Ruby the receiving side; this issue gives it the
sending side. Together the transmit family becomes symmetric across
runtimes, over one wire contract.
Why
- Rails-to-Rails and Rails-to-Node replication. An edge or on-prem
Rails install can replay operations to a central runtime with the same
at-least-once, per-actor-ordered, idempotent guarantees the browser
gets. The Node ingest (receiveTransmitEnvelope,
solid-objects-js#18)
already accepts the envelopes, so Rails-to-Node works the day this
ships. - Capability parity. The parity ledger tracks the JS package against
this gem. With RFP: SolidObjects::Transmission.receive, the server ingest for browser transmit envelopes #47 alone, the transmit family is JS-full and Ruby-half.
The staging mechanism costs little here because the primitives exist.
Why this is small
emitalready stages transactional effects, and
SolidObjects.register_effect(name, &handler)already registers drain
handlers with at-least-once retries and dead letters.OperationDispatcheris the fluent staging proxyscheduleuses. The
transmitmethod is the same pattern with anEffectIntentnamed
solid-objects.transmitwhose arguments holdoperationand
arguments.- The ordered drain (a claimed transmit effect delivers every undelivered
sibling for its actor up to its own mailbox sequence, oldest first) is
one SQL query over the effects and messages tables, ported from the JS
implementation.
Shape
Actor#transmitreturns anOperationDispatcherthat stages the
effect intent in the same commit as the state change. Same-actor target
by default;emit("solid-objects.transmit", ...)with explicit
actorType/actorIdcovers a different target, matching JS.SolidObjects.register_transmit(&deliver)wraps
register_effect("solid-objects.transmit")with envelope construction
and the ordered drain. The block receives one envelope per staged
effect and raises to retry.- The envelope leaving Ruby uses the wire contract from RFP: SolidObjects::Transmission.receive, the server ingest for browser transmit envelopes #47 verbatim:
camelCase keys (effectId,actorType,actorId,operation,
arguments), so anyreceiveTransmitEnvelope/
Transmission.receiveingest accepts it. The receiving side dedups on
transmit:<effectId>.
Plan
- Failing Minitest first:
transmit.increment(amount: 2)stages one
effect in the actor commit;register_transmitdelivers envelopes in
per-actor order across a failed delivery; a raised delivery retries
with backoff and dead-letters on exhaustion; the envelope keys are
camelCase. - Implement
Actor#transmitandSolidObjects.register_transmit. - Reuse the golden fixtures from RFP: SolidObjects::Transmission.receive, the server ingest for browser transmit envelopes #47: Ruby-staged envelopes must match
the fixture file both repositories share. - Documentation, roadmap entry, parity ledger row (staging becomes
native in both runtimes), both changelogs, both suites green.
Dependencies and order
Land #47 first; this builds on its envelope validation, error class, and
fixtures. A cross-runtime integration test (Rails stages, Node ingests)
can live in the JS repository's compatibility suite afterwards.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat: stage transmit effects with an ordered drain