Full-surface integration suite (tests/full_surface.rs, mcp feature): - one HttpAdapter over real TCP (ProtocolHandler::handle path) serving gateway endpoints, /openapi.json, /mcp, and the WS channels session - gateway: search/schema/call/subscribe/batch/publish presence, envelope shapes, error fidelity end-to-end - from_openapi import -> Internal-by-default invisible from the wire -> External facade composes it via env.invoke -> upstream HTTP API called end-to-end (ADR-015 composition model exercised) - to_openapi 6-path doc validated against openapiv3 over the wire - to_mcp: MCP client connects to /mcp on the served adapter, lists the 4 gateway tools, search returns ACL-filtered ops (Sub excluded) Production fix: the WS upgrade route was reserved but never wired into HttpAdapter's router (the ws-upgrade-session tests built their own router). Now wired with ws_bearer_auth (401 without a resolvable token) around ws_upgrade_handler. Docs sync: all 28 'Port notes' sections/blockquotes stripped from ported ADRs/specs; OQ-01/OQ-02 statuses corrected to resolved in overview.md, websocket.md, and the README table (open-questions.md was already current). Publish prep: cargo publish --dry-run --allow-dirty succeeds; cargo doc --no-deps warning-free (ADR link targets fixed); feature combinations (default / test-support / mcp / wss / all) compile warning-free under clippy -D warnings. Verified: cargo test (182 lib default), --all-features (227 lib + 29 integration), clippy -D warnings x3 feature sets, fmt, doc, publish --dry-run.
395 lines
21 KiB
Markdown
395 lines
21 KiB
Markdown
---
|
|
status: draft
|
|
last_updated: 2026-08-27
|
|
---
|
|
|
|
# WebSocket — the Browser Bidirectional Path (Channels over WS)
|
|
|
|
WebSocket is the browser bidirectional path to the call protocol and
|
|
the channels protocol. A WS session carries the **channels protocol**
|
|
([ADR-067](decisions/067-websocket-carries-channels.md)): the 8-byte
|
|
chunk header multiplexes N logical channels over the WS binary message
|
|
stream; **channel 0 is pre-negotiated as `alk/call`** and carries the
|
|
native call-protocol session, dispatched by the shared `Dispatcher`.
|
|
This supersedes the alknet design where WS carried one bare
|
|
`EventEnvelope` per message.
|
|
|
|
A WS session is a **native channels session, not the HTTP gateway
|
|
shape** — the gateway endpoints (`/search`, `/schema`, `/call`,
|
|
`/batch`, `/subscribe`, `/publish`) are the HTTP one-directional
|
|
projection and **do not appear on the WebSocket path**
|
|
([ADR-048](decisions/048-websocket-native-session-not-gateway.md), as
|
|
amended by ADR-067). Discovery is via `services/list`/`services/schema`
|
|
as call-protocol operations on channel 0.
|
|
|
|
## What
|
|
|
|
The WebSocket path is an axum WS upgrade handler on the same
|
|
`HttpAdapter` that serves `h2`/`http/1.1`
|
|
([http-server.md](http-server.md)). A browser (or any WS client — Node,
|
|
a native app, the `from_wss` consumer
|
|
([ADR-070](decisions/070-from-wss-consumer-adapter.md)) in reverse)
|
|
opens an HTTP/1.1 or HTTP/2 request to the upgrade path, authenticates
|
|
by bearer token on the upgrade request, and the resulting full-duplex
|
|
WS connection is a channels connection: the 8-byte chunk format
|
|
(alkcall ADR-034) runs over it exactly as over TCP+TLS.
|
|
|
|
## Why
|
|
|
|
WebSocket is the HTTP-family transport that restores the alk protocols'
|
|
native bidirectionality for browsers — HTTP/1.1 + HTTP/2 are
|
|
request/response (a one-directional projection of the call protocol),
|
|
while WS is a full-duplex, long-lived, framed-message channel. Carrying
|
|
the **channels protocol** (rather than a bare envelope stream, the
|
|
alknet choice) makes a browser session structurally identical to a Rust
|
|
in-line channels session (TCP+TLS): one session shape, one dispatch
|
|
path, data channels included. The decision is
|
|
[ADR-067](decisions/067-websocket-carries-channels.md); the decision
|
|
that the WS path carries the native session rather than the gateway
|
|
shape is [ADR-048](decisions/048-websocket-native-session-not-gateway.md).
|
|
|
|
### The consumer-side mirror: `from_wss`
|
|
|
|
The same session serves the outbound direction: a Rust process dials a
|
|
remote node's WSS endpoint and consumes its operations
|
|
([ADR-070](decisions/070-from-wss-consumer-adapter.md)). Server upgrade
|
|
and consumer dial share the WS↔byte-stream adapter; the session halves
|
|
are alkcall's (`ChannelsAdapter` accept path vs `ChannelClient`
|
|
consumer path).
|
|
|
|
## Architecture
|
|
|
|
### The WS upgrade handler
|
|
|
|
The WS upgrade is an HTTP/1.1 or HTTP/2 request handled by an axum
|
|
route on `HttpAdapter`'s router. The handler:
|
|
|
|
1. Receives the HTTP upgrade request (axum's `WebSocketUpgrade`
|
|
extractor).
|
|
2. Resolves the caller's identity from the `Authorization: Bearer`
|
|
header via `identity_provider.resolve_from_token(&AuthToken { raw:
|
|
token_bytes })` (`AuthToken` is in `alkcall::core::auth`) — the same
|
|
auth path as any HTTP request
|
|
([http-server.md](http-server.md) §"Auth"). The upgrade is rejected
|
|
(`401`) if no token is present; insufficient scopes for any op the
|
|
browser later calls surface as `FORBIDDEN` at call time, not at
|
|
upgrade time (the upgrade doesn't know which ops the browser will
|
|
call).
|
|
3. Upgrades to WebSocket (axum's `WebSocketUpgrade::on_upgrade`),
|
|
producing a full-duplex `WebSocket` stream.
|
|
4. Adapts the WS message stream to a byte stream (see §"Framing" below
|
|
— the adapter is this spec's core new piece).
|
|
5. Wraps the byte stream as a `Connection` (`Connection::from_bidi`,
|
|
ALPN `alk/channels`).
|
|
6. Runs the channels accept path: the in-line demux loop
|
|
(alkcall `ChannelsAdapter`) installs channel 0 via the
|
|
`install_channel_zero` hook — constructing channel 0's
|
|
`CallConnection` (identity attached) and running
|
|
`Dispatcher::run_loop_single_stream` on it.
|
|
7. Data channels (1..N) route per the deployment's openable-ALPN
|
|
registrations; the browser opens them via the per-ALPN open ops on
|
|
channel 0 (alkcall ADR-047), the same mechanism any consumer uses.
|
|
|
|
The **default upgrade path is `/alk/channels`** (was `/alknet/call` in
|
|
the alknet design). The path must not collide with the reserved
|
|
gateway/`/healthz`/`/openapi.json`/MCP/custom-route paths per
|
|
[ADR-046](decisions/046-assembly-layer-custom-http-routes.md)'s
|
|
collision rule; `/alk/channels` namespaces away from the reserved set
|
|
naturally. The upgrade runs over HTTP/1.1 (RFC 6455) or HTTP/2
|
|
(extended CONNECT, RFC 8441); axum/hyper supports both, and the handler
|
|
does not branch on which — the WS frame stream is the same once the
|
|
upgrade completes.
|
|
|
|
### Framing: WS messages carry chunks; channel 0 carries length-prefixed envelopes
|
|
|
|
The wire layering:
|
|
|
|
```
|
|
WS binary message (message boundary = transport frame)
|
|
└── chunk header [channel_id: u32 BE][length: u32 BE] (8 bytes)
|
|
└── payload
|
|
channel 0: [len: u32 BE][EventEnvelope JSON] (call frame)
|
|
channel N: handler-owned framing (opaque to channels)
|
|
```
|
|
|
|
- **WS messages are transport frames, not protocol boundaries.** The
|
|
WS binary message stream is treated as a byte stream; the 8-byte
|
|
chunk header is the only framing. A chunk may span WS messages and a
|
|
WS message may carry chunk fragments — the adapter (below) is the
|
|
seam. (In practice the write path usually emits one chunk per
|
|
message, but nothing may depend on it: channel 0's frame writer
|
|
issues two writes — length prefix, then body — which the mux can
|
|
deliver as two mpsc payloads.)
|
|
- **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. **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
|
|
`1002`). All frames are binary.
|
|
- The alknet design's "one envelope = one WS message, no length
|
|
prefix" rule is superseded; see
|
|
[ADR-048](decisions/048-websocket-native-session-not-gateway.md)'s
|
|
status amendment.
|
|
|
|
#### The WS ↔ byte-stream adapter
|
|
|
|
The channels machinery reads and writes **bytes** (`AsyncRead` +
|
|
`AsyncWrite`): the demux does `read_exact` on the 8-byte header and
|
|
payload; the mux writes chunk bytes. A WebSocket is
|
|
**message-oriented**: axum yields whole `Message`s, and writes are
|
|
whole `Message`s. The adapter bridges the two, in both directions:
|
|
|
|
- **Inbound (WS → bytes):** a background task pops WS binary messages
|
|
and appends their bytes into a shared buffer; the `AsyncRead` half
|
|
drains the buffer. Backpressure: the reader task awaits a bounded
|
|
buffer slot before admitting the next message (bound: OQ-01).
|
|
- **Outbound (bytes → WS):** the `AsyncWrite` half accumulates bytes
|
|
into a pending buffer; a background task scans the pending bytes for
|
|
complete chunks (8-byte header → payload length) and emits each
|
|
complete chunk as one WS binary message, carrying any partial tail
|
|
until its chunk completes. The adapter **parses the outgoing byte
|
|
stream** to find chunk boundaries — it does not assume one write
|
|
equals one chunk (verified against alkcall: `write_chunk` issues
|
|
header+payload as separate writes, and channel 0's `write_frame`
|
|
issues prefix+body as separate writes; each can surface as separate
|
|
mux payloads). Oversized chunks (a single chunk exceeding the
|
|
WS-message cap, up to `MAX_CHUNK_LEN` = 16 MiB) are split across
|
|
multiple WS messages — legal, since the receiver's boundary is the
|
|
chunk header, not the message. Flush semantics are an OQ-01 item.
|
|
- **Close mapping:** WS close (either side) → transport EOF → the
|
|
demux clears all channels (REQ-CH-02: every handler sees EOF) and
|
|
channel 0's dispatch loop fails outstanding pendings with
|
|
`connection closed`. `AsyncWrite::shutdown` maps to the zero-length
|
|
EOF sentinel (REQ-CH-01) followed by a WS Close frame.
|
|
|
|
The adapter is shared with the `from_wss` consumer path
|
|
([ADR-070](decisions/070-from-wss-consumer-adapter.md)) — one
|
|
implementation, both directions.
|
|
|
|
### Dispatch: channel 0 = the shared `Dispatcher`, unchanged
|
|
|
|
Channel 0's session is the alknet design's native session, verbatim:
|
|
|
|
- For `call.requested`: runs `AccessControl::check(identity)` against
|
|
the op's `AccessControl`, dispatches via the registry if allowed,
|
|
returns `FORBIDDEN` (→ `call.error`) before the handler runs if not.
|
|
- For `call.responded`/`call.completed`/`call.aborted`: correlates by
|
|
`id` via the pending map (keyed by request ID, not by transport —
|
|
alkcall ADR-015).
|
|
- For `call.published` (initiator-side `Pub` ops): routed to the
|
|
matching sink (alkcall ADR-046).
|
|
- Writes response frames back through channel 0's shared writer.
|
|
|
|
Peer authorization flows through `AccessControl::check` against the
|
|
resolved identity — an op with `AccessControl::default()` is callable
|
|
by any authenticated browser; an op with `required_scopes` only by
|
|
identities whose scopes satisfy them; an op with
|
|
`Visibility::Internal` is never callable from the wire (`NOT_FOUND`
|
|
before ACL). This is alkcall ADR-017's model; see also
|
|
[ADR-015](decisions/015-privilege-model-and-authority-context.md).
|
|
|
|
### Data channels for browsers
|
|
|
|
A browser opens a data channel exactly as any channels consumer:
|
|
|
|
1. Calls the per-ALPN open op on channel 0
|
|
(`channels/<alpn>/sub` or `channels/<alpn>/pub` — alkcall ADR-047).
|
|
2. The producer side (the hub) checks the open against the registered
|
|
openable ALPNs and `check_open(identity)`, allocates the
|
|
`channel_id`, and returns it.
|
|
3. Chunks with that `channel_id` flow over WS messages; both sides
|
|
reassemble the `BiStream`.
|
|
|
|
The deployment decides which ALPNs are openable — the browser reaches
|
|
exactly what a Rust consumer on an in-line connection would reach.
|
|
This is what makes the WASM-SSH-client-in-a-browser use case (the one
|
|
that motivated the alknet WebTransport track) workable over WS: the
|
|
SSH byte stream rides a data channel.
|
|
|
|
### Bidirectionality
|
|
|
|
The WS channels session inherits both protocols' native
|
|
bidirectionality (alkcall ADR-015 for calls; the channels protocol is
|
|
symmetric by construction):
|
|
|
|
- **Calls:** both sides can send `call.requested` on channel 0. The
|
|
browser calls hub ops; the hub can call browser-registered ops over
|
|
the same session, same pending map, same framing.
|
|
- **Channels:** the browser can open data channels (via open ops); the
|
|
hub can open them toward the browser if the browser side registers
|
|
openable ALPNs (a browser that is also a producer).
|
|
|
|
The common case — a browser that registers no ops and opens no
|
|
channels — is a use-case scoping, not an architectural limitation.
|
|
|
|
### Connection-local overlay
|
|
|
|
A browser over WS has no `PeerId` on the hub's side. Any ops the
|
|
browser registers land in a **connection-local Layer 2 overlay**
|
|
(alkcall ADR-019; the mechanism the hub reaches via the live
|
|
connection handle's `overlay_env()`). When the WS connection closes
|
|
(browser closes the tab, network drops), the overlay and all its
|
|
registered ops drop — no explicit deregistration. The hub reaches
|
|
browser ops through the connection handle, not through
|
|
`PeerRef::Specific` (the browser is not a peer — see below).
|
|
|
|
### Streaming: native `call.responded` events, no SSE
|
|
|
|
A `Sub` operation invoked on channel 0 streams `call.responded`
|
|
frames as channel-0 chunks — **no SSE framing**. SSE is the h2/http1.1
|
|
streaming projection (the gateway's `/subscribe` endpoint per
|
|
[ADR-042](decisions/042-openapi-gateway-pattern.md)); on WS it is
|
|
unnecessary because WS is already a framed full-duplex channel. The
|
|
browser receives `call.responded` events one per frame, with the same
|
|
`id` correlating them to the original `call.requested`;
|
|
`call.completed` closes the subscription; `call.aborted` closes it
|
|
with an error frame. This is identical to how subscriptions work over
|
|
any in-line channels transport.
|
|
|
|
A `Pub` operation (browser as initiator) publishes `call.published`
|
|
frames from the browser into channel 0 — the HTTP gateway's `/publish`
|
|
([ADR-068](decisions/068-gateway-publish-endpoint.md)) has no WS
|
|
equivalent because the native mechanism already exists.
|
|
|
|
On WS client disconnect (the browser closes the tab mid-subscription),
|
|
the session teardown detects the close and aborts in-flight
|
|
subscriptions — the abort cascade runs per alkcall ADR-020.
|
|
|
|
### Browsers are not alk peers
|
|
|
|
A browser over WS authenticates by bearer token, gets no `PeerId`,
|
|
does not enter `PeerCompositeEnv`, and its registered ops (if any) land
|
|
in the connection-local overlay. The rationale
|
|
([ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4,
|
|
amended by alknet ADR-044 §5) is a load-bearing distinction:
|
|
|
|
**"Peer" means an addressable node in the call-protocol peer graph** —
|
|
a stable `PeerId`, reachable via `PeerRef::Specific`, 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:
|
|
|
|
1. **No stable cryptographic identity of its own.** A peer entry is
|
|
anchored to fingerprints the *peer* presents and the local node
|
|
pins. A browser presents a bearer token the *hub* issued — hub
|
|
bookkeeping, not pinnable identity. There is nothing to put in
|
|
fingerprints.
|
|
2. **Ephemeral.** Close the tab → connection dies → the
|
|
connection-local overlay dies with it. A peer entry keyed to a
|
|
browser would be permanently dead within seconds.
|
|
3. **Not addressable from other nodes.** `PeerRef::Specific` resolves
|
|
through a peer entry; another node has no way to reach "the browser
|
|
currently connected to hub-A." The hub holds that connection as a
|
|
live handle, not a peer-graph entry.
|
|
|
|
The browser is a **bidirectional call target during a live session**,
|
|
not a **peer-graph member**; the connection-local overlay is what
|
|
gives the former without the latter.
|
|
|
|
### Auth: bearer token on the upgrade request
|
|
|
|
Inbound WS auth is `Authorization: Bearer <token>` on the HTTP upgrade
|
|
request, resolved via `IdentityProvider::resolve_from_token()` — the
|
|
same path as any HTTP request ([ADR-004](decisions/004-auth-as-shared-core.md);
|
|
mechanism owned by alkcall ADR-003). Bearer-only is the auth mechanism;
|
|
other schemes would be added as axum middleware on the upgrade route
|
|
(two-way door). The resolved identity drives `AccessControl::check` on
|
|
every `call.requested` the browser sends and `check_open` on every
|
|
channel-open op — per-privilege filtering is free via
|
|
`services/list`'s existing `AccessControl` filtering.
|
|
|
|
For the consumer side (`from_wss`), the same shape applies outbound:
|
|
the adapter presents the token on its dial, sourced from
|
|
`Capabilities` (the no-env-vars path,
|
|
[ADR-014](decisions/014-secret-material-flow-and-capability-injection.md)).
|
|
|
|
## Constraints
|
|
|
|
- **The WS path is the channels session, not the gateway shape
|
|
([ADR-067](decisions/067-websocket-carries-channels.md),
|
|
[ADR-048](decisions/048-websocket-native-session-not-gateway.md)).**
|
|
The 6 gateway endpoints are HTTP-only and do not appear on WS.
|
|
Discovery via `services/list`/`services/schema` as call-protocol ops;
|
|
streaming as native frames, not SSE.
|
|
- **Bearer-only auth on the upgrade request.** `Authorization: Bearer`
|
|
→ `resolve_from_token`. The resolved identity drives
|
|
`AccessControl::check` on calls and `check_open` on channel opens.
|
|
- **Browsers are not alk peers.** Bearer token, no `PeerId`,
|
|
connection-local Layer 2 overlay for browser-registered ops.
|
|
- **One WS binary message = one chunk; channel 0 frames are
|
|
length-prefixed JSON inside the chunk payload.** Text messages are
|
|
rejected. The alknet "one envelope per message" rule is superseded
|
|
([ADR-048](decisions/048-websocket-native-session-not-gateway.md)
|
|
status amendment).
|
|
- **The shared `Dispatcher` runs on channel 0 unchanged
|
|
(alkcall ADR-015).** The dispatch half is one implementation across
|
|
all in-line transports; only connection establishment differs.
|
|
- **The default upgrade path is `/alk/channels`; it must not collide
|
|
with reserved paths ([ADR-046](decisions/046-assembly-layer-custom-http-routes.md)).**
|
|
Overridable via `extra_routes`.
|
|
- **The WS↔byte-stream adapter is the single seam between axum's WS
|
|
and alkcall's byte-oriented channels machinery** — shared with
|
|
`from_wss`, semantics tracked in OQ-01.
|
|
|
|
## Design Decisions
|
|
|
|
| Decision | ADR | Summary |
|
|
|----------|-----|---------|
|
|
| WS carries the channels protocol (channel 0 = `alk/call`) | [ADR-067](decisions/067-websocket-carries-channels.md) | WS = in-line channels substrate; 8-byte chunk demux; upgrade path `/alk/channels`; supersedes bare-envelope framing |
|
|
| WS carries the native session, not the gateway shape | [ADR-048](decisions/048-websocket-native-session-not-gateway.md) | Channel 0 carries the native call session; gateway endpoints are HTTP-only; discovery via `services/list`/`services/schema` |
|
|
| Call protocol stream model | alkcall ADR-015 | Stream-agnostic correlation; the pending map and dispatch loop run over any transport |
|
|
| WS as the browser bidirectional path | [ADR-044](decisions/044-defer-webtransport-browsers-use-websocket.md) | Stands; WebTransport removed from alkhttp scope ([ADR-069](decisions/069-webtransport-out-of-scope.md)) |
|
|
| Operation registry layering (connection-local overlay) | alkcall ADR-019 | Browser-registered ops land in a per-connection overlay that dies with the WS connection |
|
|
| Browsers are not alk peers | [ADR-034](decisions/034-outgoing-only-x509-and-three-peer-roles.md) §4 | Bearer token, no `PeerId`; addressability vs bidirectionality rationale |
|
|
| Abort cascade on disconnect | alkcall ADR-020 | WS close mid-subscription aborts in-flight ops, cascading to descendants |
|
|
| Bearer auth via `resolve_from_token` | [ADR-004](decisions/004-auth-as-shared-core.md) | WS upgrade request credential source (same as HTTP; mechanism: alkcall ADR-003) |
|
|
| Browsers require X.509 (TLS) | [ADR-027](decisions/027-tls-identity-redesign-acme-rawkey-decoupling.md) | The WS upgrade runs over the same TLS as h2/http1.1; TLS provisioning is an alknet concern |
|
|
| Stealth: HTTP handler on standard ALPNs serves WS upgrade | [ADR-010](decisions/010-alpn-router-and-endpoint.md) | The WS upgrade route is on `HttpAdapter`'s default surface |
|
|
| Custom routes collision rule | [ADR-046](decisions/046-assembly-layer-custom-http-routes.md) | The WS upgrade route must not collide with reserved default-surface paths |
|
|
| `from_wss` consumer adapter | [ADR-070](decisions/070-from-wss-consumer-adapter.md) | The outbound mirror: same adapter, `ChannelClient` consumer half, `wss` feature |
|
|
|
|
## Open Questions
|
|
|
|
See [open-questions.md](open-questions.md) for full details.
|
|
|
|
- **OQ-01** (resolved): WS ↔ byte-stream adaptation — resolved by the
|
|
production adapter (`byte_adapter.rs`): inbound bounded mpsc (64
|
|
slots, backpressure), outbound chunk parser with 1 MiB message cap,
|
|
flush no-op, close → EOF mapping; validated end-to-end in both
|
|
directions (server upgrade path + `from_wss` client).
|
|
- **OQ-04** (open): browser client library ownership — the JS/TS
|
|
client speaking channels-over-WS (chunk framing, channel 0 open ops,
|
|
envelope handling) is needed for browser consumers; it lives outside
|
|
this crate, but the BAST chunk-header contract
|
|
(alkcall `chunk-header.bast.json`) is the shared reference.
|
|
|
|
## References
|
|
|
|
- [ADR-067](decisions/067-websocket-carries-channels.md) — the
|
|
channels-over-WS decision (this document's specification target)
|
|
- [ADR-048](decisions/048-websocket-native-session-not-gateway.md) —
|
|
the native-session decision (amended by ADR-067 for framing and path)
|
|
- [ADR-070](decisions/070-from-wss-consumer-adapter.md) — the consumer
|
|
side
|
|
- [ADR-044](decisions/044-defer-webtransport-browsers-use-websocket.md)
|
|
— WS as the browser path (stands); [ADR-069](decisions/069-webtransport-out-of-scope.md)
|
|
— WebTransport out of scope
|
|
- [http-server.md](http-server.md) — the `HttpAdapter` hosting the
|
|
upgrade route
|
|
- The alkcall crate's `docs/architecture/` — call protocol wire format
|
|
(ADR-014), stream model (ADR-015), registry layering (ADR-019), abort
|
|
cascade (ADR-020), channels wire format (ADR-034/035), channel 0
|
|
pre-negotiation (ADR-036), ChannelsAdapter (ADR-039), ChannelClient
|
|
(ADR-043), openable ALPNs (ADR-047); `channels-wire.md` for the
|
|
chunk format and wire invariants (REQ-CH-01/02/04/05)
|
|
- The alknet mono-repo's `@alkdev/pubsub` WS client/server — prior art
|
|
for the browser WS path lineage (the envelope shape the call
|
|
protocol refined) |