Skip to main content

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