Architecture
The SDK is five layers. Each is usable on its own, and each is the fallback when the one above it does not fit.
flowchart TD A[AhpRuntime<br/><i>lifecycle, reconnect, leases</i>] --> B B[AhpStateMirror<br/><i>routing, gap detection</i>] --> C C[ChannelStore<br/><i>confirmed + optimistic state</i>] --> D D[Reducers<br/><i>pure state transitions</i>] A --> E[AhpConnection<br/><i>JSON-RPC over a transport</i>] E --> F[AhpTransport<br/><i>WebSocket, or in-memory</i>]
Transport
Section titled “Transport”AhpTransport is a bidirectional stream of decoded JSON maps. Two
implementations ship:
WebSocketAhpTransport |
What talks to a real host |
InMemoryTransport |
A connected pair, for tests |
It is an interface rather than a concrete socket for exactly one reason: the entire stack above it can then be exercised without a network.
Connection
Section titled “Connection”AhpConnection implements AhpClient — the protocol’s request surface, and
the interface worth writing code against:
Future<InitializeResult> initialize(InitializeParams params);Future<ReconnectResult> reconnect(ReconnectParams params);Future<SubscribeResult> subscribe(SubscribeParams params);void unsubscribe(UnsubscribeParams params);DispatchHandle dispatch(DispatchActionParams params);Future<void> ping();Future<void> close();It also exposes actions — the broadcast stream of inbound envelopes — and
handles the JSON-RPC framing, request/response correlation, and timeouts.
Note that dispatch returns a handle synchronously rather than a Future. The
action has already applied locally by the time it returns; the future for what
the host decided lives on the handle. See
Dispatching actions.
Reducers
Section titled “Reducers”Pure functions, one per channel kind:
RootState rootReducer(RootState state, StateAction action);SessionState sessionReducer(SessionState state, StateAction action, ...);ChatState chatReducer(ChatState state, StateAction action, ...);They are hand-ports of upstream’s TypeScript, verified against the same 246 fixtures every other client uses. They never throw on an unrecognized action — the specification requires clients to ignore what they do not understand, so an unknown action returns the state it was given, identically.
That identity matters beyond correctness: see Reducers.
A ChannelStore holds two versions of one channel’s state:
confirmed |
Only what the host has acknowledged |
optimistic |
confirmed plus locally dispatched actions not yet settled |
You read optimistic. confirmed is what optimistic actions rebase onto when
the host’s echo arrives. Reconciliation covers
the mechanics.
Mirror
Section titled “Mirror”AhpStateMirror owns one store per channel and routes each inbound envelope to
the right reducer, deciding by the channel URI. It also detects sequence gaps —
serverSeq is connection-global, so a gap is only meaningful at this level, not
per-channel.
final mirror = AhpStateMirror(clientId: 'my-app');client.actions.listen(mirror.applyEnvelope);mirror.gaps.listen((gap) => /* reconnect and replay */);Runtime
Section titled “Runtime”AhpRuntime is the layer that makes a long-lived client practical. It owns the
connection, reconnects with backoff, replays what was missed, and hands out
refcounted channel leases so several widgets can watch one channel with a single
subscription on the wire.
final runtime = AhpRuntime( connect: () async => AhpConnection( await WebSocketAhpTransport.connect(url), ), clientId: 'my-app',)..start();
final lease = runtime.acquire(Uri.parse('ahp-root://'));Which layer
Section titled “Which layer”| Situation | Use |
|---|---|
| A script that connects, reads, exits | AhpConnection + AhpStateMirror |
| Anything long-lived, or any UI | AhpRuntime |
| A test | InMemoryTransport under either |
The Quickstart uses the first. Anything with a widget tree wants the second — the Flutter bindings are built on it.