#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