Transport ABI
Authority: Primary (Normative)
Purpose: Extended semantics for theTransportConnectioninterface
See also: ADR-005, transport/errors.md, transport/websocket.md
This document extends ADR-005 with normative behavioral semantics for transport implementations. ADR-005 defines the interface shape; this document specifies runtime behavior.
1. Connection Lifecycle States
1.1 State Type
type ConnectionState = "connecting" | "open" | "closing" | "closed";1.2 Interface Extensions
interface TransportConnection {
/** Current connection state. */
readonly state: ConnectionState;
/**
* Promise that resolves when the connection closes.
* MUST resolve (not reject) regardless of close reason.
*/
readonly closed: Promise<CloseInfo>;
// ... existing members from ADR-005
}
interface CloseInfo {
/** True if closed via normal close handshake. */
graceful: boolean;
/** Transport-specific close code, if applicable. */
closeCode?: number;
/** Human-readable close reason. */
reason?: string;
/** Optional; present when the close was abnormal or carries error context. */
error?: TransportError;
}1.3 State Transitions
connect() called
|
v
+------------+
| connecting | <-- Optional; MAY skip if connect() blocks until open
+-----+------+
| connection established
v
+--------+
| open |
+---+----+
| close() called OR remote close OR error
v
+---------+
| closing |
+----+----+
| close handshake complete OR timeout
v
+---------+
| closed |
+---------+1.4 Normative Rules
Initial state: A connection returned from
connect()MUST havestate === "connecting"orstate === "open".Connecting state: Implementations MAY expose
"connecting"state during connection establishment. If exposed,connect()resolves before the connection is fully open. If not exposed,connect()MUST NOT resolve until state is"open".Send guard:
send()MUST reject ifstate !== "open".Close transition: When
close()is called:stateMUST transition to"closing"synchronouslysend()MUST reject immediately after this transition
Closed transition: After the close handshake completes (or times out):
stateMUST transition to"closed"closedpromise MUST resolve withCloseInfo
Error transition: On transport error:
stateMUST transition to"closing"then"closed"closedpromise MUST resolve withCloseInfowheregraceful === false
No backwards transitions: State transitions MUST be monotonic:
connecting -> open -> closing -> closed. Implementations MUST NOT transition backwards.Promise resolution:
closedMUST resolve (not reject) for all close scenarios. Error information is conveyed viaCloseInfo.gracefulandCloseInfo.error.
2. Inbound Iterator Semantics
The inbound property exposes received messages as an async iterable.
interface TransportConnection {
readonly inbound: AsyncIterable<Uint8Array>;
}2.1 Normative Rules
Message-oriented: Each iteration MUST yield exactly one complete message as a
Uint8Array. Partial messages MUST NOT be yielded.Completion on graceful close: When the connection closes cleanly:
- The iterator MUST yield any buffered messages first
- The iterator MUST then complete (
done: true,value: undefined)
Error on abnormal close: When the connection closes abnormally:
- The iterator SHOULD yield any buffered messages first (if recoverable)
- The iterator MUST then throw a
TransportError
Single-consumer requirement: At most one active iterator is allowed per connection:
- Calling
[Symbol.asyncIterator]()while another iterator is active MUST throwTransportErrorwithkind: "transport_failure" - An iterator becomes inactive when it completes, throws, or the consumer breaks out of the loop
- Calling
Early break behavior: Breaking out of a
for awaitloop MUST NOT close the connection:- The connection remains open
- A subsequent
for awaitSHOULD resume iteration - Buffered messages received during the break SHOULD be available
Post-close message delivery: Messages buffered before close MUST be delivered before iterator completion. Implementations MUST NOT discard buffered messages on close.
Backpressure: The iterator MAY apply backpressure by delaying
next()resolution if the consumer is slow. Implementation-defined behavior.Iterator protocol compliance: Implementations MUST implement
return()on the iterator object returned by[Symbol.asyncIterator](). Thereturn()method MUST mark the iterator inactive (releasing the single-consumer lock) and resolve any pendingnext()promise with{ value: undefined, done: true }. Without this, an early exit fromfor await...of(viabreak,return, or a thrown exception in the consumer) does not callreturn(), leaving the lock held and causing the next[Symbol.asyncIterator]()call to throw.
2.2 Example
const conn = await transport.connect(endpoint);
// Normal consumption
for await (const message of conn.inbound) {
if (shouldStop(message)) break; // Does NOT close connection
}
// Can resume iteration
for await (const message of conn.inbound) {
process(message);
}
// Iterator completes when connection closes3. Send Concurrency and Ordering
3.1 Normative Rules
Concurrent sends supported: Implementations MUST support multiple concurrent
send()calls without throwing or corrupting data.Internal serialization: Implementations MUST serialize concurrent sends internally. Callers need not coordinate.
Order preservation: Messages MUST be delivered to the peer in
send()call order, not promise resolution order:typescript// msg1 is delivered before msg2, regardless of which promise resolves first const p1 = conn.send(msg1); const p2 = conn.send(msg2); await Promise.all([p1, p2]);Non-blocking semantics:
send()MAY resolve before the message is fully transmitted to the network. Resolution indicates the message has been accepted for sending.Error semantics:
send()MUST reject withTransportErrorif:state !== "open"- The message exceeds
maxMessageSize - The send buffer is full and the implementation chooses to reject (see section 4)
4. Backpressure Semantics
4.1 Normative Rules
Buffer pressure tolerance:
send()MUST NOT reject solely due to transient buffer pressure. Implementations MUST buffer messages when the network is slow.Send buffer limit: Implementations SHOULD enforce a maximum send buffer size:
- Default: 16 MiB (configurable via
limits.maxSendBufferBytes) - Recommended behavior: When exceeded, reject
send()withTransportError(kind: "buffer_overflow") - Alternative: MAY close the connection with
buffer_overflow - Using
buffer_overflow(instead oftransport_failure) enables distinct handling in retry logic
- Default: 16 MiB (configurable via
Inbound buffer limit: Implementations SHOULD enforce a maximum inbound buffer size:
- Default: 16 MiB (configurable via
limits.maxInboundBufferBytes) - When exceeded: MUST close connection with close code 1011 (WebSocket) and
TransportError(kind: "buffer_overflow") - This prevents OOM when consumers are slow
- Default: 16 MiB (configurable via
Buffered amount exposure: Implementations SHOULD expose
pendingSendByteswhen the underlying transport provides it:typescriptinterface TransportConnection { /** Bytes queued for sending. Undefined if transport doesn't expose this. */ readonly pendingSendBytes?: number; }
4.2 Clarification: Buffer Size vs Message Size
| Limit | Scope | Default | Error kind | Behavior |
|---|---|---|---|---|
maxMessageSize | Single message | 1 MiB | message_too_large | Reject immediately (close with 1009) |
maxSendBufferBytes | Total queued outbound | 16 MiB | buffer_overflow | Reject send (or close) |
maxInboundBufferBytes | Total queued inbound | 16 MiB | buffer_overflow | Close connection (1011) |
These limits are independent. A valid message may be rejected if it would exceed the send buffer limit.
5. Connect Options
All transports MUST accept a common set of connection options.
5.1 Interface
interface ConnectOptions {
/** Connection timeout in milliseconds. */
timeoutMs?: number;
/** Signal to abort the connection attempt. */
signal?: AbortSignal;
}
interface Transport {
connect(
endpoint: TransportEndpoint,
options?: ConnectOptions,
): Promise<TransportConnection>;
}5.2 Normative Rules
Timeout: If
timeoutMsis specified and the connection is not established within that time,connect()MUST reject withTransportError(kind: "timeout").Abort signal: If
signalis aborted:- Before connect starts: reject immediately with
TransportError(kind: "aborted") - During connect: abort the attempt and reject with
TransportError(kind: "aborted") - After connected: no effect (use
close()instead)
- Before connect starts: reject immediately with
Extensibility: Transport-specific options (headers, TLS, subprotocols) are defined in transport-specific specifications. See
transport/websocket.mdfor WebSocket extensions.
6. Close Semantics
6.1 Interface
interface CloseOptions {
/**
* Transport-specific close code.
* Transports that don't support close codes MAY ignore this.
* Default is transport-defined (e.g., WebSocket uses 1000).
*/
closeCode?: number;
/** Human-readable reason. */
reason?: string;
}
interface TransportConnection {
close(options?: CloseOptions): Promise<void>;
}6.2 Normative Rules
Idempotency: Multiple
close()calls MUST NOT throw. Subsequent calls after the first MUST resolve immediately (or when the first call completes).Bounded completion:
close()SHOULD complete within a bounded time:- Recommendation: 5 seconds maximum
- Implementations MAY force-close if the peer does not respond
Pending sends rejection: After
close()is called, subsequentsend()calls MUST reject immediately withTransportError.In-flight sends: Sends already accepted (promise returned but not resolved) MAY complete or be cancelled. Behavior is implementation-defined.
Default close code: If
closeCodeis not specified, transports SHOULD use a transport-appropriate default (e.g., WebSocket uses 1000). See transport-specific specs for details.State transition:
close()MUST transitionstateto"closing"synchronously, then to"closed"when complete.
7. Listener Accept Semantics
Server-side transports expose a listener for accepting connections.
type ConnectionHandler = (
connection: TransportConnection,
) => void | Promise<void>;
interface TransportListener {
readonly address: TransportEndpoint;
close(): Promise<void>;
}
interface Transport {
listen?(
endpoint: TransportEndpoint,
handler: ConnectionHandler,
): Promise<TransportListener>;
}7.1 Normative Rules
Async handler support:
ConnectionHandlerMAY return a promise. The transport MUST NOT block on handler completion before accepting the next connection.Handler error isolation: If the handler throws (sync or async):
- The transport MUST log the error
- The transport MUST close the offending connection
- The transport MUST NOT crash or stop accepting connections
- Other connections MUST NOT be affected
Concurrent accepts: Implementations SHOULD accept connections concurrently:
- Handler execution for connection A MUST NOT block acceptance of connection B
- Multiple handlers MAY execute concurrently
Listener close behavior: When
listener.close()is called:- The listener MUST stop accepting new connections
- Existing connections MUST remain valid until individually closed
close()SHOULD resolve promptly (not wait for existing connections)
Connection independence: Each accepted connection is independent:
- One connection's failure (close, error, slow consumer) MUST NOT affect other connections
- Connections share no state at the transport layer
7.2 Example
const listener = await transport.listen(endpoint, async (conn) => {
try {
for await (const msg of conn.inbound) {
await conn.send(echo(msg));
}
} catch (err) {
console.error("Connection error:", err);
}
});
// Later: stop accepting, but existing connections continue
await listener.close();8. Summary of Guarantees
| Property | Guarantee |
|---|---|
| Message framing | One send() = one inbound yield |
| Send ordering | Call order preserved |
| Concurrent sends | Safe; internally serialized |
| Iterator consumers | Single-consumer only |
| Early break | Does not close connection; releases iterator lock |
| Close idempotency | Safe to call multiple times |
| Handler isolation | One connection's error doesn't affect others |
| State observability | state property always reflects current state |
| Close notification | closed promise resolves for all close types |
| Outbound overflow | Reject with buffer_overflow (or close) |
| Inbound overflow | Close with buffer_overflow (prevents OOM) |