docs: port architecture specs and ADRs from alknet-http; write new alkhttp ADRs 067-070
Phase 1 (SDD) — architecture documentation: Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint gateway, channels-over-WS, Sub/Pub operation types): - overview.md, http-server.md, http-adapters.md, http-mcp.md - README.md index (rewritten for alkhttp) New ADRs: - 067: WebSocket carries the channels protocol (8-byte chunk demux, channel 0 = alk/call, upgrade path /alk/channels) - 068: gateway /publish endpoint for Pub operations (NDJSON body) - 069: WebTransport out of scope in alkhttp (alknet concern) - 070: from_wss consumer adapter (wss feature, tokio-tungstenite) Ported ADRs (25, same numbers, port notes + amendments where the extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027, 034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066. websocket.md rewritten for the channels session; open-questions.md seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing, OQ-03 from_wss reconnect, OQ-04 browser client ownership). Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
# ADR-044: Defer h3/WebTransport; Browsers Use WebSocket
|
||||
|
||||
*Ported from alknet ADR-044 (Defer h3/WebTransport; Browsers Use WebSocket); re-targeted to alkhttp.*
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (supersedes alknet ADR-038; parks alknet ADR-040, alknet ADR-043)
|
||||
|
||||
## Status amendment (alkhttp port)
|
||||
|
||||
- **§1 (WebSocket as the browser bidirectional path) stands** and is the
|
||||
operative decision for this crate: the browser bidirectional path in
|
||||
alkhttp is WebSocket (alkhttp ADR-048 defines what the WS session
|
||||
carries).
|
||||
- **The deferral mechanics are superseded by alkhttp ADR-069**:
|
||||
WebTransport is not "deferred" in alkhttp — it is **removed from
|
||||
alkhttp scope entirely** (an alknet concern). There is no `h3`
|
||||
feature gate, no revival trigger, and no `webtransport.md` spec in
|
||||
this crate. ADR-044's "defer, revive when the ALPN-stream-proxy use
|
||||
case arrives" framing is an alknet posture and does not carry into
|
||||
alkhttp.
|
||||
- **alknet ADR-038, ADR-040, and ADR-043 are not ported to alkhttp.**
|
||||
They remain alknet decision records (ADR-038 superseded by alknet
|
||||
ADR-044; ADR-040/ADR-043 parked per alknet ADR-044). Nothing in this
|
||||
crate implements or plans them; if WebTransport revives as an alknet
|
||||
transport, the browser path through alkhttp remains WebSocket.
|
||||
|
||||
## Context
|
||||
|
||||
alknet ADR-038 brought `h3`/WebTransport into scope as a first-class HTTP
|
||||
transport, framed against the "two-way door as deferral" anti-pattern
|
||||
(alkcall ADR-032 §"What this framework is NOT"). alknet ADR-040 (the
|
||||
ALPN-stream-proxy) and alknet ADR-043 (the bidirectional-substrate
|
||||
reframing) extended it. Three ADRs, one crate-spanning spec
|
||||
(`webtransport.md`), and a body of design work.
|
||||
|
||||
Working through the implementation path surfaced a different concern than the
|
||||
one alknet ADR-038 was written to correct. alknet ADR-038 correctly rejected *deferral-
|
||||
as-hedging*; the present decision is *deferral-as-scoping*, which
|
||||
alkcall ADR-032 explicitly permits (a decision that "genuinely doesn't
|
||||
need to be made yet because the use case isn't concrete" — scope
|
||||
management, not door-type classification). The two must not be
|
||||
confused. Three concrete findings drove the scope re-evaluation:
|
||||
|
||||
*(Findings 2 — the WebTransport standards/dependency-maturity analysis —
|
||||
and the iroh-relay precedent below concern the alknet transport layer
|
||||
where QUIC/TLS/h3 live. alkhttp carries no transport stack; it is
|
||||
retained here for provenance of why the browser path is WebSocket.)*
|
||||
|
||||
### Finding 1 — the browser bidirectional path doesn't require WebTransport
|
||||
|
||||
The load-bearing use case for `h3`/WebTransport in v1 is **a browser reaching
|
||||
the call protocol bidirectionally**. alknet ADR-043 §2 establishes that the call
|
||||
protocol's bidirectionality applies unchanged over any bidirectional stream —
|
||||
the `Dispatcher` is stream-agnostic (alkcall ADR-015). That property is not unique to
|
||||
WebTransport streams. **WebSocket is a full-duplex, long-lived connection over
|
||||
which either side can send framed messages**, and the call protocol's
|
||||
`EventEnvelope` framing fits the WebSocket path cleanly (the
|
||||
`call.requested`/`call.responded`/`call.completed`/`call.aborted`
|
||||
exchange works over WebSocket with no protocol change — the same `Dispatcher`,
|
||||
the same `PendingRequestMap`, the same correlation by request ID; see the
|
||||
framing note below and alkhttp ADR-048's status amendment for the
|
||||
current chunk/framing model).
|
||||
|
||||
What WebTransport gives *over* WebSocket — native multiplexed bidirectional
|
||||
streams, datagrams, the "carry any ALPN as a stream" substrate framing
|
||||
(alknet ADR-043) — is genuinely better engineering, but none of it is *required* for
|
||||
the call protocol from a browser. The call protocol multiplexes multiple calls
|
||||
over a single connection by request ID (alkcall ADR-015); it does not need
|
||||
WebTransport's per-stream multiplexing. The substrate/proxy framing (alknet
|
||||
ADR-040, alknet ADR-043) is the thing that *does* benefit from WebTransport's
|
||||
stream model — and that use case is the speculative one (see Finding 3).
|
||||
|
||||
**Framing correction (alkhttp port):** the original Finding 1 stated
|
||||
"one `EventEnvelope` = one WS binary message." In the current wire
|
||||
model that is superseded: the WS message boundary carries **8-byte
|
||||
chunks of the channels protocol**, and on channel 0 the call protocol's
|
||||
`EventEnvelope` frames are carried as length-prefixed JSON (alkcall's
|
||||
frame format, alkcall ADR-014) inside the chunk payload — not
|
||||
one-envelope-per-WS-message. The load-bearing property (self-delimited
|
||||
frames, correlation by request ID, no protocol change) is unchanged.
|
||||
See alkhttp ADR-067 §framing and alkhttp ADR-048's status amendment.
|
||||
|
||||
### Finding 2 — WebTransport is a draft standard on an experimental dependency stack *(alknet concern)*
|
||||
|
||||
WebTransport over HTTP/3 is still an IETF draft (`draft-ietf-webtrans-http3`,
|
||||
at `-07` at time of writing), not an RFC. The Rust implementation landscape is
|
||||
correspondingly immature:
|
||||
|
||||
- `wtransport` (the reference read during research) is a complete
|
||||
pure-Rust implementation, but its own README states it "is not considered
|
||||
completely production-ready" and "may undergo changes as the WebTransport
|
||||
specification evolves."
|
||||
- The hyperium stack (`h3` + `h3-quinn` + `h3-webtransport` + `h3-datagram`)
|
||||
fits the axum/hyper ecosystem more naturally (h3 produces `http::Request`
|
||||
types that axum consumes directly, which is load-bearing for the spec's
|
||||
"HTTP/3 requests go through the same axum `Router`" commitment), but h3's
|
||||
own README says it is "still very experimental... API could change."
|
||||
- A research spike would be needed to verify the hyperium stack's
|
||||
server-side WebTransport API before committing to it — the axum-bridge
|
||||
feasibility is the load-bearing claim and is not yet confirmed against
|
||||
actual crate APIs, only against READMEs and design philosophy.
|
||||
|
||||
Either choice puts a draft-standard protocol and an experimental Rust
|
||||
dependency on the security surface of alkhttp's first release. The `h3`
|
||||
feature gate (alknet ADR-038) isolates the risk for non-browser-facing deployments,
|
||||
but a browser-facing hub must enable it — so the risk is borne precisely by
|
||||
the deployment shape that motivates having a browser path at all.
|
||||
|
||||
*(In alkhttp this risk analysis is historical: the crate never gains an
|
||||
`h3` feature. WebTransport is removed from scope per alkhttp ADR-069.)*
|
||||
|
||||
### Finding 3 — the ALPN-stream-proxy is speculative; the call protocol is not
|
||||
|
||||
alknet ADR-040 (the ALPN-stream-proxy — a browser with a WASM parser for SSH/SFTP/git
|
||||
reaching any ALPN handler via WebTransport) is the genuinely compelling
|
||||
WebTransport use case. It is also the one that is *not* required for v1:
|
||||
|
||||
- The call protocol from a browser works over WebSocket (Finding 1).
|
||||
- The downstream crates unlocked by completing alkhttp (the SSH, git,
|
||||
SFTP crates) do not require WebTransport or the proxy. They expose their
|
||||
ALPNs natively over QUIC; the proxy is a *browser reachability* feature
|
||||
for those ALPNs, not a prerequisite for the ALPNs to exist. *(The
|
||||
transports exposing those ALPNs are alknet concerns.)*
|
||||
- The WASM parsers (the browser-side SSH/SFTP/git clients) are themselves
|
||||
downstream artifacts not yet built. The proxy is only useful once a parser
|
||||
exists to consume it.
|
||||
|
||||
The proxy is "useful, and cheap-on-top *if* WebTransport already exists" —
|
||||
but WebTransport does not yet exist, and building it speculatively to enable
|
||||
a proxy whose consumers do not yet exist is the scope inversion. *(And in
|
||||
the alkhttp crate, it was never built at all — removed from scope per
|
||||
alkhttp ADR-069.)*
|
||||
|
||||
### The iroh precedent *(alknet-adjacent, but the signal carries)*
|
||||
|
||||
iroh's own relay (`iroh-relay`, the DERP-equivalent that provides NAT traversal
|
||||
fallback) chose **WebSocket (WSS)**, not WebTransport, for its fallback path.
|
||||
This is a strong signal from a project whose entire design center is QUIC and
|
||||
P2P connectivity: when the question was "what does a browser need to reach our
|
||||
protocol bidirectionally," their answer was WSS, not WebTransport. Aligning
|
||||
with that precedent is not cutting against competent practice — it is
|
||||
matching it.
|
||||
|
||||
### Concrete prior art: `@alkdev/pubsub`
|
||||
|
||||
The WebSocket path is not speculative — there is working prior art in the
|
||||
same workspace. The `@alkdev/pubsub` package (`/workspace/@alkdev/pubsub/`)
|
||||
already has a WebSocket client (`event-target-websocket-client.ts`) and
|
||||
server (`event-target-websocket-server.ts`) built on a generalized "event
|
||||
target" abstraction with an `EventEnvelope { type, id, payload }` shape.
|
||||
The call protocol's `EventEnvelope` was derived from this envelope
|
||||
(refined with typed event names `call.requested`/`call.responded`/etc. and
|
||||
structured payloads); the sibling `@alkdev/operations` package
|
||||
(`/workspace/@alkdev/operations/`) shares the lineage and uses the
|
||||
`path.do.op` (dot-separated) vs the call protocol's `path/to/op`
|
||||
(slash-separated) convention — a minor, mechanical delta. Syncing the
|
||||
pubsub/operations WebSocket client to the call protocol's envelope is a
|
||||
small adjustment (~a day of work: the envelope shape, the event-name
|
||||
typing, the path separator), not a from-scratch browser-client build.
|
||||
This is why the WebSocket path opens doors quickly: the browser (and
|
||||
Node) client is mostly already written. *(In the current wire model the
|
||||
pubsub-style clients additionally speak the channels chunk framing on
|
||||
the WS path — see the framing correction under Finding 1 and alkhttp
|
||||
ADR-067.)*
|
||||
|
||||
### The tradeoff between two use cases, not "good enough for now"
|
||||
|
||||
It is worth being precise about *why* WSS is the right choice here, because
|
||||
"good enough until it isn't" undersells the decision. The two browser-reach
|
||||
use cases have different right tools:
|
||||
|
||||
- **The call protocol from a browser (bidirectional).** WSS is *genuinely
|
||||
the right tool*, not a stopgap. The call protocol multiplexes by request
|
||||
ID (alkcall ADR-015), not by stream — it does not need WebTransport's
|
||||
per-stream multiplexing. A WebSocket is a full-duplex, long-lived,
|
||||
framed-message channel; the call protocol's framing fits the WS path
|
||||
cleanly (see the framing correction for the current chunk-based model).
|
||||
For this use case, WebTransport's stream model is engineering
|
||||
sophistication the call protocol has no use for. WSS is not "good
|
||||
enough" — it is well-matched.
|
||||
- **The generalized ALPN router/proxy (a browser reaching a non-call ALPN
|
||||
— SSH/SFTP/git via WASM).** WebTransport's native multi-stream model is
|
||||
*genuinely the right tool* here, and WSS is *probably worse* for it. A
|
||||
browser reaching a non-call ALPN over WSS would have to multiplex
|
||||
logical streams over one WS frame stream by application-level framing —
|
||||
doable (alknet ADR-043 §"SSH/SFTP/git-over-WSS-from-a-browser is
|
||||
technically possible"), but it re-implements at the application layer
|
||||
what WebTransport gives at the transport layer. This is the use case
|
||||
WebTransport was built for, and it is the speculative one (Finding 3) —
|
||||
the consumers (WASM SSH/SFTP/git parsers) do not exist yet. *(In
|
||||
alkhttp this second use case does not exist at all: WebTransport is
|
||||
removed from scope, alkhttp ADR-069, and the ALPN-stream-proxy was
|
||||
never in this crate.)*
|
||||
|
||||
So the original deferral was not "use the worse tool now, upgrade to the
|
||||
better tool later." It was "use the right tool for the use case we *have*
|
||||
(call protocol from a browser → WSS), and defer building the tool for the
|
||||
use case we *don't have yet* (generalized ALPN proxy → WebTransport)."
|
||||
In the alkhttp port, the second half of that sentence is closed outright:
|
||||
there is no deferred WebTransport future in this crate — the crate's
|
||||
browser bidirectional path is WebSocket, full stop (alkhttp ADR-069).
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Defer `h3`/WebTransport. Browsers reach the call protocol over WebSocket. *(§1 stands; deferral mechanics superseded by alkhttp ADR-069)*
|
||||
|
||||
The `h3` ALPN, the `h3` feature gate, and the WebTransport dependency stack
|
||||
are **deferred** in the original alknet decision — not implemented in the
|
||||
initial release. In **alkhttp** this resolves further: WebTransport is
|
||||
**removed from the crate's scope entirely** (alkhttp ADR-069); there is no
|
||||
`h3` ALPN, no `h3` feature, and no deferred revival in this crate. The
|
||||
clause that stands unchanged is the browser path itself: a browser
|
||||
connecting to a hub authenticates by bearer token and upgrades an
|
||||
HTTP/1.1 or HTTP/2 request to WebSocket. The resulting full-duplex WS
|
||||
connection carries the call protocol's `EventEnvelope` frames (in the
|
||||
current wire model: length-prefixed JSON frames on channel 0 inside the
|
||||
channels 8-byte chunk framing — see the framing correction under Finding
|
||||
1 and alkhttp ADR-067/ADR-048). The browser is a bidirectional
|
||||
call-protocol client over this connection, using the same `Dispatcher`
|
||||
and `PendingRequestMap` as the `alk/call` QUIC path (alkcall ADR-015 —
|
||||
stream-agnostic correlation; a WS message stream is just another
|
||||
`BiStream`-satisfying transport, extending the stream-agnostic claim
|
||||
from QUIC bidirectional streams to any framed full-duplex byte channel).
|
||||
|
||||
The original scope-decision framing (deferral-as-scoping, per alkcall
|
||||
ADR-032) still describes why WS was chosen; the reversal trigger below
|
||||
does not apply to alkhttp — the "revival" it describes would be an
|
||||
alknet concern and would not reopen alkhttp's surface.
|
||||
|
||||
### 2. alknet ADR-038 is superseded by this ADR. *(alknet record; not ported)*
|
||||
|
||||
alknet ADR-038's core decision — that `h3` is in scope, not deferred — is
|
||||
reversed by this ADR in alknet. alknet ADR-038's *correction* of the
|
||||
"two-way-door-as-deferral" anti-pattern stands as a document (the
|
||||
anti-pattern is real); its specific decision (h3 in scope now) is
|
||||
superseded. alknet ADR-038 is marked Superseded in the alknet record. It
|
||||
is **not ported to alkhttp**; this crate has no `h3` decision to record
|
||||
browsers-transport work against.
|
||||
|
||||
### 3. alknet ADR-040 and ADR-043 are parked, not superseded. *(alknet records; not ported)*
|
||||
|
||||
alknet ADR-040 (the ALPN-stream-proxy) and alknet ADR-043 (the
|
||||
bidirectional-substrate reframing) are **not superseded** in the alknet
|
||||
record — their decisions are correct, and they revive unchanged when
|
||||
WebTransport revives *as an alknet transport*. They are marked Proposed
|
||||
with an amendment noting implementation is deferred per this ADR. In
|
||||
alkhttp, neither is ported and neither has any footprint. The two
|
||||
transfers that the original decision applied during deferment:
|
||||
|
||||
- **alknet ADR-043 §2 (call-protocol bidirectionality over WebTransport)
|
||||
transfers to WebSocket unchanged.** WebSocket is full-duplex; the call
|
||||
protocol's bidirectionality applies over a WS connection exactly as
|
||||
alknet ADR-043 §2 describes for WebTransport. The browser case where
|
||||
the client registers no ops remains a use-case scoping, not an
|
||||
architectural limitation. *(This transfer is the part of ADR-044 that
|
||||
is live in alkhttp — it is what alkhttp ADR-048 implements.)*
|
||||
- **alknet ADR-043 §3 (the no-`PeerId` connection-local overlay)
|
||||
transfers to WebSocket unchanged.** A browser over WSS has no `PeerId`
|
||||
on the hub's side for the same reasons it has none over WebTransport
|
||||
(see §5 below); the connection-local Layer 2 overlay applies
|
||||
(alkcall ADR-019). The pattern is transport-agnostic.
|
||||
|
||||
What does *not* transfer to WebSocket is alknet ADR-040 (the
|
||||
ALPN-stream-proxy) and alknet ADR-043 §4 (the non-call-ALPN substrate
|
||||
mechanism). Those require WebTransport's stream model and revive with
|
||||
it *(as alknet work; alkhttp has no such path)*. SSH/SFTP/git-over-WSS-
|
||||
from-a-browser is technically possible (multiplex logical streams over
|
||||
one WS frame stream) but is not specified here — it is the same
|
||||
speculative use case that motivated deferring WebTransport, and it is
|
||||
not needed for v1.
|
||||
|
||||
### 4. WebSocket is the browser bidirectional path; HTTP/1.1+HTTP/2 remain the one-directional projection.
|
||||
|
||||
alkhttp's browser-reachable surface is:
|
||||
|
||||
| Transport | Direction | Use case |
|
||||
|-----------|-----------|----------|
|
||||
| `http/1.1`, `h2` | one-directional (client→server) | HTTP clients (curl, axios, `fetch` for request/response); SSE for subscription streaming (alkhttp ADR-049) |
|
||||
| WebSocket (over `http/1.1` or `h2` upgrade) | **bidirectional** | Browser call-protocol clients; the path that restores the call protocol's bidirectionality for browsers |
|
||||
|
||||
WebSocket is the surface that **restores the call protocol's
|
||||
bidirectionality for browsers** (the role alknet ADR-043 §5 assigned to
|
||||
WebTransport). The one-directional projection that alknet ADR-043 §5
|
||||
names for HTTP/1.1+HTTP/2 stands unchanged. (The `h3` row from the
|
||||
original table does not exist in alkhttp — WebTransport is removed from
|
||||
scope, alkhttp ADR-069.)
|
||||
|
||||
### 5. Browsers over WebSocket are not peers — the rationale, stated.
|
||||
|
||||
alkhttp ADR-034 §4 (ported from alknet ADR-034 §4) established that a
|
||||
browser over WebTransport is not a peer (no `PeerId`, no
|
||||
`PeerCompositeEnv` entry). The same applies to a browser over WebSocket,
|
||||
and the rationale — which alknet ADR-034 §4 states as a closure without
|
||||
the supporting argument — is worth making explicit because it is the
|
||||
load-bearing distinction:
|
||||
|
||||
**"Peer" means an addressable node in the call-protocol peer graph — a
|
||||
stable `PeerId`, reachable via `PeerRef::Specific`, whose ops land in
|
||||
`PeerCompositeEnv`, whose identity is stable across reconnects.** It
|
||||
does *not* mean "any endpoint that exchanges calls during a live
|
||||
session." A browser is the second thing but not the first, on three
|
||||
concrete grounds:
|
||||
|
||||
1. **No stable cryptographic identity of its own.** A `PeerEntry` is
|
||||
anchored to fingerprints (Ed25519, X.509) that *the peer* presents
|
||||
and the local node pins. A browser presents a bearer token the *hub*
|
||||
issued; the "identity" is the hub's bookkeeping for that token, not
|
||||
something the browser owns or that could be pinned by another node.
|
||||
There is nothing to put in `PeerEntry.fingerprints`.
|
||||
2. **Ephemeral.** Close the tab → connection dies → the connection-local
|
||||
Layer 2 overlay (alkcall ADR-019) dies with it. A `PeerEntry` keyed
|
||||
to a browser would be a permanently-dead entry within seconds.
|
||||
`PeerRef::Specific("browser-X")` from another node would route to
|
||||
nothing.
|
||||
3. **Not addressable from other nodes.** `PeerRef::Specific` resolves
|
||||
through `PeerEntry` → `PeerId`. Another node has no way to reach
|
||||
"the browser currently connected to hub-A"; the hub holds that
|
||||
connection as a live `CallConnection` handle, not as a peer-graph
|
||||
entry. The connection-local overlay is precisely the mechanism that
|
||||
gives the browser bidirectional-call capability *without* peer-graph
|
||||
membership.
|
||||
|
||||
This is the explicit closure of the "browser as peer" path, on both the
|
||||
inbound (this section) and outbound (alkhttp ADR-034 §2) sides. The
|
||||
browser is a **bidirectional call target during a live session**, not a
|
||||
**peer-graph member**. The connection-local Layer 2 overlay (alkcall
|
||||
ADR-019) is what makes the former possible without requiring the
|
||||
latter.
|
||||
|
||||
This rationale applies transport-agnostically — to WebSocket, to
|
||||
WebTransport (an alknet transport, out of alkhttp scope per alkhttp
|
||||
ADR-069), and to any future browser transport. alkhttp ADR-034 §4 is
|
||||
amended by reference to this section.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- alkhttp's first release does not carry a draft-standard protocol or
|
||||
an experimental dependency stack on its security surface. The browser
|
||||
path uses WebSocket, a mature, well-understood, RFC 6455 protocol with
|
||||
first-class axum support (`axum::extract::ws`).
|
||||
- The axum-bridge research spike for h3/WebTransport is not on the
|
||||
critical path. WebSocket upgrade over HTTP/1.1 or HTTP/2 is standard
|
||||
axum territory.
|
||||
- The downstream crates that alkhttp unblocks (SSH, git, SFTP) are not
|
||||
blocked on WebTransport or the proxy. They expose their ALPNs natively
|
||||
over QUIC *(an alknet transport concern)*; browser reachability for
|
||||
them would be a WebTransport feature — and in the alkhttp crate tree,
|
||||
that is out of scope (alkhttp ADR-069), not a deferred feature.
|
||||
- The crate stays lean: no `h3`, no `wtransport`/hyperium h3 stack, no
|
||||
WebTransport feature gate. The only browser-bidirectional dependency
|
||||
is the WebSocket upgrade path.
|
||||
|
||||
**Negative:**
|
||||
- alknet ADR-038, ADR-040, and ADR-043 are not implemented in this
|
||||
crate and are not ported to it (see Status amendment). Their design
|
||||
work is preserved in the alknet record only. A reader of alkhttp docs
|
||||
must go to the alknet docs to find them; this ADR's status amendment
|
||||
is the pointer.
|
||||
- The ALPN-stream-proxy (alknet ADR-040) is not available anywhere in
|
||||
the alkhttp surface. A browser cannot reach SSH/SFTP/git ALPNs
|
||||
through this crate — it can reach the call protocol over WebSocket,
|
||||
but not the non-call ALPNs. *(This is the alknet deferral; in
|
||||
alkhttp it is a removal from scope, alkhttp ADR-069.)*
|
||||
- WebSocket is a single stream; it lacks WebTransport's native
|
||||
multi-stream multiplexing. For the call protocol this is fine
|
||||
(correlation is by request ID, not by stream — alkcall ADR-015), and
|
||||
WSS is the well-matched tool for that use case (see §"The tradeoff
|
||||
between two use cases"). Where WebTransport's stream model would
|
||||
matter is the ALPN-stream-proxy (alknet ADR-040) — the speculative
|
||||
use case, which is out of alkhttp scope entirely (alkhttp ADR-069).
|
||||
- The original ADR-044's "WebTransport restores bidirectionality"
|
||||
framing (alknet ADR-043 §5) becomes "WebSocket restores
|
||||
bidirectionality" — and in this crate that framing is final, not
|
||||
provisional.
|
||||
|
||||
## Reversal *(superseded by alkhttp ADR-069 for this crate)*
|
||||
|
||||
The original decision reversed when a concrete deployment needed the
|
||||
ALPN-stream-proxy — i.e., a real use case of a browser running a WASM
|
||||
SSH/SFTP/git client to reach a non-call ALPN over WebTransport. At that
|
||||
point, in the alknet record:
|
||||
|
||||
1. The research spike deferred there (verify the hyperium stack's
|
||||
server-side WebTransport API and the axum-bridge feasibility) is run.
|
||||
2. alknet ADR-038 / ADR-040 / ADR-043 are un-parked and implemented as
|
||||
written, with the `webtransport.md` spec as the design.
|
||||
3. The WebSocket browser path (this ADR's §4) is not removed — it
|
||||
remains as the simpler browser path for deployments that don't need
|
||||
WebTransport's stream model. The two coexist.
|
||||
|
||||
**In alkhttp this reversal path does not exist.** WebTransport is not
|
||||
deferred here — it is removed from crate scope (alkhttp ADR-069). If a
|
||||
WebTransport deployment is ever built, it is an alknet concern (a
|
||||
transport/relay feature in the alknet tree); the alkhttp surface it
|
||||
would front is the stable HTTP contract this crate publishes, and no
|
||||
alkhttp ADR or feature gate anticipates it. The crate-surface one-way-
|
||||
door reasoning in the original (an `h3` feature gate becoming part of a
|
||||
published interface) is moot: alkhttp publishes no `h3` surface.
|
||||
|
||||
## Research note (for revival) *(alknet concern; recorded for provenance only)*
|
||||
|
||||
A note from the original record: `wtransport` (the reference
|
||||
implementation read during initial research) is *probably not* the right
|
||||
dependency choice, despite being a complete and readable implementation.
|
||||
The load-bearing integration concern was that the `h3` handler must
|
||||
route HTTP/3 requests through the same axum `Router` as `h2`/`http/1.1`,
|
||||
and `wtransport` owns its own HTTP serving path — bridging its request
|
||||
type into the `http::Request` axum consumes is cross-ecosystem adapter
|
||||
work. The hyperium stack (`h3` + `h3-quinn` + `h3-webtransport`)
|
||||
operates at the stream level and produces `http::Request` types
|
||||
natively, which is a better fit for the axum integration — but its
|
||||
server-side WebTransport API needs verification before commitment.
|
||||
|
||||
This research was **not** run, and in alkhttp it never will be: there
|
||||
is no WebTransport revival in this crate (alkhttp ADR-069). The note is
|
||||
preserved only because the original record kept it for the alknet-side
|
||||
revival question.
|
||||
|
||||
## Assumptions
|
||||
|
||||
1. **The call protocol's `EventEnvelope` framing fits the WebSocket
|
||||
path cleanly.** In the original framing: an `EventEnvelope` is a
|
||||
self-delimited JSON object; one envelope per WS binary message. In
|
||||
the current wire model this is amended (see the framing correction
|
||||
under Finding 1 and the alkhttp ADR-048 status amendment): the WS
|
||||
message boundary carries channels chunks (8-byte chunk header, alkcall
|
||||
ADR-034/ADR-035), and channel 0 — pre-negotiated as `alk/call`
|
||||
(alkcall ADR-036) — carries `EventEnvelope` frames as length-prefixed
|
||||
JSON (alkcall ADR-014's frame format) inside the chunk payload. The
|
||||
load-bearing property — self-delimited frames, no streaming
|
||||
deserializer across frame boundaries, correlation by request ID — is
|
||||
unchanged. This is already verified by prior art: the
|
||||
`@alkdev/pubsub` WebSocket client/server
|
||||
(`/workspace/@alkdev/pubsub/src/event-target-websocket-client.ts`,
|
||||
`event-target-websocket-server.ts`) carries the same
|
||||
`{ type, id, payload }` envelope over WS binary messages; the call
|
||||
protocol's `EventEnvelope` is a refined superset of that shape (typed
|
||||
event names, structured payloads).
|
||||
|
||||
2. **WebSocket upgrade over HTTP/1.1 or HTTP/2 is supported by the
|
||||
axum/hyper stack natively.** `axum::extract::ws` provides the upgrade
|
||||
handler; the underlying connection is the same hyper HTTP connection
|
||||
the `h2`/`http/1.1` handler already drives. No new framing library is
|
||||
needed.
|
||||
|
||||
3. **A browser over WebSocket has the same peer-model properties as a
|
||||
browser over WebTransport.** No `PeerId`, no `PeerCompositeEnv`
|
||||
entry, connection-local Layer 2 overlay (alkcall ADR-019; alkhttp
|
||||
ADR-034 §2). The rationale in §5 is transport-agnostic and applies
|
||||
identically to WSS.
|
||||
|
||||
4. **The downstream crates (SSH, git, SFTP) do not require WebTransport
|
||||
or the ALPN-stream-proxy to exist.** They expose their ALPNs natively
|
||||
over QUIC *(an alknet transport concern)*; the proxy is a browser-
|
||||
reachability feature, not a prerequisite for the ALPNs themselves.
|
||||
Browser reachability for non-call ALPNs is the speculative use case;
|
||||
in alkhttp it is not deferred but out of scope (alkhttp ADR-069).
|
||||
|
||||
## References
|
||||
|
||||
- alkcall ADR-032 §"What this framework is NOT" — the anti-pattern
|
||||
alknet ADR-038 was written to correct; the original decision relies
|
||||
on the explicit distinction between deferral-as-hedging (rejected)
|
||||
and deferral-as-scoping (permitted: a decision that "genuinely
|
||||
doesn't need to be made yet because the use case isn't concrete" —
|
||||
scope management, not door-type classification). *(Port note: the
|
||||
one-way-door decision framework lives in the alkcall crate as
|
||||
alkcall ADR-032 — see the alkcall crate docs; alkhttp did not port
|
||||
it. The original linked alknet ADR-009 by relative path.)*
|
||||
- alknet ADR-038 — **superseded by this ADR (in alknet).** Its
|
||||
correction of the two-way-door-as-deferral anti-pattern stands; its
|
||||
specific decision (h3 in scope now) is reversed. **Not ported to
|
||||
alkhttp.**
|
||||
- alknet ADR-040 — **parked, not superseded (alknet record).** Revives
|
||||
unchanged when WebTransport revives as an alknet transport. The proxy
|
||||
is the speculative use case. **Not ported to alkhttp.**
|
||||
- alknet ADR-043 — **parked, not superseded (alknet record).** §2
|
||||
(bidirectionality) and §3 (no-`PeerId` overlay) transfer to WebSocket
|
||||
unchanged; §4 (non-call-ALPN substrate) and §5's WebTransport-specific
|
||||
framing revive with WebTransport as alknet work. **Not ported to
|
||||
alkhttp.**
|
||||
- alkhttp [ADR-034](034-outgoing-only-x509-and-three-peer-roles.md) §4 —
|
||||
browsers are not peers; this ADR's §5 states the rationale
|
||||
(addressability vs. bidirectionality) that the original §4 closes
|
||||
without arguing. alkhttp ADR-034 §4 is amended by reference to this
|
||||
ADR's §5.
|
||||
- alkcall ADR-015 — call-protocol stream model; stream-agnostic
|
||||
correlation, `Dispatcher`/`PendingRequestMap`; a WebSocket message
|
||||
stream is another `BiStream`-satisfying transport. The call protocol
|
||||
multiplexes by request ID, not by stream.
|
||||
- alkcall ADR-014 — the call protocol's hand-rolled `EventEnvelope`
|
||||
framing (length-prefixed JSON); the frame format now carried on
|
||||
channel 0 of the WS path.
|
||||
- alkcall ADR-034 / ADR-035 — the channels wire format (8-byte chunk
|
||||
header); the multiplexing layer the WS path now carries (alkhttp
|
||||
ADR-067).
|
||||
- alkcall ADR-036 — channel 0 is pre-negotiated `alk/call`.
|
||||
- alkhttp ADR-067 — the WS session carries the channels protocol; the
|
||||
amendment that defines the current framing on this crate's WS path.
|
||||
- alkhttp ADR-069 — WebTransport removed from alkhttp scope entirely.
|
||||
- alkhttp ADR-049 — streaming handler for subscription operations (the
|
||||
SSE projection for the HTTP one-directional path).
|
||||
- alkhttp ADR-048 — the WS session shape (native session, not gateway);
|
||||
the implementer-facing rule this ADR's §1 implied.
|
||||
- alkhttp ADR-001 / ADR-002 — ALPN-based dispatch and the
|
||||
`ProtocolHandler` trait (`HttpAdapter` on `h2`/`http/1.1`); the WS
|
||||
upgrade layers on the same HTTP surface.
|
||||
- The alkcall crate docs — the call-protocol spec and `EventEnvelope`
|
||||
shape (see the alkcall crate's own documentation; the old
|
||||
`call-protocol.md` relative link pointed into the alknet mono-repo
|
||||
spec tree).
|
||||
|
||||
## Port notes
|
||||
|
||||
- **Superseded in part, per the status amendment above:** the "deferred"
|
||||
framing is superseded by alkhttp ADR-069 (WebTransport removed from
|
||||
alkhttp scope entirely). §1's WebSocket-as-browser-path stands; the
|
||||
reversal trigger, the "revival" mechanics, the `webtransport.md` spec
|
||||
pointer, and the research-spike-for-revival posture are alknet-record
|
||||
content retained for provenance and annotated. alknet ADR-038/040/043
|
||||
are not ported to alkhttp.
|
||||
- **Framing correction:** the original assumed "one `EventEnvelope` = one
|
||||
WS binary message." In the current wire model the WS message boundary
|
||||
carries 8-byte channels chunks (alkcall ADR-034/ADR-035); channel 0 is
|
||||
pre-negotiated `alk/call` (alkcall ADR-036) and carries `EventEnvelope`
|
||||
frames as length-prefixed JSON (alkcall ADR-014) inside the chunk
|
||||
payload. The original sentences asserting one-envelope-per-WS-message
|
||||
are annotated inline (Finding 1 correction, Assumption 1) rather than
|
||||
silently rewritten; the operative statement for this crate is alkhttp
|
||||
ADR-067.
|
||||
- Renames: "alknet-http" → "alkhttp"; the QUIC call ALPN "alknet/call" →
|
||||
"alk/call" (alkcall ADR-004 `alk/` convention); "alknet ADR-012" (the
|
||||
old call-protocol stream model) is cited as **alkcall ADR-015**
|
||||
(alkcall ADR numbering differs from alknet numbering — alkcall ADR-012
|
||||
is `ConnectionCredentials`, not the stream model).
|
||||
- The original's references to `crates/http/webtransport.md` and
|
||||
`crates/http/http-server.md` are alknet spec-tree artifacts; the
|
||||
`webtransport.md` spec does not exist in alkhttp (no deferred
|
||||
WebTransport spec is carried in this crate). The alkhttp equivalent of
|
||||
the http-server spec work is the `websocket`/`server` subsystem of
|
||||
this crate and alkhttp ADR-048.
|
||||
- The `@alkdev/pubsub` prior-art references are kept as absolute
|
||||
workspace paths (the packages still exist at those locations); the
|
||||
framing delta note is annotated per the channels-chunk correction.
|
||||
- The original table in §4 had an implied `h3` row (WebTransport →
|
||||
alk/http in the alknet ALPN table); that row is dropped from the
|
||||
alkhttp transport table per alkhttp ADR-069, with a note in §4.
|
||||
- iroh-relay precedent: kept — it is a design-signal argument, not a
|
||||
dependency claim; alkhttp has no iroh dependency.
|
||||
- Original title preserved: "Defer h3/WebTransport; Browsers Use
|
||||
WebSocket". The title's "defer" is the alknet-record posture; in
|
||||
alkhttp the operative reading is "no WebTransport in this crate;
|
||||
browsers use WebSocket" (alkhttp ADR-069).
|
||||
Reference in New Issue
Block a user