docs: ws-byte-adapter POC complete (GO) — resolve OQ-01, update framing facts

POC at /workspace/ws-byte-adapter-poc/ (not on main), all tests green:
- call round-trip over axum WS ↔ adapter ↔ ChannelsAdapter ↔ channel-0
  Dispatcher (published alkcall API only, no forks)
- 3 MiB payload splits across 1 MiB WS messages, byte-intact reassembly
- 20 interleaved calls reassemble without corruption/cross-correlation

Findings merged:
- OQ-01 resolved: byte-stream both directions, bounded mpsc (64 slots)
  inbound, outbound chunk-header parsing (frame-as-two-chunks
  live-confirmed), 1 MiB message cap with split, flush no-op, close →
  EOF → REQ-CH-02
- websocket.md: chunk ≠ frame on channel 0 (frame reassembly required);
  operationId is the request payload key
- task ws-byte-adapter → completed with full Summary; ws-upgrade-session
  unblocks
This commit is contained in:
2026-08-28 07:14:32 +00:00
parent 63dc4b6d06
commit a85500d3d9
3 changed files with 86 additions and 37 deletions
+21 -25
View File
@@ -14,32 +14,28 @@ with their resolutions; new alkhttp OQs start at OQ-01.
### OQ-01: WS ↔ byte-stream adaptation semantics
- **Origin**: [websocket.md](websocket.md), [ADR-067](decisions/067-websocket-carries-channels.md)
- **Status**: open
- **Status**: resolved (POC `ws-byte-adapter`; validated end-to-end — see
the task's Summary in `tasks/websocket/byte-adapter.md`)
- **Priority**: high
- **Question**: The channels demux consumes bytes (`read_exact` on the
8-byte header + payload); the mux writes chunks as contiguous byte
sequences. A WebSocket is message-oriented. The adapter's contract
needs nailing down before implementation:
(a) inbound buffer bound (the alknet-tty precedent — bounded mpsc
with `try_send``Full``Pending` backpressure, and the single-
drainer ordered-write pattern from `pump_session` — is the working
reference; bound value to lock during implementation);
(b) write-side chunk boundary parsing (verified against alkcall
source: the mux emits one mpsc payload per chunk, but a logical
write above the mux — e.g. channel 0's `write_frame`, which issues
prefix and body as separate `write_all`s — can surface as multiple
chunks; the adapter must parse outgoing chunk headers rather than
assume write-per-chunk; also confirm the WS-message cap policy for
chunks up to `MAX_CHUNK_LEN` = 16 MiB — split across messages, and
what the practical cap is for browser stacks);
(c) flush mapping (`AsyncWrite::flush` → WS message emission point);
(d) close mapping (WS close code → transport EOF → REQ-CH-02
teardown; `AsyncWrite::shutdown` maps to the zero-length EOF
sentinel (REQ-CH-01) then a WS Close frame — confirm this ordering
against the mux's pump-exit behavior).
- **Blocked on**: nothing (the spike resolved the factual
sub-questions; the remaining items are implementation decisions to
lock during the WS adapter task)
- **Resolution**: The adapter treats the WS message stream as a byte
stream in both directions; the 8-byte chunk header is the only
framing. Inbound: WS read task → bounded mpsc (64 slots; backpressure
= send awaiting capacity, applying TCP-level backpressure to the
socket) → `AsyncRead` drains; channel close = EOF. Outbound: a writer
task parses 8-byte chunk headers out of the pending byte buffer (live-
confirmed: a single response frame arrives as TWO chunks —
`write_frame`'s prefix and body surface as separate mux payloads) and
emits one WS message per chunk, splitting at a 1 MiB
`WS_MESSAGE_CAP` (receiver's boundary is the chunk header, not the
message). Flush is a no-op (writes queue; the writer task emits
independently). Close: WS close → read EOF → REQ-CH-02 teardown;
`shutdown` closes the write channel (mux pumps emit zero-length
sentinels on sender drop). Client-side consequence: chunk ≠ frame —
channel-0 consumers reassemble length-prefixed frames from the byte
stream; and the dispatcher reads `operationId` from the request
payload.
- **Cross-references**: ADR-067, [websocket.md](websocket.md),
tasks/websocket/byte-adapter.md
### OQ-02: `/publish` body framing details
+7 -1
View File
@@ -123,7 +123,13 @@ WS binary message (message boundary = transport frame)
- **Channel 0's payload is the call protocol's frame format**
(alkcall ADR-014): a 4-byte big-endian length prefix + UTF-8 JSON
`EventEnvelope`. This is exactly the framing channel 0 uses over
TCP+TLS; the WS path is not special at this layer.
TCP+TLS; the WS path is not special at this layer. **Chunk ≠ frame**:
a single frame may arrive as multiple chunks (the frame writer
issues prefix and body as separate writes through the mux —
live-confirmed in the ws-byte-adapter POC), so channel-0 consumers
MUST reassemble length-prefixed frames from the channel-0 byte
stream, never parse per-chunk. The request payload's operation name
key is `operationId`.
- **Data-channel payloads are opaque** — the handler owns its framing
(alkcall ADR-035: no `stream_type` anywhere in the channels layer).
- **Text WS messages are rejected** with a protocol-level close (code