NwLoom

class NwLoom(api: NwApi, serviceType: String, val selfId: PeerId = freshPeerId(), policy: DeliveryPolicy = DeliveryPolicy.Reliable, random: Random = Random.Default, weaveTimeout: Duration = DEFAULT_WEAVE_TIMEOUT, wovenPathGrace: Duration = DEFAULT_WOVEN_PATH_GRACE) : Loom

Full-mesh Loom over an NwApi — Apple Network.framework's peer-to-peer fabric.

Symmetric full mesh (advertise + browse + dial)

There is no client/server split. Both roles do exactly the same thing on weave: advertise (NwApi.startListening) so peers can find this one, browse (NwApi.startBrowsing) to find other peers, and auto-dial (NwApi.connect) every discovered endpoint. Each unordered pair therefore double-dials (both ends dial); the redundant connection is deduplicated by NwSeam (lower-id dialer wins). Discovery is by Bonjour serviceType, so the advertised service name does not gate who connects — but it IS the dial target, which mDNS re-resolves at connect time, so it must name exactly one device: both rendezvous arms advertise this peer's own selfId (ADR-005 / #2416). Under Rendezvous.New that used to be the session name, shared by every peer, which made a dial armed for one peer able to land on another — or on self.

Redial with unbounded backoff (#1513)

Dialling is not one-shot. A RedialCoordinator keeps an outstanding dial for every discovered-but-not-currently-connected endpoint, backing off exponentially (INITIAL_REDIAL_BACKOFF → double → ceiling MAX_REDIAL_BACKOFF) with jitter drawn from the loom's injected random, unbounded (no attempt cap) for as long as the seam is open. It reads NwSeam.settledEndpoints to know which endpoints are already reached (a connected peer's endpoint, or a self-resolved endpoint) and dials only the complement; the moment an endpoint settles the loop parks, and the moment its peer drops (the seam re-forms to us.tractat.kuilt.core.SeamState.Weaving, #1513) it redials. A fresh endpointFound sighting resets that endpoint's backoff. The whole mechanism lives on the seam scope, so Seam.close cancels it.

Deferring the dial until identity resolves (#1709)

A browse add can arrive BEFORE the endpoint's TXT record does, so NwEndpoint.id is briefly the NwEndpoint.serviceName backstop rather than the peer's PeerId. When an endpoint arrives with identityResolved = false under the serviceName this loom itself advertises, the dial is deferred: the endpoint might be self, and there is no way to tell yet. It is neither dialled nor rostered into visiblePeers until identity settles one way or the other. The deferral is bounded by IDENTITY_GRACE so a peer that publishes no TXT at all is still reached — see that constant.

Since ADR-005 (#2416) the advertised name IS selfId.value, so the backstop id equals the resolved TXT id and the pre-dial self-filter already fires on an unresolved self sighting. That makes this deferral near-redundant on both arms; it is retained deliberately (its retirement is its own PR, so a revert-check can isolate either half) and still covers a sighting whose id and serviceName diverge — the JVM bridge, where loom and dylib default independent UUIDs (#2419).

UUID self-identity (#1405)

selfId defaults to a fresh random UUID via freshPeerId, so two devices never mint the same identity and collide the instant they meet — unlike a per-loom monotonic counter.

Await-first-peer

weave does not return until the seam has resolved its first remote peer (mirrors the role-split conformance contract: host()/join() both return connected seams). If no peer resolves within weaveTimeout the seam is closed CloseReason.Unreachable and weave throws NwUnreachableException — a plain exception, NOT a CancellationException, so a caller wrapping weave in runCatchingCancellable sees a fabric failure rather than its own cancellation.

Scope

Background collectors (the seam's three loops + this loom's discovery/dial loop) run on CoroutineScope(currentCoroutineContext() + SupervisorJob()), inheriting the caller's dispatcher (so tests keep virtual time) with an independent SupervisorJob cancelled when the seam is closed.

Parameters

api

the Network.framework binding (real RealNwApi, or FakeNwApi under test).

serviceType

the Bonjour service type both advertised and browsed; peers with the same type meet.

selfId

this peer's stable identity; defaults to a fresh random UUID (freshPeerId, #1405).

policy

inbound delivery policy for each woven Seam (default DeliveryPolicy.Reliable).

random

source of the seam's per-connection dedup nonces; production defaults to Random.Default, tests inject a seeded Random for a deterministic dedup tiebreak.

weaveTimeout

how long weave waits for the first peer before throwing NwUnreachableException (default DEFAULT_WEAVE_TIMEOUT). Injectable (#1447 item 1) so a "wait for a friend" lobby can pass a generous value and hold the session open far longer than the default; deliberately NOT infinite by default. Tests inject a small value.

wovenPathGrace

how long a path-lost (ready → waiting) connection is given to recover before the woven seam tears it as CloseReason.Unreachable (#1478); default DEFAULT_WOVEN_PATH_GRACE (10s), injectable for tests.

Constructors

Link copied to clipboard
constructor(api: NwApi, serviceType: String, selfId: PeerId = freshPeerId(), policy: DeliveryPolicy = DeliveryPolicy.Reliable, random: Random = Random.Default, weaveTimeout: Duration = DEFAULT_WEAVE_TIMEOUT, wovenPathGrace: Duration = DEFAULT_WOVEN_PATH_GRACE)

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard
Link copied to clipboard
val visiblePeers: StateFlow<Set<NwEndpoint>>

The endpoints this loom currently sees while browsing — a live discovery roster for a lobby view (Phase 5), distinct from a woven Seam.peers set (resolved, connected identities). An endpoint is added when the browser first reports it (NwApi.endpointFound) and pruned when the browser reports it removed (NwApi.endpointLost, #1447 item 2), so a departed peer does not linger as a ghost. Removal is best-effort (a binding with no removal signal never prunes), so this is a hint for a lobby UI, never authoritative membership — that is Seam.peers' job.

Functions

Link copied to clipboard
Link copied to clipboard
open override fun capability(): TransportCapability
Link copied to clipboard
open suspend fun host(pattern: Pattern): Seam
Link copied to clipboard
open suspend fun join(tag: Tag): Seam
Link copied to clipboard
open suspend override fun weave(rendezvous: Rendezvous): Seam