Skip to content

Architecture

This content is for the 0.1 version. Switch to the latest version for up-to-date documentation.

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>]

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.

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.

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.

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 */);

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://'));
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.