Package:
@kyneta/transportRole: The abstract transport contract — anabstract class Transport<G>, the channel lifecycle, the six-message protocol vocabulary, identity types, the wire pipeline (Pipeline<S, R>), alias transformer, stream frame parser, and reconnection utilities. Depends on:@kyneta/wire(workspace),@kyneta/machine,@kyneta/schemaDepended on by:@kyneta/exchange,@kyneta/websocket-transport,@kyneta/sse-transport,@kyneta/unix-socket-transport,@kyneta/webrtc-transport,@kyneta/bridge-transportCanonical symbols:Transport<G>,TransportFactory,TransportContext,Channel,ConnectedChannel,EstablishedChannel,GeneratedChannel,ChannelDirectory<G>,ChannelMsg,LifecycleMsg,SyncMsg,EstablishMsg,DepartMsg,PresentMsg,InterestMsg,OfferMsg,DismissMsg,AddressedEnvelope,ReturnEnvelope,PeerIdentityDetails,WireFeatures,Pipeline,Encoding,PayloadOf,WireOpts,FrameTrace,FrameStreamParser,computeBackoffDelay,DEFAULT_RECONNECT. Re-exports:Result,Ok,Err,ok,err,WireError. Key invariant(s): The protocol is exactly seven messages. Two lifecycle (establish,depart) for channel presence, five sync (present,interest,offer,dismiss,vacant) for document exchange. ThePipelineis the single wire orchestrator — all concrete transports use it rather than calling@kyneta/wiredirectly.
A small kit of shared types, one abstract base class, and one wire pipeline that every concrete transport extends and uses. It fixes the shape of a channel, the vocabulary of messages, the split between "channel created" / "channel connected" / "channel established", and the ChannelMsg ↔ wire transformation — so that the runtime in @kyneta/exchange can drive any transport without caring whether bytes flow over a WebSocket, an SSE stream, a Unix socket, or an in-process bridge.
Imported by @kyneta/exchange (which owns the sync runtime) and by every concrete transport package. Application code never imports from here directly.
- What are the six messages and why exactly six? → Message vocabulary
- What does a channel's lifecycle look like? → Channel lifecycle
- How do I write a new transport? → Writing a transport
- Why a
TransportFactoryinstead of aTransportinstance? → Factories, not instances - How does the wire pipeline work? → Wire pipeline
- What is
FrameStreamParserfor? → Stream-substrate boundary discovery - How do I test two peers without real network? →
BridgeTransport(moved) — in@kyneta/bridge-transport - What is
computeBackoffDelayfor? → Reconnection utilities
| Term | Means | Not to be confused with |
|---|---|---|
Transport<G> |
The abstract base class a concrete transport extends. G is the transport's per-channel context type (e.g. { url: string }, { socket: WebSocket }). |
A network library, a raw socket abstraction — it's an exchange-level contract |
Channel |
A single bidirectional message path to one remote peer. Moves through Generated → Connected → Established. |
A Go-style channel, a WebSocket, a pub-sub topic |
GeneratedChannel |
A channel that has actions (send, stop) but has not yet been registered with the synchronizer. |
ConnectedChannel |
ConnectedChannel |
A generated channel that now has a channelId and an onReceive handler but has not completed establish. May only send LifecycleMsg. |
EstablishedChannel |
EstablishedChannel |
A connected channel that has completed the establish handshake and knows its remote peerId. May only send SyncMsg. |
ConnectedChannel |
ChannelDirectory<G> |
The per-transport map from channelId to Channel. IDs are minted by the consumer (typically a Synchronizer) and supplied via TransportContext.mintChannelId; the directory only tracks them. |
A service discovery, a routing table — it is local to one transport instance |
ChannelMsg |
LifecycleMsg | SyncMsg — every message that ever crosses the wire. |
A wire frame (wrapped separately by the Pipeline) |
Pipeline<S, R> |
The single wire pipeline: ChannelMsg → alias → encode → fragment → wire pieces (send) and the reverse (receive). |
A UNIX pipe, a CI/CD pipeline |
Encoding |
"binary" | "text" — determines the wire substrate type. |
Character encoding (UTF-8, etc.) |
PayloadOf<E> |
{ binary: Uint8Array; text: string }[E] — the wire piece type for a given encoding. |
A message payload |
FrameStreamParser |
Stateful byte-stream → binary-frame extractor for stream-oriented transports. | Reassembler, which handles fragmentation (orthogonal concern) |
AddressedEnvelope |
{ toChannelIds: number[], message: ChannelMsg } — an outbound message plus routing. |
ReturnEnvelope, which is the inbound counterpart |
TransportFactory |
() => AnyTransport — a zero-arg function returning a fresh transport instance. |
A Transport instance itself |
TransportContext |
The callback bundle the exchange injects via _initialize (identity + four channel callbacks + mintChannelId). |
A Node.js context, a React context |
Thesis: freeze the interface between the sync runtime and every concrete wire, so that substrate-agnostic sync logic lives in one place (@kyneta/exchange) and wire-specific logic lives in another (the transport packages).
A transport is:
- An abstract base class (
Transport<G>inpackages/transport/src/transport.ts) that owns aChannelDirectory, a lifecycle state machine (created → initialized → started → stopped), and_initialize/_start/_stop/_sendinternal methods the exchange calls. - A channel abstraction (
packages/transport/src/channel.ts) that narrows what can be sent based on state: aConnectedChannel'ssendaccepts onlyLifecycleMsg; anEstablishedChannel'ssendaccepts onlySyncMsg. - A fixed message vocabulary (
packages/transport/src/messages.ts) used by every transport and understood by the runtime without per-transport knowledge. - A wire pipeline (
Pipeline<S, R>inpackages/transport/src/pipeline.ts) that handles theChannelMsg ↔ wiretransformation — alias resolution, codec invocation, fragmentation, validation.
The generic parameter G is the transport's own per-channel context (e.g. the browser WebSocket object, a { targetTransportType } for the bridge). @kyneta/transport never inspects it — only the concrete subclass's generate(context: G) does.
- Not a network library. It does not open sockets, buffer bytes, or retry connections. Those are the concrete transport's concerns.
Transport<G>is an adapter boundary — not WebSocket, not HTTP, not anything specific. - Not a socket abstraction. A single
Transportinstance typically has many channels (one per peer). A socket abstraction has one connection. - Not running code without the exchange.
_initializeinjects identity and callbacks. Without those,addChannelthrows. The class is inert until the exchange wires it up.
- Not a Go-style channel. It is not a synchronization primitive; there is no blocking read. Messages arrive via an injected
onReceivecallback. - Not a WebSocket. A channel is a logical message path to one peer. A WebSocket transport may implement a channel over a socket, but SSE implements it over
EventSource + POST, and the bridge transport implements it overqueueMicrotask. - Not persistent. A channel disappears on disconnect. Persistence across reconnects is the exchange's concern (via
peerIdcontinuity), not the channel's.
- Not service discovery. It never learns about remote peers; it only tracks the local transport's own channels.
- Not a routing table. Routing (which peers receive which message) is done in the exchange. The directory is a plain
Map<ChannelId, Channel>— the caller supplies the id. - Not the owner of the channel-id namespace. Per-directory id allocation collides when one synchronizer owns multiple transports (a relay hub, a multi-bridge client). The
Synchronizermints ids viaTransportContext.mintChannelIdso the namespace is unique across all of its transports.
Exactly six messages (source: packages/transport/src/messages.ts). Two groups:
| Message | Group | Sender | Payload | Purpose |
|---|---|---|---|---|
establish |
Lifecycle | Both peers, on connect | { identity, features?, protocolVersion? } |
Symmetric handshake — no request/response, both peers send |
depart |
Lifecycle | Departing peer | {} |
Intentional, explicit departure — the receiver skips any disconnect-grace timer |
present |
Sync | Either peer | { docs: Array<{ docId, replicaType, syncMode, schemaHash, supportedHashes? }> } |
"I hold these documents" |
interest |
Sync | Either peer | { docId, version?, reciprocate? } |
"I want this document; here is my version" |
offer |
Sync | Either peer | { docId, payload: SubstratePayload, version, reciprocate? } |
"Here is state for this document" |
dismiss |
Sync | Leaving peer | { docId } |
"I am leaving the sync graph for this document" — dual of present |
vacant |
Sync | Serving peer | { docId } |
"You expressed interest, but I don't have this document and won't serve it" — terminal negative ack; receiver records the sender vacant without tearing down its own replica |
isLifecycleMsg and isSyncMsg type-narrow a ChannelMsg. A ConnectedChannel may only send LifecycleMsg; an EstablishedChannel may only send SyncMsg. The type system enforces the ordering constraint — no sync message can be sent before establish completes.
- Not a wire frame.
ChannelMsgis the abstract shape. ThePipelinetransforms it through aliasing, codec encoding, and framing before it reaches the wire. A transport sends wire pieces, not messages directly. - Not addressed. A
ChannelMsghas notoorfromfield. Routing information lives inAddressedEnvelope/ReturnEnvelope, or implicitly in the channel the message flowed through.
payload: SubstratePayload is declared in @kyneta/schema. Its internal kind discriminant ("entirety" or "since") is meaningful to the substrate, not to the transport. The exchange hands the payload to substrate.merge(payload) without inspection. This keeps @kyneta/transport free of any Loro / Yjs / JSON-specific logic — the same message vocabulary carries every substrate type.
EstablishMsg carries a required protocolVersion: ProtocolVersion ({ major, minor }), the sync wire-contract revision a peer implements. ProtocolVersion and the PROTOCOL_VERSION = { major: 1, minor: 0 } constant live in src/types.ts — distinct from @kyneta/wire's WIRE_VERSION (frame encoding) and @kyneta/schema's SyncMode (per-doc policy). The field is required in the logical domain (every peer is some revision) but sparse on the wire: the alias transformer emits pv only when non-default, and the inbound transform defaults an absent pv back to (1, 0). So a 2.0 peer's establish is byte-identical to one without the field, yet every parsed EstablishMsg carries a concrete version (proto3-style "complete in memory, sparse on the wire").
Wire evolution is split across three mechanisms by how a peer must react to a difference:
WireFeatures— opt-in, negotiable capabilities; mutual-AND; a difference is silent (graceful no-op). Expresses any additive change.- protocol
minor— non-negotiable backward-compatible refinements; a difference is a warning (diagnostic only). - protocol
major— base abandonment; a difference is an error (no shared contract). This is the one thingWireFeaturesstructurally cannot express, which is whyprotocolVersionexists.
@kyneta/transport only declares the type/constant and (de)serializes the field; the comparison rule and its diagnostics live in @kyneta/exchange's session program (warn/error-only — never gates).
Establish negotiation-core invariant: the part of establish carrying id, y, and protocolVersion is a permanent meta-contract, invariant across all protocol revisions. Future revisions may extend establish or change other messages but may never break a peer's ability to parse another peer's identity + protocolVersion.
A channel moves through three states (source: packages/transport/src/channel.ts):
Generated ──generate()──► Connected ──establish handshake──► Established
| State | How it got here | What it can do |
|---|---|---|
GeneratedChannel |
Concrete transport's generate(context) returned it |
Has send and stop actions; not yet registered |
ConnectedChannel |
ChannelDirectory.create stored it under the caller-supplied channelId and wired onReceive |
Can send LifecycleMsg only |
EstablishedChannel |
establish handshake completed; remote peerId is known |
Can send SyncMsg only |
isEstablished(channel) is the type guard for the post-handshake state.
The Transport<G> base class enforces a four-state lifecycle (source: packages/transport/src/transport.ts → AdapterLifecycleState):
| State | Entry point | Exit |
|---|---|---|
created |
Constructor | _initialize |
initialized |
_initialize(context) injects identity + callbacks |
_start |
started |
_start() calls onStart(); channels may now be added |
_stop |
stopped |
_stop() calls onStop() and clears the directory |
terminal; re-initialization is allowed (for HMR) |
addChannel, removeChannel, and establishChannel throw outside the started state.
generate(context: G) (protected, abstract) produces a GeneratedChannel — the raw send/stop actions for one peer. addChannel(context) wraps it: requests a fresh channelId from TransportContext.mintChannelId, wires onReceive to the injected onChannelReceive callback, fires onChannelAdded, and returns the ConnectedChannel. A concrete transport implements generate and calls addChannel from onStart.
TransportContext.mintChannelId: () => ChannelId is supplied by the consumer of the transport — typically the Synchronizer, which holds a per-instance counter. The transport never invents ids itself, and ChannelDirectory.create requires the caller to supply one.
Why this matters: per-transport id counters collide when a single synchronizer owns multiple transports (a relay hub, a multi-bridge client). The synchronizer's SessionModel.channels is keyed by raw ChannelId; two transports both issuing channelId=1 would overwrite each other's entries and corrupt peer-discovery state. Pushing id issuance up to the synchronizer keeps the namespace honest — uniqueness holds wherever uniqueness is required, regardless of how many transports share the synchronizer.
Tests that need a TransportContext should use createTestTransportContext from @kyneta/transport/testing, which builds one with a fresh closure-scoped counter per call.
A concrete transport must:
- Subclass
Transport<G>and supply its per-channel context type. - Implement
generate(context: G): GeneratedChannel— create the raw send/stop closure for one peer. - Implement
onStart(): Promise<void>— open listeners, create initial channels viaaddChannel, callestablishChannel(channelId)once ready. - Implement
onStop(): Promise<void>— close listeners, callremoveChannelfor each open channel. - Export a
TransportFactory(zero-arg function returning a fresh instance) as its public entry point.
Everything else — the lifecycle state machine, channel directory, the six-message vocabulary, the send/receive typing narrow, the _send(envelope) fan-out — is inherited from Transport<G>.
Every transport package exports createXxxTransport(params): TransportFactory rather than returning an instance directly. A factory is a zero-arg function that constructs a fresh Transport. The exchange calls it on construction and again on reset (e.g. React StrictMode double-mount). Passing an instance would share mutable state across lifecycles; passing a factory guarantees a clean slate.
- Channel-ID minting via the consumer-supplied
TransportContext.mintChannelId. The transport never invents ids itself —addChannelcalls the mint function injected at_initializetime. See Channel-ID issuance below. - Lifecycle-state guards on
addChannel/removeChannel/establishChannel. - Send fan-out:
_send(envelope)iteratesenvelope.toChannelIdsand calls each channel'ssend. - Re-initialization for HMR: a second
_initializecall resets the directory and re-entersinitialized. - Type-safe
send: the compiler forbids sendingSyncMsgon aConnectedChannelandLifecycleMsgon anEstablishedChannel.
A transport's connectivity can change over its lifetime entirely behind addChannel / removeChannel, with no involvement from the consumer. The reference is @kyneta/unix-socket-transport's leaderless peer: one Transport that swaps its socket mode (bound listener ↔ connecting client) in place, churning its own channels under a stable transportId. The synchronizer cannot distinguish this from any other channel add/remove. A transport should never reach back into the Exchange to add/remove sibling transports — that inverts the dependency direction and (because Exchange carries a #private field) leaks the class into the transport's public .d.ts, creating a dual-package nominal mismatch.
Consolidation candidate (not yet done):
websocket,sse, andunix-socketshare an identical five-file shape (client-program/client-transport/connection/server-transport/types). The unix-socket package factored its connect/accept mechanics into Transport-free drivers (ConnectorDriver/ListenerDriver) over aChannelSink+attachSocketseam; that seam is shaped to be promotable here so all three could share one connection-driver kit. Deferred because the websocket/sse variants drag in browser/bun/express platform glue.
In-process testing is provided by @kyneta/bridge-transport (packages/exchange/transports/bridge). Consumers import directly from @kyneta/bridge-transport. See that package's docs for usage.
The bridge transport lives outside @kyneta/transport for historical reasons — it was originally extracted to break a circular peer-dependency when @kyneta/wire had a peer-dep on @kyneta/transport. That cycle is now resolved (wire is a leaf, transport depends on wire), but the bridge remains in its own package because it has grown its own test surface and is a natural boundary.
| Symbol | Source | Role |
|---|---|---|
Pipeline<S, R> |
src/pipeline.ts |
The single wire pipeline. S = send encoding, R = receive encoding. |
Encoding |
src/pipeline-core.ts |
"binary" | "text". |
PayloadOf<E> |
src/pipeline-core.ts |
{ binary: Uint8Array; text: string }[E]. |
WireOpts |
src/pipeline.ts |
Optional pipeline configuration (threshold, reassembly limits, onError, onFrame). |
FrameStreamParser |
src/frame-stream-parser.ts |
Stateful byte-stream → binary-frame extractor. |
Re-exports from @kyneta/wire:
| Symbol | Original source | Role |
|---|---|---|
Result<T, E>, Ok<T>, Err<E> |
wire/src/result.ts |
Typed success/failure union. |
ok, err |
wire/src/result.ts |
Constructors. |
WireError |
wire/src/wire-error.ts |
Discriminated union of all pipeline errors. |
Source: packages/transport/src/pipeline.ts, packages/transport/src/pipeline-core.ts.
The alias transformer (applyOutboundAliasing / applyInboundAliasing in src/alias-table.ts) is the single ChannelMsg ⇄ WireMessage conversion layer. It compresses repeated doc IDs and schema hashes into integer aliases that are learned by both peers during the present phase. The AliasState record tracks both outbound (doc → alias) and inbound (alias → doc) mappings.
Alias state is owned per-Pipeline (and therefore per-channel). When a channel closes, its aliases are discarded. This is why there is no parallel-keyed map to keep in sync — the alias lifecycle is the channel lifecycle.
The pipeline follows a functional core / imperative shell design:
- Functional core:
sendStepandreceiveStep(inpipeline-core.ts) are pure step functions. They take immutable state and return new state + a list ofResult<T, WireError>outputs. - Imperative shell:
Pipeline(inpipeline.ts) holds thePipelineState(alias table, reassembler, and thenextSeqframe-sequence counter) and delegates all logic to the step functions.nextSeqis pure data threaded throughPipelineStateand advanced functionally bysendStep(vianextFrameSeq) — not an injected closure — sosendStepstays a pure transition. The shell routes errors throughonErrorand per-frame traces throughonFrame(both opt-in, for observability).
ChannelMsg ──► applyOutboundAliasing ──► WireMessage
│
encodeWire ──► payload ──► fragment if > threshold
│
encodeFrame ──► wire pieces ──► transport.send()
wire piece ──► reassembler ──► decodeWire ──► applyInboundAliasing ──► ChannelMsg
pipeline.send(msg) returns Result<PayloadOf<S>, WireError>[] — zero or more wire pieces (one for unfragmented, many for fragmented). pipeline.receive(piece) returns Result<ChannelMsg, WireError>[] — zero (fragment pending) or one (complete message).
For asymmetric pipelines (SSE: send text, receive binary), the alias table is shared across both directions. Both sendStep and receiveStep read and write the same AliasState. This works because alias assignments are deterministic and both peers learn aliases through the same present messages regardless of the encoding used in each direction.
Source: packages/transport/src/frame-stream-parser.ts, packages/transport/src/frame-stream-parser-core.ts.
Unix sockets and any stream-oriented transport deliver bytes as a coalesced stream. Writes may merge; reads deliver arbitrary chunks. FrameStreamParser is the stateful class that extracts complete binary frames from the stream:
const parser = new FrameStreamParser()
const frames: Result<Uint8Array, WireError>[] = parser.feed(chunk)
// Each frame in `frames` is ok(complete binary wire frame)
parser.reset()
Internally, feedBytesStep(state, chunk) is the pure step function (FC/IS pattern). StreamParserState is a two-phase discriminated union: { phase: "header" } while the 10-byte header accumulates, then { phase: "payload" } while the declared payload accumulates. When a payload completes, the parser emits the full frame bytes (header + payload) and resets to "header". The parser delimits frames by the type byte (offset 1) and payload length (offset 2); the header seq field (offset 6) rides along untouched, so adding it required only the HEADER_SIZE constant bump.
- Not a decoder.
FrameStreamParseremits raw frame bytes; the caller pipes them through the pipeline'sreceivemethod. - Not a fragment collector. Stream framing and payload fragmentation are orthogonal — a Unix-socket transport uses stream framing because it has no gateway cap, and does not use fragmentation at all.
- Not a parser for the text pipeline. SSE has its own event-boundary framing; the text pipeline uses
Pipeline.receivedirectly on each event'sdata:field.
packages/transport/src/reconnect.ts exports pure backoff math, a decision function, and no state.
| Export | Type | Purpose |
|---|---|---|
DEFAULT_RECONNECT |
ReconnectOptions |
{ enabled: true, maxAttempts: 10, baseDelay: 1000, maxDelay: 30000, fullJitter: false } |
JITTER_FRACTION |
number |
0.2 — fraction of raw delay added as jitter in the additive strategy (one-sided, never subtractive). |
computeBackoffDelay(attempt, baseDelay, maxDelay, random, fullJitter?) |
(n, n, n, n, b?) => number |
Pure. Additive (default): min(rawDelay × (1 + random × JITTER_FRACTION), maxDelay). Full jitter (fullJitter): random × min(rawDelay, maxDelay) — spread across [0, cap). rawDelay = baseDelay × 2^(attempt−1), random ∈ [0, 1). |
shouldReconnect(opts, currentAttempt, randomFn) |
(opts, n, () => n) => ReconnectDecision |
Pure decision function. Returns { reconnect: true, attempt, delayMs } or { reconnect: false, cause } where cause is "disabled" or "max-attempts-exceeded" (the latter carries attempts). Threads opts.fullJitter into computeBackoffDelay. Replaces the tryReconnect closures that used to be copy-pasted across each client transport. |
ReconnectDecision is a discriminated union with three variants — see the type definition for the full shape. The cause discriminant matters because callers build different DisconnectReason values in the two non-reconnect cases: when disabled, the caller passes through its original reason; when max-attempts is hit, the caller constructs { type: "max-retries-exceeded", attempts }.
ReconnectOptions.fullJitter is opt-in (default false) so SSE and unix-socket reconnect timing is unchanged; only @kyneta/websocket-transport's createWebsocketClient defaults it on, because a server-initiated mass disconnect (rolling deploy) resets every client's attempt counter and needs a wide spread to avoid a thundering herd.
Scheduling (setTimeout, retry on failure) happens inside each concrete transport's client program. @kyneta/transport owns the pure decision; the transports own per-effect-type tuple construction.
| Type | File | Role |
|---|---|---|
Transport<G> |
src/transport.ts |
Abstract base class — concrete transports extend. |
AnyTransport |
src/transport.ts |
Transport<any> — for heterogeneous collections. |
TransportFactory |
src/transport.ts |
() => AnyTransport. |
TransportContext |
src/transport.ts |
{ identity, onChannelReceive, onChannelAdded, onChannelRemoved, onChannelEstablish, mintChannelId, onFrame? }. The optional onFrame?(ev: FrameTrace) is the DevTools wire-observation hook the consumer (Synchronizer) injects; transports thread it into their Pipeline (opts.onFrame) — read via the base Transport's protected get frameObserver. Additive; absent ⇒ no wire observation. |
Channel |
src/channel.ts |
ConnectedChannel | EstablishedChannel. |
GeneratedChannel |
src/channel.ts |
Pre-registration; has send + stop + transportType. |
ConnectedChannel |
src/channel.ts |
Post-registration, pre-handshake; send: (LifecycleMsg) => void. |
EstablishedChannel |
src/channel.ts |
Post-handshake; send: (SyncMsg) => void; peerId is known. |
ChannelDirectory<G> |
src/channel-directory.ts |
Per-transport channel store. IDs are supplied by the caller (Transport.addChannel via TransportContext.mintChannelId); the directory only tracks them. |
ChannelMsg / LifecycleMsg / SyncMsg |
src/messages.ts |
Message unions. |
EstablishMsg, DepartMsg, PresentMsg, InterestMsg, OfferMsg, DismissMsg |
src/messages.ts |
Individual message types. |
AddressedEnvelope / ReturnEnvelope |
src/messages.ts |
Outbound / inbound routing wrappers. |
PeerIdentityDetails |
src/types.ts |
{ peerId, name?, type }. |
PeerId / DocId / ChannelId / TransportType |
src/types.ts |
String / string / number / string identity aliases. |
Pipeline<S, R> |
src/pipeline.ts |
The single wire pipeline. |
WireOpts / FrameTrace |
src/pipeline.ts |
Pipeline configuration; FrameTrace is the per-frame onFrame event. |
Encoding / PayloadOf<E> |
src/pipeline-core.ts |
Encoding discriminant and payload type mapping. |
FrameStreamParser |
src/frame-stream-parser.ts |
Byte-stream → binary-frame extractor. |
AliasState / applyOutboundAliasing / applyInboundAliasing / emptyAliasState |
src/alias-table.ts |
Alias transformer (internal to pipeline). |
ReconnectOptions / DEFAULT_RECONNECT / JITTER_FRACTION / computeBackoffDelay / shouldReconnect / ReconnectDecision |
src/reconnect.ts |
Pure backoff math + reconnect decision. |
StateTransition / TransitionListener |
re-exported from @kyneta/machine |
Surfaced for consumers observing client-program state. |
| File | Lines | Role |
|---|---|---|
src/index.ts |
~110 | Public exports + re-exports from @kyneta/wire. |
src/types.ts |
~32 | Identity type aliases and PeerIdentityDetails. |
src/messages.ts |
~165 | The six-message vocabulary, unions, type guards, envelopes. |
src/channel.ts |
~115 | Channel lifecycle types and isEstablished guard. |
src/channel-directory.ts |
~75 | ChannelDirectory<G> — channel store; caller supplies the id. |
src/transport.ts |
~266 | Transport<G> abstract class, lifecycle, internal _initialize / _start / _stop / _send. |
src/pipeline.ts |
~115 | Pipeline<S, R> — imperative shell wrapping step functions. |
src/pipeline-core.ts |
~130 | sendStep / receiveStep — pure step functions (functional core). |
src/alias-table.ts |
~510 | AliasState, applyOutboundAliasing, applyInboundAliasing — ChannelMsg ↔ WireMessage. |
src/frame-stream-parser.ts |
~30 | FrameStreamParser — imperative shell for stream parsing. |
src/frame-stream-parser-core.ts |
~155 | feedBytesStep — pure stream frame extraction. |
src/reconnect.ts |
~122 | computeBackoffDelay, shouldReconnect, DEFAULT_RECONNECT, JITTER_FRACTION, ReconnectDecision. |
Tests run in-process using a minimal in-test TestAdapter. The bridge transport now lives in @kyneta/bridge-transport and has its own test suite. Real transport packages maintain their own test suites with their own integration harnesses; this package's tests only exercise the abstract contract.
Run with cd packages/transport && pnpm exec vitest run.