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
@@ -11,18 +11,40 @@
11
11
  *
12
12
  * Replication model — an epidemic LWW broadcast:
13
13
  * - A local put/remove builds a StorageEntry (a delete is a tombstone entry
14
- * with `deleted=true`) and broadcasts it to all connected peers.
14
+ * with `deleted=true`) and sends it to every connected peer.
15
15
  * - On receiving an entry, a node applies it under LWW. It re-forwards the
16
16
  * entry to its *other* peers ONLY if the entry actually won (carried new
17
17
  * information). A duplicate loses LWW and is not forwarded, so flooding
18
18
  * terminates naturally — no separate dedup table needed.
19
- * - On peer connect, both sides exchange a full snapshot (anti-entropy) so a
20
- * late joiner catches up. LWW makes the merge order-independent.
19
+ * - On peer connect, both sides ask each other for a full snapshot
20
+ * (anti-entropy) so a late joiner catches up. LWW makes the merge
21
+ * order-independent.
22
+ *
23
+ * Backpressure — why a snapshot is a *stream*, not a message:
24
+ * A database is unbounded, a peer's send queue is not. One message carrying
25
+ * the whole store stops being sendable at the connection's low-water mark,
26
+ * gets the peer dropped as a slow consumer past its high-water mark, and is
27
+ * not even representable past the block-size ceiling — and since both ends
28
+ * request a snapshot on connect, two of them cross on every single link. So
29
+ * a snapshot is served as a sequence of bounded SYNC_CHUNK messages walking
30
+ * the (ordered) key space, one chunk at a time, and the next chunk is only
31
+ * produced while `PeerNetwork::peer_writable()` says there is room. Every
32
+ * send() return value is honoured: a peer that fills up stops receiving
33
+ * individual entries and is instead owed a fresh snapshot, which the sync
34
+ * thread starts once on_peer_writable says the link has drained. Losing live
35
+ * updates to a congested peer is safe precisely because the store is LWW —
36
+ * the snapshot that follows carries the winning state either way.
37
+ *
38
+ * All of that runs on this module's own sync thread. Serialising even one
39
+ * chunk happens off the reactor thread, so a reactor never spends time (or
40
+ * holds storage_mutex_) proportional to the size of the database.
21
41
  *
22
42
  * Wire format (MessageType::Storage payload, opcode in byte 0):
23
43
  * ENTRY: [1][StorageEntry.serialize()]
24
44
  * SYNC_REQUEST: [2]
25
- * SYNC_RESPONSE: [3][count:u32][StorageEntry.serialize()] * count
45
+ * SYNC_CHUNK: [3][flags:u8][count:u32][StorageEntry.serialize()] * count
46
+ * flags bit0 (LAST) marks the final chunk of a snapshot; a
47
+ * snapshot of an empty store is one chunk with count 0.
26
48
  *
27
49
  * The class is also usable standalone (no network attached) as a local,
28
50
  * persistent key-value store; all network operations no-op until attach().
@@ -38,6 +60,7 @@
38
60
  #include <string>
39
61
  #include <vector>
40
62
  #include <functional>
63
+ #include <map>
41
64
  #include <memory>
42
65
  #include <mutex>
43
66
  #include <unordered_map>
@@ -113,7 +136,23 @@ struct RATS_API StorageEntry {
113
136
  // Serialize entry to binary format
114
137
  std::vector<uint8_t> serialize() const;
115
138
 
116
- // Deserialize entry from binary format
139
+ /// Append the serialized entry to `out`. What serialize() is built on: a
140
+ /// batch appends entry after entry into one buffer instead of allocating a
141
+ /// vector per entry only to copy it away again.
142
+ void serialize_into(std::vector<uint8_t>& out) const;
143
+
144
+ /// Serialized size in bytes, without serializing.
145
+ size_t serialized_size() const;
146
+
147
+ /// Deserialize one entry from `data[offset..]`, setting `bytes_read` to the
148
+ /// bytes it consumed. Every field is bounds-checked against the entry's own
149
+ /// declared length as well as the buffer, so a hostile length can neither
150
+ /// read past the buffer nor reach into the entry that follows.
151
+ static bool deserialize(const uint8_t* data, size_t size, size_t offset,
152
+ StorageEntry& entry, size_t& bytes_read);
153
+
154
+ /// Vector overload of the above; the wire path uses the pointer form to
155
+ /// parse straight out of the receive buffer without copying it first.
117
156
  static bool deserialize(const std::vector<uint8_t>& data, size_t offset,
118
157
  StorageEntry& entry, size_t& bytes_read);
119
158
 
@@ -136,23 +175,51 @@ struct StorageChangeEvent {
136
175
  };
137
176
 
138
177
  /**
139
- * Storage configuration
178
+ * Storage configuration.
179
+ *
180
+ * The two size limits are not free parameters: both are bounded by what one
181
+ * peer's send queue can hold. A connection drops its peer as a slow consumer
182
+ * past its high-water mark (8 MiB by default, NodeConfig::send_queue_limit) and
183
+ * reports "no room" at a quarter of it, so a single value — and a single
184
+ * snapshot chunk — must stay well inside that quarter, or the very first one
185
+ * would kill the link it travels on. Both are clamped to kMaxValueSize /
186
+ * kMaxSyncBatchBytes on construction and in set_config().
140
187
  */
141
188
  struct StorageConfig {
189
+ /// Ceiling on max_value_size: the default connection low-water mark. One
190
+ /// value at the ceiling makes a peer unwritable and is then waited out,
191
+ /// rather than counting toward the high-water mark that drops it.
192
+ static constexpr uint32_t kMaxValueSize = 2 * 1024 * 1024;
193
+ /// Ceiling on sync_batch_bytes, for the same reason.
194
+ static constexpr uint32_t kMaxSyncBatchBytes = 2 * 1024 * 1024;
195
+ /// Floor on sync_batch_bytes: below this a snapshot costs more in per-message
196
+ /// framing and round trips than it saves in queue occupancy.
197
+ static constexpr uint32_t kMinSyncBatchBytes = 16 * 1024;
198
+
142
199
  std::string data_directory; // Directory for storage files
143
200
  std::string database_name; // Database filename prefix
144
201
  bool enable_sync; // Enable network synchronization
145
202
  uint32_t compaction_threshold; // Number of tombstones before compaction
146
- uint32_t max_value_size; // Maximum value size in bytes
203
+ uint32_t max_value_size; // Maximum value size in bytes (<= kMaxValueSize)
147
204
  bool persist_to_disk; // Whether to persist data to disk
205
+ /// Target payload size of one snapshot chunk. The walk stops at the first
206
+ /// entry that takes the buffer past this, so a chunk is this size plus at
207
+ /// most one entry — which is why max_value_size shares the same ceiling.
208
+ uint32_t sync_batch_bytes;
209
+ /// Minimum gap between two snapshots served to the same peer. Bounds what a
210
+ /// peer can make us spend by asking, and paces the re-sync a peer is owed
211
+ /// after it has been congested.
212
+ uint32_t sync_min_interval_ms;
148
213
 
149
214
  StorageConfig()
150
215
  : data_directory("./storage"),
151
216
  database_name("rats_storage"),
152
217
  enable_sync(true),
153
218
  compaction_threshold(1000),
154
- max_value_size(16 * 1024 * 1024), // 16MB max value size
155
- persist_to_disk(true) {}
219
+ max_value_size(1024 * 1024), // 1 MiB max value size
220
+ persist_to_disk(true),
221
+ sync_batch_bytes(256 * 1024), // 256 KiB per snapshot chunk
222
+ sync_min_interval_ms(5000) {}
156
223
  };
157
224
 
158
225
  /**
@@ -167,6 +234,9 @@ struct StorageStatistics {
167
234
  uint64_t entries_sent; // Entries sent to peers
168
235
  uint64_t sync_requests_received; // Number of sync requests received
169
236
  uint64_t sync_requests_sent; // Number of sync requests sent
237
+ uint64_t sync_chunks_sent; // Snapshot chunks put on the wire
238
+ uint64_t sync_chunks_received; // Snapshot chunks applied from peers
239
+ uint64_t resyncs_scheduled; // Snapshots owed to peers that filled up
170
240
  std::chrono::steady_clock::time_point last_sync_time; // Last sync timestamp
171
241
  StorageSyncStatus sync_status; // Current sync status
172
242
  };
@@ -216,6 +286,9 @@ public:
216
286
  // Configuration
217
287
  // =========================================================================
218
288
 
289
+ /// Replace the configuration. Size limits are clamped (see StorageConfig).
290
+ /// Call it before start(): the sync thread reads its tuning once when it
291
+ /// starts, and the store's other threads read the config without a lock.
219
292
  void set_config(const StorageConfig& config);
220
293
  const StorageConfig& get_config() const;
221
294
 
@@ -287,20 +360,60 @@ private:
287
360
  // Network message handlers (run on a reactor thread).
288
361
  void on_storage_message(const PeerId& from, ByteView payload);
289
362
  void on_peer_connected(const PeerId& peer_id);
363
+ void on_peer_disconnected(const PeerId& peer_id);
364
+ void on_peer_writable(const PeerId& peer_id);
290
365
 
291
366
  PeerNetwork* network_ = nullptr;
292
367
  StorageConfig config_;
293
368
 
294
- // In-memory storage
369
+ // In-memory storage.
370
+ //
371
+ // Ordered, not hashed, and that is load-bearing: a snapshot is streamed one
372
+ // bounded chunk at a time, and between chunks the only thing carried over is
373
+ // the last key sent. An ordered map turns that key back into a position with
374
+ // upper_bound(), so a stream costs nothing to keep alive and cannot be
375
+ // derailed by concurrent writes — an unordered_map would need either a
376
+ // materialised key list per peer or iterators that a rehash invalidates.
377
+ // It also makes keys_with_prefix() a range scan instead of a full walk.
295
378
  mutable std::mutex storage_mutex_;
296
- std::unordered_map<std::string, StorageEntry> entries_;
379
+ std::map<std::string, StorageEntry> entries_;
297
380
 
298
- // Sync state
381
+ // Sync state.
382
+ //
383
+ // Lock order, exactly one rule: storage_mutex_ and sync_mutex_ are never held
384
+ // at the same time, and neither is ever held across a call into PeerNetwork.
299
385
  mutable std::mutex sync_mutex_;
300
386
  StorageSyncStatus sync_status_;
301
387
  bool initial_sync_complete_;
302
388
  std::chrono::steady_clock::time_point last_sync_time_;
303
389
 
390
+ /// What we owe one peer. A snapshot is served as a walk over the key space
391
+ /// whose whole resumable state is `cursor` — the last key already sent.
392
+ struct PeerSync {
393
+ bool streaming = false; ///< a snapshot is in flight
394
+ bool started = false; ///< cursor is meaningful
395
+ std::string cursor; ///< last key sent
396
+ bool owed = false; ///< a (re)snapshot is due
397
+ std::chrono::steady_clock::time_point last_start{}; ///< for sync_min_interval_ms
398
+ };
399
+ std::unordered_map<PeerId, PeerSync, PeerId::Hash> peers_; ///< guarded by sync_mutex_
400
+ /// Bumped, under sync_mutex_, whenever something the sync thread would act on
401
+ /// changes — a snapshot scheduled, a link drained, a peer gone. The thread
402
+ /// reads it before it scans and compares it before it waits, because between
403
+ /// those two points it is outside the lock (serializing and sending), and a
404
+ /// notify that arrives there would otherwise be one nobody is waiting for.
405
+ uint64_t sync_epoch_ = 0;
406
+
407
+ // The sync thread: serializes and paces every snapshot chunk, so no reactor
408
+ // thread ever does work proportional to the size of the database.
409
+ std::atomic<bool> sync_running_{false};
410
+ std::thread sync_thread_;
411
+ std::condition_variable sync_cv_; ///< paired with sync_mutex_
412
+ // Sync tuning, copied out of the config when the thread starts and read-only
413
+ // afterwards, so the thread never races a set_config() on another thread.
414
+ size_t batch_bytes_{0};
415
+ std::chrono::milliseconds sync_interval_{0};
416
+
304
417
  // Statistics
305
418
  mutable std::mutex stats_mutex_;
306
419
  StorageStatistics stats_;
@@ -317,14 +430,37 @@ private:
317
430
  bool dirty_; // Flag indicating unsaved changes
318
431
 
319
432
  // Wire opcodes (MessageType::Storage payload, byte 0)
320
- static constexpr uint8_t OP_ENTRY = 1;
321
- static constexpr uint8_t OP_SYNC_REQUEST = 2;
322
- static constexpr uint8_t OP_SYNC_RESPONSE = 3;
433
+ static constexpr uint8_t OP_ENTRY = 1;
434
+ static constexpr uint8_t OP_SYNC_REQUEST = 2;
435
+ static constexpr uint8_t OP_SYNC_CHUNK = 3;
436
+
437
+ // SYNC_CHUNK flags (byte 1)
438
+ static constexpr uint8_t FLAG_LAST = 0x01; ///< final chunk of this snapshot
439
+
440
+ /// Refuse an inbound Storage message larger than this before parsing it. Our
441
+ /// own sender never comes near it (one chunk is sync_batch_bytes plus at most
442
+ /// one value, both capped at 2 MiB), while the block layer would happily hand
443
+ /// us 64 MiB from a peer that means us harm.
444
+ static constexpr size_t kMaxInboundMessage = 8 * 1024 * 1024;
323
445
 
324
446
  // Private methods
325
447
  void initialize();
326
448
  void shutdown();
327
449
  void persistence_thread_loop();
450
+ void start_sync_thread();
451
+ void stop_sync_thread();
452
+ void sync_thread_loop();
453
+ /// What one turn of a peer's snapshot stream left behind.
454
+ enum class ChunkResult {
455
+ Continue, ///< a chunk went out and the link has room for the next
456
+ Blocked, ///< a chunk went out and filled the link; retry when it drains
457
+ Finished ///< the snapshot is complete (or the peer is gone)
458
+ };
459
+ /// Serialize and send the next chunk of `peer`'s snapshot.
460
+ ChunkResult stream_snapshot_chunk(const PeerId& peer);
461
+ /// Note that `peer` is due a snapshot; the sync thread starts it once the
462
+ /// link has room and the per-peer interval has elapsed.
463
+ void schedule_snapshot(const PeerId& peer, bool requested_by_peer);
328
464
 
329
465
  // Internal put with full control
330
466
  bool put_internal(const std::string& key, StorageValueType type,
@@ -341,14 +477,20 @@ private:
341
477
  double deserialize_double(const std::vector<uint8_t>& data) const;
342
478
  std::string deserialize_string(const std::vector<uint8_t>& data) const;
343
479
 
344
- // Network operations
345
- void broadcast_entry(const StorageEntry& entry); ///< to all peers
346
- void forward_entry(const StorageEntry& entry, const PeerId& except); ///< re-flood
480
+ // Network operations. Every one of them honours send()'s return value: a peer
481
+ // that answers "no room" is owed a snapshot instead of further entries.
482
+ /// Send one entry to every connected peer except `except` (null for none).
483
+ void replicate_entry(const StorageEntry& entry, const PeerId* except);
484
+ void broadcast_entry(const StorageEntry& entry) { replicate_entry(entry, nullptr); }
485
+ void forward_entry(const StorageEntry& entry, const PeerId& except) {
486
+ replicate_entry(entry, &except);
487
+ }
347
488
  void send_sync_request(const PeerId& peer_id);
348
- void send_sync_response(const PeerId& peer_id);
349
489
 
350
490
  // Apply a remote entry with LWW; fills `out_event` and returns true if applied.
351
491
  bool apply_remote_entry(const StorageEntry& entry, StorageChangeEvent* out_event);
492
+ /// Parse and apply the entries in one SYNC_CHUNK body. @return how many won.
493
+ uint32_t apply_chunk(const PeerId& from, const uint8_t* data, size_t size, uint32_t count);
352
494
 
353
495
  // File path helpers
354
496
  std::string get_data_file_path() const;
@@ -359,6 +501,8 @@ private:
359
501
  bool read_data_file();
360
502
 
361
503
  // Utility
504
+ /// Clamp the size limits to what one connection's send queue can carry.
505
+ static void sanitize_config(StorageConfig& config);
362
506
  uint64_t get_current_timestamp_ms() const;
363
507
  std::string get_our_peer_id() const;
364
508
  void notify_change(const StorageChangeEvent& event);
@@ -80,7 +80,7 @@ void FileTransfer::attach(NodeContext& ctx) {
80
80
  network_ = &ctx.network;
81
81
  network_->on(MessageType::FileChunk,
82
82
  [this](const Peer& peer, ByteView payload) { on_message(peer, payload); });
83
- network_->on_peer_disconnected([this](const PeerId& id) {
83
+ network_->on_peer_disconnected([this](const PeerId& id, CloseReason) {
84
84
  // Fail every in-flight transfer with the departed peer and reclaim temps.
85
85
  std::vector<std::shared_ptr<Outgoing>> out;
86
86
  std::vector<std::shared_ptr<Incoming>> in;
@@ -106,7 +106,7 @@ void PubSub::attach(NodeContext& ctx) {
106
106
  network_->on(MessageType::Gossip,
107
107
  [this](const Peer& peer, ByteView payload) { on_gossip(peer, payload); });
108
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); });
109
+ network_->on_peer_disconnected([this](const PeerId& id, CloseReason) { on_peer_gone(id); });
110
110
  }
111
111
 
112
112
  void PubSub::start() {
@@ -74,7 +74,7 @@ std::vector<Address> ReconnectionService::known_peers(size_t n) const {
74
74
  void ReconnectionService::attach(NodeContext& ctx) {
75
75
  network_ = &ctx.network;
76
76
  network_->on_peer_connected([this](const Peer& peer) { on_connected(peer); });
77
- network_->on_peer_disconnected([this](const PeerId& id) { on_disconnected(id); });
77
+ network_->on_peer_disconnected([this](const PeerId& id, CloseReason) { on_disconnected(id); });
78
78
  network_->on_dial_failed([this](const Address& addr) { on_dial_failed(addr); });
79
79
  }
80
80
 
@@ -1072,7 +1072,8 @@ void Relay::attach(NodeContext& ctx) {
1072
1072
  ctx.network.on(MessageType::Relay,
1073
1073
  [state](const Peer& peer, ByteView payload) { state->handle(peer, payload); });
1074
1074
  ctx.network.on_peer_connected([state](const Peer& peer) { state->on_peer_connected(peer); });
1075
- ctx.network.on_peer_disconnected([state](const PeerId& id) { state->on_peer_disconnected(id); });
1075
+ ctx.network.on_peer_disconnected(
1076
+ [state](const PeerId& id, CloseReason) { state->on_peer_disconnected(id); });
1076
1077
  ctx.network.on_peer_writable([state](const Peer& peer) { state->on_peer_writable(peer.id()); });
1077
1078
 
1078
1079
  services_ = &ctx.services;
@@ -51,6 +51,46 @@ uint8_t Connection::reactor_index() const noexcept { return reactor_.index(); }
51
51
  bool Connection::send(FrameHeader header, ByteView payload) {
52
52
  if (state_ != ConnState::Established) return false; // frames only flow post-handshake
53
53
 
54
+ // The one thing that is refused outright rather than queued: a message too
55
+ // large to be framed at all. The block prefix tops out at kMaxBlockSize and the
56
+ // peer's decoder rejects anything past it, so queueing this would spend the
57
+ // link only to have the far end hang up on a protocol error it was handed on
58
+ // purpose. Waiting cannot help either — no amount of draining makes it fit — so
59
+ // the honest answer is a refusal, with the frame unqueued and the connection
60
+ // untouched. Checked before encrypt() because the nonce counters run in
61
+ // lockstep: a message encrypted and then dropped would break every one after.
62
+ if (framer::kHeaderSize + payload.size() + session_->overhead() > framer::kMaxBlockSize) {
63
+ LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " refused a "
64
+ << payload.size() << " B message: past the " << framer::kMaxBlockSize
65
+ << " B block ceiling; split it");
66
+ return false;
67
+ }
68
+
69
+ // Weighed on the backlog as it stands BEFORE this message, never after it has
70
+ // been added. A message is indivisible — there is no queueing half of one — so
71
+ // judging a caller by the mark its own message has just crossed answers "that
72
+ // message was bigger than a limit nobody published" with a disconnection, on a
73
+ // connection that is idle, healthy and draining at full speed. What the mark is
74
+ // for is a peer that cannot keep up, and the evidence for that is a caller
75
+ // piling MORE on top of a backlog that is already over it.
76
+ //
77
+ // So one message may always be queued, whatever its size — the queue may exceed
78
+ // the mark by exactly that message and no more — and offering another before it
79
+ // has drained is what makes a caller a slow consumer. Nothing is ever dropped
80
+ // or refused here, which is what the layers above rely on: a relayed circuit
81
+ // (transport/relay_link.cpp) reports bytes as written the moment it hands them
82
+ // over, and a byte stream cannot survive one of them going missing.
83
+ const size_t backlog_before = backlog();
84
+ if (backlog_before > send_high_water_) {
85
+ LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " offered more with "
86
+ << backlog_before << " B still queued past the high-water mark ("
87
+ << send_high_water_ << " B); closing as slow consumer");
88
+ close_reason_ = CloseReason::SlowConsumer;
89
+ state_ = ConnState::Closing;
90
+ reactor_.close(id_, CloseReason::SlowConsumer);
91
+ return false;
92
+ }
93
+
54
94
  Bytes inner;
55
95
  framer::encode_message(inner, header, payload);
56
96
 
@@ -64,14 +104,6 @@ bool Connection::send(FrameHeader header, ByteView payload) {
64
104
  queue_block(std::move(cipher));
65
105
 
66
106
  const size_t backlog_now = backlog();
67
- if (backlog_now > send_high_water_) {
68
- LOG_WARN("connection", "Peer " << remote_id_.short_hex() << " over send high-water ("
69
- << backlog_now << " B); closing as slow consumer");
70
- close_reason_ = CloseReason::SlowConsumer;
71
- state_ = ConnState::Closing;
72
- reactor_.close(id_, CloseReason::SlowConsumer);
73
- return false;
74
- }
75
107
 
76
108
  // The queue has grown past what a caller should keep adding to. Everything
77
109
  // still goes out — nothing is dropped here — but the answer to "may I send
@@ -85,8 +85,11 @@ public:
85
85
 
86
86
  class Connection {
87
87
  public:
88
- /// High-water mark for the memory held by the send queue; exceeding it closes the
89
- /// connection with CloseReason::SlowConsumer.
88
+ /// High-water mark for the memory held by the send queue. A caller that offers
89
+ /// another message while the queue is *still* over it has the connection closed
90
+ /// with CloseReason::SlowConsumer. The mark is never applied to the message that
91
+ /// crosses it: one message may always be queued, whatever its size (see send()),
92
+ /// so the queue's ceiling is this plus one message rather than this exactly.
90
93
  static constexpr size_t kDefaultSendHighWater = 8 * 1024 * 1024;
91
94
 
92
95
  /// Where send() starts answering "no room". A quarter of the hard limit, so a
@@ -125,6 +128,13 @@ public:
125
128
 
126
129
  /// Queue an application frame for the peer. No-op unless Established.
127
130
  ///
131
+ /// Never refuses and never drops: whatever it is handed is queued, however
132
+ /// large. The size of a single message is not what the high-water mark is
133
+ /// about — a message cannot be queued by halves, so a connection that is
134
+ /// draining perfectly well must not be torn down over one big frame. What
135
+ /// closes a connection is offering *another* message while the queue is still
136
+ /// over the mark, which is the actual evidence that the peer is not keeping up.
137
+ ///
128
138
  /// @return whether there is still room for more. False means the queue is
129
139
  /// past its low-water mark — this frame is queued like any other,
130
140
  /// nothing is dropped, but the caller should stop and wait for
@@ -29,6 +29,9 @@
29
29
  #endif
30
30
  #else
31
31
  #include <unistd.h>
32
+ #if defined(__ANDROID__)
33
+ #include <android/log.h>
34
+ #endif
32
35
  #endif
33
36
 
34
37
  namespace librats {
@@ -149,7 +152,24 @@ public:
149
152
  if (level < min_level_.load(std::memory_order_relaxed)) {
150
153
  return;
151
154
  }
152
-
155
+
156
+ #if defined(__ANDROID__)
157
+ // An Android app's stdout/stderr are discarded by default, so the cout/cerr
158
+ // sink below is invisible in logcat. Route console output through the platform
159
+ // logger instead: logcat supplies its own timestamp, level and coloring, so the
160
+ // bare message goes out under the module name as the tag. The file sink is
161
+ // unaffected and stays identical to every other platform.
162
+ if (console_logging_enabled_) {
163
+ __android_log_print(get_android_priority(level),
164
+ module.empty() ? "librats" : module.c_str(),
165
+ "%s", message.c_str());
166
+ }
167
+ if (file_logging_enabled_ && log_file_.is_open()) {
168
+ write_to_file(level, module, message);
169
+ }
170
+ return;
171
+ #endif
172
+
153
173
  // Prepare console output with colors
154
174
  std::ostringstream console_oss;
155
175
 
@@ -239,6 +259,18 @@ private:
239
259
  default: return "UNKNOWN";
240
260
  }
241
261
  }
262
+
263
+ #if defined(__ANDROID__)
264
+ int get_android_priority(LogLevel level) {
265
+ switch (level) {
266
+ case LogLevel::DEBUG: return ANDROID_LOG_DEBUG;
267
+ case LogLevel::INFO: return ANDROID_LOG_INFO;
268
+ case LogLevel::WARN: return ANDROID_LOG_WARN;
269
+ case LogLevel::ERROR: return ANDROID_LOG_ERROR;
270
+ default: return ANDROID_LOG_INFO;
271
+ }
272
+ }
273
+ #endif
242
274
 
243
275
  std::string get_color_code(LogLevel level) {
244
276
  if (!colors_enabled_ || !is_terminal_) return "";
@@ -411,10 +443,9 @@ private:
411
443
  std::string old_name = log_file_path_ + "." + std::to_string(i);
412
444
  std::string new_name = log_file_path_ + "." + std::to_string(i + 1);
413
445
 
414
- // Delete the oldest file if it exists
415
- if (i == max_log_files_ - 1) {
416
- std::remove(new_name.c_str());
417
- }
446
+ // std::rename() does not replace an existing destination on Windows,
447
+ // so clear the target first - not just the oldest file.
448
+ std::remove(new_name.c_str());
418
449
 
419
450
  // Rename old file to new name
420
451
  std::rename(old_name.c_str(), new_name.c_str());
@@ -422,6 +453,7 @@ private:
422
453
 
423
454
  // Move current log file to .1
424
455
  std::string backup_name = log_file_path_ + ".1";
456
+ std::remove(backup_name.c_str());
425
457
  std::rename(log_file_path_.c_str(), backup_name.c_str());
426
458
  }
427
459
 
@@ -25,7 +25,21 @@
25
25
  #include <cerrno>
26
26
  #elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
27
27
  defined(__OpenBSD__) || defined(__DragonFly__)
28
+ #ifdef __APPLE__
29
+ #include <TargetConditionals.h>
30
+ #endif
31
+ // Apple ships <net/route.h> in the macOS SDK only. On iOS the PF_ROUTE
32
+ // socket itself is usable but the message declarations are not public, so
33
+ // there is nothing to parse against — those targets take the polling
34
+ // fallback at the bottom of this file (backend_start() returns false).
35
+ // A native backend for iOS belongs on Network.framework's nw_path_monitor
36
+ // rather than on route messages.
37
+ #if !defined(__APPLE__) || (defined(TARGET_OS_OSX) && TARGET_OS_OSX)
28
38
  #define RATS_MONITOR_BSD_ROUTES 1
39
+ #endif
40
+ #endif
41
+
42
+ #if defined(RATS_MONITOR_BSD_ROUTES)
29
43
  #include <sys/types.h>
30
44
  #include <sys/socket.h>
31
45
  #include <net/route.h>
@@ -17,8 +17,17 @@
17
17
  #include <ifaddrs.h>
18
18
  #endif
19
19
 
20
+ #ifdef __APPLE__
21
+ #include <TargetConditionals.h>
22
+ #endif
23
+
20
24
  // macOS / BSD default-gateway lookup via the PF_ROUTE sysctl routing table.
21
- #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \
25
+ // Apple ships <net/route.h> in the macOS SDK only: on iOS (and tvOS/watchOS)
26
+ // the sysctl still exists but its declarations are not public, so those
27
+ // targets skip this branch and rely on append_gateway_heuristics() below,
28
+ // which every platform falls back to anyway.
29
+ #if (defined(__APPLE__) && defined(TARGET_OS_OSX) && TARGET_OS_OSX) || \
30
+ defined(__FreeBSD__) || defined(__NetBSD__) || \
22
31
  defined(__OpenBSD__) || defined(__DragonFly__)
23
32
  #define RATS_HAVE_BSD_ROUTES 1
24
33
  #include <sys/types.h>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librats",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
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",