Skip to content

Commit e3ac8cd

Browse files
committed
docs: track the transmit family as a shared capability
Ruby PR solid-objects-ruby#49 adopts the whole transmit family, so the parity ledger no longer files it under the JavaScript-only browser runtime. A new section records the shared wire contract (camelCase keys, optional arguments defaulting to an empty object, the transmit:<effectId> idempotency key, conflict-on-changed-replay) and the cross-runtime QA that validated it in both directions. The branch already carried the shared fixture and its consuming suite; this commit keeps that suite as the single consumer and adds the one assertion it lacked: the idempotency key the ingest actually stores matches the fixture's pinned key, row for row.
1 parent 78f49ea commit e3ac8cd

4 files changed

Lines changed: 49 additions & 5 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
the next host promotes, and the runtime continues from the same OPFS
2222
state. A Playwright test proves shared state across two tabs and
2323
failover after the leader closes.
24+
- Share the transmit wire contract with the Ruby gem. The golden fixture
25+
file `compatibility/transmit-envelopes.json` is committed to both
26+
repositories with a consuming test on each side; `receiveTransmitEnvelope`
27+
now defaults a missing `arguments` to an empty object, matching the Ruby
28+
ingest and the staging side.
2429
- Add `solid-objects/transmit`, milestone M4 of the plan. An actor
2530
stages a transmit intent with `this.transmit().operation(arguments)` (or with
2631
`emit(TRANSMIT_EFFECT, ...)` for a different target) in the same

‎docs/api.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,10 @@ outbox with at-least-once delivery, per-actor order, and retry backoff.
487487
- `InvalidTransmitEnvelope`: the non-retryable rejection for malformed staged
488488
arguments; the effect dead-letters instead of retrying forever.
489489

490-
Both modules are browser-safe and also run in Node.
490+
The tab host and transmit modules are browser-safe and also run in Node.
491+
The transmit wire contract is shared with the Ruby gem
492+
([solid-objects-ruby#49](https://github.com/cardmagic/solid-objects-ruby/pull/49));
493+
`compatibility/transmit-envelopes.json` pins it in both repositories.
491494

492495
## `solid-objects/web`
493496

‎docs/parity.md‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,12 @@ gap between them.
110110
## JavaScript-only: the browser runtime
111111

112112
`0.14.0` ships an in-browser runtime with SQLite WASM storage
113-
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)). It is a
114-
JavaScript-only capability. The Ruby gem has no browser target, so no Ruby
115-
parity row exists for it. All four milestones are complete:
113+
([#17](https://github.com/cardmagic/solid-objects-js/issues/17)). The
114+
runtime itself is a JavaScript-only capability: the Ruby gem has no browser
115+
target, so no Ruby parity row exists for milestones M1 through M3. The
116+
transmit family (milestone M4) started here but is not JavaScript-only; the
117+
next section tracks it as a shared capability. All four milestones are
118+
complete:
116119

117120
- M1: the shared modules no longer import Node built-in modules, a
118121
registered platform factory supplies async context propagation, and
@@ -137,7 +140,33 @@ parity row exists for it. All four milestones are complete:
137140
- M4: `solid-objects/transmit` drains the local effects outbox to a
138141
server runtime with at-least-once delivery, per-actor order, and an
139142
idempotent server ingest. Vitest proves order under transmit failures,
140-
replay deduplication, and recovery after an offline period.
143+
replay deduplication, and recovery after an offline period, on SQLite,
144+
PostgreSQL, and MySQL.
145+
146+
## Shared capability: the transmit family
147+
148+
The transmit family is the one part of the browser work that both runtimes
149+
share. The Ruby gem adopts it in
150+
[solid-objects-ruby#49](https://github.com/cardmagic/solid-objects-ruby/pull/49)
151+
(proposals [#47](https://github.com/cardmagic/solid-objects-ruby/issues/47)
152+
and [#48](https://github.com/cardmagic/solid-objects-ruby/issues/48)):
153+
`SolidObjects::Transmission.receive` is the ingest, and `Actor#transmit`
154+
with `register_transmit` is the staging side. Identifiers differ by
155+
runtime idiom; the wire contract is what both sides guarantee:
156+
157+
- envelope keys are camelCase (`effectId`, `actorType`, `actorId`,
158+
`operation`, and an optional `arguments` that defaults to an empty
159+
object);
160+
- the ingest idempotency key is `transmit:<effectId>`, byte for byte;
161+
- a replay with changed arguments raises the idempotency conflict on both
162+
sides and leaves the first application intact.
163+
164+
`compatibility/transmit-envelopes.json` is committed to both repositories
165+
with a consuming test on each side, so the contract is enforced from both
166+
sides of the repository boundary. Manual cross-runtime QA (Node to Rails
167+
and Rails to Node) ran in solid-objects-ruby#49; the one disagreement it
168+
found (the optional `arguments` default) is fixed and pinned by the shared
169+
fixture.
141170

142171
## Rails-specific surfaces
143172

‎test/transmit-fixtures.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ describe("shared transmit envelope fixtures", () => {
6565
const first = await receiveTransmitEnvelope({ runtime, envelope: fixture.envelope })
6666
const replay = await receiveTransmitEnvelope({ runtime, envelope: fixture.envelope })
6767
expect(replay.messageId, fixture.name).toBe(first.messageId)
68+
const stored = await runtime.settings.database.connection((connection) =>
69+
connection.get<{ idempotency_key: string }>(
70+
`SELECT idempotency_key FROM ${runtime.repository.table("messages")} WHERE id = ?`,
71+
[first.messageId],
72+
),
73+
)
74+
expect(stored?.idempotency_key, fixture.name).toBe(fixture.idempotencyKey)
6875
}
6976
await runtime.testing.drain({ roles: ["actors"] })
7077

0 commit comments

Comments
 (0)