librats 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -205,7 +205,7 @@ client.publishJsonToTopic('general-chat', JSON.stringify(message));
205
205
  - `startDhtDiscovery(dhtPort: number): boolean` - Start DHT discovery
206
206
  - `stopDhtDiscovery(): void` - Stop DHT discovery
207
207
  - `isDhtRunning(): boolean` - Check if DHT is running
208
- - `announceForHash(contentHash: string, port: number): boolean` - Announce for hash
208
+ - `announceForHash(contentHash: string, port: number, callback?: (peers: string[]) => void): boolean` - Announce for hash with optional peer discovery callback
209
209
 
210
210
  #### Encryption
211
211
 
package/lib/index.d.ts CHANGED
@@ -370,9 +370,10 @@ export class RatsClient {
370
370
  * Announce availability for a content hash
371
371
  * @param contentHash - Hash to announce for
372
372
  * @param port - Port to announce
373
+ * @param callback - Optional callback to receive discovered peers during DHT traversal
373
374
  * @returns true if announced successfully
374
375
  */
375
- announceForHash(contentHash: string, port: number): boolean;
376
+ announceForHash(contentHash: string, port: number, callback?: (peers: string[]) => void): boolean;
376
377
 
377
378
  // ============ mDNS ============
378
379
 
@@ -210,7 +210,7 @@ bool DhtClient::find_peers(const InfoHash& info_hash, PeerDiscoveryCallback call
210
210
  return true;
211
211
  }
212
212
 
213
- bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
213
+ bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port, PeerDiscoveryCallback callback) {
214
214
  if (!running_) {
215
215
  LOG_DHT_ERROR("DHT client not running");
216
216
  return false;
@@ -220,24 +220,64 @@ bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
220
220
  port = port_;
221
221
  }
222
222
 
223
- LOG_DHT_INFO("Announcing peer for info hash: " << node_id_to_hex(info_hash) << " on port " << port);
223
+ std::string hash_key = node_id_to_hex(info_hash);
224
+ LOG_DHT_INFO("Announcing peer for info hash: " << hash_key << " on port " << port);
224
225
 
225
- // First find nodes close to the info hash and send get_peers to them
226
- // This is the proper BEP 5 flow: get_peers -> collect tokens -> announce_peer
227
- auto closest_nodes = find_closest_nodes(info_hash, ALPHA);
228
- for (const auto& node : closest_nodes) {
229
- // Generate transaction ID and track this as a pending announce for KRPC
230
- std::string transaction_id = KrpcProtocol::generate_transaction_id();
231
-
232
- {
233
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
234
- pending_announces_.emplace(transaction_id, PendingAnnounce(info_hash, port));
226
+ // BEP 5 compliant announce:
227
+ // 1. Perform iterative Kademlia lookup (like find_peers)
228
+ // 2. Collect tokens from responding nodes
229
+ // 3. Send announce_peer to k closest nodes with their tokens
230
+
231
+ // Get initial nodes from routing table
232
+ auto closest_nodes = find_closest_nodes(info_hash, K_BUCKET_SIZE);
233
+
234
+ if (closest_nodes.empty()) {
235
+ LOG_DHT_WARN("No nodes in routing table to announce to for info_hash " << hash_key);
236
+ return false;
237
+ }
238
+
239
+ DeferredCallbacks deferred;
240
+
241
+ {
242
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
243
+
244
+ // Check if a search/announce is already ongoing for this info_hash
245
+ auto search_it = pending_searches_.find(hash_key);
246
+ if (search_it != pending_searches_.end()) {
247
+ if (search_it->second.is_announce) {
248
+ LOG_DHT_INFO("Announce already in progress for info hash " << hash_key);
249
+ return true;
235
250
  }
236
-
237
- auto message = KrpcProtocol::create_get_peers_query(transaction_id, node_id_, info_hash);
238
- send_krpc_message(message, node.peer);
251
+ // Regular find_peers in progress - let it complete, then user can announce again
252
+ LOG_DHT_WARN("find_peers already in progress for info hash " << hash_key << " - announce will wait");
253
+ return false;
254
+ }
255
+
256
+ // Create new search with announce flag
257
+ PendingSearch new_search(info_hash);
258
+ new_search.is_announce = true;
259
+ new_search.announce_port = port;
260
+
261
+ // Add callback if provided - peers discovered during traversal will be returned through it
262
+ if (callback) {
263
+ new_search.callbacks.push_back(callback);
264
+ }
265
+
266
+ // Initialize search_nodes with closest nodes from routing table (already sorted)
267
+ new_search.search_nodes = std::move(closest_nodes);
268
+
269
+ auto insert_result = pending_searches_.emplace(hash_key, std::move(new_search));
270
+ PendingSearch& search_ref = insert_result.first->second;
271
+
272
+ LOG_DHT_DEBUG("Initialized announce search with " << search_ref.search_nodes.size() << " nodes from routing table");
273
+
274
+ // Start sending requests
275
+ add_search_requests(search_ref, deferred);
239
276
  }
240
277
 
278
+ // Invoke callbacks outside the lock to avoid deadlock
279
+ deferred.invoke();
280
+
241
281
  return true;
242
282
  }
243
283
 
@@ -262,6 +302,13 @@ bool DhtClient::is_search_active(const InfoHash& info_hash) const {
262
302
  return it != pending_searches_.end() && !it->second.is_finished;
263
303
  }
264
304
 
305
+ bool DhtClient::is_announce_active(const InfoHash& info_hash) const {
306
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
307
+ std::string hash_key = node_id_to_hex(info_hash);
308
+ auto it = pending_searches_.find(hash_key);
309
+ return it != pending_searches_.end() && !it->second.is_finished && it->second.is_announce;
310
+ }
311
+
265
312
  size_t DhtClient::get_active_searches_count() const {
266
313
  std::lock_guard<std::mutex> lock(pending_searches_mutex_);
267
314
  size_t count = 0;
@@ -273,6 +320,17 @@ size_t DhtClient::get_active_searches_count() const {
273
320
  return count;
274
321
  }
275
322
 
323
+ size_t DhtClient::get_active_announces_count() const {
324
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
325
+ size_t count = 0;
326
+ for (const auto& [hash, search] : pending_searches_) {
327
+ if (!search.is_finished && search.is_announce) {
328
+ count++;
329
+ }
330
+ }
331
+ return count;
332
+ }
333
+
276
334
  std::vector<Peer> DhtClient::get_default_bootstrap_nodes() {
277
335
  return {
278
336
  {"router.bittorrent.com", 6881},
@@ -340,9 +398,6 @@ void DhtClient::maintenance_loop() {
340
398
  // Cleanup stale peer tokens
341
399
  cleanup_stale_peer_tokens();
342
400
 
343
- // Cleanup stale pending announces
344
- cleanup_stale_announces();
345
-
346
401
  // Cleanup stale pending searches
347
402
  cleanup_stale_searches();
348
403
 
@@ -763,9 +818,16 @@ void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sen
763
818
  handle_get_peers_empty_response(message.transaction_id, sender);
764
819
  }
765
820
 
766
- // Check if this is a response to a pending announce (get_peers with token)
821
+ // Save write token if present (needed for announce_peer after traversal completes)
767
822
  if (!message.token.empty()) {
768
- handle_get_peers_response_for_announce(message.transaction_id, sender, message.token);
823
+ std::lock_guard<std::mutex> lock(pending_searches_mutex_);
824
+ auto trans_it = transaction_to_search_.find(message.transaction_id);
825
+ if (trans_it != transaction_to_search_.end()) {
826
+ auto search_it = pending_searches_.find(trans_it->second.info_hash_hex);
827
+ if (search_it != pending_searches_.end()) {
828
+ save_write_token(search_it->second, trans_it->second.queried_node_id, message.token);
829
+ }
830
+ }
769
831
  }
770
832
 
771
833
  // Clean up finished searches AFTER all response data has been processed
@@ -1028,8 +1090,10 @@ void DhtClient::print_statistics() {
1028
1090
 
1029
1091
  // Pending searches statistics
1030
1092
  size_t pending_searches = 0;
1093
+ size_t pending_announces = 0;
1031
1094
  size_t total_search_nodes = 0;
1032
1095
  size_t total_found_peers = 0;
1096
+ size_t total_write_tokens = 0;
1033
1097
  size_t active_transactions = 0;
1034
1098
  {
1035
1099
  std::lock_guard<std::mutex> search_lock(pending_searches_mutex_);
@@ -1038,16 +1102,13 @@ void DhtClient::print_statistics() {
1038
1102
  for (const auto& [hash, search] : pending_searches_) {
1039
1103
  total_search_nodes += search.search_nodes.size();
1040
1104
  total_found_peers += search.found_peers.size();
1105
+ total_write_tokens += search.write_tokens.size();
1106
+ if (search.is_announce) {
1107
+ pending_announces++;
1108
+ }
1041
1109
  }
1042
1110
  }
1043
1111
 
1044
- // Pending announces statistics
1045
- size_t pending_announces_count = 0;
1046
- {
1047
- std::lock_guard<std::mutex> announce_lock(pending_announces_mutex_);
1048
- pending_announces_count = pending_announces_.size();
1049
- }
1050
-
1051
1112
  // Announced peers statistics
1052
1113
  size_t announced_peers_total = 0;
1053
1114
  size_t announced_peers_infohashes = 0;
@@ -1084,9 +1145,11 @@ void DhtClient::print_statistics() {
1084
1145
  << ", Max bucket size: " << max_bucket_size << "/" << K_BUCKET_SIZE);
1085
1146
  LOG_DHT_INFO("[ACTIVE OPERATIONS]");
1086
1147
  LOG_DHT_INFO(" Pending searches: " << pending_searches
1087
- << " (nodes: " << total_search_nodes << ", found peers: " << total_found_peers << ")");
1148
+ << " (announces: " << pending_announces
1149
+ << ", nodes: " << total_search_nodes
1150
+ << ", peers: " << total_found_peers
1151
+ << ", tokens: " << total_write_tokens << ")");
1088
1152
  LOG_DHT_INFO(" Active transactions: " << active_transactions);
1089
- LOG_DHT_INFO(" Pending announces: " << pending_announces_count);
1090
1153
  LOG_DHT_INFO(" Pending ping verifications: " << pending_pings
1091
1154
  << " (nodes being replaced: " << nodes_being_replaced << ")");
1092
1155
  LOG_DHT_INFO("[STORED DATA]");
@@ -1203,23 +1266,6 @@ void DhtClient::refresh_buckets() {
1203
1266
  }
1204
1267
  }
1205
1268
 
1206
- void DhtClient::cleanup_stale_announces() {
1207
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
1208
-
1209
- auto now = std::chrono::steady_clock::now();
1210
- auto stale_threshold = std::chrono::minutes(5); // Remove announces older than 5 minutes
1211
-
1212
- auto it = pending_announces_.begin();
1213
- while (it != pending_announces_.end()) {
1214
- if (now - it->second.created_at > stale_threshold) {
1215
- LOG_DHT_DEBUG("Removing stale pending announce for transaction " << it->first);
1216
- it = pending_announces_.erase(it);
1217
- } else {
1218
- ++it;
1219
- }
1220
- }
1221
- }
1222
-
1223
1269
  void DhtClient::cleanup_stale_searches() {
1224
1270
  std::lock_guard<std::mutex> lock(pending_searches_mutex_);
1225
1271
 
@@ -1475,24 +1521,6 @@ void DhtClient::cleanup_timed_out_search_requests() {
1475
1521
  }
1476
1522
  }
1477
1523
 
1478
- void DhtClient::handle_get_peers_response_for_announce(const std::string& transaction_id, const Peer& responder, const std::string& token) {
1479
- std::lock_guard<std::mutex> lock(pending_announces_mutex_);
1480
-
1481
- auto it = pending_announces_.find(transaction_id);
1482
- if (it != pending_announces_.end()) {
1483
- const auto& pending_announce = it->second;
1484
- LOG_DHT_DEBUG("Found pending announce for transaction " << transaction_id
1485
- << " - sending announce_peer for info_hash " << node_id_to_hex(pending_announce.info_hash)
1486
- << " to " << responder.ip << ":" << responder.port);
1487
-
1488
- // Send announce_peer with the received token
1489
- send_krpc_announce_peer(responder, pending_announce.info_hash, pending_announce.port, token);
1490
-
1491
- // Remove the pending announce since we've handled it
1492
- pending_announces_.erase(it);
1493
- }
1494
- }
1495
-
1496
1524
  void DhtClient::handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder) {
1497
1525
  DeferredCallbacks deferred;
1498
1526
  {
@@ -1796,6 +1824,86 @@ void DhtClient::add_node_to_search(PendingSearch& search, const DhtNode& node) {
1796
1824
  }
1797
1825
  }
1798
1826
 
1827
+ void DhtClient::save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token) {
1828
+ // Save the write token received from a node (BEP 5 compliant)
1829
+ // This token will be used later when sending announce_peer to this node
1830
+
1831
+ if (token.empty()) {
1832
+ return;
1833
+ }
1834
+
1835
+ // Only save token if we don't already have one from this node
1836
+ // (first token is usually the valid one)
1837
+ if (search.write_tokens.find(node_id) == search.write_tokens.end()) {
1838
+ search.write_tokens[node_id] = token;
1839
+ LOG_DHT_DEBUG("Saved write token from node " << node_id_to_hex(node_id)
1840
+ << " for info_hash " << node_id_to_hex(search.info_hash)
1841
+ << " (total tokens: " << search.write_tokens.size() << ")");
1842
+ }
1843
+ }
1844
+
1845
+ void DhtClient::send_announce_to_closest_nodes(PendingSearch& search) {
1846
+ // BEP 5: Send announce_peer to the k closest nodes that:
1847
+ // 1. Responded to our get_peers query
1848
+ // 2. Gave us a valid write token
1849
+
1850
+ if (!search.is_announce) {
1851
+ return;
1852
+ }
1853
+
1854
+ std::string hash_key = node_id_to_hex(search.info_hash);
1855
+
1856
+ LOG_DHT_INFO("Sending announce_peer to closest nodes for info_hash " << hash_key
1857
+ << " on port " << search.announce_port);
1858
+
1859
+ // Collect nodes that responded and have tokens, sorted by distance (closest first)
1860
+ std::vector<std::pair<DhtNode, std::string>> announce_targets;
1861
+ announce_targets.reserve(K_BUCKET_SIZE);
1862
+
1863
+ for (const auto& node : search.search_nodes) {
1864
+ if (announce_targets.size() >= K_BUCKET_SIZE) {
1865
+ break; // We have enough targets
1866
+ }
1867
+
1868
+ // Check if node responded successfully
1869
+ auto state_it = search.node_states.find(node.id);
1870
+ if (state_it == search.node_states.end()) {
1871
+ continue;
1872
+ }
1873
+ if (!(state_it->second & SearchNodeFlags::RESPONDED)) {
1874
+ continue; // Node didn't respond
1875
+ }
1876
+
1877
+ // Check if we have a token from this node
1878
+ auto token_it = search.write_tokens.find(node.id);
1879
+ if (token_it == search.write_tokens.end()) {
1880
+ LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " responded but no token - skipping");
1881
+ continue; // No token from this node
1882
+ }
1883
+
1884
+ announce_targets.emplace_back(node, token_it->second);
1885
+ }
1886
+
1887
+ if (announce_targets.empty()) {
1888
+ LOG_DHT_WARN("No nodes with tokens to announce to for info_hash " << hash_key);
1889
+ return;
1890
+ }
1891
+
1892
+ LOG_DHT_INFO("Announcing to " << announce_targets.size() << " closest nodes with tokens");
1893
+
1894
+ // Send announce_peer to each target
1895
+ for (const auto& [node, token] : announce_targets) {
1896
+ LOG_DHT_DEBUG("Sending announce_peer to node " << node_id_to_hex(node.id)
1897
+ << " at " << node.peer.ip << ":" << node.peer.port
1898
+ << " with token (distance: " << get_bucket_index(node.id) << ")");
1899
+
1900
+ send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token);
1901
+ }
1902
+
1903
+ LOG_DHT_INFO("Announce completed: sent announce_peer to " << announce_targets.size()
1904
+ << " nodes for info_hash " << hash_key);
1905
+ }
1906
+
1799
1907
  bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& deferred) {
1800
1908
  // Returns true if search is done (completed or should be finished)
1801
1909
 
@@ -1875,7 +1983,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1875
1983
  queries_sent++;
1876
1984
  }
1877
1985
 
1878
- LOG_DHT_DEBUG("Search [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
1986
+ LOG_DHT_DEBUG((search.is_announce ? "Announce" : "Search") << " [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
1879
1987
  LOG_DHT_DEBUG(" * search_nodes: " << search.search_nodes.size());
1880
1988
  LOG_DHT_DEBUG(" * queries_sent: " << queries_sent);
1881
1989
  LOG_DHT_DEBUG(" * invoke_count: " << search.invoke_count);
@@ -1903,7 +2011,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1903
2011
  if (f & SearchNodeFlags::ABANDONED) abandoned_total++;
1904
2012
  }
1905
2013
 
1906
- LOG_DHT_INFO("=== Search Completed for info_hash " << hash_key << " ===");
2014
+ LOG_DHT_INFO("=== " << (search.is_announce ? "Announce" : "Search") << " Completed for info_hash " << hash_key << " ===");
1907
2015
  LOG_DHT_INFO(" Duration: " << duration_ms << "ms");
1908
2016
  LOG_DHT_INFO(" Total nodes queried: " << queried_total);
1909
2017
  LOG_DHT_INFO(" Total nodes responded: " << responded_total);
@@ -1911,8 +2019,18 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
1911
2019
  LOG_DHT_INFO(" Nodes with short timeout: " << short_timeout_total);
1912
2020
  LOG_DHT_INFO(" Nodes abandoned (truncation): " << abandoned_total);
1913
2021
  LOG_DHT_INFO(" Final branch_factor: " << search.branch_factor << " (initial: " << ALPHA << ")");
1914
- LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
1915
- LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
2022
+ if (search.is_announce) {
2023
+ LOG_DHT_INFO(" Write tokens collected: " << search.write_tokens.size());
2024
+ LOG_DHT_INFO(" Announce port: " << search.announce_port);
2025
+ } else {
2026
+ LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
2027
+ LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
2028
+ }
2029
+
2030
+ // If this is an announce search, send announce_peer to k closest nodes with tokens
2031
+ if (search.is_announce) {
2032
+ send_announce_to_closest_nodes(search);
2033
+ }
1916
2034
 
1917
2035
  // Collect callbacks for deferred invocation (avoid deadlock - don't call user callbacks while holding mutex)
1918
2036
  deferred.should_invoke = true;
@@ -198,9 +198,10 @@ public:
198
198
  * Announce that this node is a peer for a specific info hash
199
199
  * @param info_hash The info hash to announce
200
200
  * @param port The port to announce (0 for DHT port)
201
+ * @param callback Optional callback to receive discovered peers during traversal
201
202
  * @return true if announcement started successfully, false otherwise
202
203
  */
203
- bool announce_peer(const InfoHash& info_hash, uint16_t port = 0);
204
+ bool announce_peer(const InfoHash& info_hash, uint16_t port = 0, PeerDiscoveryCallback callback = nullptr);
204
205
 
205
206
  /**
206
207
  * Get our node ID
@@ -227,12 +228,25 @@ public:
227
228
  */
228
229
  bool is_search_active(const InfoHash& info_hash) const;
229
230
 
231
+ /**
232
+ * Check if an announce is currently active for an info hash
233
+ * @param info_hash The info hash to check
234
+ * @return true if announce is active, false otherwise
235
+ */
236
+ bool is_announce_active(const InfoHash& info_hash) const;
237
+
230
238
  /**
231
239
  * Get number of active searches
232
240
  * @return Number of active searches
233
241
  */
234
242
  size_t get_active_searches_count() const;
235
243
 
244
+ /**
245
+ * Get number of active announces
246
+ * @return Number of active announces
247
+ */
248
+ size_t get_active_announces_count() const;
249
+
236
250
  /**
237
251
  * Check if DHT is running
238
252
  * @return true if running, false otherwise
@@ -279,10 +293,9 @@ private:
279
293
  // 1. pending_pings_mutex_ (Ping verification state, nodes_being_replaced_)
280
294
  // 2. pending_searches_mutex_ (Search state and transaction mappings)
281
295
  // 3. routing_table_mutex_ (core routing data)
282
- // 4. pending_announces_mutex_ (Announce state)
283
- // 5. announced_peers_mutex_ (Stored peer data)
284
- // 6. peer_tokens_mutex_ (Token validation data)
285
- // 7. shutdown_mutex_ (Lowest priority - can be locked independently)
296
+ // 4. announced_peers_mutex_ (Stored peer data)
297
+ // 5. peer_tokens_mutex_ (Token validation data)
298
+ // 6. shutdown_mutex_ (Lowest priority - can be locked independently)
286
299
  //
287
300
  // Routing table (k-buckets)
288
301
  std::vector<std::vector<DhtNode>> routing_table_;
@@ -301,17 +314,6 @@ private:
301
314
  std::mutex peer_tokens_mutex_; // Lock order: 6
302
315
 
303
316
 
304
- // Pending announce tracking (for BEP 5 compliance)
305
- struct PendingAnnounce {
306
- InfoHash info_hash;
307
- uint16_t port;
308
- std::chrono::steady_clock::time_point created_at;
309
-
310
- PendingAnnounce(const InfoHash& hash, uint16_t p)
311
- : info_hash(hash), port(p), created_at(std::chrono::steady_clock::now()) {}
312
- };
313
- std::unordered_map<std::string, PendingAnnounce> pending_announces_;
314
- std::mutex pending_announces_mutex_; // Lock order: 4
315
317
 
316
318
  // Pending find_peers tracking (to map transaction IDs to info_hash)
317
319
  struct PendingSearch {
@@ -332,9 +334,16 @@ private:
332
334
  // Callbacks to invoke when peers are found (supports multiple concurrent searches for same info_hash)
333
335
  std::vector<PeerDiscoveryCallback> callbacks;
334
336
 
337
+ // Announce support: tokens collected during traversal (BEP 5 compliant)
338
+ // Maps node_id -> write_token received from that node
339
+ std::unordered_map<NodeId, std::string> write_tokens;
340
+ bool is_announce; // true if this search is for announce_peer
341
+ uint16_t announce_port; // port to announce (only valid if is_announce)
342
+
335
343
  PendingSearch(const InfoHash& hash)
336
344
  : info_hash(hash), created_at(std::chrono::steady_clock::now()),
337
- invoke_count(0), branch_factor(ALPHA), is_finished(false) {}
345
+ invoke_count(0), branch_factor(ALPHA), is_finished(false),
346
+ is_announce(false), announce_port(0) {}
338
347
  };
339
348
  std::unordered_map<std::string, PendingSearch> pending_searches_; // info_hash (hex) -> PendingSearch
340
349
  mutable std::mutex pending_searches_mutex_; // Lock order: 2
@@ -434,10 +443,6 @@ private:
434
443
  void refresh_buckets();
435
444
  void print_statistics();
436
445
 
437
- // Pending announce management
438
- void cleanup_stale_announces();
439
- void handle_get_peers_response_for_announce(const std::string& transaction_id, const Peer& responder, const std::string& token);
440
-
441
446
  // Pending search management
442
447
  void cleanup_stale_searches();
443
448
  void cleanup_timed_out_search_requests();
@@ -445,8 +450,10 @@ private:
445
450
  void handle_get_peers_response_for_search(const std::string& transaction_id, const Peer& responder, const std::vector<Peer>& peers);
446
451
  void handle_get_peers_response_with_nodes(const std::string& transaction_id, const Peer& responder, const std::vector<KrpcNode>& nodes);
447
452
  void handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder);
453
+ void save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token);
448
454
  bool add_search_requests(PendingSearch& search, DeferredCallbacks& deferred);
449
455
  void add_node_to_search(PendingSearch& search, const DhtNode& node);
456
+ void send_announce_to_closest_nodes(PendingSearch& search);
450
457
 
451
458
  // Peer announcement storage management
452
459
  void store_announced_peer(const InfoHash& info_hash, const Peer& peer);
@@ -1817,7 +1817,8 @@ bool RatsClient::find_peers_by_hash(const std::string& content_hash, std::functi
1817
1817
  });
1818
1818
  }
1819
1819
 
1820
- bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port) {
1820
+ bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port,
1821
+ std::function<void(const std::vector<std::string>&)> callback) {
1821
1822
  if (!dht_client_ || !dht_client_->is_running()) {
1822
1823
  LOG_CLIENT_ERROR("DHT client not running");
1823
1824
  return false;
@@ -1832,10 +1833,25 @@ bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t por
1832
1833
  port = listen_port_;
1833
1834
  }
1834
1835
 
1835
- LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port);
1836
+ LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port
1837
+ << (callback ? " with peer callback" : ""));
1836
1838
 
1837
1839
  InfoHash info_hash = hex_to_node_id(content_hash);
1838
- return dht_client_->announce_peer(info_hash, port);
1840
+
1841
+ // Create wrapper callback that converts Peer to string addresses (if callback provided)
1842
+ PeerDiscoveryCallback peer_callback = nullptr;
1843
+ if (callback) {
1844
+ peer_callback = [callback](const std::vector<Peer>& peers, const InfoHash& hash) {
1845
+ std::vector<std::string> peer_addresses;
1846
+ peer_addresses.reserve(peers.size());
1847
+ for (const auto& peer : peers) {
1848
+ peer_addresses.push_back(peer.ip + ":" + std::to_string(peer.port));
1849
+ }
1850
+ callback(peer_addresses);
1851
+ };
1852
+ }
1853
+
1854
+ return dht_client_->announce_peer(info_hash, port, peer_callback);
1839
1855
  }
1840
1856
 
1841
1857
  bool RatsClient::is_dht_running() const {
@@ -1931,46 +1947,23 @@ void RatsClient::automatic_discovery_loop() {
1931
1947
  }
1932
1948
  }
1933
1949
 
1934
- // Search immediately
1935
- search_rats_peers();
1936
-
1937
- {
1938
- std::unique_lock<std::mutex> lock(shutdown_mutex_);
1939
- if (shutdown_cv_.wait_for(lock, std::chrono::seconds(10), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
1940
- LOG_CLIENT_INFO("Automatic peer discovery loop stopped during search delay");
1941
- return;
1942
- }
1943
- }
1944
-
1945
- // Announce immediately
1950
+ // Announce immediately - this also discovers peers during traversal
1946
1951
  announce_rats_peer();
1947
1952
 
1948
1953
  auto last_announce = std::chrono::steady_clock::now();
1949
- auto last_search = std::chrono::steady_clock::now();
1950
1954
 
1951
1955
  while (auto_discovery_running_.load()) {
1952
1956
  auto now = std::chrono::steady_clock::now();
1953
1957
 
1954
- if (get_peer_count() == 0) {
1955
- // No peers: aggressive search and announce
1956
- if (now - last_search >= std::chrono::seconds(5)) {
1957
- search_rats_peers();
1958
- last_search = now;
1959
- }
1960
- if (now - last_announce >= std::chrono::seconds(20)) {
1961
- announce_rats_peer();
1962
- last_announce = now;
1963
- }
1964
- } else {
1965
- // Peers connected: less aggressive, similar to original logic
1966
- if (now - last_search >= std::chrono::minutes(5)) {
1967
- search_rats_peers();
1968
- last_search = now;
1969
- }
1970
- if (now - last_announce >= std::chrono::minutes(10)) {
1971
- announce_rats_peer();
1972
- last_announce = now;
1973
- }
1958
+ // Announce combines both announcing our presence and discovering peers
1959
+ // Adjust frequency based on whether we have peers
1960
+ auto interval = (get_peer_count() == 0)
1961
+ ? std::chrono::seconds(15) // Aggressive when no peers
1962
+ : std::chrono::minutes(10); // Less aggressive when connected
1963
+
1964
+ if (now - last_announce >= interval) {
1965
+ announce_rats_peer();
1966
+ last_announce = now;
1974
1967
  }
1975
1968
 
1976
1969
  // Use conditional variable for responsive shutdown
@@ -1994,8 +1987,35 @@ void RatsClient::announce_rats_peer() {
1994
1987
  std::string discovery_hash = get_discovery_hash();
1995
1988
  LOG_CLIENT_INFO("Announcing peer for discovery hash: " << discovery_hash << " on port " << listen_port_);
1996
1989
 
1997
- if (announce_for_hash(discovery_hash, listen_port_)) {
1998
- LOG_CLIENT_DEBUG("Successfully announced peer for discovery");
1990
+ InfoHash info_hash = hex_to_node_id(discovery_hash);
1991
+
1992
+ if (dht_client_->is_announce_active(info_hash)) {
1993
+ LOG_CLIENT_WARN("Announce already in progress for info hash: " << node_id_to_hex(info_hash));
1994
+ return;
1995
+ }
1996
+
1997
+ // Use announce with callback - combines announce and find_peers in one traversal
1998
+ // Peers discovered during traversal will be returned through the callback
1999
+ if (announce_for_hash(discovery_hash, listen_port_, [this, info_hash](const std::vector<std::string>& peer_addresses) {
2000
+ LOG_CLIENT_INFO("Announce discovered " << peer_addresses.size() << " peers during traversal");
2001
+
2002
+ // Convert peer addresses to Peer objects for handle_dht_peer_discovery()
2003
+ std::vector<Peer> peers;
2004
+ peers.reserve(peer_addresses.size());
2005
+ for (const auto& peer_address : peer_addresses) {
2006
+ std::string ip;
2007
+ int port;
2008
+ if (parse_address_string(peer_address, ip, port)) {
2009
+ peers.push_back(Peer(ip, port));
2010
+ }
2011
+ }
2012
+
2013
+ // Auto-connect to discovered peers
2014
+ if (!peers.empty()) {
2015
+ handle_dht_peer_discovery(peers, info_hash);
2016
+ }
2017
+ })) {
2018
+ LOG_CLIENT_DEBUG("Successfully started announce with peer discovery for discovery hash");
1999
2019
  } else {
2000
2020
  LOG_CLIENT_WARN("Failed to announce peer for discovery");
2001
2021
  }
@@ -606,12 +606,15 @@ public:
606
606
  std::function<void(const std::vector<std::string>&)> callback);
607
607
 
608
608
  /**
609
- * Announce our presence for a content hash
609
+ * Announce our presence for a content hash with optional peer discovery callback
610
+ * If callback is provided, peers discovered during DHT traversal will be returned through it
610
611
  * @param content_hash Hash to announce for (40-character hex string)
611
612
  * @param port Port to announce (default: our listen port)
613
+ * @param callback Optional function to call with discovered peers during traversal
612
614
  * @return true if announced successfully
613
615
  */
614
- bool announce_for_hash(const std::string& content_hash, uint16_t port = 0);
616
+ bool announce_for_hash(const std::string& content_hash, uint16_t port = 0,
617
+ std::function<void(const std::vector<std::string>&)> callback = nullptr);
615
618
 
616
619
  /**
617
620
  * Check if DHT is currently running
@@ -386,13 +386,29 @@ int rats_is_dht_running(rats_client_t handle) {
386
386
  return wrap->client->is_dht_running() ? 1 : 0;
387
387
  }
388
388
 
389
- rats_error_t rats_announce_for_hash(rats_client_t handle, const char* content_hash, int port) {
389
+ rats_error_t rats_announce_for_hash(rats_client_t handle, const char* content_hash, int port,
390
+ rats_peers_found_cb callback, void* user_data) {
390
391
  if (!handle || !content_hash) return RATS_ERROR_INVALID_PARAMETER;
391
392
  if (strlen(content_hash) != 40) return RATS_ERROR_INVALID_PARAMETER; // SHA1 hash must be 40 chars
392
393
  rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
393
394
 
394
395
  uint16_t announce_port = (port <= 0) ? 0 : static_cast<uint16_t>(port);
395
- return wrap->client->announce_for_hash(std::string(content_hash), announce_port) ?
396
+
397
+ // Create C++ callback wrapper if C callback is provided
398
+ std::function<void(const std::vector<std::string>&)> cpp_callback = nullptr;
399
+ if (callback) {
400
+ cpp_callback = [callback, user_data](const std::vector<std::string>& peers) {
401
+ // Convert vector to C-style array
402
+ std::vector<const char*> c_peers;
403
+ c_peers.reserve(peers.size());
404
+ for (const auto& peer : peers) {
405
+ c_peers.push_back(peer.c_str());
406
+ }
407
+ callback(user_data, c_peers.data(), static_cast<int>(c_peers.size()));
408
+ };
409
+ }
410
+
411
+ return wrap->client->announce_for_hash(std::string(content_hash), announce_port, cpp_callback) ?
396
412
  RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
397
413
  }
398
414
 
@@ -82,6 +82,7 @@ typedef void (*rats_json_cb)(void* user_data, const char* peer_id, const char* j
82
82
  typedef void (*rats_disconnect_cb)(void* user_data, const char* peer_id);
83
83
  typedef void (*rats_peer_discovered_cb)(void* user_data, const char* host, int port, const char* service_name);
84
84
  typedef void (*rats_message_cb)(void* user_data, const char* peer_id, const char* message_data);
85
+ typedef void (*rats_peers_found_cb)(void* user_data, const char** peer_addresses, int count);
85
86
 
86
87
  // Peer configuration
87
88
  RATS_API rats_error_t rats_set_max_peers(rats_client_t client, int max_peers);
@@ -106,7 +107,8 @@ RATS_API int rats_broadcast_json(rats_client_t client, const char* json_str);
106
107
  RATS_API rats_error_t rats_start_dht_discovery(rats_client_t client, int dht_port);
107
108
  RATS_API void rats_stop_dht_discovery(rats_client_t client);
108
109
  RATS_API int rats_is_dht_running(rats_client_t client);
109
- RATS_API rats_error_t rats_announce_for_hash(rats_client_t client, const char* content_hash, int port);
110
+ RATS_API rats_error_t rats_announce_for_hash(rats_client_t client, const char* content_hash, int port,
111
+ rats_peers_found_cb callback, void* user_data);
110
112
  RATS_API size_t rats_get_dht_routing_table_size(rats_client_t client);
111
113
 
112
114
  // Automatic discovery
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Node.js bindings for librats - A high-performance peer-to-peer networking library",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -473,7 +473,52 @@ private:
473
473
  std::string content_hash = info[0].As<Napi::String>().Utf8Value();
474
474
  int port = info[1].As<Napi::Number>().Int32Value();
475
475
 
476
- rats_error_t result = rats_announce_for_hash(client_, content_hash.c_str(), port);
476
+ // Check if optional callback is provided
477
+ rats_peers_found_cb c_callback = nullptr;
478
+ void* callback_user_data = nullptr;
479
+ Napi::ThreadSafeFunction* tsfn_ptr = nullptr;
480
+
481
+ if (info.Length() >= 3 && info[2].IsFunction()) {
482
+ // Create thread-safe function for callback
483
+ auto tsfn = new Napi::ThreadSafeFunction();
484
+ *tsfn = Napi::ThreadSafeFunction::New(
485
+ env,
486
+ info[2].As<Napi::Function>(),
487
+ "AnnounceForHashCallback",
488
+ 0,
489
+ 1,
490
+ [](Napi::Env) {} // Release callback
491
+ );
492
+ tsfn_ptr = tsfn;
493
+
494
+ c_callback = [](void* user_data, const char** peer_addresses, int count) {
495
+ auto* tsfn = static_cast<Napi::ThreadSafeFunction*>(user_data);
496
+ if (!tsfn) return;
497
+
498
+ // Copy peer addresses for async callback
499
+ std::vector<std::string>* peers = new std::vector<std::string>();
500
+ for (int i = 0; i < count; i++) {
501
+ if (peer_addresses[i]) {
502
+ peers->push_back(peer_addresses[i]);
503
+ }
504
+ }
505
+
506
+ tsfn->BlockingCall(peers, [](Napi::Env env, Napi::Function jsCallback, std::vector<std::string>* data) {
507
+ Napi::Array arr = Napi::Array::New(env, data->size());
508
+ for (size_t i = 0; i < data->size(); i++) {
509
+ arr.Set(static_cast<uint32_t>(i), Napi::String::New(env, (*data)[i]));
510
+ }
511
+ jsCallback.Call({arr});
512
+ delete data;
513
+ });
514
+
515
+ tsfn->Release();
516
+ delete tsfn;
517
+ };
518
+ callback_user_data = tsfn_ptr;
519
+ }
520
+
521
+ rats_error_t result = rats_announce_for_hash(client_, content_hash.c_str(), port, c_callback, callback_user_data);
477
522
  return Napi::Boolean::New(env, result == RATS_SUCCESS);
478
523
  }
479
524