docs: correct WS framing claims from spike; add implementation plan
Spike against alkcall source resolved ADR-067 assumptions: - write_chunk issues header+payload as separate write_alls; channel 0's write_frame issues prefix+body separately — a logical write can surface as multiple chunks, so the WS adapter must parse outgoing chunk boundaries (byte-stream treatment both directions), not assume write-per-chunk or message-per-chunk - MAX_CHUNK_LEN is 16 MiB; the WS path needs a practical message cap with oversized chunks split across messages - install_channel_zero + run_loop_single_stream confirmed as the exact server-side seam; EOF/teardown invariants already specified by alkcall (REQ-CH-01/02) Corrections applied to websocket.md, ADR-067, OQ-01. docs/plans/implementation.md: scoped plan guiding task decomposition — spike findings, 4-phase build order, OQ dispositions, task conventions.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# Plan: alkhttp Implementation
|
||||
|
||||
Working plan guiding task decomposition and implementation. Less
|
||||
specific than the tasks it produces, more specific than the
|
||||
architecture docs. Status fields in task frontmatter are the source of
|
||||
truth for progress; this document explains the *why* of the ordering.
|
||||
|
||||
Source crate: `/workspace/@alkdev/alknet/crates/alknet-http` (~11k LOC).
|
||||
Target: this crate, on alkcall 0.1.1 (crates.io).
|
||||
|
||||
## What the spike established
|
||||
|
||||
A validation pass against alkcall's actual source resolved the factual
|
||||
unknowns that would have shaped tasks incorrectly:
|
||||
|
||||
1. **The WS adapter must parse outgoing chunk boundaries.** The mux
|
||||
emits one mpsc payload per chunk, but a logical write above the mux
|
||||
(channel 0's `write_frame` issues length-prefix and body as two
|
||||
`write_all`s) can surface as multiple chunks. "One write = one
|
||||
chunk" and "one chunk = one WS message" are both unusable as
|
||||
invariants. The adapter treats the WS message stream as a byte
|
||||
stream in both directions and parses the 8-byte header on both
|
||||
read and write sides.
|
||||
2. **Chunk size cap.** `MAX_CHUNK_LEN` is 16 MiB; browser WS stacks
|
||||
and intermediaries commonly cap messages far lower. The WS path
|
||||
needs its own practical cap (default ~1 MiB) with oversized chunks
|
||||
split across messages — legal, since the receiver's boundary is the
|
||||
chunk header.
|
||||
3. **The `install_channel_zero` hook is the exact seam** for the
|
||||
server WS path: alkcall's `ChannelsAdapter` runs the in-line demux
|
||||
loop, and the hook receives channel 0's `Connection` + `AuthContext`
|
||||
— alkhttp's job is to construct the `CallConnection`, attach the
|
||||
bearer-resolved identity, and run
|
||||
`Dispatcher::run_loop_single_stream`. `alkcall`'s own tests
|
||||
(`channels/client.rs`) demonstrate this wiring end-to-end over
|
||||
duplex pairs.
|
||||
4. **EOF/teardown semantics are already specified** by alkcall
|
||||
(REQ-CH-01/02): `AsyncWrite::shutdown` → zero-length sentinel; demux
|
||||
EOF → all channels cleared. The adapter maps WS close to transport
|
||||
EOF and lets alkcall's invariants do the rest.
|
||||
5. **`Dispatcher::run_loop_single_stream` exists** and is the channel-0
|
||||
dispatch loop — no new dispatch code is needed anywhere in alkhttp.
|
||||
|
||||
This de-risks the two "high" tasks (WS adapter, WS session) from
|
||||
"unknown design" to "known shape, careful implementation."
|
||||
|
||||
## Build order (dependency spine)
|
||||
|
||||
```
|
||||
Phase 1 — server foundation (no WS, no adapters)
|
||||
core types → server adapter + auth → gateway dispatch + routes
|
||||
→ verified over tokio DuplexStream end-to-end
|
||||
|
||||
Phase 2 — WS + channels (the novel part)
|
||||
ws byte-stream adapter → ws upgrade handler + channels session
|
||||
→ verified browser-session-style over duplex WS
|
||||
|
||||
Phase 3 — adapters (mostly ports)
|
||||
http client host → from_openapi/from_jsonschema → to_openapi
|
||||
→ from_wss (depends on ws adapter) → mcp feature (from_mcp/to_mcp)
|
||||
→ /publish endpoint + to_openapi v2 of the gateway doc
|
||||
|
||||
Phase 4 — hardening
|
||||
integration test suite (full surface over duplex) → docs sync
|
||||
→ publish prep (dry-run, semver check)
|
||||
```
|
||||
|
||||
Rationale for key orderings:
|
||||
|
||||
- **Server core before WS** because the WS upgrade is a route on the
|
||||
`HttpAdapter` — the router, auth middleware, and decoy must exist
|
||||
first, and they're verifiable without WS (gateway over duplex).
|
||||
- **WS adapter before `from_wss`** — same adapter, both directions;
|
||||
building the consumer first would mean validating the adapter
|
||||
without its hardest user (the axum WS type).
|
||||
- **`/publish` in Phase 3** — the gateway spine (`/call`/`/subscribe`)
|
||||
lands in Phase 1; `/publish` adds a dispatch mode to a working
|
||||
gateway and gates OQ-02's version bump. Building it early would
|
||||
couple an unresolved OQ to the critical path.
|
||||
- **MCP last** — feature-gated, rmcp-heavy, and the gateway dispatch
|
||||
spine it consumes is stable by then.
|
||||
|
||||
## OQ dispositions
|
||||
|
||||
| OQ | Disposition | Where it resolves |
|
||||
|----|-------------|-------------------|
|
||||
| OQ-01 (WS adapter semantics) | Partially resolved by the spike: byte-stream treatment both directions, boundary = chunk header, split oversized chunks, shutdown → EOF sentinel + Close frame. Remaining: exact buffer bounds, flush semantics — locked during the WS adapter task. | `tasks/websocket/` |
|
||||
| OQ-02 (/publish framing) | Resolve in the `/publish` task: first line carries `{operation, chunk}`; terminal error = plain HTTP status + JSON body (not an NDJSON line). Then bump the gateway doc version. | `tasks/gateway/` |
|
||||
| OQ-03 (from_wss reconnect) | v1: connection drop → retryable failures; policy deferred. Documented in ADR-070; no task. | — |
|
||||
| OQ-04 (browser client) | Out of scope for this crate. | — |
|
||||
|
||||
## Conventions for the tasks
|
||||
|
||||
- Topic subdirectories: `tasks/server/`, `tasks/websocket/`,
|
||||
`tasks/gateway/`, `tasks/adapters/`, `tasks/client/`, plus
|
||||
`tasks/infra/` for repo-level concerns (CI, publish prep).
|
||||
- Every task carries the full frontmatter set (`scope`, `risk`,
|
||||
`impact`, `level`) — taskgraph's analysis commands rely on them.
|
||||
- Ported-source references: each task cites the alknet-http source
|
||||
file(s) it ports from, so the implementing agent can diff against
|
||||
the original rather than re-derive.
|
||||
- Verification per task: `cargo test -p <affected>` at minimum; the
|
||||
Phase 4 integration task runs the full `cargo test --all-features`.
|
||||
- No task crosses a subsystem boundary except through its declared
|
||||
`depends_on`.
|
||||
|
||||
## What the tasks will NOT cover
|
||||
|
||||
- The alknet-side wiring (endpoint, TLS, ALPN router registration) —
|
||||
alkhttp exposes `HttpAdapter` as a `ProtocolHandler`; the dial/accept
|
||||
composition is the consumer's job (AGENTS.md convention 9).
|
||||
- A browser/JS client for channels-over-WS (OQ-04, deferred).
|
||||
- WebTransport (ADR-069: out of scope entirely).
|
||||
Reference in New Issue
Block a user