Peer recovery burned its whole retry budget while signaling was down, then dropped the tracker with no re-arm, so a peer stayed dead until an unrelated roster event healed it. Recovery now waits for a usable transport before spending an attempt. Initiator election also compared a home actor id against foreign roster ids, which is not antisymmetric across signal servers - both sides offered, or neither did. Election moves into `peer-role.rules` and compares ids only within one signal server's identity space.
38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
/**
|
|
* Deterministic WebRTC role election.
|
|
*
|
|
* Both ids must come from the **same signal server's identity space**. One human has a
|
|
* separate account - and therefore a separate actor id - on every signal server they are
|
|
* provisioned on, so comparing a home id against a foreign actor id is not antisymmetric:
|
|
* each side compares its own home id against the other's foreign id, and both can elect
|
|
* themselves initiator (glare) or neither can (no offer ever sent).
|
|
*/
|
|
export function shouldInitiatePeerConnection(
|
|
localActorId: string | null | undefined,
|
|
remoteActorId: string | null | undefined
|
|
): boolean {
|
|
const local = localActorId?.trim();
|
|
const remote = remoteActorId?.trim();
|
|
|
|
if (!local || !remote)
|
|
return false;
|
|
|
|
if (local === remote)
|
|
return false;
|
|
|
|
return local < remote;
|
|
}
|
|
|
|
/**
|
|
* Perfect-negotiation politeness, derived from the same election so the two can never
|
|
* disagree: the elected initiator is the impolite side and keeps its offer, the other
|
|
* side rolls back. Falls back to polite when the local actor id is unknown, so an
|
|
* unidentified client yields instead of deadlocking the negotiation.
|
|
*/
|
|
export function isPoliteOnOfferCollision(
|
|
localActorId: string | null | undefined,
|
|
remoteActorId: string | null | undefined
|
|
): boolean {
|
|
return !shouldInitiatePeerConnection(localActorId, remoteActorId);
|
|
}
|