|
| 1 | +# Transmit ingest |
| 2 | + |
| 3 | +`SolidObjects::Transmission.receive(envelope)` is the server side of the |
| 4 | +browser transmit family in |
| 5 | +[solid-objects-js](https://github.com/cardmagic/solid-objects-js). A |
| 6 | +solid-objects-js actor in a browser stages a transmit intent with |
| 7 | +`this.transmit().increment({ amount })` in the same transaction as its state |
| 8 | +change. The browser's effect worker drains that outbox with at-least-once |
| 9 | +delivery, per-actor order, and retry backoff. It posts one JSON envelope per |
| 10 | +effect to a route the host application owns. `Transmission.receive` replays |
| 11 | +that envelope onto a server actor. |
| 12 | + |
| 13 | +## Wire contract |
| 14 | + |
| 15 | +The JS side owns the envelope format. The Ruby ingest accepts it verbatim. |
| 16 | + |
| 17 | +- Keys arrive camelCase: `effectId`, `actorType`, `actorId`, `operation`, |
| 18 | + and an optional `arguments` object. There is no snake_case dialect. |
| 19 | +- The idempotency key is `transmit:<effectId>`, byte-identical to the JS |
| 20 | + server ingest. A replayed envelope applies once. |
| 21 | +- Delivery is at-least-once and per-actor ordered by the browser's drain. |
| 22 | + The server preserves mailbox order and adds no ordering of its own. |
| 23 | + |
| 24 | +`compatibility/transmit-envelopes.json` pins the contract. Both runtimes |
| 25 | +run a consuming test against the same fixture file. |
| 26 | + |
| 27 | +## What `receive` does |
| 28 | + |
| 29 | +1. It validates the envelope shape. A malformed envelope raises |
| 30 | + `SolidObjects::InvalidTransmission`. |
| 31 | +2. It resolves the actor type and looks it up in the registry. An unknown |
| 32 | + type raises `SolidObjects::UnknownActorType`. An undeclared operation |
| 33 | + raises `SolidObjects::UnknownMessage`. |
| 34 | +3. It enqueues one internal message with the idempotency key |
| 35 | + `transmit:<effectId>`. Oversized arguments raise |
| 36 | + `SolidObjects::PayloadTooLarge` before persistence. |
| 37 | + |
| 38 | +The enqueue uses `delivery_mode: "internal"`, the same mode the effect |
| 39 | +executor uses. Internal delivery skips `authorize_message` by construction. |
| 40 | +The host application must authenticate the request before it calls |
| 41 | +`receive`. |
| 42 | + |
| 43 | +## The host application owns HTTP |
| 44 | + |
| 45 | +The gem draws the same boundary here that it draws for realtime transport: |
| 46 | +the host owns the route, authentication, and rate limits. |
| 47 | + |
| 48 | +```ruby |
| 49 | +class TransmitController < ApplicationController |
| 50 | + skip_forgery_protection |
| 51 | + |
| 52 | + def create |
| 53 | + head :forbidden and return unless authenticated_device? |
| 54 | + |
| 55 | + SolidObjects::Transmission.receive(JSON.parse(request.body.read)) |
| 56 | + head :ok |
| 57 | + rescue SolidObjects::InvalidTransmission, SolidObjects::UnknownActorType, |
| 58 | + SolidObjects::UnknownMessage, SolidObjects::PayloadTooLarge, JSON::ParserError |
| 59 | + head :unprocessable_entity |
| 60 | + end |
| 61 | +end |
| 62 | +``` |
| 63 | + |
| 64 | +Return 422 for an envelope the server can never apply. The browser outbox |
| 65 | +dead-letters that effect instead of retrying it forever. Return a 5xx for a |
| 66 | +transient server fault, so the browser retries with backoff. |
| 67 | + |
| 68 | +## Actor type mapping |
| 69 | + |
| 70 | +When both runtimes use the same actor type strings, no configuration is |
| 71 | +needed. When the names diverge, pass `resolve_actor_type:` per call: |
| 72 | + |
| 73 | +```ruby |
| 74 | +SolidObjects::Transmission.receive( |
| 75 | + envelope, |
| 76 | + resolve_actor_type: ->(actor_type) { actor_type.sub("browser-", "server-") } |
| 77 | +) |
| 78 | +``` |
| 79 | + |
| 80 | +## Scope |
| 81 | + |
| 82 | +`receive` is ingest only. The staging side in Ruby, an `actor.transmit` for |
| 83 | +Ruby-to-Ruby replication, is a separate feature. An engine-mounted route |
| 84 | +with an authentication hook is a possible follow-up; it stays out because |
| 85 | +it carries authentication, CSRF, and rate-limit decisions of its own. |
0 commit comments