MuxClientLoom
A client Loom that weaves one base fabric and serves many logical sessions as named channels over a single NamedMux — the fix for "every join opens a new socket."
In plain terms: a client that joins a lobby and a table normally opens two connections. MuxClientLoom opens one connection and splits it into independent named channels, so join("lobby") and join("table-7") share a single underlying link.
How it works
The first weave lazily weaves base once (using baseRendezvous) and wraps the resulting Seam in a NamedMux. Every weave thereafter returns namedMux.channel(nameOf(rendezvous)) — so host and join for the same logical tag (mapped through nameOf) land on the same channel name. Concurrent first-weaves are serialised by an internal Mutex; the base weaves exactly once.
Each returned channel is a stable, resumable handle: weaving the same name twice returns the same Seam, and closing it (Seam.close) tears down only that channel — the base stays live for the others (per-channel close).
Resume — heal every channel over one re-established base
If the base fabric tears (the socket drops), the next weave re-weaves the base once and re-keys every previously-woven channel name onto the new base. Each prior handle heals transparently — callers keep the same Seam instances and the same stable Seam.selfId, so a server can re-associate each per-channel membership by PeerId. One re-established base heals all channels; no fan-out of N reconnections.
A handle's observable surfaces heal asymmetrically, by design: Seam.selfId is frozen across resumes, Seam.state and Seam.peers follow the handle onto the fresh generation (a flow captured before the resume keeps up), but Seam.incoming is per-generation — it completes at its generation's SeamState.Torn and must be re-collected after a resume. Making incoming follow would break its "completes on Torn" termination contract, which downstream onCompletion cleanup relies on.
Parameters
the underlying transport Loom (any fabric — WebSocket, TCP, in-memory).
how to weave the single base fabric (host a new session or join one).
a CoroutineScope owning the per-generation NamedMux collectors. Required — no real-dispatcher default, so tests drive the mux under virtual time.
maps a Rendezvous to the channel name a weave resolves to. host and join for one logical session must map to the same name.
Samples
runTest(UnconfinedTestDispatcher()) {
val client = MuxClientLoom(
base = InMemoryLoom(),
baseRendezvous = Rendezvous.New(Pattern("base")),
scope = backgroundScope,
nameOf = { rendezvous ->
when (rendezvous) {
is Rendezvous.New -> rendezvous.pattern.sessionName
is Rendezvous.Existing -> rendezvous.tag.sessionName
}
},
)
val lobby: Seam = client.join(InMemoryTag("lobby"))
val table: Seam = client.join(InMemoryTag("table-7"))
// Both channels ride one base socket; the same name is idempotent.
check(lobby !== table)
check(client.join(InMemoryTag("lobby")) === lobby)
}Constructors
Functions
Whether this fabric can be attempted now — the availability half of capability. Derived; do not override.
The base fabric's verdict, verbatim. Multiplexing changes how many logical sessions share a link — not which medium carries it, nor whether that medium is usable on this runtime — so the base's capability is this loom's capability, in both halves.
Closes the current base fabric, tearing down the single shared socket. The next weave re-weaves the base and heals every channel handle onto it.
Establish a Seam according to rendezvous — either host a new session or join an existing one.