Skip to main content

whatsapp_rust/client/
accessors.rs

1//! Small accessors, config setters, node waiters and sync-error helpers.
2
3use super::*;
4
5/// Identity for span/error tagging. Named fields, not a tuple — LID/PN transposition would
6/// otherwise be a silent, unchecked bug at call sites.
7#[cfg(feature = "tracing")]
8#[derive(Debug, Clone, Default)]
9pub struct IdentityTags {
10    pub lid: Option<String>,
11    pub pn: Option<String>,
12}
13
14impl Client {
15    pub(crate) async fn get_group_cache(&self) -> Arc<GroupCache> {
16        let mut guard = self.group_cache.lock().await;
17        if let Some(cache) = guard.as_ref() {
18            return cache.clone();
19        }
20        debug!("Initializing Group Cache for the first time.");
21        let cache = Arc::new(
22            self.cache_config
23                .group_cache
24                .build_typed_ttl(self.cache_config.cache_stores.group_cache.clone(), "group"),
25        );
26        *guard = Some(cache.clone());
27        cache
28    }
29
30    /// Subscribe an external event handler with an explicit event filter.
31    pub fn subscribe(
32        &self,
33        interest: wacore::types::events::EventInterest,
34        handler: Arc<dyn wacore::types::events::EventHandler>,
35    ) -> wacore::types::events::Subscription {
36        self.core.event_bus.subscribe(interest, handler)
37    }
38
39    /// Subscribe using the handler's current registration-time interest hint.
40    pub fn subscribe_handler(
41        &self,
42        handler: Arc<dyn wacore::types::events::EventHandler>,
43    ) -> wacore::types::events::Subscription {
44        self.core.event_bus.subscribe_handler(handler)
45    }
46
47    /// Acquire raw decoded stanza forwarding for one consumer.
48    ///
49    /// `Event::RawNode` remains enabled until every acquired lease is dropped.
50    pub fn acquire_raw_node_forwarding(self: &Arc<Self>) -> RawNodeLease {
51        let incremented = self
52            .raw_node_forwarding
53            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |count| {
54                count.checked_add(1)
55            })
56            .is_ok();
57        assert!(incremented, "raw-node forwarding lease counter overflow");
58        RawNodeLease {
59            client: Arc::downgrade(self),
60        }
61    }
62
63    pub(crate) fn raw_node_forwarding_enabled(&self) -> bool {
64        self.raw_node_forwarding.load(Ordering::Relaxed) != 0
65    }
66
67    /// Enable or disable skipping of history sync notifications at runtime.
68    ///
69    /// When enabled, the client will acknowledge incoming history sync
70    /// notifications but will not download or process the data.
71    pub fn set_skip_history_sync(&self, enabled: bool) {
72        self.skip_history_sync.store(enabled, Ordering::Relaxed);
73    }
74
75    /// Returns `true` if history sync notifications are currently being skipped.
76    pub fn skip_history_sync_enabled(&self) -> bool {
77        self.skip_history_sync.load(Ordering::Relaxed)
78    }
79
80    /// Set how many one-time pre-keys are generated per upload batch.
81    ///
82    /// Defaults to WA Web's UPLOAD_KEYS_COUNT (812). Call before connecting; it
83    /// takes effect on the next pre-key upload. The value is clamped to the
84    /// protocol-safe range at upload time, so out-of-range values are coerced
85    /// (and logged) rather than rejected here.
86    pub fn set_wanted_pre_key_count(&self, count: usize) {
87        self.wanted_pre_key_count.store(count, Ordering::Relaxed);
88    }
89
90    /// Returns the configured pre-key upload batch size (the raw value, before
91    /// the upload-time clamp).
92    pub fn wanted_pre_key_count(&self) -> usize {
93        self.wanted_pre_key_count.load(Ordering::Relaxed)
94    }
95
96    /// Retune the per-chat outbound resend rate limiter live (no reconnect).
97    ///
98    /// Outbound resends to a chat are bounded by a token bucket: `burst` is the
99    /// instantaneous allowance and `refill_per_min` the sustained ceiling per
100    /// chat. This caps the aggregate resend rate that WhatsApp's anti-abuse
101    /// penalizes during a PN to LID migration fan-out, while throttled devices
102    /// still recover via the fresh-SKDM mark. A `burst` of 0 disables the limiter.
103    ///
104    /// Takes effect on each chat's next retry; a lowered `burst` clamps a live
105    /// bucket on its next access.
106    pub fn set_resend_rate_limit(&self, burst: u32, refill_per_min: u32) {
107        self.resend_rate_limiter.set_rate(burst, refill_per_min);
108    }
109
110    /// Register a [`RetryAdmission`] policy: an opt-in gate that can drop inbound
111    /// group/status retry receipts from other accounts before any repair work
112    /// runs. Unset (the default) admits every receipt, matching WhatsApp Web,
113    /// with zero overhead on the receive path.
114    ///
115    /// Set once, before connecting; a later call is ignored and returns `false`
116    /// (the already-registered policy stays in effect). Live tuning belongs
117    /// inside the policy itself (e.g. atomics), not in re-registration. See
118    /// `examples/retry_quarantine.rs`.
119    ///
120    /// [`RetryAdmission`]: crate::types::retry_admission::RetryAdmission
121    pub fn set_retry_admission(
122        &self,
123        policy: Arc<dyn crate::types::retry_admission::RetryAdmission>,
124    ) -> bool {
125        self.retry_admission.set(policy).is_ok()
126    }
127
128    /// Cumulative wire I/O and activity counters for this client session.
129    ///
130    /// Always available, no feature gate: recording costs one relaxed atomic
131    /// add per wire frame. Byte counts are post-noise wire bytes (frame
132    /// headers and AEAD tags included; handshake and TLS/WebSocket overhead
133    /// excluded), so two clients in one process can be compared directly.
134    pub fn stats(&self) -> StatsSnapshot {
135        let mut snapshot = self.stats.snapshot();
136        snapshot.reconnect_errors = self.auto_reconnect_errors.load(Ordering::Relaxed);
137        snapshot.resends_throttled = self.resend_rate_limiter.throttled_total();
138        snapshot
139    }
140
141    /// Entry counts plus estimated retained heap bytes for the client's
142    /// internal collections. See [`MemoryReport`] for the semantics of the
143    /// byte figures.
144    ///
145    /// On-demand only: walks the in-process caches under their locks when
146    /// called, costs nothing otherwise. Counts are approximate (caches may
147    /// have pending evictions); call `run_pending_tasks()` on individual
148    /// caches first if you need exact counts.
149    pub async fn memory_report(&self) -> MemoryReport {
150        use wacore::stats::{CollectionStats, HeapSize};
151
152        let (signal_sessions, signal_identities, signal_sender_keys) =
153            self.signal_cache.memory_stats().await;
154        let (lid_pn_lid_entries, lid_pn_pn_entries) = self.lid_pn_cache.memory_stats().await;
155        let pending_retries_count = self
156            .pending_retries
157            .lock()
158            .unwrap_or_else(|p| p.into_inner())
159            .len();
160
161        // Only the Arc is taken under the mutex — the walk must not block
162        // get_group_cache(), which every group send goes through.
163        let group_cache_arc = self.group_cache.lock().await.clone();
164        let group_cache = match group_cache_arc {
165            // Arc<T>'s HeapSize already includes size_of::<GroupInfo>().
166            Some(cache) => {
167                cache
168                    .memory_stats(|k, v| k.heap_bytes() + v.heap_bytes())
169                    .await
170            }
171            None => CollectionStats::default(),
172        };
173
174        let recent_messages = self
175            .recent_messages
176            .memory_stats(|k, v| k.chat.heap_bytes() + k.id.heap_bytes() + v.heap_bytes())
177            .await;
178
179        let group_devices_memo = self
180            .group_devices_memo
181            .memory_stats(|k, v| k.heap_bytes() + v.heap_bytes())
182            .await;
183        let dm_devices_memo = self
184            .dm_devices_memo
185            .memory_stats(|k, v| k.heap_bytes() + v.heap_bytes())
186            .await;
187        let group_distribution_locks = self.group_distribution_locks.capacity_stats().await;
188
189        // Each count read into a local so no two guards are ever held at once.
190        let response_waiters = self.response_waiters_guard().len();
191        let presence_subscriptions = self.presence_subscriptions.lock().await.len();
192        let app_state_key_requests = self.app_state_key_requests.lock().await.len();
193        let app_state_syncing = self.app_state_syncing.len();
194        let chatstate_handlers = self.chatstate_handlers.read().await.len();
195        let history_sync_activity = self.history_sync_activity.snapshot();
196        let history_sync_tasks = CollectionStats::new(
197            history_sync_activity.tasks as u64,
198            history_sync_activity.payload_bytes as u64,
199        );
200        #[cfg(feature = "voip-runtime")]
201        let pending_call_link_updates = self
202            .pending_call_link_joins
203            .lock()
204            .unwrap_or_else(|poisoned| poisoned.into_inner())
205            .memory_stats();
206        #[cfg(feature = "voip-runtime")]
207        let active_calls = self.call_registry.memory_stats();
208        #[cfg(feature = "plugins")]
209        let plugin_stats = self.plugin_stats();
210        #[cfg(feature = "plugins")]
211        let (
212            plugins,
213            plugin_install_tasks,
214            plugin_connection_tasks,
215            plugin_connection_generations,
216            plugin_core_event_subscriptions,
217        ) = plugin_stats
218            .as_ref()
219            .map(|host| {
220                host.plugins.iter().fold(
221                    (
222                        u64::try_from(host.plugins.len()).unwrap_or(u64::MAX),
223                        0u64,
224                        0u64,
225                        0u64,
226                        0u64,
227                    ),
228                    |(plugins, install, connection, generations, subscriptions), plugin| {
229                        (
230                            plugins,
231                            install.saturating_add(plugin.install_tasks),
232                            connection.saturating_add(plugin.connection_tasks),
233                            generations.saturating_add(plugin.connection_generations),
234                            subscriptions.saturating_add(plugin.core_event_subscriptions),
235                        )
236                    },
237                )
238            })
239            .unwrap_or_default();
240        #[cfg(feature = "plugins")]
241        let plugin_event_router = plugin_stats
242            .as_ref()
243            .and_then(|host| host.event_router)
244            .unwrap_or_default();
245
246        MemoryReport {
247            group_cache,
248            device_registry_cache: self.device_registry_cache.memory_stats().await,
249            lid_pn_lid_entries,
250            lid_pn_pn_entries,
251            recent_messages,
252            sender_key_device_cache: self.sender_key_device_cache.memory_stats().await,
253            group_devices_memo,
254            dm_devices_memo,
255            message_retry_counts: self.message_retry_counts.entry_count(),
256            undecryptable_dispatched: self.undecryptable_dispatched.entry_count(),
257            pdo_pending_requests: self.pdo_pending_requests.entry_count(),
258            pdo_requested: self.pdo_requested.entry_count(),
259            history_sync_tasks,
260            history_sync_tasks_peak: history_sync_activity.tasks_peak as u64,
261            history_sync_payload_bytes_peak: history_sync_activity.payload_bytes_peak as u64,
262            session_locks: self.session_locks.entry_count(),
263            chat_lanes: self.chat_lanes.entry_count(),
264            group_distribution_locks: group_distribution_locks.entries,
265            group_distribution_lock_evictions: group_distribution_locks.evictions,
266            group_distribution_lock_eviction_blocks: group_distribution_locks.eviction_blocks,
267            resend_rate_limiter_chats: self.resend_rate_limiter.entry_count(),
268            transport_ack_queue: self.transport_ack_queue.get().map_or(0, |tx| tx.len()),
269            delivery_receipt_queue: self.delivery_receipt_queue.get().map_or(0, |tx| tx.len()),
270            response_waiters,
271            node_waiters: self.node_waiter_count.load(Ordering::Relaxed),
272            pending_retries: pending_retries_count,
273            presence_subscriptions,
274            app_state_key_requests,
275            app_state_syncing,
276            signal_sessions,
277            signal_identities,
278            signal_sender_keys,
279            #[cfg(feature = "voip-runtime")]
280            pending_call_link_updates,
281            #[cfg(feature = "voip-runtime")]
282            active_calls,
283            #[cfg(feature = "plugins")]
284            plugins,
285            #[cfg(feature = "plugins")]
286            plugin_install_tasks,
287            #[cfg(feature = "plugins")]
288            plugin_connection_tasks,
289            #[cfg(feature = "plugins")]
290            plugin_connection_generations,
291            #[cfg(feature = "plugins")]
292            plugin_core_event_subscriptions,
293            #[cfg(feature = "plugins")]
294            plugin_event_endpoints: plugin_event_router.active_endpoints,
295            #[cfg(feature = "plugins")]
296            plugin_event_endpoint_capacity: plugin_event_router.endpoint_capacity,
297            #[cfg(feature = "plugins")]
298            plugin_event_queue: CollectionStats::new(
299                plugin_event_router.queued_events,
300                plugin_event_router.queued_payload_bytes,
301            ),
302            chatstate_handlers,
303            custom_enc_handlers: self.custom_enc_handlers.get().map_or(0, |m| m.len()),
304        }
305    }
306
307    /// Unified per-session resource estimate: the client's own collections
308    /// ([`Client::memory_report`]) **plus** the components that live outside the
309    /// `Client` and dominate real per-session RAM — the storage backend's page
310    /// cache, the transport's buffers + TLS/noise state, the HTTP client's pool
311    /// — and, when a [`AllocMeter`](wacore::stats::AllocMeter) is installed
312    /// (`with_alloc_meter`), an allocation-churn snapshot.
313    ///
314    /// On-demand only, no hot-path cost. Each out-of-client figure is best
315    /// effort: a component reports only what it can introspect, so
316    /// [`ResourceReport::total_estimated_bytes`] is a **lower bound** (see its
317    /// docs for which parts are exact vs. estimated). No PII — sizes and counts
318    /// only. `Send`, so multi-session consumers can await it off a worker.
319    pub async fn resource_report(&self) -> ResourceReport {
320        let client = self.memory_report().await;
321        let storage = self.persistence_manager.backend().resource_report().await;
322        let transport = {
323            let guard = self.transport.lock().await;
324            guard.as_ref().and_then(|t| t.resource_report())
325        };
326        let http = self.http_client.resource_report();
327        let alloc = self.alloc_meter.get().map(|m| m.snapshot());
328        ResourceReport {
329            client,
330            storage,
331            transport,
332            http,
333            alloc,
334        }
335    }
336
337    /// Get access to the PersistenceManager for this client.
338    /// This is useful for multi-account scenarios to get the device ID.
339    pub fn persistence_manager(&self) -> Arc<PersistenceManager> {
340        self.persistence_manager.clone()
341    }
342
343    // The owned returns below are the only clones left: the snapshot read
344    // itself is an Arc refcount bump (no lock against writers). Callers that
345    // only need a borrow can hold `persistence_manager().get_device_snapshot()`
346    // and read fields directly.
347    /// This device's push name (the display name peers see).
348    pub fn push_name(&self) -> String {
349        self.persistence_manager
350            .get_device_snapshot()
351            .push_name
352            .clone()
353    }
354
355    /// This device's phone-number JID, or `None` before pairing completes.
356    pub fn pn(&self) -> Option<Jid> {
357        self.persistence_manager.get_device_snapshot().pn.clone()
358    }
359
360    /// This device's LID JID, or `None` before pairing completes.
361    pub fn lid(&self) -> Option<Jid> {
362        self.persistence_manager.get_device_snapshot().lid.clone()
363    }
364
365    /// Snapshot-consistent identity for span/error tagging (redacted PN, raw LID). Named
366    /// fields, not a tuple — LID/PN transposition would otherwise be a silent, unchecked bug.
367    #[cfg(feature = "tracing")]
368    pub fn identity_tags(&self) -> IdentityTags {
369        let snapshot = self.persistence_manager.get_device_snapshot();
370        IdentityTags {
371            lid: snapshot.lid.as_ref().map(|j| j.to_string()),
372            pn: snapshot.pn.as_ref().map(|j| j.observe().to_string()),
373        }
374    }
375
376    /// Shared so every identity-tagged span leaves a field absent (not `""`) when unknown —
377    /// duplicating this per call site would drift out of sync. Skips the snapshot read when
378    /// the span is disabled.
379    #[cfg(feature = "tracing")]
380    pub(crate) fn record_identity_on_span(&self, span: &tracing::Span) {
381        if span.is_disabled() {
382            return;
383        }
384        let tags = self.identity_tags();
385        if let Some(lid) = tags.lid {
386            span.record("lid", tracing::field::display(lid));
387        }
388        if let Some(pn) = tags.pn {
389            span.record("pn", tracing::field::display(pn));
390        }
391    }
392
393    pub(crate) fn require_pn(&self) -> Result<Jid> {
394        self.pn().ok_or(ClientError::NotLoggedIn.into())
395    }
396
397    /// Resolve our own JID for a group, respecting its addressing mode.
398    ///
399    /// Returns LID for LID-addressing groups, PN otherwise.
400    /// Matches WhatsApp Web's `getMeUserLidOrJidForChat`.
401    pub(crate) async fn get_own_jid_for_group(
402        &self,
403        group_jid: &Jid,
404    ) -> Result<Jid, anyhow::Error> {
405        let device_snapshot = self.persistence_manager.get_device_snapshot();
406        let own_pn = device_snapshot
407            .pn
408            .clone()
409            .ok_or_else(|| anyhow::Error::from(ClientError::NotLoggedIn))?;
410
411        let addressing_mode = self
412            .groups()
413            .query_info(group_jid)
414            .await
415            .map(|info| info.addressing_mode)
416            .unwrap_or(crate::types::message::AddressingMode::Pn);
417
418        Ok(match addressing_mode {
419            crate::types::message::AddressingMode::Lid => {
420                device_snapshot.lid.clone().unwrap_or(own_pn)
421            }
422            crate::types::message::AddressingMode::Pn => own_pn,
423        })
424    }
425
426    pub(crate) async fn update_push_name_and_notify(self: &Arc<Self>, new_name: String) {
427        let device_snapshot = self.persistence_manager.get_device_snapshot();
428        let old_name = device_snapshot.push_name.clone();
429
430        if old_name == new_name {
431            return;
432        }
433
434        log::debug!("Updating push name from '{}' -> '{}'", old_name, new_name);
435        self.persistence_manager
436            .process_command(DeviceCommand::SetPushName(new_name.clone()))
437            .await;
438
439        self.core.event_bus.dispatch(Event::SelfPushNameUpdated(
440            crate::types::events::SelfPushNameUpdated::builder()
441                .from_server(true)
442                .old_name(old_name)
443                .new_name(new_name.clone())
444                .build(),
445        ));
446
447        let client_clone = self.clone();
448        self.runtime
449            .spawn(Box::pin(async move {
450                if let Err(e) = client_clone.presence().set_available().await {
451                    log::warn!("Failed to send presence after push name update: {:?}", e);
452                } else {
453                    log::debug!("Sent presence after push name update.");
454                }
455            }))
456            .detach();
457    }
458
459    /// Register a waiter for an incoming node matching the given filter.
460    ///
461    /// Returns a receiver that resolves when a matching node arrives.
462    /// The waiter starts buffering immediately, so register it **before**
463    /// performing the action that triggers the expected node.
464    ///
465    /// When multiple waiters match the same node, each matching waiter
466    /// receives a clone of the node (broadcast within a single resolve pass).
467    ///
468    /// # Example
469    /// ```ignore
470    /// let waiter = client.wait_for_node(
471    ///     NodeFilter::tag("notification").attr("type", "w:gp2"),
472    /// );
473    /// client.groups().add_participants(&group_jid, &[jid_c]).await?;
474    /// let node = waiter.await.expect("notification arrived");
475    /// ```
476    pub fn wait_for_node(
477        &self,
478        filter: NodeFilter,
479    ) -> futures::channel::oneshot::Receiver<Arc<wacore_binary::OwnedNodeRef>> {
480        let (tx, rx) = futures::channel::oneshot::channel();
481        self.node_waiter_count.fetch_add(1, Ordering::Release);
482        let mut waiters = self
483            .node_waiters
484            .lock()
485            .unwrap_or_else(|poisoned| poisoned.into_inner());
486        waiters.push(NodeWaiter { filter, tx });
487        rx
488    }
489
490    /// Register a waiter for an outgoing node before it is encrypted and sent.
491    ///
492    /// This is intended for tests and diagnostics that need to inspect the raw
493    /// stanza built by the client, such as asserting whether `<tctoken>` or
494    /// `<cstoken>` was attached.
495    pub fn wait_for_sent_node(
496        &self,
497        filter: NodeFilter,
498    ) -> futures::channel::oneshot::Receiver<Arc<Node>> {
499        let (tx, rx) = futures::channel::oneshot::channel();
500        self.sent_node_waiter_count.fetch_add(1, Ordering::Release);
501        let mut waiters = self
502            .sent_node_waiters
503            .lock()
504            .unwrap_or_else(|poisoned| poisoned.into_inner());
505        waiters.push(SentNodeWaiter { filter, tx });
506        rx
507    }
508
509    /// Poison-recovering lock of the `response_waiters` map. Centralizes the
510    /// `unwrap_or_else(into_inner)` so no call site reaches for a bare
511    /// `.lock().unwrap()` that would panic if a holder ever panicked. The critical
512    /// section is a trivial map op, never held across an `.await`.
513    #[inline]
514    pub(crate) fn response_waiters_guard(&self) -> std::sync::MutexGuard<'_, ResponseWaiterMap> {
515        self.response_waiters
516            .lock()
517            .unwrap_or_else(|p| p.into_inner())
518    }
519
520    /// Check pending node waiters against an incoming node.
521    /// Only called when `node_waiter_count > 0`.
522    pub(crate) fn resolve_node_waiters(&self, node: &Arc<wacore_binary::OwnedNodeRef>) {
523        resolve_waiters(&self.node_waiters, &self.node_waiter_count, node);
524    }
525
526    pub(crate) fn resolve_sent_node_waiters(&self, node: &Arc<Node>) {
527        let nr = node.as_node_ref();
528        let mut waiters = self
529            .sent_node_waiters
530            .lock()
531            .unwrap_or_else(|poisoned| poisoned.into_inner());
532        let mut i = 0;
533        while i < waiters.len() {
534            if waiters[i].tx.is_canceled() {
535                waiters.swap_remove(i);
536                self.sent_node_waiter_count.fetch_sub(1, Ordering::Release);
537            } else if waiters[i].filter.matches(&nr) {
538                let w = waiters.swap_remove(i);
539                self.sent_node_waiter_count.fetch_sub(1, Ordering::Release);
540                let _ = w.tx.send(Arc::clone(node));
541            } else {
542                i += 1;
543            }
544        }
545    }
546
547    pub(crate) fn clear_sent_node_waiters(&self) {
548        let mut waiters = self
549            .sent_node_waiters
550            .lock()
551            .unwrap_or_else(|poisoned| poisoned.into_inner());
552        let count = waiters.len();
553        if count > 0 {
554            waiters.clear();
555            self.sent_node_waiter_count
556                .fetch_sub(count, Ordering::Release);
557        }
558    }
559
560    fn should_downgrade_sync_error(&self, err: &anyhow::Error) -> bool {
561        if self.is_shutting_down() {
562            return true;
563        }
564
565        matches!(
566            err.downcast_ref::<crate::request::IqError>(),
567            Some(
568                crate::request::IqError::NotConnected
569                    | crate::request::IqError::InternalChannelClosed
570            )
571        )
572    }
573
574    /// Log a sync error, downgrading to debug level during shutdown/disconnect.
575    pub(crate) fn log_sync_error(&self, context: &str, err: &anyhow::Error) {
576        if self.should_downgrade_sync_error(err) {
577            debug!("Skipping {context} during shutdown: {err}");
578        } else {
579            warn!("Failed {context}: {err}");
580        }
581    }
582
583    /// Create and configure the stanza router with all the handlers.
584    pub(crate) fn create_stanza_router() -> crate::handlers::router::StanzaRouter {
585        use crate::handlers::{
586            basic::{AckHandler, FailureHandler, StreamErrorHandler, SuccessHandler},
587            chatstate::ChatstateHandler,
588            ib::IbHandler,
589            iq::IqHandler,
590            message::MessageHandler,
591            notification::NotificationHandler,
592            receipt::ReceiptHandler,
593            router::StanzaRouter,
594        };
595
596        let mut router = StanzaRouter::new();
597
598        // Register all handlers
599        router.register(Arc::new(MessageHandler));
600        router.register(Arc::new(ReceiptHandler));
601        router.register(Arc::new(IqHandler));
602        router.register(Arc::new(SuccessHandler));
603        router.register(Arc::new(FailureHandler));
604        router.register(Arc::new(StreamErrorHandler));
605        router.register(Arc::new(IbHandler));
606        router.register(Arc::new(NotificationHandler));
607        router.register(Arc::new(AckHandler));
608        router.register(Arc::new(ChatstateHandler));
609
610        router.register(Arc::new(crate::handlers::call::CallHandler));
611
612        // Register unimplemented handlers
613        router.register(Arc::new(crate::handlers::presence::PresenceHandler));
614
615        router
616    }
617}
618
619#[cfg(test)]
620mod raw_node_tests {
621    #[tokio::test]
622    async fn raw_node_forwarding_stays_enabled_until_the_last_lease_drops() {
623        let client = crate::test_utils::create_test_client().await;
624        assert!(!client.raw_node_forwarding_enabled());
625
626        let first = client.acquire_raw_node_forwarding();
627        let second = client.acquire_raw_node_forwarding();
628        assert!(client.raw_node_forwarding_enabled());
629
630        drop(first);
631        assert!(client.raw_node_forwarding_enabled());
632        drop(second);
633        assert!(!client.raw_node_forwarding_enabled());
634    }
635}
636
637#[cfg(test)]
638mod send_checks {
639    fn assert_send<T: Send>(_: &T) {}
640
641    /// Compile-time guard that `memory_report()` stays `Send`: a `!Send` value held
642    /// across an `.await` (e.g. a raw-pointer dedup set) would silently break
643    /// `tokio::spawn` / axum callers. Built for its type only, never polled.
644    #[allow(dead_code)]
645    fn memory_report_future_is_send(c: &super::Client) {
646        assert_send(&c.memory_report());
647    }
648
649    /// Same guard for `resource_report()` — it awaits the backend's async report
650    /// and locks the transport, so a `!Send` future here would break the same
651    /// multi-threaded consumers (per #964).
652    #[allow(dead_code)]
653    fn resource_report_future_is_send(c: &super::Client) {
654        assert_send(&c.resource_report());
655    }
656}