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 |
|
|
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:
availability() reports whether the fabric is usable on this runtime.
A fabric that is absent on a platform (for example, Multipeer on wasmJs) simply is not on the classpath.
Unavailable(reason)is for a capability that exists in principle but is missing right now (for example, Play Services absent on an AOSP build).
A host composing fabrics can pick the first available loom:
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: