librats 2.2.0 → 2.3.1

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 (28) hide show
  1. package/lib/index.d.ts +30 -2
  2. package/lib/index.js +16 -1
  3. package/native-src/CMakeLists.txt +45 -3
  4. package/native-src/src/librats/bindings/rats.cpp +56 -3
  5. package/native-src/src/librats/bindings/rats.h +62 -1
  6. package/native-src/src/librats/core/types.cpp +1 -1
  7. package/native-src/src/librats/core/types.h +9 -6
  8. package/native-src/src/librats/node/config.h +9 -2
  9. package/native-src/src/librats/node/node.cpp +20 -5
  10. package/native-src/src/librats/node/node.h +26 -6
  11. package/native-src/src/librats/node/peer_network.h +19 -2
  12. package/native-src/src/librats/peer/peer.h +17 -6
  13. package/native-src/src/librats/security/noise_security.cpp +2 -0
  14. package/native-src/src/librats/security/plaintext_security.h +1 -0
  15. package/native-src/src/librats/security/session.h +7 -0
  16. package/native-src/src/librats/storage/storage.cpp +434 -213
  17. package/native-src/src/librats/storage/storage.h +163 -19
  18. package/native-src/src/librats/subsystems/file_transfer.cpp +1 -1
  19. package/native-src/src/librats/subsystems/pubsub.cpp +1 -1
  20. package/native-src/src/librats/subsystems/reconnection.cpp +1 -1
  21. package/native-src/src/librats/subsystems/relay.cpp +2 -1
  22. package/native-src/src/librats/transport/connection.cpp +40 -8
  23. package/native-src/src/librats/transport/connection.h +12 -2
  24. package/native-src/src/librats/util/logger.h +37 -5
  25. package/native-src/src/librats/util/network_monitor.cpp +14 -0
  26. package/native-src/src/librats/util/network_utils.cpp +10 -1
  27. package/package.json +1 -1
  28. package/src/librats_node.cpp +50 -1
package/lib/index.d.ts CHANGED
@@ -91,9 +91,23 @@ declare module 'librats' {
91
91
  preferredTransport?: TransportValue;
92
92
  /** Delay before the other transport is raced alongside; 0 disables. Default 1200. */
93
93
  transportFallbackMs?: number;
94
+ /**
95
+ * Bytes a peer's send queue may hold before an app that keeps sending anyway
96
+ * has that peer dropped with reason `RATS_CLOSE_SLOW_CONSUMER`. 0 (default)
97
+ * uses the library's 8 MiB. A quarter of it is where `peerWritable()` starts
98
+ * returning false. Not a maximum message size.
99
+ */
100
+ sendQueueLimit?: number;
94
101
  }
95
102
 
96
103
  export type PeerHandler = (peerId: string) => void;
104
+ /**
105
+ * `reason` is why the peer went — "RATS_CLOSE_PEER_CLOSED",
106
+ * "RATS_CLOSE_SLOW_CONSUMER" and so on. Worth branching on: a peer that left is
107
+ * one to redial, while SLOW_CONSUMER means *you* were sending faster than the
108
+ * link drained, and redialing that one just repeats the overload.
109
+ */
110
+ export type PeerDisconnectHandler = (peerId: string, reason: string) => void;
97
111
  export type MessageHandler = (peerId: string, data: Buffer) => void;
98
112
  export type TopicHandler = (peerId: string, topic: string, data: Buffer) => void;
99
113
  export type JsonHandler = (peerId: string, value: any) => void;
@@ -180,17 +194,31 @@ declare module 'librats' {
180
194
 
181
195
  // ---- raw channel messaging ----
182
196
 
183
- /** Send raw bytes on a named channel to one peer. */
197
+ /**
198
+ * Send raw bytes on a named channel to one peer. Returning means the message
199
+ * was queued, never that it arrived — if you send in bulk, follow it with
200
+ * `peerWritable()`.
201
+ */
184
202
  send(peerId: string, channel: string, data: string | Buffer): void;
185
203
  /** Broadcast raw bytes on a named channel to every connected peer. */
186
204
  broadcast(channel: string, data: string | Buffer): void;
205
+ /**
206
+ * Whether this peer's send queue still has room (false also when it is not
207
+ * connected). False means stop: what you just sent was queued like anything
208
+ * else and nothing was dropped, but keep piling on and the peer is dropped
209
+ * with reason `RATS_CLOSE_SLOW_CONSUMER`. Wait for `onPeerWritable`, or poll
210
+ * this. Not a size limit — one message of any size is always queued.
211
+ */
212
+ peerWritable(peerId: string): boolean;
187
213
  /** Register a handler for a named channel. Additive; register before `start()`. */
188
214
  on(channel: string, handler: MessageHandler): void;
189
215
 
190
216
  // ---- peer events (register before start) ----
191
217
 
192
218
  onPeerConnected(handler: PeerHandler): void;
193
- onPeerDisconnected(handler: PeerHandler): void;
219
+ onPeerDisconnected(handler: PeerDisconnectHandler): void;
220
+ /** A peer whose queue had filled has drained back under its mark. */
221
+ onPeerWritable(handler: PeerHandler): void;
194
222
 
195
223
  // ---- discovery (enable before start) ----
196
224
 
package/lib/index.js CHANGED
@@ -193,6 +193,14 @@ class RatsNode {
193
193
  */
194
194
  broadcast(channel, data) { this._native.broadcast(channel, data); }
195
195
 
196
+ /**
197
+ * Whether this peer's send queue still has room. False means pause and wait for
198
+ * `onPeerWritable` — keep sending regardless and the peer is dropped with
199
+ * reason `RATS_CLOSE_SLOW_CONSUMER`.
200
+ * @param {string} peerId @returns {boolean}
201
+ */
202
+ peerWritable(peerId) { return this._native.peerWritable(peerId); }
203
+
196
204
  /**
197
205
  * Register a handler for a named channel. Additive; register before `start()`.
198
206
  * @param {string} channel
@@ -205,9 +213,16 @@ class RatsNode {
205
213
  /** @param {(peerId: string) => void} handler fired when a peer connects. */
206
214
  onPeerConnected(handler) { this._native.onPeerConnected(handler); }
207
215
 
208
- /** @param {(peerId: string) => void} handler fired when a peer disconnects. */
216
+ /**
217
+ * @param {(peerId: string, reason: string) => void} handler fired when a peer
218
+ * disconnects. `reason` is a name like "RATS_CLOSE_SLOW_CONSUMER", which means
219
+ * this node was sending faster than the link drained.
220
+ */
209
221
  onPeerDisconnected(handler) { this._native.onPeerDisconnected(handler); }
210
222
 
223
+ /** @param {(peerId: string) => void} handler fired when a full queue drained. */
224
+ onPeerWritable(handler) { this._native.onPeerWritable(handler); }
225
+
211
226
  // ---- discovery (enable before start) ----
212
227
 
213
228
  /**
@@ -492,16 +492,58 @@ if(WIN32)
492
492
  target_link_libraries(rats ws2_32 iphlpapi bcrypt advapi32)
493
493
  endif()
494
494
 
495
- if(ANDROID)
495
+ # Android can be built two ways, and only one of them sets CMake's ANDROID variable:
496
+ # through the NDK's android.toolchain.cmake (sets ANDROID / ANDROID_PLATFORM), or by
497
+ # simply pointing CC/CXX at the NDK's <triple><api>-clang wrappers, which is what the
498
+ # Android CI does. The compiler is the only thing that knows in both cases, so ask it
499
+ # rather than trusting a toolchain-file-only variable.
500
+ if(NOT ANDROID)
501
+ check_cxx_source_compiles("
502
+ #ifndef __ANDROID__
503
+ #error not android
504
+ #endif
505
+ int main() { return 0; }
506
+ " RATS_TARGET_IS_ANDROID)
507
+ else()
508
+ set(RATS_TARGET_IS_ANDROID TRUE)
509
+ endif()
510
+
511
+ if(RATS_TARGET_IS_ANDROID)
512
+ # The logger routes console output through __android_log_print, because an app's
513
+ # stdout/stderr go nowhere on Android. This is used from librats/util/logger.h,
514
+ # i.e. from consumers of the static library too, so the dependency is PUBLIC.
515
+ # Only search for it when the NDK toolchain file configured a sysroot to search;
516
+ # with a bare NDK clang, find_library would look at host paths, so let the driver
517
+ # resolve -llog out of its own sysroot instead.
518
+ if(ANDROID)
519
+ find_library(log-lib log)
520
+ target_link_libraries(rats ${log-lib})
521
+ else()
522
+ target_link_libraries(rats log)
523
+ endif()
524
+
525
+ # API level: from the toolchain file when it set one, otherwise from the compiler.
496
526
  if(DEFINED ANDROID_PLATFORM)
497
527
  string(REGEX REPLACE "android-" "" ANDROID_API_LEVEL ${ANDROID_PLATFORM})
498
528
  math(EXPR ANDROID_API_LEVEL "${ANDROID_API_LEVEL}")
499
529
  message(STATUS "Android API level detected: ${ANDROID_API_LEVEL}")
530
+ if(ANDROID_API_LEVEL LESS 24)
531
+ set(RATS_ANDROID_NEEDS_IFADDRS TRUE)
532
+ endif()
500
533
  else()
501
- message(FATAL_ERROR "ANDROID_PLATFORM is not defined!")
534
+ check_cxx_source_compiles("
535
+ #include <android/api-level.h>
536
+ #if __ANDROID_API__ < 24
537
+ #error too old
538
+ #endif
539
+ int main() { return 0; }
540
+ " RATS_ANDROID_API_24_OR_NEWER)
541
+ if(NOT RATS_ANDROID_API_24_OR_NEWER)
542
+ set(RATS_ANDROID_NEEDS_IFADDRS TRUE)
543
+ endif()
502
544
  endif()
503
545
 
504
- if(ANDROID_API_LEVEL LESS 24)
546
+ if(RATS_ANDROID_NEEDS_IFADDRS)
505
547
  target_sources(rats PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/android/ifaddrs-android.c)
506
548
  target_include_directories(rats
507
549
  PRIVATE
@@ -85,6 +85,24 @@ char* dup_string(const std::string& s) {
85
85
  return out;
86
86
  }
87
87
 
88
+ rats_close_reason_t to_c_reason(CloseReason r) {
89
+ switch (r) {
90
+ case CloseReason::LocalClose: return RATS_CLOSE_LOCAL;
91
+ case CloseReason::PeerClosed: return RATS_CLOSE_PEER_CLOSED;
92
+ case CloseReason::PeerReset: return RATS_CLOSE_PEER_RESET;
93
+ case CloseReason::ConnectFailed: return RATS_CLOSE_CONNECT_FAILED;
94
+ case CloseReason::HandshakeFailed: return RATS_CLOSE_HANDSHAKE_FAILED;
95
+ case CloseReason::ProtocolError: return RATS_CLOSE_PROTOCOL_ERROR;
96
+ case CloseReason::SlowConsumer: return RATS_CLOSE_SLOW_CONSUMER;
97
+ case CloseReason::ReactorShutdown: return RATS_CLOSE_SHUTDOWN;
98
+ case CloseReason::DuplicateConn: return RATS_CLOSE_DUPLICATE;
99
+ case CloseReason::PeerLimit: return RATS_CLOSE_PEER_LIMIT;
100
+ case CloseReason::IdleTimeout: return RATS_CLOSE_IDLE_TIMEOUT;
101
+ case CloseReason::DialSuperseded: return RATS_CLOSE_DIAL_SUPERSEDED;
102
+ }
103
+ return RATS_CLOSE_PEER_CLOSED;
104
+ }
105
+
88
106
  } // namespace
89
107
 
90
108
  extern "C" {
@@ -103,6 +121,24 @@ const char* rats_error_str(rats_error_t err) {
103
121
  return "RATS_ERR_UNKNOWN";
104
122
  }
105
123
 
124
+ const char* rats_close_reason_str(rats_close_reason_t reason) {
125
+ switch (reason) {
126
+ case RATS_CLOSE_LOCAL: return "RATS_CLOSE_LOCAL";
127
+ case RATS_CLOSE_PEER_CLOSED: return "RATS_CLOSE_PEER_CLOSED";
128
+ case RATS_CLOSE_PEER_RESET: return "RATS_CLOSE_PEER_RESET";
129
+ case RATS_CLOSE_CONNECT_FAILED: return "RATS_CLOSE_CONNECT_FAILED";
130
+ case RATS_CLOSE_HANDSHAKE_FAILED: return "RATS_CLOSE_HANDSHAKE_FAILED";
131
+ case RATS_CLOSE_PROTOCOL_ERROR: return "RATS_CLOSE_PROTOCOL_ERROR";
132
+ case RATS_CLOSE_SLOW_CONSUMER: return "RATS_CLOSE_SLOW_CONSUMER";
133
+ case RATS_CLOSE_SHUTDOWN: return "RATS_CLOSE_SHUTDOWN";
134
+ case RATS_CLOSE_DUPLICATE: return "RATS_CLOSE_DUPLICATE";
135
+ case RATS_CLOSE_PEER_LIMIT: return "RATS_CLOSE_PEER_LIMIT";
136
+ case RATS_CLOSE_IDLE_TIMEOUT: return "RATS_CLOSE_IDLE_TIMEOUT";
137
+ case RATS_CLOSE_DIAL_SUPERSEDED: return "RATS_CLOSE_DIAL_SUPERSEDED";
138
+ }
139
+ return "RATS_CLOSE_UNKNOWN";
140
+ }
141
+
106
142
  /* — construction / lifecycle — */
107
143
 
108
144
  rats_config_t rats_config_default(void) {
@@ -118,6 +154,7 @@ rats_config_t rats_config_default(void) {
118
154
  c.enable_udp = 1;
119
155
  c.preferred_transport = RATS_TRANSPORT_UDP;
120
156
  c.transport_fallback_ms = 1200;
157
+ c.send_queue_limit = 0;
121
158
  return c;
122
159
  }
123
160
 
@@ -153,6 +190,7 @@ rats_t rats_create_config(const rats_config_t* cfg) {
153
190
  ? TransportKind::Tcp
154
191
  : TransportKind::Udp;
155
192
  config.transport_fallback_ms = cfg->transport_fallback_ms;
193
+ config.send_queue_limit = cfg->send_queue_limit;
156
194
  }
157
195
  return make_handle(std::move(config));
158
196
  }
@@ -224,6 +262,13 @@ rats_error_t rats_send(rats_t node, const char* peer_id_hex,
224
262
  return RATS_OK;
225
263
  }
226
264
 
265
+ int rats_peer_writable(rats_t node, const char* peer_id_hex) {
266
+ if (!peer_id_hex) return 0;
267
+ auto id = PeerId::from_hex(peer_id_hex);
268
+ if (!id) return 0;
269
+ return node_of(node)->peer_writable(*id) ? 1 : 0;
270
+ }
271
+
227
272
  rats_error_t rats_broadcast(rats_t node, const char* channel, const void* data, size_t len) {
228
273
  if (!channel) return RATS_ERR_INVALID_ARG;
229
274
  node_of(node)->broadcast(channel, ByteView(static_cast<const uint8_t*>(data), len));
@@ -238,10 +283,18 @@ rats_error_t rats_on_peer_connected(rats_t node, rats_peer_cb cb, void* user) {
238
283
  return RATS_OK;
239
284
  }
240
285
 
241
- rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_cb cb, void* user) {
286
+ rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_disconnect_cb cb, void* user) {
287
+ if (!cb) return RATS_ERR_INVALID_ARG;
288
+ node_of(node)->on_peer_disconnected([cb, user](const PeerId& id, CloseReason reason) {
289
+ cb(user, id.to_hex().c_str(), to_c_reason(reason));
290
+ });
291
+ return RATS_OK;
292
+ }
293
+
294
+ rats_error_t rats_on_peer_writable(rats_t node, rats_peer_cb cb, void* user) {
242
295
  if (!cb) return RATS_ERR_INVALID_ARG;
243
- node_of(node)->on_peer_disconnected([cb, user](const PeerId& id) {
244
- cb(user, id.to_hex().c_str());
296
+ node_of(node)->on_peer_writable([cb, user](const Peer& peer) {
297
+ cb(user, peer.id().to_hex().c_str());
245
298
  });
246
299
  return RATS_OK;
247
300
  }
@@ -53,6 +53,33 @@ typedef enum {
53
53
  RATS_TRANSPORT_RELAY = 2
54
54
  } rats_transport_t;
55
55
 
56
+ /* Why a peer connection ended, handed to rats_on_peer_disconnected(). The
57
+ * reason is what tells "I was sending too fast" apart from "the peer left" —
58
+ * without it both look the same and the usual answer to either is to reconnect
59
+ * and repeat whatever caused it. Use rats_close_reason_str() for a name. */
60
+ typedef enum {
61
+ RATS_CLOSE_LOCAL = 0, /* we asked to disconnect */
62
+ RATS_CLOSE_PEER_CLOSED = 1, /* the peer closed cleanly */
63
+ RATS_CLOSE_PEER_RESET = 2, /* connection reset / socket error */
64
+ RATS_CLOSE_CONNECT_FAILED = 3, /* the dial never completed */
65
+ RATS_CLOSE_HANDSHAKE_FAILED = 4, /* secure handshake failed or timed out */
66
+ RATS_CLOSE_PROTOCOL_ERROR = 5, /* malformed frame / decryption failure */
67
+ /* We kept sending while this peer's queue was already past its limit. THE
68
+ * ONE REASON THE APPLICATION CAN PREVENT: heed rats_send()'s answer and
69
+ * rats_peer_writable(), and wait for the writable callback. It does NOT mean
70
+ * a single message was too big — one message is always accepted whatever its
71
+ * size; only piling more on a full queue gets a peer dropped. */
72
+ RATS_CLOSE_SLOW_CONSUMER = 6,
73
+ RATS_CLOSE_SHUTDOWN = 7, /* the node is stopping */
74
+ RATS_CLOSE_DUPLICATE = 8, /* superseded by another link to the same peer */
75
+ RATS_CLOSE_PEER_LIMIT = 9, /* inbound refused: peer limit reached */
76
+ RATS_CLOSE_IDLE_TIMEOUT = 10, /* datagram link went silent */
77
+ RATS_CLOSE_DIAL_SUPERSEDED = 11 /* a racing dial over the other transport won */
78
+ } rats_close_reason_t;
79
+
80
+ /** Static human-readable name for a close reason (never NULL, never freed). */
81
+ RATS_API const char* rats_close_reason_str(rats_close_reason_t reason);
82
+
56
83
  /* Bitmask of transports (see rats_transports / rats_peer_transports). */
57
84
  #define RATS_TRANSPORT_MASK_TCP 0x1u
58
85
  #define RATS_TRANSPORT_MASK_UDP 0x2u
@@ -102,6 +129,14 @@ typedef struct {
102
129
  rats_transport_t preferred_transport; /* tried first when dialing (default UDP) */
103
130
  uint32_t transport_fallback_ms; /* delay before racing the other transport;
104
131
  * 0 = never fall back (default 1200) */
132
+
133
+ /* Bytes a peer's send queue may hold before an application that keeps sending
134
+ * anyway has that peer dropped with RATS_CLOSE_SLOW_CONSUMER. 0 = the library
135
+ * default (8 MiB). A quarter of it is where rats_peer_writable() starts
136
+ * answering 0, so lowering this makes an application feel backpressure sooner.
137
+ * It is NOT a maximum message size: one message is always queued whatever its
138
+ * size. */
139
+ size_t send_queue_limit;
105
140
  } rats_config_t;
106
141
 
107
142
  /** A config pre-filled with the library defaults (listening, Noise, ephemeral
@@ -153,18 +188,44 @@ RATS_API size_t rats_max_peers(rats_t node);
153
188
 
154
189
  /* — messaging (named application channel, raw bytes) — */
155
190
 
191
+ /** Queue bytes for one peer on a named channel. RATS_OK means accepted and
192
+ * queued, never that it arrived. Pair it with rats_peer_writable() below if you
193
+ * send in bulk — that is the only way to learn you are outrunning the link
194
+ * before the peer is dropped for it. */
156
195
  RATS_API rats_error_t rats_send(rats_t node, const char* peer_id_hex,
157
196
  const char* channel, const void* data, size_t len);
158
197
  RATS_API rats_error_t rats_broadcast(rats_t node, const char* channel,
159
198
  const void* data, size_t len);
160
199
 
200
+ /** Whether this peer's send queue still has room — the same question rats_send()
201
+ * answers, asked without sending. Returns 1 for room, 0 for none (and 0 for a
202
+ * peer that is not connected).
203
+ *
204
+ * A 0 says "stop". The message you just sent was queued like any other and
205
+ * nothing was dropped, but keep piling on and this peer is dropped with
206
+ * RATS_CLOSE_SLOW_CONSUMER. Wait for the callback registered with
207
+ * rats_on_peer_writable(), or poll this from a thread of your own.
208
+ *
209
+ * Ask it right after rats_send(): the bytes you just handed over are already
210
+ * counted, so the answer covers your own send and not just what the reactor has
211
+ * got round to. It is NOT a size limit — a single message of any size is always
212
+ * accepted; only sending more on top of a full queue drops a peer. */
213
+ RATS_API int rats_peer_writable(rats_t node, const char* peer_id_hex);
214
+
161
215
  /* — callbacks (register before start; invoked on a reactor thread) — */
162
216
 
163
217
  typedef void (*rats_peer_cb)(void* user, const char* peer_id_hex);
218
+ typedef void (*rats_peer_disconnect_cb)(void* user, const char* peer_id_hex,
219
+ rats_close_reason_t reason);
164
220
  typedef void (*rats_message_cb)(void* user, const char* peer_id_hex, const void* data, size_t len);
165
221
 
166
222
  RATS_API rats_error_t rats_on_peer_connected(rats_t node, rats_peer_cb cb, void* user);
167
- RATS_API rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_cb cb, void* user);
223
+ RATS_API rats_error_t rats_on_peer_disconnected(rats_t node, rats_peer_disconnect_cb cb,
224
+ void* user);
225
+ /** "This peer can be written to again" — fired when a peer whose queue had filled
226
+ * past its mark has drained back under it. The other half of rats_send()
227
+ * reporting 0; an application that never asks never needs this. */
228
+ RATS_API rats_error_t rats_on_peer_writable(rats_t node, rats_peer_cb cb, void* user);
168
229
  RATS_API rats_error_t rats_on(rats_t node, const char* channel, rats_message_cb cb, void* user);
169
230
 
170
231
  /* — optional subsystems (enable before start) — */
@@ -13,7 +13,7 @@ const char* to_string(ConnState s) noexcept {
13
13
  return "?";
14
14
  }
15
15
 
16
- const char* to_string(CloseReason r) noexcept {
16
+ RATS_API const char* to_string(CloseReason r) noexcept {
17
17
  switch (r) {
18
18
  case CloseReason::LocalClose: return "LocalClose";
19
19
  case CloseReason::PeerClosed: return "PeerClosed";
@@ -84,7 +84,9 @@ enum class CloseReason {
84
84
  ConnectFailed, ///< Outbound transport connect never completed.
85
85
  HandshakeFailed, ///< Secure-channel handshake failed or timed out.
86
86
  ProtocolError, ///< Malformed frame / decryption failure on the wire.
87
- SlowConsumer, ///< Send buffer exceeded its high-water mark.
87
+ SlowConsumer, ///< Kept sending with the queue already past its high-water
88
+ ///< mark. Not "a message was too large": one message is
89
+ ///< always queued whatever its size (Connection::send).
88
90
  ReactorShutdown, ///< Reactor is stopping.
89
91
  DuplicateConn, ///< Redundant connection to a peer we already hold; superseded.
90
92
  PeerLimit, ///< Inbound rejected: the configured peer limit is reached.
@@ -93,11 +95,12 @@ enum class CloseReason {
93
95
  };
94
96
 
95
97
  const char* to_string(ConnState) noexcept;
96
- const char* to_string(CloseReason) noexcept;
97
- /// Exported, unlike its siblings above: TransportKind is part of the public
98
- /// surface (NodeConfig::preferred_transport, PeerInfo::transport), so a consumer
99
- /// of the shared build needs to be able to render one. ConnState/CloseReason only
100
- /// ever appear on Connection, which does not cross the library boundary.
98
+ /// Exported: a CloseReason is handed to every on_peer_disconnected handler, so a
99
+ /// consumer of the shared build has to be able to render one. (ConnState above is
100
+ /// not it never leaves Connection.)
101
+ RATS_API const char* to_string(CloseReason) noexcept;
102
+ /// Exported for the same reason: TransportKind is part of the public surface
103
+ /// (NodeConfig::preferred_transport, PeerInfo::transport).
101
104
  RATS_API const char* to_string(TransportKind) noexcept;
102
105
 
103
106
  } // namespace librats
@@ -52,8 +52,15 @@ struct RATS_API NodeConfig {
52
52
  /// connection. 0 disables the fallback: only the preferred transport is tried.
53
53
  uint32_t transport_fallback_ms = 1200;
54
54
 
55
- /// Bytes a peer's send queue may hold before the peer is dropped as a slow
56
- /// consumer. 0 uses the library default (8 MiB).
55
+ /// Bytes a peer's send queue may hold before an application that keeps
56
+ /// sending anyway has the peer dropped as a slow consumer. 0 uses the library
57
+ /// default (8 MiB).
58
+ ///
59
+ /// It is not a maximum message size. A single message is always queued
60
+ /// whatever its size — a message cannot be sent by halves, and a healthy
61
+ /// connection must not die over one large frame — so the queue's real ceiling
62
+ /// is this plus one message. What gets a peer dropped is offering *another*
63
+ /// message while the queue is still over the mark.
57
64
  ///
58
65
  /// A quarter of this is the mark at which send() starts answering "no room"
59
66
  /// and on_peer_writable is what says the room is back — so lowering it makes
@@ -488,7 +488,15 @@ bool Node::send(const PeerId& to, std::string_view channel, ByteView payload) {
488
488
 
489
489
  bool Node::peer_writable(const PeerId& id) const {
490
490
  const auto dest = peers_.destination(id);
491
- return dest && dest->writable;
491
+ if (!dest) return false;
492
+ // Both halves of the backlog, exactly as send() weighs them: what the reactor
493
+ // has already queued (`writable`) *and* what a caller has handed over that the
494
+ // reactor has not taken up yet (`owed`). Reporting only the first would answer
495
+ // "there is room" to a caller that has just filled the queue itself and is
496
+ // waiting to hear otherwise — the queue it filled has not been looked at yet,
497
+ // so nothing about it has changed, and no event is coming either.
498
+ return dest->writable &&
499
+ dest->owed->load(std::memory_order_relaxed) <= send_low_water();
492
500
  }
493
501
 
494
502
  bool Node::broadcast(std::string_view channel, ByteView payload) {
@@ -682,7 +690,7 @@ void Node::on_closed(Connection& conn, CloseReason reason) {
682
690
  // let a long-gone peer keep voting on how our NAT behaves.
683
691
  nat_status_.forget(id);
684
692
  LOG_INFO("node", "Peer " << id.short_hex() << " disconnected (" << to_string(reason) << ")");
685
- for (auto& cb : peer_disconnected_) cb(id);
693
+ for (auto& cb : peer_disconnected_) cb(id, reason);
686
694
  }
687
695
 
688
696
  void Node::on_writable_changed(Connection& conn, bool writable) {
@@ -849,9 +857,16 @@ std::vector<Address> Node::observed_addresses() const {
849
857
 
850
858
  // ── Peer handle methods (defined here for the full Node type) ────────────────
851
859
 
852
- void Peer::send(std::string_view channel, ByteView payload) const {
853
- node_->route_send(route_, FrameHeader{MessageType::App, 0, MessageRouter::channel_id(channel)},
854
- payload.to_bytes());
860
+ bool Peer::send(std::string_view channel, ByteView payload) const {
861
+ // Deliberately by id, not by this handle's route. The route existed to skip the
862
+ // directory lookup on the reply path — but answering "is there room?" needs the
863
+ // peer's writability and its in-transit counter, both of which live in the
864
+ // directory, so the lookup is unavoidable the moment send() has an answer to
865
+ // give. Once it is being paid anyway, the captured route buys nothing and can
866
+ // only be wrong: a handle outlives its connection (a relayed link superseded by
867
+ // a direct one, a dial race, a reconnect), and sending on the superseded one
868
+ // drops the message with nothing said. The peer is what the caller means.
869
+ return node_->send(id_, channel, payload);
855
870
  }
856
871
 
857
872
  void Peer::disconnect() const {
@@ -193,6 +193,14 @@ public:
193
193
  /// low-water mark, and an application that keeps going regardless
194
194
  /// will eventually have the peer dropped as a slow consumer. Wait for
195
195
  /// on_peer_writable instead. Also false if the peer is not connected.
196
+ ///
197
+ /// "Stop" is meant literally, and yielding is part of it: the mark is
198
+ /// re-tested inside the reactor task this call hands off to, and it is
199
+ /// that test which flips the peer to unwritable and later raises
200
+ /// on_peer_writable. A caller that answers a false by looping straight
201
+ /// back into send() therefore starves the very thread that would tell
202
+ /// it to stop — the queue keeps growing while the signal it is waiting
203
+ /// for never gets a turn to be produced.
196
204
  bool send(const PeerId& to, std::string_view channel, ByteView payload);
197
205
  /// Send raw bytes on a named channel to every connected peer.
198
206
  /// @return whether *every* one of them still has room — a fan-out can only
@@ -205,12 +213,24 @@ public:
205
213
  /// transfer, a large stream — address peers individually with send().
206
214
  bool broadcast(std::string_view channel, ByteView payload);
207
215
 
208
- /// Whether a peer's send queue currently has room the same answer send()
209
- /// returns, without sending anything. False for a peer that is not connected.
210
- /// Note that the answer describes the queue as the reactor last left it: a
211
- /// message handed over a moment ago may not be counted yet, which is why the
212
- /// signal to stop is the *return of send()* rather than a poll before it.
213
- bool peer_writable(const PeerId& id) const;
216
+ /// Whether a peer's send queue has room for more. False for a peer that is
217
+ /// not connected.
218
+ ///
219
+ /// The same question send() answers, asked without sending anything: it
220
+ /// weighs both halves of what the peer is carrying the bytes the reactor
221
+ /// has queued, and the bytes a caller has handed to send() that the reactor
222
+ /// has not taken up yet. So it is safe to poll: a caller that has just filled
223
+ /// the queue in a tight loop keeps being told "no room" until the reactor has
224
+ /// actually looked at what it was given, rather than being told "go on"
225
+ /// because nothing observable has changed yet.
226
+ ///
227
+ /// It stays a hint about a queue that drains as it is read, so the ordinary
228
+ /// flow is unchanged: the signal to stop is the return of send(), and the
229
+ /// signal to resume is on_peer_writable. This is for a caller that must wait
230
+ /// for room on a thread of its own — the event alone cannot serve it, because
231
+ /// a queue that filled *only* with bytes still in transit never crossed
232
+ /// anything on the connection and so raises no event when they drain.
233
+ bool peer_writable(const PeerId& id) const override;
214
234
 
215
235
  // — events (register before start(); invoked on a reactor thread). Multiple
216
236
  // listeners are supported, so subsystems and the app can both subscribe. —
@@ -17,6 +17,7 @@
17
17
  #include "librats/peer/peer_id.h"
18
18
  #include "librats/peer/peer_info.h"
19
19
  #include "librats/core/address.h"
20
+ #include "librats/core/types.h" // CloseReason
20
21
 
21
22
  #include <cstdint>
22
23
  #include <functional>
@@ -33,7 +34,12 @@ public:
33
34
  using MessageHandler = std::function<void(const Peer&, ByteView)>;
34
35
 
35
36
  using PeerEventHandler = std::function<void(const Peer&)>;
36
- using PeerDisconnectHandler = std::function<void(const PeerId&)>;
37
+ /// A peer went away, and why. The reason matters as much as the event: an
38
+ /// application dropped as a slow consumer (CloseReason::SlowConsumer) has to
39
+ /// slow down, while one whose peer simply left should reconnect — and without
40
+ /// the reason those look identical, so the usual answer to both is to redial
41
+ /// and repeat whatever caused it.
42
+ using PeerDisconnectHandler = std::function<void(const PeerId&, CloseReason)>;
37
43
  using DialFailedHandler = std::function<void(const Address&)>;
38
44
 
39
45
  virtual const PeerId& local_id() const = 0;
@@ -49,7 +55,9 @@ public:
49
55
  /// Send to one peer. @return whether that peer's send queue still has room;
50
56
  /// false means "stop and wait for on_peer_writable" — the message is queued
51
57
  /// either way, but continuing past this is what gets a peer dropped as a slow
52
- /// consumer. Also false if the peer is not connected.
58
+ /// consumer. Also false if the peer is not connected. Wait on the event and
59
+ /// not on a poll of the queue's state: the answer is re-derived on the reactor
60
+ /// thread, so a caller that spins instead of yielding never lets it change.
53
61
  virtual bool send(const PeerId& to, MessageType type, ByteView payload) = 0;
54
62
  /// Send to every connected peer. @return whether *every* one of them still
55
63
  /// has room, so a subsystem that fans out can pause on the slowest.
@@ -72,6 +80,15 @@ public:
72
80
  /// subsystem that never checks that return never needs this either. Runs on a
73
81
  /// reactor thread. Default: not offered (nothing subscribes).
74
82
  virtual void on_peer_writable(PeerEventHandler /*handler*/) {}
83
+ /// The same question send() answers — "has this peer room for more?" — asked
84
+ /// without sending anything. For a subsystem that paces a long fan-out from a
85
+ /// thread of its own (StorageManager streaming a snapshot): the event alone
86
+ /// cannot serve it, because a queue filled *only* with bytes handed to send()
87
+ /// that the reactor has not taken up yet never crossed anything on the
88
+ /// connection, so nothing raises on_peer_writable when they drain. Safe to
89
+ /// poll — it weighs those in-flight bytes too. False for an unknown peer.
90
+ /// Default: always writable, all a mock that merely moves messages can claim.
91
+ virtual bool peer_writable(const PeerId& /*id*/) const { return true; }
75
92
  };
76
93
 
77
94
  struct NodeContext; // node/node_context.h — bundles network + events + services
@@ -4,10 +4,12 @@
4
4
  * @file peer.h
5
5
  * @brief A lightweight handle to a connected peer.
6
6
  *
7
- * Peer is a value passed to callbacks. It carries the peer's id and its
8
- * route (which reactor + connection), so send()/disconnect() reach the right
9
- * reactor directly no PeerTable lookup on the reply path. info() does
10
- * consult the directory, on demand, for metadata.
7
+ * Peer is a value passed to callbacks. It carries the peer's id and its route
8
+ * (which reactor + connection). disconnect() uses the route, reaching that exact
9
+ * connection with no directory lookup. send() goes by id instead: it has to
10
+ * consult the directory anyway for the backpressure answer it returns, and by
11
+ * then the peer's *current* route is the better destination — a handle can
12
+ * outlive the link it names. info() consults the directory on demand too.
11
13
  */
12
14
 
13
15
  #include "librats/util/rats_export.h"
@@ -27,8 +29,17 @@ class RATS_API Peer {
27
29
  public:
28
30
  const PeerId& id() const noexcept { return id_; }
29
31
 
30
- /// Send bytes on a named application channel.
31
- void send(std::string_view channel, ByteView payload) const;
32
+ /// Send bytes on a named application channel, to this peer over whichever
33
+ /// connection currently serves it.
34
+ ///
35
+ /// @return whether that peer's queue still has room — the same answer, and the
36
+ /// same contract, as Node::send(): false means stop and wait for
37
+ /// on_peer_writable rather than keep going. A handler that replies
38
+ /// through this handle is the most ordinary way to write to a peer, so
39
+ /// it has to be able to feel backpressure like any other sender; while
40
+ /// this returned void it could not, and the bytes it queued were
41
+ /// invisible to peer_writable() into the bargain.
42
+ bool send(std::string_view channel, ByteView payload) const;
32
43
 
33
44
  /// Request this peer be disconnected.
34
45
  void disconnect() const;
@@ -44,6 +44,8 @@ public:
44
44
  return true;
45
45
  }
46
46
 
47
+ size_t overhead() const noexcept override { return librats::NOISE_TAG_SIZE; }
48
+
47
49
  const PeerId& remote_id() const override { return remote_id_; }
48
50
  bool is_secure() const override { return true; }
49
51
 
@@ -32,6 +32,7 @@ class PlaintextSession final : public Session {
32
32
  public:
33
33
  explicit PlaintextSession(PeerId remote) : remote_id_(remote) {}
34
34
  bool encrypt(ByteView plain, Bytes& out) override { out.assign(plain.begin(), plain.end()); return true; }
35
+ size_t overhead() const noexcept override { return 0; }
35
36
  bool decrypt(ByteView cipher, Bytes& out) override { out.assign(cipher.begin(), cipher.end()); return true; }
36
37
  const PeerId& remote_id() const override { return remote_id_; }
37
38
  bool is_secure() const override { return false; }
@@ -30,6 +30,13 @@ public:
30
30
  /// The remote peer's identity, proven during the handshake.
31
31
  virtual const PeerId& remote_id() const = 0;
32
32
 
33
+ /// Bytes encrypt() adds to a plaintext of any size (an AEAD tag, typically).
34
+ /// The send path needs the ciphertext's size *before* encrypting it, because
35
+ /// a message it turns out it cannot frame must be refused without a nonce
36
+ /// having been spent on it — the counters run in lockstep at both ends, so a
37
+ /// message encrypted and then not sent would break every one after it.
38
+ virtual size_t overhead() const noexcept = 0;
39
+
33
40
  /// True if traffic is actually encrypted (false for the plaintext passthrough).
34
41
  virtual bool is_secure() const = 0;
35
42
  };