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}