How connections work
kuilt's connection contract is a small, stable API that sits in front of every fabric. Learn it once; your app code stays the same when you swap WebSocket for Bluetooth or LAN.
At a high level, it is three things:
open or join a session (
Loom),send and receive frames (
Seam+Swatch),react when peers join or leave (
peers).
Different transports fail in different ways. The contract keeps those differences out of your app code.
Type | Role |
|---|---|
| Factory — |
| One peer's symmetric view of a live session |
| Immutable binary frame — |
| Sum type: |
| Config for opening a session: display name, max peers |
| Discovery handle for joining a session ( |
| Stable identifier for a peer within a session |
|
|
| A fabric's self-report: the |
| What a transport does — e.g. |
Loom
Loom is where sessions come from: host a new one or join an existing one. Formally, its single abstract method is:
Two convenience wrappers delegate to it:
Before you connect, a fabric can tell you two things: what it is and whether it can run right now. Both come from one method, capability():
Roles describe what the transport does — carrying data, discovering peers, Wi-Fi on a shared network vs. peer-to-peer Wi-Fi, Bluetooth, WebRTC, or relaying through a server. A single fabric can hold several roles at once.
Availability says whether you can attempt it now. It is three-valued:
Available— good to go.Unavailable(reason)— a capability that exists in principle but is missing right now (for example, Play Services absent on an AOSP build).Unknown(reason)— the platform gives no ground truth up front, so the only way to find out is to try (some peer-to-peer radios report this).
A fabric that is absent on a platform (for example, Multipeer on wasmJs) simply is not on the classpath.
availability() is a convenience shortcut for the availability half of capability(); fabric authors override capability(), never availability().
A host composing fabrics can pick the first available loom:
At runtime the same report is available live per session as Seam.capability, so a host can react as a transport's real-world reachability changes.
A Loom can also combine other Looms rather than pick one: CompositeLoom runs several transports as one bonded session for the same peer. See Multipath.
Seam
Seam is the API your app actually uses at runtime. It is one peer's symmetric view of a multi-peer session. There is no client Seam and no server Seam — every peer holds the same interface.
The rules
These are the load-bearing invariants. Violating them breaks consumers in ways the type system won't catch:
incoming is single-collection
One Flow<Swatch> carries all peers' frames, in send order, delivered to one collector. Collect it once per Seam. A second concurrent collector races and is unsupported.
If several parts of your application need the frames, wrap with shareIn:
Swatch is binary-only
No text-frame variant. The wire layer never interprets the bytes — that is the consumer's job.
sender and sequence are stamped on receipt
Sending peers leave sender null and sequence zero. The receiving Seam stamps them:
No client/server split
A 2-peer WebSocket connection is the degenerate peers.size == 2 case of the symmetric model. This is why the WebSocket fabric and an N-peer Multipeer mesh share one contract.
close() is idempotent
Calling close() twice must not throw:
When a peer closes, it is removed from every other peer's peers set atomically and sending to it becomes an error.
peers tracks membership
peers: StateFlow<Set<PeerId>> always includes selfId. When peers join and leave, the flow emits the updated set on every Seam in the session:
Sequence numbers
The receiving Seam assigns a monotonically increasing sequence number per receiver. Sequence numbers are receiver-local — A and B have independent counters: