docs(porch-store): call replies are scalars — response goes via the table

- WO-E226: call's reply must be a copyable scalar, and every receive
  program-wide must declare the same return type. Verified by fixture:
  "call's reply type `Out` is not a copyable scalar"
- the spec had the actor return the response object, which cannot cross
  the mailbox. Corrected: the actor stores the response and returns an
  outcome code; the middleware reads the row and builds the Resp
- owner and duplicate now read the SAME durable row, so byte-identical
  replay is structural rather than careful copying
- blocking, exactly-once execution and the mailbox queue are unchanged

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 77e06c1690b92d456a9bc53503695fdaa2b4b44e)
This commit is contained in:
shoney.arickathil 2026-08-29 23:40:44 +02:00
parent 4be826f9ab
commit 88b61f7522
2 changed files with 26 additions and 10 deletions

View file

@ -216,11 +216,18 @@ from Task 1.
- [ ] **Step 3 — the digest.** Compute a digest of method, path and body when - [ ] **Step 3 — the digest.** Compute a digest of method, path and body when
`include_body` is set, and pass it alongside the bare key. Keep `use `include_body` is set, and pass it alongside the bare key. Keep `use
json` — the file did not typecheck without it. json` — the file did not typecheck without it.
- [ ] **Step 4 — the actor arm for the begin message.** Look the bare key up. - [ ] **Step 4 — the actor arm for the begin message.** `call`'s reply must be
A hit whose digest matches returns the stored response. A hit whose a copyable scalar (WO-E226 — a response object cannot cross the mailbox),
digest differs returns a refusal, answered as **422**. A miss runs the so the arm returns an **outcome code** and the response travels through
handler, stores status, body and `content-type` with the digest and the the table. Look the bare key up. A hit whose digest matches returns
current tick, and returns the response. "replayed" without running the handler. A hit whose digest differs
returns "mismatch". A miss runs the handler, stores status, body and
`content-type` with the digest and the current tick, and returns
"executed". The middleware then reads the stored row and builds the
response from it — so owner and duplicate return the same durable row,
and byte-identical replay is structural rather than careful.
**Keep the return type identical to the one Task 2's count arm uses**;
WO-E226 also requires every `receive` program-wide to agree.
- [ ] **Step 5 — the replay allowlist.** Store and replay `content-type` only. - [ ] **Step 5 — the replay allowlist.** Store and replay `content-type` only.
Never `Set-Cookie`, never `Date`. Cookies arrive in porch 2; this is Never `Set-Cookie`, never `Date`. Cookies arrive in porch 2; this is
cheap now and expensive to retrofit after. cheap now and expensive to retrofit after.

View file

@ -123,11 +123,20 @@ History. A keyed request does not run its own handler; it calls its pool actor,
passing the request and the route's `Handler`, and the actor invokes the passing the request and the route's `Handler`, and the actor invokes the
handler inside its own `receive`. handler inside its own `receive`.
| Actor state for that key | What `receive` returns | **The reply is a scalar, so the response travels through the table.** `call`'s
| --- | --- | reply must be a copyable scalar (WO-E226), and every `receive` program-wide
| A stored response exists, digest matches | the stored response; the handler never runs | must declare the same return type. A response object cannot cross the mailbox.
| A stored response exists, digest differs | a refusal verdict, answered as 422 | So the actor stores the response in the `@table` and returns an outcome code;
| No record | run the handler here, store the response, return it | the middleware reads the stored row and builds the response from it. Owner and
duplicate therefore read the *same durable row* — which is what the store is
for, and it means the replay is byte-identical by construction rather than by
careful copying.
| Actor state for that key | Actor does | Returns |
| --- | --- | --- |
| A stored response exists, digest matches | nothing; the handler never runs | "replayed" |
| A stored response exists, digest differs | nothing | "mismatch", answered as 422 |
| No record | runs the handler here and stores the response | "executed" |
There is no fourth row, and that is the point. **A duplicate arriving while the There is no fourth row, and that is the point. **A duplicate arriving while the
owner's handler runs waits in the mailbox**, because an actor processes one owner's handler runs waits in the mailbox**, because an actor processes one