SoloDeadlineDetector
Notices when a session nobody else ever joined has been waiting long enough to give up on.
Think of a two-player table someone opens and then walks away from, or a lobby created by a misfired tap. The connection is perfectly healthy — the person is simply alone in it. After the deadline you probably want to tidy that session away rather than leave it open forever.
Give it the smallest number of members the session needs, how long to wait, a clock and a scope, then feed it the membership roster whenever it changes. It answers once, on events:
SoloDeadlineEvent.Paired — somebody arrived in time. The detector disarms for good.
SoloDeadlineEvent.NeverPaired — the deadline passed and the session was still short.
It only tells you; it never closes anything. Whether an abandoned session is closed, logged or archived is your policy, not the library's.
This detects never paired, not currently solo. The first roster at or above minimumMembers disarms it permanently, so a session that fills up and later empties out emits nothing more. Losing a peer that was present is HeartbeatPartitionDetector's job — the two are complementary, not alternatives.
In kuilt terms it is a projection of a state the contract already documents: us.tractat.kuilt.core.SeamState's KDoc blesses Woven with peers == {selfId} as a fully legitimate, well-defined state — the fabric is live and this peer is simply alone. This type adds only the deadline on top of that state. It is deliberately not a PartitionEvent variant: every PartitionEvent carries a non-null PartitionEvent.peerId, and "nobody ever came" has no peer to name.
The detector arms on construction. It is safe to call observeMembership from any thread.
Parameters
the smallest membership that counts as paired, including this peer. 2 means "never paired with anyone". Must be at least 2.
how long to wait for that membership. Must be positive.
supplies the SoloDeadlineEvent.at timestamps; injected so tests can fix it — never Clock.System reached for directly.
the scope the deadline timer runs on. Required: a default real dispatcher would silently decouple the timer from a test's virtual clock.
Samples
val detector = SoloDeadlineDetector(
minimumMembers = 2, // this peer plus one — "never paired"
deadline = 5.minutes,
clock = clock,
scope = scope,
)
// Feed it the roster on every change.
scope.launch { link.peers.collect { detector.observeMembership(it) } }
when (detector.events.first()) {
is SoloDeadlineEvent.NeverPaired -> closeRoom() // nobody came; reaping policy is yours
is SoloDeadlineEvent.Paired -> Unit // someone joined in time; the detector is done
}