offline_protocol/protocol/config_accessors.rs
1//! Configuration accessors, diagnostics, and service registration.
2
3use super::mesh_relay::{MeshRelayConfig, MeshRelayStats};
4use super::{
5 lock_shared_state, OfflineProtocol, PendingQueueMetrics, ProtocolState, KNOWN_PEER_TTL_SECS,
6 MEDIA_TRANSFER_STALE_TIMEOUT_SECS,
7};
8use crate::events::Event;
9use crate::file_transfer::FileTransferManager;
10use crate::{Error, ProtocolConfig, Result, TransportManager};
11use offline_protocol_core::{MessageId, ServiceDescriptor};
12use offline_protocol_mls::MlsManager;
13use offline_protocol_reliability::{
14 AckConfig, AckManager, DeduplicatorConfig, DeduplicatorStats, RetryConfig, RetryQueue,
15};
16use offline_protocol_router::{DorsConfig, RelayConfig};
17use offline_protocol_services::MeshServices;
18use std::sync::{Arc, RwLock};
19use std::time::Duration as StdDuration;
20use tracing::{error, warn};
21
22impl OfflineProtocol {
23 /// Gets the current protocol state.
24 pub fn state(&self) -> ProtocolState {
25 let Ok(state) = lock_shared_state(&self.shared_state) else {
26 error!("Failed to lock shared state in state()");
27 return ProtocolState::Stopped;
28 };
29 state.state
30 }
31
32 /// Gets the configuration.
33 pub fn config(&self) -> &ProtocolConfig {
34 &self.config
35 }
36
37 /// Gets a reference to the mesh services registry.
38 pub fn mesh_services(&self) -> &MeshServices {
39 &self.mesh_services
40 }
41
42 /// Gets a mutable reference to the transport manager.
43 ///
44 /// This allows external code (e.g., FFI) to add transports dynamically.
45 pub fn transport_manager_mut(&mut self) -> &mut TransportManager {
46 &mut self.transport_manager
47 }
48
49 /// Gets a reference to the transport manager.
50 pub fn transport_manager(&self) -> &TransportManager {
51 &self.transport_manager
52 }
53
54 /// Returns a mutable reference to the file transfer manager.
55 pub fn file_transfer_manager_mut(&mut self) -> &mut FileTransferManager {
56 &mut self.file_transfer_manager
57 }
58
59 /// Returns a reference to the file transfer manager.
60 pub fn file_transfer_manager(&self) -> &FileTransferManager {
61 &self.file_transfer_manager
62 }
63
64 /// Gets access to the MLS manager for advanced operations.
65 ///
66 /// Returns `None` if MLS is not initialized.
67 pub fn mls_manager(&self) -> Option<&Arc<RwLock<MlsManager>>> {
68 self.mls_manager.as_ref()
69 }
70
71 /// Updates the DORS configuration at runtime.
72 ///
73 /// This replaces the current DORS selector configuration with the provided config.
74 pub fn update_dors_config(&mut self, config: DorsConfig) {
75 self.transport_manager.update_selector_config(config);
76 }
77
78 /// The DORS configuration transport selection is actually using.
79 ///
80 /// Read from the selector, not from the stored [`ProtocolConfig`], for the
81 /// reason [`Self::mesh_relay_config`] gives: a caller reading a section
82 /// back is asking what the consumer has, and a copy kept anywhere else is
83 /// one that can disagree with it. Here the two genuinely do diverge, since
84 /// [`Self::update_dors_config`] rewrites the selector and leaves the
85 /// stored config alone.
86 pub fn dors_config(&self) -> DorsConfig {
87 self.transport_manager.selector_config().clone()
88 }
89
90 /// Records the host's battery reading for this device.
91 ///
92 /// The platform owns this number — no transport can observe it — and until
93 /// it arrives every battery-dependent policy in the SDK runs in its
94 /// unknown-level branch: DORS energy scoring skips its battery term, the
95 /// relay role is never evaluated (so `relay_promoted`/`relay_demoted` can
96 /// never fire), and message forwarding takes the "unknown means willing"
97 /// path past [`RelayConfig::min_battery_for_relay`]. Feeding it is what
98 /// makes those policies live.
99 ///
100 /// `is_charging` is not cosmetic: a charging device is deliberately
101 /// excused the soft relay minimum and stopped only by the hard
102 /// `CRITICAL_RELAY_BATTERY_LEVEL` floor, so reporting a level without the
103 /// charging state strips relay duty from plugged-in devices that should
104 /// keep it.
105 ///
106 /// `level` is clamped to 0-100.
107 pub fn set_device_battery(&mut self, level: u8, is_charging: bool) {
108 self.transport_manager
109 .set_device_battery(level, is_charging);
110 }
111
112 /// Returns the host-reported `(battery_level, is_charging)` pair, or
113 /// `(None, false)` when no platform reading has been supplied.
114 pub fn device_battery(&self) -> (Option<u8>, bool) {
115 self.transport_manager.device_battery()
116 }
117
118 /// Updates the relay configuration at runtime.
119 ///
120 /// Validated the same way [`Self::update_ack_config`] is — by building the
121 /// candidate configuration and running the real validator — so a
122 /// constraint added to `ProtocolConfig::validate` applies on this path too.
123 ///
124 /// Takes effect on the next forwarding decision and the next `process()`
125 /// tick, which is where the relay standing is re-derived. One copy, read by
126 /// every relay decision: the forwarding gate, the capability bias pushed
127 /// into the governor, and the battery floor the demotion event reports. An
128 /// earlier shape kept a second copy on the router's relay manager and had
129 /// to write both, which is exactly the split that lets a device decline the
130 /// relay role while still carrying other people's traffic.
131 pub fn update_relay_config(&mut self, config: RelayConfig) -> crate::Result<()> {
132 let mut candidate = self.config.clone();
133 candidate.relay = config.clone();
134 candidate.validate()?;
135 self.config.relay = config;
136 Ok(())
137 }
138
139 /// Returns the current relay configuration.
140 pub fn relay_config(&self) -> &RelayConfig {
141 &self.config.relay
142 }
143
144 /// Whether this device is currently acting as a relay.
145 ///
146 /// Reports what this device has been *doing* — carrying traffic for other
147 /// devices — not what its battery and neighbor count suggest it could. It
148 /// is the standing the `relay_promoted` / `relay_demoted` /
149 /// `relay_demoted_battery` events announce, re-derived on each `process()`
150 /// tick from the forwarding governor's own record of frames carried.
151 ///
152 /// Consequently it answers `false` on a device that has every capability to
153 /// relay but has had no third-party traffic to carry — including any device
154 /// whose peers all have their own route to each other, and any device on
155 /// which an infrastructure carrier is up, since the mesh is offered frames
156 /// only when nothing else can reach the recipient.
157 ///
158 /// Needs no battery feed. The feed scales *how much* this device forwards
159 /// (see [`Self::set_device_battery`]); forwarding is observable either way.
160 pub fn is_relay(&self) -> bool {
161 self.mesh_relay.is_active_relay()
162 }
163
164 /// Updates the ACK configuration at runtime.
165 ///
166 /// Note: This affects new ACK registrations; existing pending ACKs keep their original timeout.
167 /// Validated the same way [`Self::update_retry_config`] is — by building
168 /// the candidate configuration and running the real validator — rather than
169 /// by repeating its checks inline. Hand-rolled copies drift: a constraint
170 /// added to `ProtocolConfig::validate` for a new `AckConfig` field would be
171 /// enforced at construction and silently skipped on the runtime-update path.
172 pub fn update_ack_config(&mut self, config: AckConfig) -> crate::Result<()> {
173 let mut candidate = self.config.clone();
174 candidate.reliability.ack = config.clone();
175 candidate.validate()?;
176 self.ack_manager = AckManager::with_config(config.clone());
177 self.config.reliability.ack = config;
178 Ok(())
179 }
180
181 /// Updates the retry configuration at runtime.
182 ///
183 /// Note: This affects new retry entries; existing entries keep their original timing.
184 pub fn update_retry_config(&mut self, config: RetryConfig) -> crate::Result<()> {
185 let mut candidate = self.config.clone();
186 candidate.reliability.retry = config.clone();
187 candidate.validate()?;
188 self.retry_queue = RetryQueue::with_config(config.clone());
189 self.config.reliability.retry = config;
190 self.recompute_next_pending_message_expiry();
191 self.cleanup_expired_pending_messages();
192 Ok(())
193 }
194
195 /// Updates the deduplication configuration at runtime.
196 ///
197 /// The tracked set is carried across, re-bounded by the new configuration
198 /// (`Deduplicator::reconfigure`), rather than cleared. Clearing it was a
199 /// silent restart while the set lived in memory only; with the set
200 /// persisted it would discard the ids restored at launch — and the React
201 /// Native layer applies `reliability.dedup` through this method on every
202 /// `start()`, right after that restore — so the socket copy of a message
203 /// already consumed from a push injection would reach a spent ratchet
204 /// generation after all. The change is counted for persistence so the next
205 /// batch write re-states the record under the new bounds.
206 ///
207 /// Validated the same way its two siblings are, and for a reason that only
208 /// appeared once they were. Both of those run the *whole*
209 /// `ProtocolConfig::validate`, which checks the Bloom parameters this
210 /// method installs — so while this one accepted anything, a configuration
211 /// it had already stored could make a perfectly valid
212 /// [`Self::update_retry_config`] fail, complaining about a Bloom filter the
213 /// caller never mentioned. `Deduplicator::with_config` fails safe on those
214 /// values rather than panicking, so the cost was a confusing rejection
215 /// rather than a crash — but a rejection attributed to the wrong call is
216 /// its own kind of expensive.
217 pub fn update_dedup_config(&mut self, config: DeduplicatorConfig) -> crate::Result<()> {
218 let mut candidate = self.config.clone();
219 candidate.reliability.dedup = config.clone();
220 candidate.validate()?;
221 self.deduplicator.reconfigure(config.clone());
222 self.config.reliability.dedup = config;
223 self.note_dedup_change();
224 Ok(())
225 }
226
227 /// Gets deduplicator statistics for monitoring.
228 pub fn deduplicator_stats(&self) -> DeduplicatorStats {
229 self.deduplicator.stats()
230 }
231
232 /// Gets pending encrypted message queue counters and gauges.
233 pub fn pending_queue_metrics(&self) -> PendingQueueMetrics {
234 self.pending_queue.metrics().clone()
235 }
236
237 /// Gets the current ACK manager statistics.
238 pub fn pending_ack_count(&self) -> usize {
239 self.ack_manager.pending_count()
240 }
241
242 /// Gets the current retry queue statistics.
243 pub fn retry_queue_size(&self) -> usize {
244 self.retry_queue.len()
245 }
246
247 /// Returns whether `peer_id` is currently tracked as a discovered
248 /// neighbor. Populated by [`Self::on_neighbor_discovered`] and cleared
249 /// by [`Self::on_neighbor_lost`], by the periodic TTL sweep when the
250 /// peer has not been re-seen for `KNOWN_PEER_TTL_SECS`, or by
251 /// least-recently-seen eviction when tracking is at capacity — so this
252 /// can flip to false without any explicit loss signal. Blocked users
253 /// are never tracked.
254 pub fn is_known_peer(&self, peer_id: &str) -> bool {
255 self.known_peers.contains_key(peer_id)
256 }
257
258 /// Reports how much traffic this device is carrying for other people.
259 ///
260 /// Useful for showing a user what their device is contributing, and for
261 /// spotting a neighborhood in trouble: a rising `rate_deferred` means
262 /// forwarding is hitting its ceiling, and `peer_rate_limited` means a
263 /// single neighbor is sending more than its share. `dropped_for_capacity`
264 /// is expected to stay at zero — see [`MeshRelayStats::dropped_for_capacity`].
265 ///
266 /// `refused_queue_full` and `abandoned_overdue` are the two that record
267 /// frames actually lost under pressure. Without them a shedding device
268 /// reports only healthy-looking deferrals, so a caller diagnosing a mesh
269 /// that is dropping traffic has nothing to see it with.
270 pub fn mesh_relay_stats(&self) -> MeshRelayStats {
271 let counters = self.mesh_relay.counters();
272 MeshRelayStats {
273 forwarded: counters.forwarded,
274 transmissions: counters.transmissions,
275 queued: counters.queued,
276 awaiting_transmission: self.mesh_relay.pending_len(),
277 duplicates_suppressed: counters.duplicates_suppressed,
278 covered_by_a_neighbor: counters.cancelled_by_duplicate,
279 peer_rate_limited: counters.peer_rate_limited,
280 refused_queue_full: counters.queue_full,
281 rate_deferred: counters.rate_deferred,
282 abandoned_overdue: counters.abandoned_overdue,
283 hop_limit_reached: counters.hop_limit_reached,
284 reach_clamped: counters.ttl_clamped,
285 dropped_for_capacity: self.mesh_relay.seen_capacity_evictions(),
286 }
287 }
288
289 /// Reports the mesh forwarding tunables currently in force.
290 ///
291 /// Read from the governor itself, not from the stored [`ProtocolConfig`],
292 /// so this reports what forwarding decisions actually use. The section is
293 /// fixed at construction today, which makes the two identical — reading
294 /// the consumer is what keeps that an implementation detail rather than
295 /// something a caller has to know.
296 pub fn mesh_relay_config(&self) -> MeshRelayConfig {
297 self.mesh_relay.config().clone()
298 }
299
300 /// Returns a mutable reference to the retry queue (test-only).
301 #[cfg(test)]
302 pub(crate) fn retry_queue_mut(&mut self) -> &mut offline_protocol_reliability::RetryQueue {
303 &mut self.retry_queue
304 }
305
306 /// Cleans up expired entries from deduplicator, retry queue, outbox, and ack manager.
307 /// Also checks for Internet availability transitions to sync groups with relay.
308 pub(crate) fn cleanup_expired_entries(&mut self) {
309 self.deduplicator.cleanup_expired();
310 self.retry_queue.cleanup_expired();
311 self.mesh_relay.maintain(std::time::Instant::now());
312 self.prune_stale_known_peers(std::time::Instant::now());
313 self.reachability.prune(std::time::Instant::now());
314 self.cleanup_expired_pending_messages_if_due();
315 self.cleanup_outbox();
316 self.mesh_services.cleanup_expired();
317 self.cleanup_group_message_dedup();
318 self.check_epoch_forks();
319 self.check_leave_election_timeouts();
320 self.check_relay_group_sync();
321 let stale_transfers = self
322 .file_transfer_manager
323 .cleanup_stale_transfers(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
324 for stale in stale_transfers {
325 let dropped = self.pending_media_metadata.remove(&stale.file_id);
326 // An entry exists only once chunk 0 was accepted, and chunk 0 is
327 // what says whose transfer this is. Without one there is no
328 // knowledge to report from: the transfer may be the document
329 // layer's, and the app has heard nothing about it either, because
330 // progress is withheld under the same rule.
331 let identified = dropped.is_some();
332 // A document-layer transfer has no file to report failing, and
333 // saying otherwise hands somebody a failed download they never
334 // started. It is reported to the layer that asked for it
335 // instead, which is what tells a waiting fetch to stop waiting.
336 if let Some(purpose) = dropped.and_then(|entry| entry.data_purpose) {
337 self.report_data_media_transfer_failure(&stale.sender, &purpose, "stale_timeout");
338 continue;
339 }
340 if !identified {
341 tracing::debug!(
342 file_id = %stale.file_id,
343 "A transfer expired before its purpose was known; no failure reported"
344 );
345 continue;
346 }
347 // Like the resource-limit drops, a stale transfer is
348 // unrecoverable (its chunks were ACKed and will not be
349 // retransmitted) — tell the app instead of going silent.
350 self.emit_event(Event::file_receive_failed(
351 stale.file_id,
352 stale.file_name,
353 stale.sender,
354 "stale_timeout".to_string(),
355 ));
356 }
357 self.cleanup_stale_media_state(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
358 #[cfg(feature = "data")]
359 self.expire_attachment_fetches();
360 // Prune old timed-out ACKs that weren't cleaned up by normal retry flow
361 self.ack_manager
362 .prune_old_timeouts(std::time::Duration::from_secs(300)); // 5 minutes
363 }
364
365 /// Evicts known peers not re-seen within `KNOWN_PEER_TTL_SECS`.
366 ///
367 /// This is the only removal path for peers on carriers without a
368 /// disconnect signal (Internet, Reticulum, Nostr, WiFi Direct
369 /// message-path senders). `now` is injected for test determinism.
370 pub(crate) fn prune_stale_known_peers(&mut self, now: std::time::Instant) {
371 let ttl = StdDuration::from_secs(KNOWN_PEER_TTL_SECS);
372 let stale: Vec<String> = self
373 .known_peers
374 .iter()
375 .filter(|(_, seen)| now.saturating_duration_since(**seen) > ttl)
376 .map(|(id, _)| id.clone())
377 .collect();
378 if stale.is_empty() {
379 return;
380 }
381 tracing::debug!(
382 count = stale.len(),
383 ttl_secs = KNOWN_PEER_TTL_SECS,
384 "Evicting stale known peers"
385 );
386 for peer_id in &stale {
387 self.evict_known_peer(peer_id);
388 }
389 }
390
391 // ========================================================================
392 // SERVICE DISCOVERY & REQUEST/RESPONSE
393 // ========================================================================
394
395 /// Registers a local service that this node offers.
396 pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()> {
397 self.mesh_services
398 .register_service(descriptor)
399 .map_err(Error::Service)
400 }
401
402 /// Unregisters a local service. Returns true if the service was found and removed.
403 pub fn unregister_service(&mut self, service_id: &str) -> Result<bool> {
404 self.mesh_services
405 .unregister_service(service_id)
406 .map_err(Error::Service)
407 }
408
409 /// Broadcasts a service discovery query to all known peers.
410 /// Returns a query_id. Responses arrive asynchronously as `ServiceDiscovered` events.
411 ///
412 /// **Note:** Discovery responses currently travel only one hop back (to the
413 /// immediate sender of the query). Multi-hop response relay is not yet
414 /// implemented, so services more than one hop away will generate responses
415 /// that reach intermediate forwarders but not the original querier.
416 pub fn discover_services(&mut self, service_id: Option<&str>) -> Result<String> {
417 // Service discovery messages are internal control messages (not user
418 // content), so they are exempt from require_encryption.
419
420 let peers: Vec<String> = self.known_peers.keys().cloned().collect();
421 let result = self
422 .mesh_services
423 .discover_services(&self.local_id, &peers, service_id)
424 .map_err(Error::Service)?;
425 let mut send_failures = 0usize;
426 for msg in result.messages {
427 if self
428 .send_internal_message(&msg.recipient, msg.content, msg.priority)
429 .is_err()
430 {
431 send_failures += 1;
432 }
433 }
434 if send_failures > 0 {
435 warn!(
436 failures = send_failures,
437 total = peers.len(),
438 "Some discovery broadcasts failed to send"
439 );
440 }
441 Ok(result.query_id)
442 }
443
444 /// Sends a typed service request to a specific provider peer.
445 /// Returns a request_id. The response arrives as a `ServiceResponseReceived` event.
446 pub fn send_service_request(
447 &mut self,
448 provider: &str,
449 service_id: &str,
450 method: &str,
451 body: &str,
452 ) -> Result<String> {
453 // Service requests are internal control messages (not user content),
454 // so they are exempt from require_encryption.
455 Self::validate_outbound_recipient(provider)?;
456
457 let result = self
458 .mesh_services
459 .send_service_request(provider, service_id, method, body)
460 .map_err(Error::Service)?;
461 let msg = result.message;
462 self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
463 Ok(result.request_id)
464 }
465
466 /// Responds to a service request from another peer.
467 pub fn respond_to_service_request(
468 &mut self,
469 request_id: &str,
470 requester: &str,
471 service_id: &str,
472 status: &str,
473 body: &str,
474 ) -> Result<MessageId> {
475 // Service responses are internal control messages (not user content),
476 // so they are exempt from require_encryption.
477 Self::validate_outbound_recipient(requester)?;
478
479 let result = self
480 .mesh_services
481 .respond_to_service_request(request_id, requester, service_id, status, body)
482 .map_err(Error::Service)?;
483 let msg = result.message;
484 let message_id = self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
485 Ok(message_id)
486 }
487}