Skip to content

RFP: Actor#transmit and register_transmit, the staging side of the transmit family #48

Description

@cardmagic

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
end

This 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

  1. 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.
  2. 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

  • emit already stages transactional effects, and
    SolidObjects.register_effect(name, &handler) already registers drain
    handlers with at-least-once retries and dead letters.
  • OperationDispatcher is the fluent staging proxy schedule uses. The
    transmit method is the same pattern with an EffectIntent named
    solid-objects.transmit whose arguments hold operation and
    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#transmit returns an OperationDispatcher that stages the
    effect intent in the same commit as the state change. Same-actor target
    by default; emit("solid-objects.transmit", ...) with explicit
    actorType / actorId covers 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 any receiveTransmitEnvelope /
    Transmission.receive ingest accepts it. The receiving side dedups on
    transmit:<effectId>.

Plan

  1. Failing Minitest first: transmit.increment(amount: 2) stages one
    effect in the actor commit; register_transmit delivers 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.
  2. Implement Actor#transmit and SolidObjects.register_transmit.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions