OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / swarm_manager.h

351 lines · c ref:
#ifndef HYPERSWARM_SWARM_MANAGER_H
#define HYPERSWARM_SWARM_MANAGER_H

// SwarmManager — Hyperswarm-compatible connection management in C++.
//
// Wraps DhtConnector with auto-connect, exponential backoff retry,
// priority queuing, proven peer tracking, and max-peers capacity management.
//
// Verified against JS Hyperswarm v4.17.0:
//   lib/peer-info.js, lib/bulk-timer.js, lib/retry-timer.js, index.js

#include <cstdint>
#include <cstring>
#include <deque>
#include <map>
#include <set>
#include <string>
#include <vector>

// Forward declarations — avoid pulling in full headers.
class DhtConnector;
class HyperDht;

// ---- Constants (verified against JS Hyperswarm v4.17.0) ----

static constexpr int SWARM_MAX_PEERS = 64;
static constexpr int SWARM_MAX_PARALLEL = 3;

static constexpr int SWARM_PRIORITY_VERY_LOW = 0;
static constexpr int SWARM_PRIORITY_LOW = 1;
static constexpr int SWARM_PRIORITY_NORMAL = 2;
static constexpr int SWARM_PRIORITY_HIGH = 3;
static constexpr int SWARM_PRIORITY_VERY_HIGH = 4;

static constexpr uint64_t SWARM_MIN_CONNECTION_TIME = 15000; // 15s = "proven"

// JS backoff bases (jitter added at init time, see retry-timer.js):
//   BACKOFF_S = 1000 + round(500 * random())
//   BACKOFF_M = 5000 + round(1000 * random())
//   BACKOFF_L = 15000 + round(2000 * random())
//   BACKOFF_X = 600000 + round(120000 * random())
// Per-instance BulkTimer adds: backoff + round(jitter * random())
// BulkTimer interval = floor(time * 0.66)
static constexpr uint64_t SWARM_BACKOFF_S_BASE = 1000;
static constexpr uint64_t SWARM_BACKOFF_S_JITTER = 500;
static constexpr uint64_t SWARM_BACKOFF_M_BASE = 5000;
static constexpr uint64_t SWARM_BACKOFF_M_JITTER = 1000;
static constexpr uint64_t SWARM_BACKOFF_L_BASE = 15000;
static constexpr uint64_t SWARM_BACKOFF_L_JITTER = 2000;
static constexpr uint64_t SWARM_BACKOFF_X_BASE = 600000;
static constexpr uint64_t SWARM_BACKOFF_X_JITTER = 120000;
static constexpr uint64_t SWARM_INSTANCE_JITTER = 500; // Per-instance jitter (default)

// ---- SwarmPeerInfo ----
// Matches JS lib/peer-info.js

struct SwarmPeerInfo {
	uint8_t public_key[32];
	int priority;           // 0=VERY_LOW .. 4=VERY_HIGH
	int attempts;
	bool proven;            // Connected >= 15s at least once
	bool tried;             // At least one connect attempt made
	bool banned;
	bool queued;            // Currently in priority queue
	bool is_explicit;       // Added via swarm_join_peer()
	bool reconnecting;      // true = will retry on disconnect (default true)
	bool waiting;           // true = scheduled in retry timer
	bool connecting;        // true = dht.connect() called, handshake in progress
	                        // Matches JS _allConnections.has(pk) before 'open' event.
	bool client;            // true = we initiated, false = they connected to us
	uint64_t connected_at;  // ms timestamp, -1 = not connected (matches JS connectedTime = -1)
	uint64_t disconnected_at;
	int32_t peer_id;        // Godot peer_id if connected, 0 if not
	std::set<std::string> topics; // Topic hex strings this peer was found on

	SwarmPeerInfo() : priority(SWARM_PRIORITY_NORMAL), attempts(0),
		proven(false), tried(false), banned(false), queued(false),
		is_explicit(false), reconnecting(true), waiting(false), connecting(false),
		client(false),
		connected_at(UINT64_MAX), disconnected_at(0), peer_id(0) {
		memset(public_key, 0, 32);
	}

	// JS: get prioritized() { return this.priority >= NORMAL_PRIORITY }
	bool prioritized() const {
		return priority >= SWARM_PRIORITY_NORMAL;
	}

	// JS: get server() { return !this.client }
	bool server() const {
		return !client;
	}

	// JS: _getPriority() — exact match
	int get_priority() const {
		bool stale = tried && !proven;
		if (stale || attempts > 3) return SWARM_PRIORITY_VERY_LOW;
		if (attempts == 3) return SWARM_PRIORITY_LOW;
		if (attempts == 2) return SWARM_PRIORITY_HIGH;
		if (attempts == 1) return SWARM_PRIORITY_VERY_HIGH;
		return SWARM_PRIORITY_NORMAL; // attempts == 0
	}

	// JS: _connected()
	void on_connected(uint64_t now_ms) {
		proven = true;
		connected_at = now_ms;
	}

	// JS: _disconnected() — exact match
	void on_disconnected(uint64_t now_ms) {
		disconnected_at = now_ms;
		if (connected_at != UINT64_MAX) {
			if (now_ms - connected_at >= SWARM_MIN_CONNECTION_TIME) {
				attempts = 0; // fast retry
			}
			connected_at = UINT64_MAX;
		}
		attempts++;
	}

	// JS: _deprioritize()
	void deprioritize() {
		attempts = 3;
	}

	// JS: _reset() — exact match
	void reset() {
		client = false;
		proven = false;
		tried = false;
		attempts = 0;
	}

	// JS: _updatePriority() — exact match
	// Returns true if peer should be enqueued, false otherwise.
	bool update_priority() {
		if (is_explicit && attempts > 3) deprioritize();
		if (banned || queued || attempts > 3) return false;
		priority = get_priority();
		return true;
	}

	// JS: _topic(topic)
	void add_topic(const std::string &topic_hex) {
		topics.insert(topic_hex);
	}

	// JS: shouldGC()
	bool should_gc() const {
		return !(banned || queued || is_explicit || waiting);
	}
};

// ---- SwarmBulkTimer ----
// Exact match of JS lib/bulk-timer.js.
// setInterval(fn, floor(time * 0.66)).
// Double-buffer: pending → next → flush.

class SwarmBulkTimer {
public:
	SwarmBulkTimer();

	// Set the timer's base time (already includes module + instance jitter).
	// Stores floor(time * 0.66) as the tick interval.
	void set_time(uint64_t time_ms);
	void add(SwarmPeerInfo *info);
	void tick(uint64_t now_ms, std::vector<SwarmPeerInfo *> &flushed);
	bool empty() const;

private:
	uint64_t _interval_ms; // floor(time * 0.66)
	uint64_t _next_fire;
	bool _started;

	std::vector<SwarmPeerInfo *> _pending;
	std::vector<SwarmPeerInfo *> _next;
};

// ---- SwarmRetryTimer ----
// Exact match of JS lib/retry-timer.js.

class SwarmRetryTimer {
public:
	SwarmRetryTimer();

	// Initialize with per-instance jitter. Computes all 4 timer intervals.
	void init(uint64_t instance_jitter);

	// Schedule a peer for retry. Returns false if peer should not be retried.
	// JS: add(peerInfo)
	bool add(SwarmPeerInfo *info);

	// Tick all 4 timers.
	void tick(uint64_t now_ms, std::vector<SwarmPeerInfo *> &flushed);

private:
	SwarmBulkTimer _timer_s; // ~1s
	SwarmBulkTimer _timer_m; // ~5s
	SwarmBulkTimer _timer_l; // ~15s
	SwarmBulkTimer _timer_x; // ~10min

	// JS: _selectRetryTimer(peerInfo) — exact match
	SwarmBulkTimer *_select_timer(SwarmPeerInfo *info);
};

// ---- SwarmEvent ----

struct SwarmEvent {
	int32_t peer_id;
	uint8_t public_key[32];
	bool is_client;    // true if we initiated the connection
	bool connected;    // true=connected, false=disconnected
};

// ---- SwarmStatus ----

struct SwarmStatus {
	int peers_known;
	int peers_connected;
	int connecting;
	int queue_size;
	int max_peers;
	int max_parallel;
	int explicit_count;
	int all_connections;     // JS: _allConnections.size (pending + established)
	int client_connections;  // JS: _clientConnections (client-initiated, pending + open)
	bool flushed;            // JS: connecting===0 && queue empty && within max peers
};

// ---- SwarmManager ----

class SwarmManager {
public:
	SwarmManager();
	~SwarmManager();

	void init(DhtConnector *connector, HyperDht *dht,
			  const uint8_t *local_pk);

	void tick(uint64_t now_ms);

	// ---- API (called from command queue on UDX thread) ----

	void join(const uint8_t *topic, bool server, bool client);
	void leave(const uint8_t *topic);
	void join_peer(const uint8_t *pk);
	void leave_peer(const uint8_t *pk);

	// ---- DHT callbacks (called on UDX thread) ----

	// JS: _handlePeer(peer, topic) — called when DHT discovers a peer.
	void on_peer_found(const uint8_t *pk);
	void on_peer_found(const uint8_t *pk, const uint8_t *topic);

	// Called when DhtConnector completes a connection.
	void on_peer_connected(int32_t peer_id, const uint8_t *pk, bool is_initiator);

	// Called when a peer stream closes.
	void on_peer_disconnected(int32_t peer_id);

	// Called when a connect attempt fails (no stream established).
	void on_connect_failed(const uint8_t *pk);

	// ---- Event queue (UDX thread → main thread) ----

	bool pop_event(SwarmEvent &out);

	// ---- Diagnostics ----

	SwarmStatus get_status() const;

private:
	int _max_peers;
	int _max_parallel;
	int _connecting;        // In-flight connect attempts (JS: this.connecting)
	int _all_connections;   // Pending client + established (JS: this._allConnections.size)
	int _client_connections; // Client-initiated connections, pending + open (JS: this._clientConnections)

	// All known peers, keyed by hex(public_key). (JS: this.peers)
	std::map<std::string, SwarmPeerInfo *> _peers;

	// Reverse map: peer_id → hex pk (for disconnect lookup).
	std::map<int32_t, std::string> _peers_by_id;

	// Explicit peers set (JS: this.explicitPeers).
	std::set<SwarmPeerInfo *> _explicit_peers;

	// Topics we've joined via swarm_join (JS: this._discovery).
	struct TopicSession {
		bool server;
		bool client;
	};
	std::map<std::string, TopicSession> _topics;

	// Priority queue (JS: this._queue = spq()).
	std::deque<SwarmPeerInfo *> _queue;

	SwarmRetryTimer _retry;
	DhtConnector *_connector;
	HyperDht *_dht;
	uint8_t _local_pk[32];
	uint64_t _now_ms;

	std::deque<SwarmEvent> _events;

	int _total_connections; // Established connections only (JS: this.connections.size)

	// Re-entry guard (JS: this._drainingQueue).
	bool _draining_queue;

	// ---- Internal helpers (match JS method names) ----

	// JS: _shouldConnect()
	bool _should_connect() const;

	// JS: _shouldConnectExplicit() — explicit peers bypass _withinMaxPeers
	bool _should_connect_explicit() const;

	// JS: _withinMaxPeers()
	bool _within_max_peers() const;

	// JS: _shouldRequeue(peerInfo)
	bool _should_requeue(SwarmPeerInfo *info) const;

	// JS: _enqueue(peerInfo)
	void _enqueue(SwarmPeerInfo *info);

	// JS: _requeue(batch) — BulkTimer callback
	void _requeue(std::vector<SwarmPeerInfo *> &batch);

	// JS: _attemptClientConnections()
	void _attempt_client_connections();

	// JS: _connect(peerInfo, queued)
	void _connect(SwarmPeerInfo *info);

	// JS: _connectDone()
	void _connect_done();

	// JS: _upsertPeer(publicKey)
	SwarmPeerInfo *_upsert_peer(const uint8_t *pk);

	// JS: _maybeDeletePeer(peerInfo)
	void _maybe_delete_peer(SwarmPeerInfo *info);

	static std::string _pk_to_hex(const uint8_t *pk);
};

#endif // HYPERSWARM_SWARM_MANAGER_H