librats 1.0.2 → 2.2.0

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.
Files changed (319) hide show
  1. package/README.md +145 -331
  2. package/binding.gyp +16 -3
  3. package/lib/index.d.ts +288 -696
  4. package/lib/index.js +407 -44
  5. package/native-src/3rdparty/android/ifaddrs-android.c +1 -0
  6. package/native-src/3rdparty/android/ifaddrs-android.h +1 -0
  7. package/native-src/CMakeLists.txt +404 -179
  8. package/native-src/LICENSE +1 -1
  9. package/native-src/src/librats/bindings/rats.cpp +762 -0
  10. package/native-src/src/librats/bindings/rats.h +380 -0
  11. package/native-src/src/librats/bittorrent/bencode.cpp +437 -0
  12. package/native-src/src/librats/bittorrent/bencode.h +176 -0
  13. package/native-src/src/librats/bittorrent/bitfield.cpp +97 -0
  14. package/native-src/src/librats/bittorrent/bitfield.h +76 -0
  15. package/native-src/src/librats/bittorrent/byte_io.h +58 -0
  16. package/native-src/src/librats/bittorrent/choker.cpp +25 -0
  17. package/native-src/src/librats/bittorrent/choker.h +46 -0
  18. package/native-src/src/librats/bittorrent/client.cpp +413 -0
  19. package/native-src/src/librats/bittorrent/client.h +227 -0
  20. package/native-src/src/librats/bittorrent/disk_io.cpp +209 -0
  21. package/native-src/src/librats/bittorrent/disk_io.h +150 -0
  22. package/native-src/src/librats/bittorrent/extensions.cpp +191 -0
  23. package/native-src/src/librats/bittorrent/extensions.h +94 -0
  24. package/native-src/src/librats/bittorrent/file_storage.cpp +77 -0
  25. package/native-src/src/librats/bittorrent/file_storage.h +79 -0
  26. package/native-src/src/librats/bittorrent/log.h +42 -0
  27. package/native-src/src/librats/bittorrent/magnet_uri.cpp +98 -0
  28. package/native-src/src/librats/bittorrent/magnet_uri.h +35 -0
  29. package/native-src/src/librats/bittorrent/peer_connection.cpp +502 -0
  30. package/native-src/src/librats/bittorrent/peer_connection.h +194 -0
  31. package/native-src/src/librats/bittorrent/peer_list.cpp +68 -0
  32. package/native-src/src/librats/bittorrent/peer_list.h +75 -0
  33. package/native-src/src/librats/bittorrent/piece_picker.cpp +352 -0
  34. package/native-src/src/librats/bittorrent/piece_picker.h +201 -0
  35. package/native-src/src/librats/bittorrent/reactor.cpp +97 -0
  36. package/native-src/src/librats/bittorrent/reactor.h +89 -0
  37. package/native-src/src/librats/bittorrent/resume_data.cpp +72 -0
  38. package/native-src/src/librats/bittorrent/resume_data.h +41 -0
  39. package/native-src/src/librats/bittorrent/store_buffer.cpp +48 -0
  40. package/native-src/src/librats/bittorrent/store_buffer.h +47 -0
  41. package/native-src/src/librats/bittorrent/torrent.cpp +870 -0
  42. package/native-src/src/librats/bittorrent/torrent.h +260 -0
  43. package/native-src/src/librats/bittorrent/torrent_creator.cpp +129 -0
  44. package/native-src/src/librats/bittorrent/torrent_creator.h +58 -0
  45. package/native-src/src/librats/bittorrent/torrent_info.cpp +314 -0
  46. package/native-src/src/librats/bittorrent/torrent_info.h +118 -0
  47. package/native-src/src/librats/bittorrent/tracker.cpp +374 -0
  48. package/native-src/src/librats/bittorrent/tracker.h +108 -0
  49. package/native-src/src/librats/bittorrent/types.cpp +206 -0
  50. package/native-src/src/librats/bittorrent/types.h +86 -0
  51. package/native-src/src/librats/core/address.cpp +35 -0
  52. package/native-src/src/librats/core/address.h +78 -0
  53. package/native-src/src/librats/core/bytes.h +69 -0
  54. package/native-src/src/librats/core/chained_send_buffer.cpp +172 -0
  55. package/native-src/src/librats/core/chained_send_buffer.h +183 -0
  56. package/native-src/src/librats/core/endpoint_parse.cpp +41 -0
  57. package/native-src/src/librats/core/endpoint_parse.h +31 -0
  58. package/native-src/src/librats/core/event_bus.h +70 -0
  59. package/native-src/src/librats/core/host_endpoint.h +56 -0
  60. package/native-src/src/{io_poller.cpp → librats/core/io_poller.cpp} +520 -65
  61. package/native-src/src/{io_poller.h → librats/core/io_poller.h} +12 -6
  62. package/native-src/src/librats/core/ip_address.cpp +120 -0
  63. package/native-src/src/librats/core/ip_address.h +109 -0
  64. package/native-src/src/librats/core/mpsc_queue.h +47 -0
  65. package/native-src/src/librats/core/notifier.h +74 -0
  66. package/native-src/src/librats/core/receive_buffer.cpp +219 -0
  67. package/native-src/src/librats/core/receive_buffer.h +171 -0
  68. package/native-src/src/librats/core/service_registry.h +58 -0
  69. package/native-src/src/{socket.cpp → librats/core/socket.cpp} +625 -118
  70. package/native-src/src/librats/core/socket.h +496 -0
  71. package/native-src/src/librats/core/timer_queue.h +105 -0
  72. package/native-src/src/librats/core/types.cpp +43 -0
  73. package/native-src/src/librats/core/types.h +103 -0
  74. package/native-src/src/librats/core/wakeup_pipe.h +83 -0
  75. package/native-src/src/{crypto → librats/crypto}/blake2_endian.h +21 -23
  76. package/native-src/src/{crypto → librats/crypto}/blake2b.c +34 -33
  77. package/native-src/src/{crypto → librats/crypto}/blake2b.h +7 -6
  78. package/native-src/src/{crypto → librats/crypto}/blake2s.c +55 -54
  79. package/native-src/src/{crypto → librats/crypto}/blake2s.h +13 -12
  80. package/native-src/src/{crypto → librats/crypto}/chacha.c +22 -21
  81. package/native-src/src/{crypto → librats/crypto}/chacha.h +14 -13
  82. package/native-src/src/{crypto → librats/crypto}/chachapoly.c +56 -56
  83. package/native-src/src/{crypto → librats/crypto}/chachapoly.h +24 -17
  84. package/native-src/src/{crc32.cpp → librats/crypto/crc32.cpp} +1 -1
  85. package/native-src/src/{crc32.h → librats/crypto/crc32.h} +3 -1
  86. package/native-src/src/{crypto → librats/crypto}/curve25519.c +6 -4
  87. package/native-src/src/{crypto → librats/crypto}/curve25519.h +6 -3
  88. package/native-src/src/librats/crypto/hkdf.c +266 -0
  89. package/native-src/src/{crypto → librats/crypto}/hkdf.h +19 -19
  90. package/native-src/src/{noise.cpp → librats/crypto/noise.cpp} +84 -73
  91. package/native-src/src/{noise.h → librats/crypto/noise.h} +18 -8
  92. package/native-src/src/{crypto → librats/crypto}/poly1305.c +47 -46
  93. package/native-src/src/librats/crypto/poly1305.h +37 -0
  94. package/native-src/src/{sha1.cpp → librats/crypto/sha1.cpp} +33 -1
  95. package/native-src/src/{sha1.h → librats/crypto/sha1.h} +14 -6
  96. package/native-src/src/{crypto → librats/crypto}/sha256.c +15 -14
  97. package/native-src/src/{crypto → librats/crypto}/sha256.h +8 -7
  98. package/native-src/src/{crypto → librats/crypto}/sha512.c +15 -14
  99. package/native-src/src/{crypto → librats/crypto}/sha512.h +8 -7
  100. package/native-src/src/librats/dht/announce.cpp +37 -0
  101. package/native-src/src/librats/dht/announce.h +41 -0
  102. package/native-src/src/librats/dht/bep42.cpp +109 -0
  103. package/native-src/src/librats/dht/bep42.h +48 -0
  104. package/native-src/src/librats/dht/dht.cpp +501 -0
  105. package/native-src/src/librats/dht/dht.h +119 -0
  106. package/native-src/src/librats/dht/dht_runner.cpp +103 -0
  107. package/native-src/src/librats/dht/dht_runner.h +71 -0
  108. package/native-src/src/librats/dht/dos_blocker.cpp +42 -0
  109. package/native-src/src/librats/dht/dos_blocker.h +47 -0
  110. package/native-src/src/librats/dht/find_peers.cpp +52 -0
  111. package/native-src/src/librats/dht/find_peers.h +73 -0
  112. package/native-src/src/librats/dht/id.h +167 -0
  113. package/native-src/src/{krpc.cpp → librats/dht/krpc.cpp} +32 -81
  114. package/native-src/src/{krpc.h → librats/dht/krpc.h} +19 -23
  115. package/native-src/src/librats/dht/log.h +38 -0
  116. package/native-src/src/librats/dht/node.cpp +473 -0
  117. package/native-src/src/librats/dht/node.h +164 -0
  118. package/native-src/src/librats/dht/node_entry.h +81 -0
  119. package/native-src/src/librats/dht/observer.h +72 -0
  120. package/native-src/src/librats/dht/persistence.cpp +90 -0
  121. package/native-src/src/librats/dht/persistence.h +32 -0
  122. package/native-src/src/librats/dht/routing_table.cpp +559 -0
  123. package/native-src/src/librats/dht/routing_table.h +185 -0
  124. package/native-src/src/librats/dht/rpc_manager.cpp +127 -0
  125. package/native-src/src/librats/dht/rpc_manager.h +77 -0
  126. package/native-src/src/librats/dht/storage.cpp +92 -0
  127. package/native-src/src/librats/dht/storage.h +74 -0
  128. package/native-src/src/librats/dht/transport.h +27 -0
  129. package/native-src/src/librats/dht/traversal.cpp +326 -0
  130. package/native-src/src/librats/dht/traversal.h +120 -0
  131. package/native-src/src/librats/dht/udp_transport.cpp +49 -0
  132. package/native-src/src/librats/dht/udp_transport.h +51 -0
  133. package/native-src/src/librats/mdns/log.h +22 -0
  134. package/native-src/src/{mdns.cpp → librats/mdns/mdns.cpp} +75 -40
  135. package/native-src/src/{mdns.h → librats/mdns/mdns.h} +9 -8
  136. package/native-src/src/{natpmp.cpp → librats/nat/natpmp.cpp} +12 -9
  137. package/native-src/src/{natpmp.h → librats/nat/natpmp.h} +3 -3
  138. package/native-src/src/{port_mapping.h → librats/nat/port_mapping.h} +3 -2
  139. package/native-src/src/{stun.cpp → librats/nat/stun.cpp} +4 -4
  140. package/native-src/src/{stun.h → librats/nat/stun.h} +1 -1
  141. package/native-src/src/{upnp.cpp → librats/nat/upnp.cpp} +6 -6
  142. package/native-src/src/{upnp.h → librats/nat/upnp.h} +2 -2
  143. package/native-src/src/librats/node/circuit_service.h +84 -0
  144. package/native-src/src/librats/node/config.h +110 -0
  145. package/native-src/src/librats/node/dial_service.h +54 -0
  146. package/native-src/src/librats/node/dialer.cpp +264 -0
  147. package/native-src/src/librats/node/dialer.h +188 -0
  148. package/native-src/src/librats/node/host_events.h +26 -0
  149. package/native-src/src/librats/node/identify.cpp +130 -0
  150. package/native-src/src/librats/node/identify.h +71 -0
  151. package/native-src/src/librats/node/nat_status.cpp +103 -0
  152. package/native-src/src/librats/node/nat_status.h +118 -0
  153. package/native-src/src/librats/node/node.cpp +865 -0
  154. package/native-src/src/librats/node/node.h +344 -0
  155. package/native-src/src/librats/node/node_context.h +33 -0
  156. package/native-src/src/librats/node/peer_network.h +91 -0
  157. package/native-src/src/librats/peer/peer.h +49 -0
  158. package/native-src/src/librats/peer/peer_book.cpp +181 -0
  159. package/native-src/src/librats/peer/peer_book.h +88 -0
  160. package/native-src/src/librats/peer/peer_id.cpp +72 -0
  161. package/native-src/src/librats/peer/peer_id.h +62 -0
  162. package/native-src/src/librats/peer/peer_info.h +37 -0
  163. package/native-src/src/librats/peer/peer_table.cpp +170 -0
  164. package/native-src/src/librats/peer/peer_table.h +148 -0
  165. package/native-src/src/librats/security/handshaker.h +66 -0
  166. package/native-src/src/librats/security/identity.h +43 -0
  167. package/native-src/src/librats/security/noise_security.cpp +122 -0
  168. package/native-src/src/librats/security/noise_security.h +37 -0
  169. package/native-src/src/librats/security/plaintext_security.h +106 -0
  170. package/native-src/src/librats/security/session.h +37 -0
  171. package/native-src/src/{storage.cpp → librats/storage/storage.cpp} +369 -522
  172. package/native-src/src/{storage.h → librats/storage/storage.h} +135 -299
  173. package/native-src/src/librats/subsystems/bittorrent.cpp +211 -0
  174. package/native-src/src/librats/subsystems/bittorrent.h +136 -0
  175. package/native-src/src/librats/subsystems/dht_discovery.cpp +202 -0
  176. package/native-src/src/librats/subsystems/dht_discovery.h +123 -0
  177. package/native-src/src/librats/subsystems/dht_service.h +36 -0
  178. package/native-src/src/librats/subsystems/file_transfer.cpp +972 -0
  179. package/native-src/src/librats/subsystems/file_transfer.h +367 -0
  180. package/native-src/src/librats/subsystems/hole_punch.cpp +605 -0
  181. package/native-src/src/librats/subsystems/hole_punch.h +290 -0
  182. package/native-src/src/librats/subsystems/hole_punch_service.h +38 -0
  183. package/native-src/src/librats/subsystems/mdns_discovery.cpp +66 -0
  184. package/native-src/src/librats/subsystems/mdns_discovery.h +55 -0
  185. package/native-src/src/librats/subsystems/message_json.cpp +112 -0
  186. package/native-src/src/librats/subsystems/message_json.h +88 -0
  187. package/native-src/src/librats/subsystems/peer_exchange.cpp +241 -0
  188. package/native-src/src/librats/subsystems/peer_exchange.h +136 -0
  189. package/native-src/src/librats/subsystems/ping_service.cpp +98 -0
  190. package/native-src/src/librats/subsystems/ping_service.h +66 -0
  191. package/native-src/src/librats/subsystems/port_mapping_service.cpp +192 -0
  192. package/native-src/src/librats/subsystems/port_mapping_service.h +84 -0
  193. package/native-src/src/librats/subsystems/pubsub.cpp +567 -0
  194. package/native-src/src/librats/subsystems/pubsub.h +175 -0
  195. package/native-src/src/librats/subsystems/reconnection.cpp +239 -0
  196. package/native-src/src/librats/subsystems/reconnection.h +126 -0
  197. package/native-src/src/librats/subsystems/relay.cpp +1142 -0
  198. package/native-src/src/librats/subsystems/relay.h +211 -0
  199. package/native-src/src/librats/subsystems/relay_service.h +46 -0
  200. package/native-src/src/librats/transport/connection.cpp +343 -0
  201. package/native-src/src/librats/transport/connection.h +283 -0
  202. package/native-src/src/librats/transport/link.h +96 -0
  203. package/native-src/src/librats/transport/reactor.cpp +588 -0
  204. package/native-src/src/librats/transport/reactor.h +262 -0
  205. package/native-src/src/librats/transport/reactor_pool.h +81 -0
  206. package/native-src/src/librats/transport/relay_link.cpp +208 -0
  207. package/native-src/src/librats/transport/relay_link.h +303 -0
  208. package/native-src/src/librats/transport/tcp_link.cpp +49 -0
  209. package/native-src/src/librats/transport/tcp_link.h +43 -0
  210. package/native-src/src/librats/transport/udp_mux.cpp +617 -0
  211. package/native-src/src/librats/transport/udp_mux.h +363 -0
  212. package/native-src/src/librats/transport/udp_packet.cpp +121 -0
  213. package/native-src/src/librats/transport/udp_packet.h +190 -0
  214. package/native-src/src/librats/transport/udp_stream.cpp +1194 -0
  215. package/native-src/src/librats/transport/udp_stream.h +614 -0
  216. package/native-src/src/librats/util/features.h.in +51 -0
  217. package/native-src/src/{fs.cpp → librats/util/fs.cpp} +51 -3
  218. package/native-src/src/librats/util/fs.h +136 -0
  219. package/native-src/src/librats/util/json.cpp +1002 -0
  220. package/native-src/src/librats/util/json.h +444 -0
  221. package/native-src/src/{logger.cpp → librats/util/logger.cpp} +1 -1
  222. package/native-src/src/{logger.h → librats/util/logger.h} +43 -31
  223. package/native-src/src/{network_monitor.cpp → librats/util/network_monitor.cpp} +12 -4
  224. package/native-src/src/{network_monitor.h → librats/util/network_monitor.h} +2 -1
  225. package/native-src/src/{network_utils.cpp → librats/util/network_utils.cpp} +38 -23
  226. package/native-src/src/{network_utils.h → librats/util/network_utils.h} +15 -8
  227. package/native-src/src/{os.cpp → librats/util/os.cpp} +48 -18
  228. package/native-src/src/librats/util/rats_export.h +69 -0
  229. package/native-src/src/{version.cpp → librats/util/version.cpp} +2 -2
  230. package/native-src/src/{version.h.in → librats/util/version.h.in} +1 -1
  231. package/native-src/src/librats/wire/frame.cpp +76 -0
  232. package/native-src/src/librats/wire/frame.h +111 -0
  233. package/native-src/src/librats/wire/message_router.cpp +45 -0
  234. package/native-src/src/librats/wire/message_router.h +47 -0
  235. package/package.json +5 -4
  236. package/scripts/build-librats.js +1 -0
  237. package/scripts/postinstall.js +3 -3
  238. package/scripts/prepare-package.js +4 -4
  239. package/scripts/verify-installation.js +63 -105
  240. package/src/librats_node.cpp +1067 -1323
  241. package/native-src/src/bencode.cpp +0 -485
  242. package/native-src/src/bencode.h +0 -145
  243. package/native-src/src/bittorrent.cpp +0 -14
  244. package/native-src/src/bittorrent.h +0 -74
  245. package/native-src/src/bt_bitfield.cpp +0 -372
  246. package/native-src/src/bt_bitfield.h +0 -316
  247. package/native-src/src/bt_choker.cpp +0 -228
  248. package/native-src/src/bt_choker.h +0 -147
  249. package/native-src/src/bt_client.cpp +0 -1047
  250. package/native-src/src/bt_client.h +0 -445
  251. package/native-src/src/bt_create_torrent.cpp +0 -677
  252. package/native-src/src/bt_create_torrent.h +0 -473
  253. package/native-src/src/bt_extension.cpp +0 -469
  254. package/native-src/src/bt_extension.h +0 -309
  255. package/native-src/src/bt_file_storage.cpp +0 -261
  256. package/native-src/src/bt_file_storage.h +0 -298
  257. package/native-src/src/bt_handshake.cpp +0 -134
  258. package/native-src/src/bt_handshake.h +0 -157
  259. package/native-src/src/bt_messages.cpp +0 -364
  260. package/native-src/src/bt_messages.h +0 -324
  261. package/native-src/src/bt_network.cpp +0 -1007
  262. package/native-src/src/bt_network.h +0 -417
  263. package/native-src/src/bt_peer_connection.cpp +0 -742
  264. package/native-src/src/bt_peer_connection.h +0 -592
  265. package/native-src/src/bt_piece_picker.cpp +0 -786
  266. package/native-src/src/bt_piece_picker.h +0 -473
  267. package/native-src/src/bt_resume_data.cpp +0 -410
  268. package/native-src/src/bt_resume_data.h +0 -249
  269. package/native-src/src/bt_torrent.cpp +0 -2120
  270. package/native-src/src/bt_torrent.h +0 -641
  271. package/native-src/src/bt_torrent_info.cpp +0 -659
  272. package/native-src/src/bt_torrent_info.h +0 -418
  273. package/native-src/src/bt_types.h +0 -621
  274. package/native-src/src/chained_send_buffer.cpp +0 -75
  275. package/native-src/src/chained_send_buffer.h +0 -137
  276. package/native-src/src/crypto/hkdf.c +0 -266
  277. package/native-src/src/crypto/poly1305.h +0 -36
  278. package/native-src/src/dht.cpp +0 -3311
  279. package/native-src/src/dht.h +0 -717
  280. package/native-src/src/disk_io.cpp +0 -632
  281. package/native-src/src/disk_io.h +0 -315
  282. package/native-src/src/file_transfer.cpp +0 -1415
  283. package/native-src/src/file_transfer.h +0 -286
  284. package/native-src/src/fs.h +0 -108
  285. package/native-src/src/gossipsub.cpp +0 -1139
  286. package/native-src/src/gossipsub.h +0 -403
  287. package/native-src/src/ice.cpp +0 -893
  288. package/native-src/src/ice.h +0 -559
  289. package/native-src/src/json.hpp +0 -25526
  290. package/native-src/src/librats.cpp +0 -2378
  291. package/native-src/src/librats.h +0 -2324
  292. package/native-src/src/librats_bittorrent.cpp +0 -601
  293. package/native-src/src/librats_c.cpp +0 -1557
  294. package/native-src/src/librats_c.h +0 -323
  295. package/native-src/src/librats_discovery.cpp +0 -402
  296. package/native-src/src/librats_encryption.cpp +0 -275
  297. package/native-src/src/librats_file_transfer.cpp +0 -144
  298. package/native-src/src/librats_gossipsub.cpp +0 -289
  299. package/native-src/src/librats_ice.cpp +0 -213
  300. package/native-src/src/librats_log_macros.h +0 -36
  301. package/native-src/src/librats_logging.cpp +0 -173
  302. package/native-src/src/librats_mdns.cpp +0 -166
  303. package/native-src/src/librats_persistence.cpp +0 -796
  304. package/native-src/src/librats_portmap.cpp +0 -419
  305. package/native-src/src/librats_reconnection.cpp +0 -218
  306. package/native-src/src/librats_statistic.cpp +0 -105
  307. package/native-src/src/librats_storage.cpp +0 -189
  308. package/native-src/src/rats_export.h +0 -17
  309. package/native-src/src/receive_buffer.cpp +0 -82
  310. package/native-src/src/receive_buffer.h +0 -127
  311. package/native-src/src/socket.h +0 -228
  312. package/native-src/src/threadmanager.cpp +0 -105
  313. package/native-src/src/threadmanager.h +0 -53
  314. package/native-src/src/tracker.cpp +0 -1264
  315. package/native-src/src/tracker.h +0 -319
  316. package/native-src/src/turn.cpp +0 -762
  317. package/native-src/src/turn.h +0 -460
  318. package/native-src/src/wakeup_pipe.h +0 -60
  319. /package/native-src/src/{os.h → librats/util/os.h} +0 -0
@@ -0,0 +1,567 @@
1
+ #include "librats/subsystems/pubsub.h"
2
+ #include "librats/node/node_context.h"
3
+ #include "librats/util/logger.h"
4
+
5
+ #include <algorithm>
6
+ #include <random>
7
+ #include <tuple>
8
+
9
+ namespace librats {
10
+
11
+ namespace {
12
+
13
+ // Wire ops (see the format table in pubsub.h).
14
+ enum : uint8_t {
15
+ OP_PUBLISH = 0,
16
+ OP_SUBSCRIBE = 1,
17
+ OP_UNSUBSCRIBE = 2,
18
+ OP_GRAFT = 3,
19
+ OP_PRUNE = 4,
20
+ OP_IHAVE = 5,
21
+ OP_IWANT = 6,
22
+ };
23
+
24
+ constexpr size_t kIdSize = PeerId::kSize + 8; // origin(32) || seqno(u64) = 40 bytes
25
+
26
+ void put_u16(Bytes& b, uint16_t v) { b.push_back(v >> 8); b.push_back(v & 0xFF); }
27
+ void put_u64(Bytes& b, uint64_t v) { for (int i = 7; i >= 0; --i) b.push_back((v >> (i * 8)) & 0xFF); }
28
+ void put_topic(Bytes& b, const std::string& t) {
29
+ put_u16(b, static_cast<uint16_t>(t.size()));
30
+ b.insert(b.end(), t.begin(), t.end());
31
+ }
32
+
33
+ /// The dedup / cache key for a message: its origin id followed by the origin's
34
+ /// per-message sequence number. Globally unique without a clock or hash.
35
+ std::string make_id(const PeerId& origin, uint64_t seqno) {
36
+ std::string id(reinterpret_cast<const char*>(origin.bytes().data()), PeerId::kSize);
37
+ for (int i = 7; i >= 0; --i) id.push_back(static_cast<char>((seqno >> (i * 8)) & 0xFF));
38
+ return id;
39
+ }
40
+
41
+ struct Reader {
42
+ const uint8_t* p;
43
+ const uint8_t* end;
44
+ bool ok = true;
45
+
46
+ uint8_t u8() { if (p >= end) { ok = false; return 0; } return *p++; }
47
+ uint16_t u16() { if (end - p < 2) { ok = false; return 0; } uint16_t v = (uint16_t(p[0]) << 8) | p[1]; p += 2; return v; }
48
+ uint64_t u64() { if (end - p < 8) { ok = false; return 0; } uint64_t v = 0; for (int i = 0; i < 8; ++i) v = (v << 8) | *p++; return v; }
49
+ ByteView bytes(size_t n) { if (size_t(end - p) < n) { ok = false; return {}; } ByteView v(p, n); p += n; return v; }
50
+ ByteView rest() { ByteView v(p, size_t(end - p)); p = end; return v; }
51
+ std::string str(size_t n) { ByteView v = bytes(n); return ok ? std::string(reinterpret_cast<const char*>(v.data()), v.size()) : std::string(); }
52
+ };
53
+
54
+ // Frame builders.
55
+ Bytes build_publish(const PeerId& origin, uint64_t seqno, const std::string& topic, ByteView data) {
56
+ Bytes f;
57
+ f.push_back(OP_PUBLISH);
58
+ f.insert(f.end(), origin.bytes().begin(), origin.bytes().end());
59
+ put_u64(f, seqno);
60
+ put_topic(f, topic);
61
+ f.insert(f.end(), data.begin(), data.end());
62
+ return f;
63
+ }
64
+ Bytes build_ctrl(uint8_t op, const std::string& topic) {
65
+ Bytes f;
66
+ f.push_back(op);
67
+ put_topic(f, topic);
68
+ return f;
69
+ }
70
+ Bytes build_ihave(const std::string& topic, const std::vector<std::string>& ids) {
71
+ Bytes f;
72
+ f.push_back(OP_IHAVE);
73
+ put_topic(f, topic);
74
+ put_u16(f, static_cast<uint16_t>(std::min<size_t>(ids.size(), 0xFFFF)));
75
+ for (const std::string& id : ids) { if (id.size() == kIdSize) f.insert(f.end(), id.begin(), id.end()); }
76
+ return f;
77
+ }
78
+ Bytes build_iwant(const std::vector<std::string>& ids) {
79
+ Bytes f;
80
+ f.push_back(OP_IWANT);
81
+ put_u16(f, static_cast<uint16_t>(std::min<size_t>(ids.size(), 0xFFFF)));
82
+ for (const std::string& id : ids) { if (id.size() == kIdSize) f.insert(f.end(), id.begin(), id.end()); }
83
+ return f;
84
+ }
85
+
86
+ } // namespace
87
+
88
+ // ── Construction ──────────────────────────────────────────────────────────────
89
+
90
+ PubSub::PubSub() : PubSub(Config{}) {}
91
+
92
+ PubSub::PubSub(Config config) : config_(config) {
93
+ std::random_device rd;
94
+ rng_.seed(rd());
95
+ // Seed the sequence counter randomly so a restarted node does not reuse the
96
+ // (origin, seqno) ids a previous instance with the same PeerId already burned.
97
+ seqno_ = (static_cast<uint64_t>(rd()) << 32) ^ rd();
98
+ }
99
+
100
+ PubSub::~PubSub() { stop(); }
101
+
102
+ // ── Subsystem lifecycle ───────────────────────────────────────────────────────
103
+
104
+ void PubSub::attach(NodeContext& ctx) {
105
+ network_ = &ctx.network;
106
+ network_->on(MessageType::Gossip,
107
+ [this](const Peer& peer, ByteView payload) { on_gossip(peer, payload); });
108
+ network_->on_peer_connected([this](const Peer& peer) { on_new_peer(peer); });
109
+ network_->on_peer_disconnected([this](const PeerId& id) { on_peer_gone(id); });
110
+ }
111
+
112
+ void PubSub::start() {
113
+ if (running_.exchange(true)) return;
114
+ {
115
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
116
+ if (history_.empty()) history_.push_front({}); // current gossip window
117
+ }
118
+ heartbeat_thread_ = std::thread(&PubSub::heartbeat_loop, this);
119
+ }
120
+
121
+ void PubSub::stop() {
122
+ if (!running_.exchange(false)) return;
123
+ hb_cv_.notify_all();
124
+ if (heartbeat_thread_.joinable()) heartbeat_thread_.join();
125
+ }
126
+
127
+ // ── Local subscription API ────────────────────────────────────────────────────
128
+
129
+ void PubSub::subscribe(const std::string& topic, Handler handler) {
130
+ bool is_new;
131
+ CtrlList grafts, prunes;
132
+ {
133
+ std::lock_guard<std::mutex> lock(mutex_);
134
+ is_new = subscriptions_.find(topic) == subscriptions_.end();
135
+ subscriptions_[topic] = std::move(handler);
136
+ topics_[topic]; // ensure peering state exists
137
+ if (is_new) maintain_mesh_locked(topic, grafts, prunes); // graft from peers already known
138
+ }
139
+ if (is_new && network_) {
140
+ broadcast_ctrl(OP_SUBSCRIBE, topic);
141
+ for (const auto& g : grafts) send_ctrl(g.first, OP_GRAFT, g.second);
142
+ for (const auto& p : prunes) send_ctrl(p.first, OP_PRUNE, p.second);
143
+ }
144
+ }
145
+
146
+ void PubSub::unsubscribe(const std::string& topic) {
147
+ std::vector<PeerId> mesh_to_prune;
148
+ bool removed;
149
+ {
150
+ std::lock_guard<std::mutex> lock(mutex_);
151
+ removed = subscriptions_.erase(topic) > 0;
152
+ auto it = topics_.find(topic);
153
+ if (removed && it != topics_.end()) {
154
+ mesh_to_prune.assign(it->second.mesh.begin(), it->second.mesh.end());
155
+ it->second.mesh.clear();
156
+ }
157
+ }
158
+ if (removed && network_) {
159
+ broadcast_ctrl(OP_UNSUBSCRIBE, topic);
160
+ for (const PeerId& p : mesh_to_prune) send_ctrl(p, OP_PRUNE, topic);
161
+ }
162
+ }
163
+
164
+ void PubSub::publish(const std::string& topic, ByteView data) {
165
+ if (!network_) return;
166
+
167
+ const PeerId origin = network_->local_id();
168
+ uint64_t seqno;
169
+ std::vector<PeerId> targets;
170
+ bool subscribed;
171
+ {
172
+ std::lock_guard<std::mutex> lock(mutex_);
173
+ seqno = seqno_++;
174
+ subscribed = subscriptions_.count(topic) > 0;
175
+ Topic& t = topics_[topic];
176
+ if (subscribed) {
177
+ targets.assign(t.mesh.begin(), t.mesh.end()); // eager push along the mesh
178
+ } else {
179
+ // Fanout: top up to fanout_size from known subscribers, then publish to them.
180
+ if (static_cast<int>(t.fanout.size()) < config_.fanout_size) {
181
+ std::vector<PeerId> cands;
182
+ for (const PeerId& p : t.subscribers)
183
+ if (!t.fanout.count(p)) cands.push_back(p);
184
+ for (const PeerId& p : random_sample(std::move(cands),
185
+ config_.fanout_size - static_cast<int>(t.fanout.size())))
186
+ t.fanout.insert(p);
187
+ }
188
+ t.last_fanout = std::chrono::steady_clock::now();
189
+ targets.assign(t.fanout.begin(), t.fanout.end());
190
+ }
191
+ }
192
+
193
+ const std::string id = make_id(origin, seqno);
194
+ const Bytes frame = build_publish(origin, seqno, topic, data);
195
+ mark_seen(id); // so an echo of our own message is dropped
196
+ cache_message(id, topic, frame); // keep content so we can answer IWANT
197
+ deliver_local(origin, topic, data); // a subscribed publisher hears itself
198
+
199
+ for (const PeerId& t : targets) network_->send(t, MessageType::Gossip, ByteView(frame));
200
+ }
201
+
202
+ // ── Inbound dispatch ──────────────────────────────────────────────────────────
203
+
204
+ void PubSub::on_gossip(const Peer& peer, ByteView payload) {
205
+ Reader r{payload.data(), payload.data() + payload.size()};
206
+ const uint8_t op = r.u8();
207
+ const PeerId& from = peer.id();
208
+
209
+ switch (op) {
210
+ case OP_SUBSCRIBE:
211
+ case OP_UNSUBSCRIBE: {
212
+ std::string topic = r.str(r.u16());
213
+ if (r.ok && !topic.empty()) recv_subscription(from, topic, op == OP_SUBSCRIBE);
214
+ return;
215
+ }
216
+ case OP_GRAFT: {
217
+ std::string topic = r.str(r.u16());
218
+ if (r.ok && !topic.empty()) recv_graft(from, topic);
219
+ return;
220
+ }
221
+ case OP_PRUNE: {
222
+ std::string topic = r.str(r.u16());
223
+ if (r.ok && !topic.empty()) recv_prune(from, topic);
224
+ return;
225
+ }
226
+ case OP_IHAVE: {
227
+ std::string topic = r.str(r.u16());
228
+ const uint16_t count = r.u16();
229
+ std::vector<std::string> ids;
230
+ for (uint16_t i = 0; i < count && r.ok; ++i) ids.push_back(r.str(kIdSize));
231
+ if (r.ok && !topic.empty()) recv_ihave(from, topic, ids);
232
+ return;
233
+ }
234
+ case OP_IWANT: {
235
+ const uint16_t count = r.u16();
236
+ std::vector<std::string> ids;
237
+ for (uint16_t i = 0; i < count && r.ok; ++i) ids.push_back(r.str(kIdSize));
238
+ if (r.ok) recv_iwant(from, ids);
239
+ return;
240
+ }
241
+ case OP_PUBLISH: {
242
+ ByteView origin_bytes = r.bytes(PeerId::kSize);
243
+ const uint64_t seqno = r.u64();
244
+ const uint16_t topic_len = r.u16();
245
+ ByteView topic_bytes = r.bytes(topic_len);
246
+ if (!r.ok) return;
247
+ ByteView data = r.rest();
248
+ auto origin = PeerId::from_bytes(origin_bytes);
249
+ if (!origin) return;
250
+ std::string topic(reinterpret_cast<const char*>(topic_bytes.data()), topic_bytes.size());
251
+ recv_publish(from, payload, *origin, seqno, topic, data);
252
+ return;
253
+ }
254
+ default:
255
+ return;
256
+ }
257
+ }
258
+
259
+ void PubSub::recv_subscription(const PeerId& from, const std::string& topic, bool subscribe) {
260
+ CtrlList grafts, prunes;
261
+ {
262
+ std::lock_guard<std::mutex> lock(mutex_);
263
+ Topic& t = topics_[topic];
264
+ if (subscribe) {
265
+ t.subscribers.insert(from);
266
+ if (subscriptions_.count(topic)) maintain_mesh_locked(topic, grafts, prunes);
267
+ } else {
268
+ t.subscribers.erase(from);
269
+ t.mesh.erase(from);
270
+ t.fanout.erase(from);
271
+ }
272
+ }
273
+ for (const auto& g : grafts) send_ctrl(g.first, OP_GRAFT, g.second);
274
+ for (const auto& p : prunes) send_ctrl(p.first, OP_PRUNE, p.second);
275
+ }
276
+
277
+ void PubSub::recv_graft(const PeerId& from, const std::string& topic) {
278
+ bool reject = false;
279
+ {
280
+ std::lock_guard<std::mutex> lock(mutex_);
281
+ Topic& t = topics_[topic];
282
+ if (subscriptions_.count(topic)) {
283
+ t.subscribers.insert(from); // a peer that grafts us is interested
284
+ t.mesh.insert(from);
285
+ } else {
286
+ reject = true; // we don't carry this topic — bounce them back out of the mesh
287
+ }
288
+ }
289
+ if (reject) send_ctrl(from, OP_PRUNE, topic);
290
+ }
291
+
292
+ void PubSub::recv_prune(const PeerId& from, const std::string& topic) {
293
+ std::lock_guard<std::mutex> lock(mutex_);
294
+ auto it = topics_.find(topic);
295
+ if (it != topics_.end()) it->second.mesh.erase(from);
296
+ }
297
+
298
+ void PubSub::recv_publish(const PeerId& from, ByteView frame, const PeerId& origin, uint64_t seqno,
299
+ const std::string& topic, ByteView data) {
300
+ const std::string id = make_id(origin, seqno);
301
+ if (!mark_seen(id)) return; // already processed → stop the loop
302
+
303
+ switch (validate(origin, topic, data)) {
304
+ case ValidationResult::Accept: break;
305
+ case ValidationResult::Reject:
306
+ case ValidationResult::Ignore: return;
307
+ }
308
+
309
+ cache_message(id, topic, frame.to_bytes());
310
+ deliver_local(origin, topic, data);
311
+
312
+ // Forward along our mesh for the topic (only meaningful if we subscribe), never
313
+ // back to the peer we got it from.
314
+ std::vector<PeerId> forward;
315
+ {
316
+ std::lock_guard<std::mutex> lock(mutex_);
317
+ if (subscriptions_.count(topic)) {
318
+ auto it = topics_.find(topic);
319
+ if (it != topics_.end())
320
+ for (const PeerId& p : it->second.mesh)
321
+ if (p != from) forward.push_back(p);
322
+ }
323
+ }
324
+ for (const PeerId& p : forward) network_->send(p, MessageType::Gossip, frame);
325
+ }
326
+
327
+ void PubSub::recv_ihave(const PeerId& from, const std::string& /*topic*/, const std::vector<std::string>& ids) {
328
+ std::vector<std::string> wanted;
329
+ {
330
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
331
+ for (const std::string& id : ids)
332
+ if (id.size() == kIdSize && !seen_.count(id)) wanted.push_back(id);
333
+ }
334
+ if (!wanted.empty()) {
335
+ Bytes frame = build_iwant(wanted);
336
+ network_->send(from, MessageType::Gossip, ByteView(frame));
337
+ }
338
+ }
339
+
340
+ void PubSub::recv_iwant(const PeerId& from, const std::vector<std::string>& ids) {
341
+ std::vector<Bytes> frames;
342
+ {
343
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
344
+ for (const std::string& id : ids) {
345
+ auto it = mcache_.find(id);
346
+ if (it != mcache_.end()) frames.push_back(it->second.frame); // resend real content
347
+ }
348
+ }
349
+ for (const Bytes& f : frames) network_->send(from, MessageType::Gossip, ByteView(f));
350
+ }
351
+
352
+ // ── Peer lifecycle ────────────────────────────────────────────────────────────
353
+
354
+ void PubSub::on_new_peer(const Peer& peer) {
355
+ std::vector<std::string> topics;
356
+ {
357
+ std::lock_guard<std::mutex> lock(mutex_);
358
+ topics.reserve(subscriptions_.size());
359
+ for (const auto& kv : subscriptions_) topics.push_back(kv.first);
360
+ }
361
+ // Announce our interests so the new peer can route to (and mesh with) us.
362
+ for (const std::string& topic : topics) send_ctrl(peer.id(), OP_SUBSCRIBE, topic);
363
+ }
364
+
365
+ void PubSub::on_peer_gone(const PeerId& id) {
366
+ std::lock_guard<std::mutex> lock(mutex_);
367
+ for (auto& kv : topics_) {
368
+ kv.second.subscribers.erase(id);
369
+ kv.second.mesh.erase(id);
370
+ kv.second.fanout.erase(id);
371
+ }
372
+ }
373
+
374
+ // ── Heartbeat: mesh maintenance + gossip emission ─────────────────────────────
375
+
376
+ void PubSub::heartbeat_loop() {
377
+ while (running_.load()) {
378
+ do_heartbeat();
379
+ std::unique_lock<std::mutex> lock(hb_mutex_);
380
+ hb_cv_.wait_for(lock, config_.heartbeat_interval, [this] { return !running_.load(); });
381
+ }
382
+ }
383
+
384
+ void PubSub::do_heartbeat() {
385
+ if (!network_) return;
386
+
387
+ // 1. Collect the ids worth gossiping, grouped by topic, from the recent windows.
388
+ std::unordered_map<std::string, std::vector<std::string>> gossip_by_topic;
389
+ {
390
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
391
+ const int windows = std::min<int>(config_.history_gossip, static_cast<int>(history_.size()));
392
+ for (int i = 0; i < windows; ++i)
393
+ for (const std::string& id : history_[i]) {
394
+ auto it = mcache_.find(id);
395
+ if (it != mcache_.end()) gossip_by_topic[it->second.topic].push_back(id);
396
+ }
397
+ }
398
+
399
+ // 2. Under the topic lock: maintain each subscribed mesh and pick IHAVE targets.
400
+ CtrlList grafts, prunes;
401
+ std::vector<std::pair<std::string, std::vector<PeerId>>> ihave_plan;
402
+ const auto now = std::chrono::steady_clock::now();
403
+ {
404
+ std::lock_guard<std::mutex> lock(mutex_);
405
+ for (const auto& kv : subscriptions_) {
406
+ const std::string& topic = kv.first;
407
+ maintain_mesh_locked(topic, grafts, prunes);
408
+ Topic& t = topics_[topic];
409
+ std::vector<PeerId> cands;
410
+ for (const PeerId& p : t.subscribers)
411
+ if (!t.mesh.count(p)) cands.push_back(p);
412
+ ihave_plan.emplace_back(topic, random_sample(std::move(cands), config_.gossip_factor));
413
+ }
414
+ // Expire idle fanout sets for topics we no longer publish to.
415
+ for (auto& kv : topics_) {
416
+ Topic& t = kv.second;
417
+ if (!subscriptions_.count(kv.first) && !t.fanout.empty() &&
418
+ now - t.last_fanout >= config_.fanout_ttl)
419
+ t.fanout.clear();
420
+ }
421
+ }
422
+
423
+ // 3. Dispatch mesh control + per-topic IHAVE outside the lock.
424
+ for (const auto& g : grafts) send_ctrl(g.first, OP_GRAFT, g.second);
425
+ for (const auto& p : prunes) send_ctrl(p.first, OP_PRUNE, p.second);
426
+ for (const auto& plan : ihave_plan) {
427
+ auto git = gossip_by_topic.find(plan.first);
428
+ if (git == gossip_by_topic.end() || plan.second.empty()) continue;
429
+ Bytes frame = build_ihave(plan.first, git->second);
430
+ for (const PeerId& p : plan.second) network_->send(p, MessageType::Gossip, ByteView(frame));
431
+ }
432
+
433
+ // 4. Shift the gossip history forward and evict messages that aged out.
434
+ {
435
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
436
+ history_.push_front({});
437
+ while (static_cast<int>(history_.size()) > config_.history_length) {
438
+ for (const std::string& id : history_.back()) mcache_.erase(id);
439
+ history_.pop_back();
440
+ }
441
+ }
442
+ }
443
+
444
+ void PubSub::maintain_mesh_locked(const std::string& topic, CtrlList& grafts, CtrlList& prunes) {
445
+ if (!subscriptions_.count(topic)) return; // mesh only exists for topics we subscribe to
446
+ Topic& t = topics_[topic];
447
+
448
+ if (static_cast<int>(t.mesh.size()) < config_.mesh_low) {
449
+ // Below the low watermark: graft random non-mesh subscribers up to the target.
450
+ std::vector<PeerId> cands;
451
+ for (const PeerId& p : t.subscribers)
452
+ if (!t.mesh.count(p)) cands.push_back(p);
453
+ const int need = config_.mesh_target - static_cast<int>(t.mesh.size());
454
+ for (const PeerId& p : random_sample(std::move(cands), need)) {
455
+ t.mesh.insert(p);
456
+ grafts.emplace_back(p, topic);
457
+ }
458
+ } else if (static_cast<int>(t.mesh.size()) > config_.mesh_high) {
459
+ // Above the high watermark: prune random mesh peers back down to the target.
460
+ std::vector<PeerId> mesh_vec(t.mesh.begin(), t.mesh.end());
461
+ const int excess = static_cast<int>(t.mesh.size()) - config_.mesh_target;
462
+ for (const PeerId& p : random_sample(std::move(mesh_vec), excess)) {
463
+ t.mesh.erase(p);
464
+ prunes.emplace_back(p, topic);
465
+ }
466
+ }
467
+ }
468
+
469
+ // ── Helpers ───────────────────────────────────────────────────────────────────
470
+
471
+ void PubSub::send_ctrl(const PeerId& to, uint8_t op, const std::string& topic) {
472
+ if (!network_) return;
473
+ Bytes frame = build_ctrl(op, topic);
474
+ network_->send(to, MessageType::Gossip, ByteView(frame));
475
+ }
476
+
477
+ void PubSub::broadcast_ctrl(uint8_t op, const std::string& topic) {
478
+ if (!network_) return;
479
+ Bytes frame = build_ctrl(op, topic);
480
+ network_->broadcast(MessageType::Gossip, ByteView(frame));
481
+ }
482
+
483
+ void PubSub::deliver_local(const PeerId& from, const std::string& topic, ByteView data) {
484
+ Handler handler = handler_for(topic);
485
+ if (handler) handler(from, topic, data);
486
+ }
487
+
488
+ PubSub::Handler PubSub::handler_for(const std::string& topic) const {
489
+ std::lock_guard<std::mutex> lock(mutex_);
490
+ auto it = subscriptions_.find(topic);
491
+ return it == subscriptions_.end() ? Handler{} : it->second;
492
+ }
493
+
494
+ ValidationResult PubSub::validate(const PeerId& from, const std::string& topic, ByteView data) {
495
+ Validator v;
496
+ {
497
+ std::lock_guard<std::mutex> lock(mutex_);
498
+ auto it = validators_.find(topic);
499
+ if (it != validators_.end()) v = it->second;
500
+ else v = global_validator_;
501
+ }
502
+ return v ? v(from, topic, data) : ValidationResult::Accept;
503
+ }
504
+
505
+ void PubSub::set_validator(const std::string& topic, Validator validator) {
506
+ std::lock_guard<std::mutex> lock(mutex_);
507
+ if (topic.empty()) global_validator_ = std::move(validator);
508
+ else validators_[topic] = std::move(validator);
509
+ }
510
+
511
+ bool PubSub::mark_seen(const std::string& id) {
512
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
513
+ if (!seen_.insert(id).second) return false;
514
+ seen_order_.push_back(id);
515
+ if (seen_order_.size() > config_.seen_limit) {
516
+ seen_.erase(seen_order_.front());
517
+ seen_order_.pop_front();
518
+ }
519
+ return true;
520
+ }
521
+
522
+ void PubSub::cache_message(const std::string& id, const std::string& topic, const Bytes& frame) {
523
+ std::lock_guard<std::mutex> lock(mcache_mutex_);
524
+ if (history_.empty()) history_.push_front({});
525
+ auto inserted = mcache_.emplace(id, CachedMessage{topic, frame});
526
+ if (inserted.second) history_.front().push_back(id); // remember it in the current window
527
+ }
528
+
529
+ std::vector<PeerId> PubSub::random_sample(std::vector<PeerId> in, int n) {
530
+ if (n <= 0 || in.empty()) return {};
531
+ if (static_cast<int>(in.size()) <= n) return in;
532
+ std::lock_guard<std::mutex> lock(rng_mutex_);
533
+ std::shuffle(in.begin(), in.end(), rng_);
534
+ in.resize(n);
535
+ return in;
536
+ }
537
+
538
+ // ── Read-only queries ─────────────────────────────────────────────────────────
539
+
540
+ bool PubSub::is_subscribed(const std::string& topic) const {
541
+ std::lock_guard<std::mutex> lock(mutex_);
542
+ return subscriptions_.count(topic) > 0;
543
+ }
544
+
545
+ std::vector<std::string> PubSub::subscribed_topics() const {
546
+ std::lock_guard<std::mutex> lock(mutex_);
547
+ std::vector<std::string> out;
548
+ out.reserve(subscriptions_.size());
549
+ for (const auto& kv : subscriptions_) out.push_back(kv.first);
550
+ return out;
551
+ }
552
+
553
+ std::vector<PeerId> PubSub::peers_for_topic(const std::string& topic) const {
554
+ std::lock_guard<std::mutex> lock(mutex_);
555
+ auto it = topics_.find(topic);
556
+ if (it == topics_.end()) return {};
557
+ return std::vector<PeerId>(it->second.subscribers.begin(), it->second.subscribers.end());
558
+ }
559
+
560
+ std::vector<PeerId> PubSub::mesh_peers(const std::string& topic) const {
561
+ std::lock_guard<std::mutex> lock(mutex_);
562
+ auto it = topics_.find(topic);
563
+ if (it == topics_.end()) return {};
564
+ return std::vector<PeerId>(it->second.mesh.begin(), it->second.mesh.end());
565
+ }
566
+
567
+ } // namespace librats