Skip to content
minip2p
Esc
navigateopen⌘Jpreview
On this page

Drive events

Use focused waits for milestones, next_event for application loops, and next_wake when capability queues matter.

minip2p remains caller-driven at every layer. The application chooses when to poll, how long to wait, and which event queue to consume. Read where events go first. If you are working with custom streams, also read Register a protocol.

Pick a driving method

Use Endpoint::wait for the ADR 0007 Endpoint wait outcomes (event, deadline, or interrupted). If NAT, pubsub, discovery, or relay-server is enabled, keep using next_wake until capability events join the stream (#177) — wait does not wake on capability progress. Prefer connect plus ConnectSettled for a Connection attempt. Use focused waits such as nat_wait_path (nat) and wait_peer_ready when you need a particular milestone during migration. Use next_event for legacy application loops, or next_wake when your loop also handles capability queues and interruptions. All these methods use transport readiness when supported. Each call drives only its own endpoint, so blocking on one endpoint can delay others sharing the same thread.

Method Behavior Good fit
wait(deadline) Drive until one Endpoint event, deadline, or interruption. No driver-progress. New application event loops; correlating an operation while dispatching others
nat_wait_path / wait_peer_ready / wait_ping_rtt Drive until one milestone, buffering unrelated events. Connection and setup milestones (migration)
Feature-focused waits Drive until a NAT, pubsub, signed-discovery, or mDNS result. Feature-specific workflows (migration)
next_event(deadline) Drive until one ordinary application event or the deadline; swallows interruption. Legacy synchronous loops
next_wake(deadline) Drive until an application event, optional-capability progress, interruption, or the deadline. One interruptible loop over ordinary and capability events
poll() One non-blocking drive; returns currently available application events. Existing game, UI, or reactor loops

Deadline forms

Every endpoint wait accepts impl Into<Deadline>:

use std::time::{Duration, Instant};

use minip2p::Deadline;

let relative = Duration::from_secs(5);
let absolute = Instant::now() + Duration::from_secs(5);
let forever = Deadline::NEVER;

An already-passed absolute Instant deadline returns Deadline from wait / next_wake (or None from next_event) before delivering another queued event. Relative Duration deadlines — including Duration::ZERO non-blocking drains — still inspect buffered events and poll once. The endpoint does not sleep or poll repeatedly past the deadline.

Wait for a milestone

use std::time::Duration;

use minip2p::{ConnectOutcome, Endpoint, EndpointEvent, EndpointWaitOutcome};

fn connect_and_ready(
    node: &mut Endpoint,
    target: minip2p::PeerAddr,
) -> Result<(), Box<dyn std::error::Error>> {
    let connect_id = node.connect(target)?;
    let deadline = std::time::Instant::now() + Duration::from_secs(10);
    loop {
        match node.wait(deadline)? {
            EndpointWaitOutcome::Event(EndpointEvent::ConnectSettled {
                connect_id: settled,
                outcome: ConnectOutcome::Connected { .. },
                ..
            }) if settled == connect_id => return Ok(()),
            EndpointWaitOutcome::Event(EndpointEvent::ConnectSettled {
                connect_id: settled,
                outcome,
                ..
            }) if settled == connect_id => {
                return Err(format!("connect failed: {outcome:?}").into());
            }
            EndpointWaitOutcome::Event(_) | EndpointWaitOutcome::Interrupted => {}
            EndpointWaitOutcome::Deadline => return Err("connect did not settle".into()),
        }
    }
}

With the nat feature, connect races direct candidates against a relay leg when one is configured. Use nat_wait_path for the first usable NAT path (the provisional Relayed path, when that lands first); ConnectSettled is the attempt’s terminal. Use wait_peer_ready only when the application needs Identify’s protocol list (for example early RemoteDoesNotSupport checks). open_stream is allowed once the peer is connected; Identify is not a stack gate for opening known app protocols.

Application event loops

Prefer wait and dispatch unrelated events while correlating an operation (for capability-enabled endpoints, prefer next_wake until #177):

use std::time::{Duration, Instant};

use minip2p::{Endpoint, EndpointEvent, EndpointWaitOutcome};

fn run(mut node: Endpoint, peer: minip2p::PeerId) -> Result<(), minip2p::Error> {
    // One absolute deadline for the whole correlated wait — recreating a
    // relative Duration inside the loop would reset the timeout on every
    // unrelated event or interruption.
    let deadline = Instant::now() + Duration::from_secs(5);
    loop {
        match node.wait(deadline)? {
            EndpointWaitOutcome::Event(EndpointEvent::PeerReady { peer_id, .. })
                if peer_id == peer =>
            {
                println!("ready={peer_id}");
                break;
            }
            EndpointWaitOutcome::Event(EndpointEvent::Error(error)) => {
                eprintln!("runtime error: {error:?}");
            }
            EndpointWaitOutcome::Event(_) => {
                // Handle unrelated connection / stream / ping events.
            }
            EndpointWaitOutcome::Deadline => break,
            EndpointWaitOutcome::Interrupted => {
                // Service external commands, then continue.
            }
        }
    }
    Ok(())
}

Synchronous method failures are returned as minip2p::Error. Non-fatal problems that occur later while driving appear as Event::Error or a feature-specific failure event.

Focused waits preserve other events

Suppose nat_wait_path is waiting for a NAT result while an application stream receives data. The stream event is retained and becomes available through a later next_event; it is not discarded by the NAT wait.

That retained backlog is bounded by RUN_UNTIL_SKIP_LIMIT (currently 1024 events). If a focused wait cannot find its result while unrelated events keep arriving, it returns:

minip2p::Error::EventBacklogExceeded { limit: RUN_UNTIL_SKIP_LIMIT }

Drain ordinary application events with next_event or poll, handle the high-volume source, and then retry the focused wait.

Stream and connection shutdown

  • Call close_stream_write after the final byte for a graceful half-close.
  • Call reset_stream when the peer should observe abrupt termination and the application still wants terminal events.
  • Call abandon_stream when no matching buffered or future events should be delivered.
  • Call disconnect to close the active peer connection.

Endpoint does not run a background shutdown sequence after it is dropped. Drive any graceful application-level close exchanges before leaving scope, then call endpoint.close()?. close consumes the endpoint, disconnects peers, and briefly drains the resulting events. Dropping also disconnects, but ignores errors.

With the mdns feature, endpoint.shutdown() has a narrower meaning: it sends mDNS goodbyes and stops mDNS while leaving QUIC and TCP usable.

Wait for every event family

next_event waits for ordinary endpoint events. Use next_wake when one loop also needs prompt access to NAT, pubsub, discovery, or relay-server events:

use minip2p::{Deadline, EndpointWake};

loop {
    match node.next_wake(Deadline::NEVER)? {
        EndpointWake::Event(event) => handle_event(event),
        EndpointWake::DriverProgress => {
            for event in node.take_gossipsub_events() {
                handle_pubsub_event(event);
            }
        }
        EndpointWake::Deadline | EndpointWake::Interrupted => {}
    }
}

DriverProgress remains ready while any enabled capability queue contains events. Drain every enabled queue before calling next_wake again. Leaving one non-empty causes the next call to return immediately.

See Rust troubleshooting for common event symptoms and responses.

Last updated on September 21, 2026

Was this page helpful?