DhtRpc
The live KRPC request and response manager. This is the coroutine port of libtorrent's rpc_manager (src/kademlia/rpc_manager.cpp) reduced to the one job that a commonMain engine can carry: match an outgoing query to its reply.
libtorrent's rpc_manager::invoke tags every query with a 2-byte transaction id (std::uint16_t, written big-endian into t, see rpc_manager.cpp m_transactions keyed by aux::read_uint16) and parks an observer until either the reply with the matching t arrives (incoming) or the 15-second timeout fires (check_timeouts). Here the observer collapses to a CompletableDeferred: query sends the datagram, registers the deferred under the freshly-minted id, and suspends on it under a withTimeout. The receive side (dispatchResponse) looks the id up and completes the waiter.
Threading model: the libtorrent strand. libtorrent runs the whole DHT on one io_context (effectively a single-threaded strand): there are never two concurrent touches of m_transactions. We reproduce that contract rather than locking: this manager and the DhtNode receive loop that drives it MUST share one kotlinx.coroutines.CoroutineScope backed by a single-threaded dispatcher confinement (e.g. Dispatchers.Default.limitedParallelism(1), which DhtNode arranges). All access to the pending map happens in non-suspending sections between suspension points, so on that confined dispatcher it is race-free without a mutex, and commonMain has no portable lock primitive anyway. The only suspension inside query (the socket.send and the await) happens after the id is registered and before it is removed, so a reply can never arrive at an unregistered waiter.
What this owns vs. what DhtNode owns. This class is purely the outgoing-query bookkeeping: the m_transactions map and the wire encoding and decoding of one datagram. It does not run the socket receive loop and it does not answer inbound queries; DhtNode owns the single udp.receive() loop and routes each parsed DhtMessage either to dispatchResponse (responses/errors) or to its own query-handler (inbound queries). This mirrors libtorrent, where dht_tracker pumps packets into node::incoming, which in turn hands responses to rpc_manager::incoming.
Simplifications vs. libtorrent (each deliberate):
No RTT / short-timeout escalation. libtorrent bumps an observer to a 1-second "short timeout" and adjusts the traversal branch factor; we use a single flat timeoutMillis per query and surface a timeout as a
nullresult. The traversal in DhtNode simply treats anullas "this node didn't answer".No per-endpoint observer identity / address spoof check.
rpc_manager::incomingverifies the reply's source endpoint matches the observer's; because we key only on the transaction id (which is what actually demultiplexes the socket), a reply from an unexpected address that happens to echo a live id would be accepted. With 2-byte ids drawn at random per in-flight query the collision window is tiny, and DhtNode re-validates the responder's node id against its routing-table slot.Errors resolve the waiter too. A
y:"e"reply for a live id completes the deferred exceptionally with a DhtProtocolException rather than dropping it. The caller decides how to treat a protocol error (libtorrent logs and drops it).
Constructors
Functions
Feed one already-parsed inbound message to the waiter table. Returns true if it was a response/error that matched a live transaction id (and so was consumed here), and false otherwise. On false the caller (DhtNode) should treat it as an inbound query, or as a stray or late reply, and handle it itself.
Number of queries currently awaiting a reply (rpc_manager::num_allocated-ish).
Decode a raw datagram payload and route it. Convenience used by DhtNode's receive loop when it wants the RPC manager to claim responses before it inspects the message as a possible inbound query. Returns the parsed DhtMessage (so the caller can handle queries / refresh its routing table from a response's node id), or null if the bytes were not a well-formed KRPC envelope.