Plan updated with the alknet-tty findings: the drainer pattern (single ordered writer) makes outbound chunk-boundary parsing sound; TestStdinSink's try_send→Full→Pending is the inbound backpressure precedent; OQ-01(a) now cites the reference. Task graph (taskgraph-validated, 17 tasks, 6 generations, no cycles): - tasks/server/: core-types, auth, healthz-decoy, adapter - tasks/gateway/: dispatch, routes, publish - tasks/websocket/: byte-adapter (research POC), upgrade-session, overlay-ops - tasks/adapters/: from-openapi, from-jsonschema, to-openapi, from-wss, mcp - tasks/client/: http-host - tasks/infra/: integration-suite (phase 4) Critical path runs through server core → adapter → WS session → overlay tests → integration suite. High-risk tasks are the three WS tasks, de-risked by the ws-byte-adapter POC blocking upgrade-session.
130 lines
6.7 KiB
Markdown
130 lines
6.7 KiB
Markdown
# 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 dispatch code is needed anywhere in alkhttp.
|
|
6. **The alknet-tty crates provide the precedents for the adapter.**
|
|
The channels protocol was abstracted from TTY's earlier 5-byte
|
|
demux work, and `alknet-tty`'s adapter embodies the two patterns
|
|
OQ-01 needed:
|
|
- **The drainer pattern** (`pump_session`): all producer pumps feed
|
|
one bounded mpsc; a single drainer writes chunks in arrival
|
|
order. The transport write side is therefore sequential — a
|
|
boundary-parsing state machine in the WS adapter's write path is
|
|
sound (no interleaving to handle).
|
|
- **Bounded backpressure** (`TestStdinSink::poll_write`):
|
|
`try_send` → `Full` → `Poll::Pending` — the inbound buffer
|
|
pattern for the WS→bytes direction.
|
|
|
|
This de-risks the two "high" tasks (WS adapter, WS session) from
|
|
"unknown design" to "known shape, careful implementation." A targeted
|
|
POC (research task) still validates the full loop — axum WS ↔ adapter
|
|
↔ alkcall ChannelsAdapter + channel-0 dispatcher over duplex — before
|
|
the production implementation builds on it, since axum's WS API
|
|
specifics (message sizing, backpressure interplay) are the one part
|
|
neither the specs nor the TTY precedent exercise.
|
|
|
|
## 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). |