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, 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}