PeerIdentityRegistry
The one place a fabric decides whether a peer-supplied identity may join its roster, and who is allowed to take it away again.
A fabric learns a remote peer's PeerId from bytes the remote itself supplied — a display name, a handshake payload, a native callback's string. Those bytes are not trustworthy and not necessarily well-formed, and every fabric that decided for itself what to do about that decided slightly differently. This type is that decision, once: bind answers "may this identity join, and who holds it", unbind answers "may this device take it away", and peers is the roster that follows from the two.
Membership is keyed by the underlying device identity T rather than by the bare PeerId, which is what makes the answers possible:
bind REFUSES to overwrite an id already held by a different device (BindResult.COLLISION) rather than merging them;
unbind removes an id only when the same device that holds it drops, so a colliding newcomer's drop leaves the incumbent connected;
a blank id (BindResult.REFUSED_BLANK) and this peer's own selfId (BindResult.REFUSED_SELF) are refused outright — neither names a remote peer.
The incidents this closes
Historically each fabric kept peers in a bare Set<PeerId> fed from those bytes:
#1494 / the #1466 class — two devices colliding on one id collapsed to a single set entry, and a disconnect for either removed that entry, evicting BOTH.
MCSessionLinkwas the first path fixed.#1466 self-dial — a peer handed its own advertisement registers itself as a remote, and the eventual drop of that self-link evicts the peer from its own roster.
#1821 — the same shape, still open in two sibling paths, plus the blank-id variant: an unaddressable
PeerId("")that a set-equality teardown test (remaining == setOf(selfId)) can never clear, so a seam holding one stays Woven after its last real peer has gone.
What it is and is not
This is defence-in-depth against a malformed or hostile announcement, not a substitute for a fabric minting distinct identities in the first place (a per-device nonce, freshPeerId). Its guarantee is that the failure mode is "a peer is refused and logged", never "the wrong peer is evicted" or "the seam never tears".
It also cannot un-merge what the layer beneath it already merged. A fabric whose transport hands up only the id string — with no device handle to key by — must pass the id as its own T, and BindResult.COLLISION is then structurally unreachable for it: two devices really have become one identity before this type ever sees them. Such a caller still gets the blank/self refusals and the identity-scoped unbind; closing the collision needs a change one layer down.
Threading
Callers fire from framework queues with no cross-peer serialization guarantee (an MCSession delegate, a JNA callback thread), so bound is guarded by an explicit reentrantLock — matching the Quilter/SeamRoom exemplars. Correctness is a local property of this type, never an assumption about caller threading. There are no suspend calls, so the whole body of each operation runs under the lock.