openrtc 1.0.1

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
//! Shared iroh connection install/replacement policy.
//!
//! Browser and native nodes both see the same symmetric-dial and post-restart
//! races. Keeping this decision table target-neutral prevents wasm/native
//! behavior from drifting as each wrapper evolves.

use crate::lifecycle_reason::{
    REASON_DUPLICATE_ACCEPT_SUPERSEDED, REASON_DUPLICATE_DIAL_SUPERSEDED,
};

/// Briefly let the canonical initiator establish the shared physical leg before
/// the noncanonical endpoint falls back to its own dial. This prevents the two
/// independent QUIC connections from closing each other during symmetric
/// reconnect while still allowing a one-sided caller to make progress.
pub(crate) const NONCANONICAL_OUTBOUND_DIAL_GRACE_MS: u64 = 250;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IrohConnectionDirection {
    Inbound,
    Outbound,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ExistingConnectionState {
    pub same_stable_id: bool,
    pub alive: bool,
    pub direction: IrohConnectionDirection,
    pub age_ms: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IrohConnectionInstallDecision {
    /// Adopt the fresh connection into the endpoint map.
    Install,
    /// Adopt the fresh connection and retire the non-canonical existing leg.
    /// Endpoint-id ordering makes both peers select opposite halves of the
    /// same physical dial instead of each keeping its own outbound connection.
    ReplaceExisting { close_existing_reason: &'static str },
    /// Keep the existing endpoint map entry and close the fresh connection.
    KeepExisting { close_fresh_reason: &'static str },
}

pub(crate) fn should_start_outbound_dial(
    existing: Option<(bool, IrohConnectionDirection)>,
    local_prefers_outbound: bool,
) -> bool {
    let Some((alive, direction)) = existing else {
        return true;
    };
    !alive || (local_prefers_outbound && direction != IrohConnectionDirection::Outbound)
}

pub(crate) fn should_wait_for_canonical_inbound(
    existing: Option<(bool, IrohConnectionDirection)>,
    local_prefers_outbound: bool,
) -> bool {
    if local_prefers_outbound {
        return false;
    }
    match existing {
        None => true,
        Some((alive, _)) => !alive,
    }
}

pub(crate) fn decide_inbound_install(
    existing: Option<ExistingConnectionState>,
    local_prefers_outbound: bool,
) -> IrohConnectionInstallDecision {
    decide_install(
        existing,
        IrohConnectionDirection::Inbound,
        local_prefers_outbound,
    )
}

pub(crate) fn decide_outbound_install(
    existing: Option<ExistingConnectionState>,
    local_prefers_outbound: bool,
) -> IrohConnectionInstallDecision {
    decide_install(
        existing,
        IrohConnectionDirection::Outbound,
        local_prefers_outbound,
    )
}

fn replace_existing_decision(
    fresh_direction: IrohConnectionDirection,
) -> IrohConnectionInstallDecision {
    IrohConnectionInstallDecision::ReplaceExisting {
        close_existing_reason: match fresh_direction {
            IrohConnectionDirection::Inbound => {
                crate::lifecycle_reason::REASON_REPLACED_BY_NEW_INBOUND
            }
            IrohConnectionDirection::Outbound => {
                crate::lifecycle_reason::REASON_REPLACED_BY_NEW_OUTBOUND
            }
        },
    }
}

fn decide_install(
    existing: Option<ExistingConnectionState>,
    fresh_direction: IrohConnectionDirection,
    local_prefers_outbound: bool,
) -> IrohConnectionInstallDecision {
    let Some(existing) = existing else {
        return IrohConnectionInstallDecision::Install;
    };
    if existing.same_stable_id || !existing.alive {
        return IrohConnectionInstallDecision::Install;
    }
    let canonical_direction = if local_prefers_outbound {
        IrohConnectionDirection::Outbound
    } else {
        IrohConnectionDirection::Inbound
    };
    if fresh_direction != canonical_direction {
        return IrohConnectionInstallDecision::KeepExisting {
            close_fresh_reason: match fresh_direction {
                IrohConnectionDirection::Inbound => REASON_DUPLICATE_ACCEPT_SUPERSEDED,
                IrohConnectionDirection::Outbound => REASON_DUPLICATE_DIAL_SUPERSEDED,
            },
        };
    }
    // Direction resolves symmetric dials immediately. A live same-direction
    // leg remains authoritative regardless of age: elapsed time alone is not
    // evidence that a QUIC connection is stale. Replacing it on every later
    // canonical dial makes admission proofs alternate between two physical
    // generations and can prevent bilateral settlement forever. Restart
    // recovery is driven by close/heartbeat evidence (existing.alive=false),
    // while explicit custom-transport replacement has its own fenced path.
    if existing.direction != canonical_direction {
        return replace_existing_decision(fresh_direction);
    }
    IrohConnectionInstallDecision::KeepExisting {
        close_fresh_reason: match fresh_direction {
            IrohConnectionDirection::Inbound => REASON_DUPLICATE_ACCEPT_SUPERSEDED,
            IrohConnectionDirection::Outbound => REASON_DUPLICATE_DIAL_SUPERSEDED,
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn install_without_existing_or_for_same_stable_id() {
        assert_eq!(
            decide_inbound_install(None, false),
            IrohConnectionInstallDecision::Install
        );
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: true,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: 0,
                }),
                true
            ),
            IrohConnectionInstallDecision::Install
        );
    }

    #[test]
    fn install_over_dead_existing_connection() {
        assert_eq!(
            decide_inbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: false,
                    direction: IrohConnectionDirection::Outbound,
                    age_ms: 0,
                }),
                false
            ),
            IrohConnectionInstallDecision::Install
        );
    }

    #[test]
    fn recent_same_direction_canonical_duplicate_keeps_existing_connection() {
        assert_eq!(
            decide_inbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: 1_999,
                }),
                false
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_ACCEPT_SUPERSEDED,
            }
        );
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Outbound,
                    age_ms: 0,
                }),
                true
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_DIAL_SUPERSEDED,
            }
        );
    }

    #[test]
    fn elapsed_time_cannot_replace_a_live_same_direction_connection() {
        assert_eq!(
            decide_inbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: u64::MAX,
                }),
                false
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_ACCEPT_SUPERSEDED,
            }
        );
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Outbound,
                    age_ms: u64::MAX,
                }),
                true
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_DIAL_SUPERSEDED,
            }
        );
    }

    #[test]
    fn canonical_fresh_leg_replaces_the_non_canonical_live_leg() {
        assert_eq!(
            decide_inbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Outbound,
                    age_ms: 0,
                }),
                false
            ),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_existing_reason: crate::lifecycle_reason::REASON_REPLACED_BY_NEW_INBOUND,
            }
        );
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: 0,
                }),
                true
            ),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_existing_reason: crate::lifecycle_reason::REASON_REPLACED_BY_NEW_OUTBOUND,
            }
        );
    }

    #[test]
    fn noncanonical_fresh_leg_cannot_replace_an_existing_live_leg() {
        assert_eq!(
            decide_inbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Outbound,
                    age_ms: u64::MAX,
                }),
                true
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_ACCEPT_SUPERSEDED,
            }
        );
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: u64::MAX,
                }),
                false
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_DIAL_SUPERSEDED,
            }
        );
    }

    #[test]
    fn connection_age_never_overrides_noncanonical_direction() {
        assert_eq!(
            decide_outbound_install(
                Some(ExistingConnectionState {
                    same_stable_id: false,
                    alive: true,
                    direction: IrohConnectionDirection::Inbound,
                    age_ms: u64::MAX,
                }),
                false,
            ),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_DIAL_SUPERSEDED,
            }
        );
    }

    #[test]
    fn outbound_precheck_is_idempotent_and_repairs_only_noncanonical_live_legs() {
        assert!(should_start_outbound_dial(None, true));
        assert!(should_start_outbound_dial(
            Some((false, IrohConnectionDirection::Outbound)),
            true,
        ));
        assert!(!should_start_outbound_dial(
            Some((true, IrohConnectionDirection::Outbound)),
            true,
        ));
        assert!(should_start_outbound_dial(
            Some((true, IrohConnectionDirection::Inbound)),
            true,
        ));
        assert!(!should_start_outbound_dial(
            Some((true, IrohConnectionDirection::Outbound)),
            false,
        ));
    }

    #[test]
    fn noncanonical_endpoint_waits_only_when_no_live_leg_exists() {
        assert!(should_wait_for_canonical_inbound(None, false));
        assert!(should_wait_for_canonical_inbound(
            Some((false, IrohConnectionDirection::Outbound)),
            false,
        ));
        assert!(!should_wait_for_canonical_inbound(
            Some((true, IrohConnectionDirection::Inbound)),
            false,
        ));
        assert!(!should_wait_for_canonical_inbound(None, true));
    }
}