How connections work
kuilt gives your app one way to send and receive. Learn it once; swap how devices connect without rewriting the code that uses the link.
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)— nobody has established the answer up front, so the only way to find out is to try. This is what a fabric says by default, and thereasontells you what was never checked (a permission that was not asked for, a radio nobody read). A fabric only claimsAvailableonce it can back the claim up.
A fabric that is genuinely absent on a platform — no class to construct at all — simply is not on the classpath. That is different from a fabric that ships a placeholder you can construct: Apple's Multipeer does exactly that on Android and in the browser, so you really do hold one there, and it answers
Unavailablerather than pretending.
availability() is a convenience shortcut for the availability half of capability(); fabric authors override capability(), never availability().
A host composing fabrics should skip the ones that have ruled themselves out, rather than keep only the ones that have proved themselves — a fabric saying "I don't know" is usually still worth trying, and is often the honest answer:
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: