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.cleanup_expired_pending_messages_if_due();
306        self.cleanup_outbox();
307        self.mesh_services.cleanup_expired();
308        self.cleanup_group_message_dedup();
309        self.check_epoch_forks();
310        self.check_leave_election_timeouts();
311        self.check_relay_group_sync();
312        let stale_transfers = self
313            .file_transfer_manager
314            .cleanup_stale_transfers(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
315        for stale in stale_transfers {
316            self.pending_media_metadata.remove(&stale.file_id);
317            // Like the resource-limit drops, a stale transfer is
318            // unrecoverable (its chunks were ACKed and will not be
319            // retransmitted) — tell the app instead of going silent.
320            self.emit_event(Event::file_receive_failed(
321                stale.file_id,
322                stale.file_name,
323                stale.sender,
324                "stale_timeout".to_string(),
325            ));
326        }
327        self.cleanup_stale_media_state(StdDuration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS));
328        // Prune old timed-out ACKs that weren't cleaned up by normal retry flow
329        self.ack_manager
330            .prune_old_timeouts(std::time::Duration::from_secs(300)); // 5 minutes
331    }
332
333    /// Evicts known peers not re-seen within `KNOWN_PEER_TTL_SECS`.
334    ///
335    /// This is the only removal path for peers on carriers without a
336    /// disconnect signal (Internet, Reticulum, Nostr, WiFi Direct
337    /// message-path senders). `now` is injected for test determinism.
338    pub(crate) fn prune_stale_known_peers(&mut self, now: std::time::Instant) {
339        let ttl = StdDuration::from_secs(KNOWN_PEER_TTL_SECS);
340        let stale: Vec<String> = self
341            .known_peers
342            .iter()
343            .filter(|(_, seen)| now.saturating_duration_since(**seen) > ttl)
344            .map(|(id, _)| id.clone())
345            .collect();
346        if stale.is_empty() {
347            return;
348        }
349        tracing::debug!(
350            count = stale.len(),
351            ttl_secs = KNOWN_PEER_TTL_SECS,
352            "Evicting stale known peers"
353        );
354        for peer_id in &stale {
355            self.evict_known_peer(peer_id);
356        }
357    }
358
359    // ========================================================================
360    // SERVICE DISCOVERY & REQUEST/RESPONSE
361    // ========================================================================
362
363    /// Registers a local service that this node offers.
364    pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<()> {
365        self.mesh_services
366            .register_service(descriptor)
367            .map_err(Error::Service)
368    }
369
370    /// Unregisters a local service. Returns true if the service was found and removed.
371    pub fn unregister_service(&mut self, service_id: &str) -> Result<bool> {
372        self.mesh_services
373            .unregister_service(service_id)
374            .map_err(Error::Service)
375    }
376
377    /// Broadcasts a service discovery query to all known peers.
378    /// Returns a query_id. Responses arrive asynchronously as `ServiceDiscovered` events.
379    ///
380    /// **Note:** Discovery responses currently travel only one hop back (to the
381    /// immediate sender of the query). Multi-hop response relay is not yet
382    /// implemented, so services more than one hop away will generate responses
383    /// that reach intermediate forwarders but not the original querier.
384    pub fn discover_services(&mut self, service_id: Option<&str>) -> Result<String> {
385        // Service discovery messages are internal control messages (not user
386        // content), so they are exempt from require_encryption.
387
388        let peers: Vec<String> = self.known_peers.keys().cloned().collect();
389        let result = self
390            .mesh_services
391            .discover_services(&self.local_id, &peers, service_id)
392            .map_err(Error::Service)?;
393        let mut send_failures = 0usize;
394        for msg in result.messages {
395            if self
396                .send_internal_message(&msg.recipient, msg.content, msg.priority)
397                .is_err()
398            {
399                send_failures += 1;
400            }
401        }
402        if send_failures > 0 {
403            warn!(
404                failures = send_failures,
405                total = peers.len(),
406                "Some discovery broadcasts failed to send"
407            );
408        }
409        Ok(result.query_id)
410    }
411
412    /// Sends a typed service request to a specific provider peer.
413    /// Returns a request_id. The response arrives as a `ServiceResponseReceived` event.
414    pub fn send_service_request(
415        &mut self,
416        provider: &str,
417        service_id: &str,
418        method: &str,
419        body: &str,
420    ) -> Result<String> {
421        // Service requests are internal control messages (not user content),
422        // so they are exempt from require_encryption.
423        Self::validate_outbound_recipient(provider)?;
424
425        let result = self
426            .mesh_services
427            .send_service_request(provider, service_id, method, body)
428            .map_err(Error::Service)?;
429        let msg = result.message;
430        self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
431        Ok(result.request_id)
432    }
433
434    /// Responds to a service request from another peer.
435    pub fn respond_to_service_request(
436        &mut self,
437        request_id: &str,
438        requester: &str,
439        service_id: &str,
440        status: &str,
441        body: &str,
442    ) -> Result<MessageId> {
443        // Service responses are internal control messages (not user content),
444        // so they are exempt from require_encryption.
445        Self::validate_outbound_recipient(requester)?;
446
447        let result = self
448            .mesh_services
449            .respond_to_service_request(request_id, requester, service_id, status, body)
450            .map_err(Error::Service)?;
451        let msg = result.message;
452        let message_id = self.send_internal_message(&msg.recipient, msg.content, msg.priority)?;
453        Ok(message_id)
454    }
455}