1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use thiserror::Error;
6
7use super::Client;
8#[cfg(feature = "client-lifecycle")]
9use super::{ClientLifecycle, LifecycleRegistration};
10use crate::cache_config::CacheConfig;
11use crate::http::HttpClient;
12#[cfg(feature = "plugins")]
13use crate::plugins::{
14 ClientPlugin, PluginHost, PluginHostConfig, PluginPlan, PluginPlanError, PluginRegistration,
15 UntypedClientPlugin,
16};
17use crate::store::error::StoreError;
18use crate::store::persistence_manager::PersistenceManager;
19use crate::sync_task::MajorSyncTask;
20use crate::transport::TransportFactory;
21use crate::types::durability_hook::InboundDurabilityHook;
22use crate::types::enc_handler::EncHandler;
23use wacore::runtime::Runtime;
24
25pub struct ClientBuild {
30 client: Arc<Client>,
31 sync_task_receiver: async_channel::Receiver<MajorSyncTask>,
32}
33
34impl ClientBuild {
35 pub(crate) fn new(
36 client: Arc<Client>,
37 sync_task_receiver: async_channel::Receiver<MajorSyncTask>,
38 ) -> Self {
39 Self {
40 client,
41 sync_task_receiver,
42 }
43 }
44
45 pub fn into_client(self) -> Arc<Client> {
47 let (client, sync_task_receiver) = self.into_parts();
48 client.start_sync_task_worker(sync_task_receiver);
49 client
50 }
51
52 pub fn into_parts(self) -> (Arc<Client>, async_channel::Receiver<MajorSyncTask>) {
55 (self.client, self.sync_task_receiver)
56 }
57}
58
59#[derive(Debug, Error)]
61#[non_exhaustive]
62pub enum ClientBuilderError {
63 #[error("missing async runtime")]
64 MissingRuntime,
65 #[error("missing persistence manager")]
66 MissingPersistenceManager,
67 #[error("missing transport factory")]
68 MissingTransportFactory,
69 #[error("missing HTTP client")]
70 MissingHttpClient,
71 #[error("background saver interval must be greater than zero")]
72 InvalidBackgroundSaverInterval,
73 #[cfg(feature = "plugins")]
74 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
75 #[error("plugin install timeout must be greater than zero")]
76 InvalidPluginInstallTimeout,
77 #[cfg(feature = "plugins")]
78 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
79 #[error("plugin callback timeout must be greater than zero")]
80 InvalidPluginCallbackTimeout,
81 #[cfg(feature = "plugins")]
82 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
83 #[error("plugin task-drain timeout must be greater than zero")]
84 InvalidPluginTaskDrainTimeout,
85 #[error("the configured backend does not support the inbound durability hook: {0}")]
86 UnsupportedDurabilityBackend(String),
87 #[cfg(feature = "client-lifecycle")]
88 #[cfg_attr(docsrs, doc(cfg(feature = "client-lifecycle")))]
89 #[error("client lifecycle installation failed: {0}")]
90 LifecycleInstall(#[source] anyhow::Error),
91 #[cfg(feature = "plugins")]
92 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
93 #[error("plugin host installation failed: {0}")]
94 PluginInstall(#[source] anyhow::Error),
95 #[cfg(feature = "plugins")]
96 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
97 #[error("invalid plugin plan: {0}")]
98 PluginPlan(#[from] PluginPlanError),
99}
100
101#[must_use = "call .build() to produce the Client; the builder does nothing on its own"]
107pub struct ClientBuilder {
108 runtime: Option<Arc<dyn Runtime>>,
109 persistence_manager: Option<Arc<PersistenceManager>>,
110 transport_factory: Option<Arc<dyn TransportFactory>>,
111 http_client: Option<Arc<dyn HttpClient>>,
112 override_version: Option<(u32, u32, u32)>,
113 cache_config: CacheConfig,
114 custom_enc_handlers: HashMap<String, Arc<dyn EncHandler>>,
115 inbound_durability_hook: Option<Arc<dyn InboundDurabilityHook>>,
116 skip_history_sync: bool,
117 wanted_pre_key_count: Option<usize>,
118 resend_rate_limit: Option<(u32, u32)>,
119 task_instrument: Option<Arc<dyn wacore::stats::TaskInstrument>>,
120 alloc_meter: Option<Arc<wacore::stats::AllocMeter>>,
121 background_saver_interval: Option<Duration>,
122 #[cfg(feature = "client-lifecycle")]
123 lifecycle: Option<Arc<dyn ClientLifecycle>>,
124 #[cfg(feature = "plugins")]
125 plugins: Vec<PluginRegistration>,
126 #[cfg(feature = "plugins")]
127 plugin_host_config: PluginHostConfig,
128}
129
130impl Default for ClientBuilder {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl ClientBuilder {
137 pub fn new() -> Self {
139 Self {
140 runtime: None,
141 persistence_manager: None,
142 transport_factory: None,
143 http_client: None,
144 override_version: None,
145 cache_config: CacheConfig::default(),
146 custom_enc_handlers: HashMap::new(),
147 inbound_durability_hook: None,
148 skip_history_sync: false,
149 wanted_pre_key_count: None,
150 resend_rate_limit: None,
151 task_instrument: None,
152 alloc_meter: None,
153 background_saver_interval: None,
154 #[cfg(feature = "client-lifecycle")]
155 lifecycle: None,
156 #[cfg(feature = "plugins")]
157 plugins: Vec::new(),
158 #[cfg(feature = "plugins")]
159 plugin_host_config: PluginHostConfig::default(),
160 }
161 }
162
163 pub fn with_runtime<R>(mut self, runtime: R) -> Self
164 where
165 R: Runtime,
166 {
167 self.runtime = Some(Arc::new(runtime));
168 self
169 }
170
171 pub fn with_runtime_arc(mut self, runtime: Arc<dyn Runtime>) -> Self {
172 self.runtime = Some(runtime);
173 self
174 }
175
176 pub fn with_persistence_manager(
177 mut self,
178 persistence_manager: Arc<PersistenceManager>,
179 ) -> Self {
180 self.persistence_manager = Some(persistence_manager);
181 self
182 }
183
184 pub fn with_transport_factory<T>(mut self, transport_factory: T) -> Self
185 where
186 T: TransportFactory + 'static,
187 {
188 self.transport_factory = Some(Arc::new(transport_factory));
189 self
190 }
191
192 pub fn with_transport_factory_arc(
193 mut self,
194 transport_factory: Arc<dyn TransportFactory>,
195 ) -> Self {
196 self.transport_factory = Some(transport_factory);
197 self
198 }
199
200 pub fn with_http_client<H>(mut self, http_client: H) -> Self
201 where
202 H: HttpClient + 'static,
203 {
204 self.http_client = Some(Arc::new(http_client));
205 self
206 }
207
208 pub fn with_http_client_arc(mut self, http_client: Arc<dyn HttpClient>) -> Self {
209 self.http_client = Some(http_client);
210 self
211 }
212
213 pub fn with_version_override(mut self, version: (u32, u32, u32)) -> Self {
214 self.override_version = Some(version);
215 self
216 }
217
218 pub fn with_cache_config(mut self, cache_config: CacheConfig) -> Self {
219 self.cache_config = cache_config;
220 self
221 }
222
223 pub fn with_enc_handler<H>(mut self, payload_type: impl Into<String>, handler: H) -> Self
225 where
226 H: EncHandler + 'static,
227 {
228 self.custom_enc_handlers
229 .insert(payload_type.into(), Arc::new(handler));
230 self
231 }
232
233 pub fn with_enc_handler_arc(
235 mut self,
236 payload_type: impl Into<String>,
237 handler: Arc<dyn EncHandler>,
238 ) -> Self {
239 self.custom_enc_handlers
240 .insert(payload_type.into(), handler);
241 self
242 }
243
244 pub(crate) fn with_custom_enc_handlers(
245 mut self,
246 handlers: HashMap<String, Arc<dyn EncHandler>>,
247 ) -> Self {
248 self.custom_enc_handlers = handlers;
249 self
250 }
251
252 pub fn with_inbound_durability_hook<H>(mut self, hook: H) -> Self
254 where
255 H: InboundDurabilityHook + 'static,
256 {
257 self.inbound_durability_hook = Some(Arc::new(hook));
258 self
259 }
260
261 pub fn with_inbound_durability_hook_arc(
263 mut self,
264 hook: Arc<dyn InboundDurabilityHook>,
265 ) -> Self {
266 self.inbound_durability_hook = Some(hook);
267 self
268 }
269
270 pub fn with_skip_history_sync(mut self, skip: bool) -> Self {
271 self.skip_history_sync = skip;
272 self
273 }
274
275 pub fn with_wanted_pre_key_count(mut self, count: usize) -> Self {
276 self.wanted_pre_key_count = Some(count);
277 self
278 }
279
280 pub fn with_resend_rate_limit(mut self, burst: u32, refill_per_min: u32) -> Self {
281 self.resend_rate_limit = Some((burst, refill_per_min));
282 self
283 }
284
285 pub fn with_task_instrument(
287 mut self,
288 instrument: Arc<dyn wacore::stats::TaskInstrument>,
289 ) -> Self {
290 self.task_instrument = Some(instrument);
291 self.alloc_meter = None;
292 self
293 }
294
295 pub fn with_alloc_meter(mut self, meter: Arc<wacore::stats::AllocMeter>) -> Self {
297 self.task_instrument = Some(meter.clone());
298 self.alloc_meter = Some(meter);
299 self
300 }
301
302 pub fn with_background_saver_interval(mut self, interval: Duration) -> Self {
304 self.background_saver_interval = Some(interval);
305 self
306 }
307
308 #[cfg(feature = "client-lifecycle")]
310 #[cfg_attr(docsrs, doc(cfg(feature = "client-lifecycle")))]
311 pub fn with_lifecycle<L>(mut self, lifecycle: L) -> Self
312 where
313 L: ClientLifecycle + 'static,
314 {
315 self.lifecycle = Some(Arc::new(lifecycle));
316 self
317 }
318
319 #[cfg(feature = "client-lifecycle")]
321 #[cfg_attr(docsrs, doc(cfg(feature = "client-lifecycle")))]
322 pub fn with_lifecycle_arc(mut self, lifecycle: Arc<dyn ClientLifecycle>) -> Self {
323 self.lifecycle = Some(lifecycle);
324 self
325 }
326
327 #[cfg(feature = "plugins")]
329 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
330 pub fn with_plugin<P: ClientPlugin>(mut self, plugin: P) -> Self {
331 self.plugins.push(PluginRegistration::new(plugin));
332 self
333 }
334
335 #[cfg(feature = "plugins")]
337 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
338 pub fn with_plugin_arc<P: ClientPlugin>(mut self, plugin: Arc<P>) -> Self {
339 self.plugins.push(PluginRegistration::new_arc(plugin));
340 self
341 }
342
343 #[cfg(feature = "plugins")]
345 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
346 pub fn with_untyped_plugin<P: UntypedClientPlugin>(mut self, plugin: P) -> Self {
347 self.plugins.push(PluginRegistration::new_untyped(plugin));
348 self
349 }
350
351 #[cfg(feature = "plugins")]
353 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
354 pub fn with_untyped_plugin_arc<P: UntypedClientPlugin + ?Sized>(
355 mut self,
356 plugin: Arc<P>,
357 ) -> Self {
358 self.plugins
359 .push(PluginRegistration::new_untyped_arc(plugin));
360 self
361 }
362
363 #[cfg(feature = "plugins")]
365 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
366 pub fn with_plugin_host_config(mut self, config: PluginHostConfig) -> Self {
367 self.plugin_host_config = config;
368 self
369 }
370
371 #[cfg(feature = "plugins")]
372 pub(crate) fn with_plugin_registrations(
373 mut self,
374 registrations: Vec<PluginRegistration>,
375 ) -> Self {
376 self.plugins = registrations;
377 self
378 }
379
380 pub async fn build(self) -> Result<ClientBuild, ClientBuilderError> {
382 self.build_boxed().await
383 }
384
385 #[inline(never)]
386 fn build_boxed(
387 self,
388 ) -> wacore::runtime::BoxFuture<'static, Result<ClientBuild, ClientBuilderError>> {
389 Box::pin(async move {
390 let runtime = self
391 .runtime
392 .as_ref()
393 .cloned()
394 .ok_or(ClientBuilderError::MissingRuntime)?;
395 let persistence_manager = self
396 .persistence_manager
397 .as_ref()
398 .cloned()
399 .ok_or(ClientBuilderError::MissingPersistenceManager)?;
400 let transport_factory = self
401 .transport_factory
402 .as_ref()
403 .cloned()
404 .ok_or(ClientBuilderError::MissingTransportFactory)?;
405 let http_client = self
406 .http_client
407 .as_ref()
408 .cloned()
409 .ok_or(ClientBuilderError::MissingHttpClient)?;
410
411 if self.background_saver_interval == Some(Duration::ZERO) {
412 return Err(ClientBuilderError::InvalidBackgroundSaverInterval);
413 }
414
415 #[cfg(feature = "plugins")]
416 if self.plugin_host_config.install_timeout() == Duration::ZERO {
417 return Err(ClientBuilderError::InvalidPluginInstallTimeout);
418 }
419 #[cfg(feature = "plugins")]
420 if self.plugin_host_config.callback_timeout() == Duration::ZERO {
421 return Err(ClientBuilderError::InvalidPluginCallbackTimeout);
422 }
423 #[cfg(feature = "plugins")]
424 if self.plugin_host_config.task_drain_timeout() == Duration::ZERO {
425 return Err(ClientBuilderError::InvalidPluginTaskDrainTimeout);
426 }
427
428 if self.inbound_durability_hook.is_some() {
429 probe_durability_backend(&persistence_manager.backend()).await?;
430 }
431
432 self.finish(runtime, persistence_manager, transport_factory, http_client)
433 .await
434 })
435 }
436
437 pub(crate) async fn build_required(
438 runtime: Arc<dyn Runtime>,
439 persistence_manager: Arc<PersistenceManager>,
440 transport_factory: Arc<dyn TransportFactory>,
441 http_client: Arc<dyn HttpClient>,
442 override_version: Option<(u32, u32, u32)>,
443 cache_config: CacheConfig,
444 ) -> ClientBuild {
445 let result = Self {
446 override_version,
447 cache_config,
448 ..Self::new()
449 }
450 .finish(runtime, persistence_manager, transport_factory, http_client)
451 .await;
452 match result {
453 Ok(build) => build,
454 Err(error) => unreachable!("default lifecycle-free build failed: {error}"),
455 }
456 }
457
458 async fn finish(
459 self,
460 runtime: Arc<dyn Runtime>,
461 persistence_manager: Arc<PersistenceManager>,
462 transport_factory: Arc<dyn TransportFactory>,
463 http_client: Arc<dyn HttpClient>,
464 ) -> Result<ClientBuild, ClientBuilderError> {
465 #[cfg(feature = "plugins")]
466 let plugin_plan = PluginPlan::prepare(self.plugins)?;
467 let runtime: Arc<dyn Runtime> = match self.task_instrument {
468 Some(instrument) => {
469 Arc::new(wacore::stats::InstrumentedRuntime::new(runtime, instrument))
470 }
471 None => runtime,
472 };
473
474 #[cfg(feature = "client-lifecycle")]
475 let lifecycle_handler = self.lifecycle;
476 #[cfg(feature = "plugins")]
477 let (lifecycle_handler, plugin_host) = {
478 let mut lifecycle_handler = lifecycle_handler;
479 let plugin_host = plugin_plan.map(|plan| {
480 let host = PluginHost::new(plan, lifecycle_handler.take(), self.plugin_host_config);
481 lifecycle_handler = Some(host.clone());
482 host
483 });
484 (lifecycle_handler, plugin_host)
485 };
486 #[cfg(feature = "client-lifecycle")]
487 let lifecycle = lifecycle_handler.map(|handler| {
488 #[cfg(feature = "plugins")]
489 if let Some(plugin_host) = &plugin_host {
490 return Arc::new(LifecycleRegistration::new_with_timeout(
491 handler,
492 Arc::clone(&runtime),
493 plugin_host.lifecycle_callback_timeout(),
494 ));
495 }
496 Arc::new(LifecycleRegistration::new(handler, Arc::clone(&runtime)))
497 });
498 let assembly = Client::assemble(
499 Arc::clone(&runtime),
500 Arc::clone(&persistence_manager),
501 transport_factory,
502 http_client,
503 self.override_version,
504 self.cache_config,
505 ClientExtensions {
506 #[cfg(feature = "client-lifecycle")]
507 lifecycle,
508 #[cfg(feature = "plugins")]
509 plugin_host,
510 },
511 );
512 let client = assembly.client();
513 #[cfg(feature = "client-lifecycle")]
514 let mut construction = ClientConstructionGuard::new(Arc::clone(&client));
515
516 if !self.custom_enc_handlers.is_empty() {
517 let _ = client.custom_enc_handlers.set(self.custom_enc_handlers);
518 }
519 if let Some(hook) = self.inbound_durability_hook {
520 let _ = client.inbound_durability_hook.set(hook);
521 }
522 if self.skip_history_sync {
523 client.set_skip_history_sync(true);
524 }
525 if let Some(count) = self.wanted_pre_key_count {
526 client.set_wanted_pre_key_count(count);
527 }
528 if let Some((burst, refill_per_min)) = self.resend_rate_limit {
529 client.set_resend_rate_limit(burst, refill_per_min);
530 }
531 if let Some(meter) = self.alloc_meter {
532 let _ = client.alloc_meter.set(meter);
533 }
534 #[cfg(feature = "client-lifecycle")]
535 if let Some(lifecycle) = &client.lifecycle
536 && let Err(error) = lifecycle.install(Arc::downgrade(&client)).await
537 {
538 #[cfg(feature = "plugins")]
539 if client.plugin_host.is_some() {
540 return Err(ClientBuilderError::PluginInstall(error));
541 }
542 return Err(ClientBuilderError::LifecycleInstall(error));
543 }
544
545 let build = assembly.start();
546 if let Some(interval) = self.background_saver_interval {
547 let saver_handle = persistence_manager.run_background_saver(
548 runtime,
549 interval,
550 build.client.shutdown_signal(),
551 );
552 let _ = build.client.saver_handle.set(saver_handle);
553 }
554 #[cfg(feature = "client-lifecycle")]
555 if let Some(lifecycle) = &client.lifecycle {
556 #[cfg(feature = "plugins")]
557 let activated = if let Some(plugin_host) = &client.plugin_host {
558 lifecycle.activate_with(|| plugin_host.commit())
559 } else {
560 lifecycle.activate()
561 };
562 #[cfg(not(feature = "plugins"))]
563 let activated = lifecycle.activate();
564 if !activated {
565 client.signal_shutdown_sync();
566 client.shutdown_lifecycle().await;
567 #[cfg(feature = "plugins")]
568 if client.plugin_host.is_some() {
569 return Err(ClientBuilderError::PluginInstall(anyhow::anyhow!(
570 "client shutdown raced plugin publication"
571 )));
572 }
573 return Err(ClientBuilderError::LifecycleInstall(anyhow::anyhow!(
574 "client shutdown raced lifecycle activation"
575 )));
576 }
577 }
578 #[cfg(feature = "client-lifecycle")]
579 construction.disarm();
580 Ok(build)
581 }
582}
583
584async fn probe_durability_backend(
585 backend: &Arc<dyn crate::store::traits::Backend>,
586) -> Result<(), ClientBuilderError> {
587 use portable_atomic::{AtomicU64, Ordering};
588
589 static PROBE_SEQ: AtomicU64 = AtomicU64::new(0);
590 const PROBE_JID: &str = "0@s.whatsapp.net";
591 const PROBE_PAYLOAD: &[u8] = b"probe";
592 let probe_id = format!(
593 "__wa_durability_probe_{}_{}__",
594 std::process::id(),
595 PROBE_SEQ.fetch_add(1, Ordering::Relaxed)
596 );
597 let map_err =
598 |error: StoreError| ClientBuilderError::UnsupportedDurabilityBackend(error.to_string());
599
600 backend
601 .store_pending_inbound(PROBE_JID, PROBE_JID, &probe_id, PROBE_PAYLOAD)
602 .await
603 .map_err(map_err)?;
604 let stored = backend
605 .get_pending_inbound(PROBE_JID, PROBE_JID, &probe_id)
606 .await
607 .map_err(map_err)?;
608 backend
609 .delete_pending_inbound(PROBE_JID, PROBE_JID, &probe_id)
610 .await
611 .map_err(map_err)?;
612
613 if stored.as_deref() != Some(PROBE_PAYLOAD) {
614 return Err(ClientBuilderError::UnsupportedDurabilityBackend(
615 "pending-inbound buffer did not round-trip".to_string(),
616 ));
617 }
618 Ok(())
619}
620
621pub(super) struct ClientAssembly {
624 client: Arc<Client>,
625 sync_task_receiver: async_channel::Receiver<MajorSyncTask>,
626}
627
628#[cfg(feature = "client-lifecycle")]
629struct ClientConstructionGuard {
630 client: Arc<Client>,
631 armed: bool,
632}
633
634#[cfg(feature = "client-lifecycle")]
635impl ClientConstructionGuard {
636 fn new(client: Arc<Client>) -> Self {
637 Self {
638 client,
639 armed: true,
640 }
641 }
642
643 fn disarm(&mut self) {
644 self.armed = false;
645 }
646}
647
648#[cfg(feature = "client-lifecycle")]
649impl Drop for ClientConstructionGuard {
650 fn drop(&mut self) {
651 if self.armed {
652 self.client.signal_shutdown_sync();
653 }
654 }
655}
656
657#[derive(Default)]
658pub(super) struct ClientExtensions {
659 #[cfg(feature = "client-lifecycle")]
660 pub(super) lifecycle: Option<Arc<LifecycleRegistration>>,
661 #[cfg(feature = "plugins")]
662 pub(super) plugin_host: Option<Arc<PluginHost>>,
663}
664
665impl ClientAssembly {
666 pub(super) fn new(
667 client: Arc<Client>,
668 sync_task_receiver: async_channel::Receiver<MajorSyncTask>,
669 ) -> Self {
670 Self {
671 client,
672 sync_task_receiver,
673 }
674 }
675
676 pub(super) fn start(self) -> ClientBuild {
677 self.client.start_services();
678 ClientBuild::new(self.client, self.sync_task_receiver)
679 }
680
681 fn client(&self) -> Arc<Client> {
682 Arc::clone(&self.client)
683 }
684}
685
686#[cfg(test)]
687mod tests {
688 use std::future::Future;
689 use std::pin::Pin;
690 use std::sync::atomic::{AtomicUsize, Ordering};
691 use std::time::Duration;
692
693 use super::*;
694 use crate::runtime_impl::TokioRuntime;
695 use crate::test_utils::MockHttpClient;
696 use crate::transport::mock::MockTransportFactory;
697 use wacore::runtime::AbortHandle;
698
699 #[cfg(feature = "client-lifecycle")]
700 struct FailingLifecycle {
701 spawns: Arc<AtomicUsize>,
702 installed_client: std::sync::Mutex<Option<std::sync::Weak<Client>>>,
703 }
704
705 #[cfg(feature = "client-lifecycle")]
706 struct RunDuringInstallLifecycle {
707 client: async_channel::Sender<Arc<Client>>,
708 release: async_channel::Receiver<()>,
709 run_finished: async_channel::Sender<()>,
710 }
711
712 #[cfg(feature = "client-lifecycle")]
713 struct ConnectDuringInstallLifecycle {
714 client: async_channel::Sender<Arc<Client>>,
715 release: async_channel::Receiver<()>,
716 connect_invoked: async_channel::Sender<()>,
717 connect_finished: async_channel::Sender<bool>,
718 }
719
720 #[cfg(feature = "client-lifecycle")]
721 struct BlockingTransportFactory {
722 started: async_channel::Sender<()>,
723 release: async_channel::Receiver<()>,
724 }
725
726 #[cfg(feature = "client-lifecycle")]
727 #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
728 #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
729 impl TransportFactory for BlockingTransportFactory {
730 async fn create_transport(
731 &self,
732 ) -> Result<
733 (
734 Arc<dyn crate::transport::Transport>,
735 async_channel::Receiver<crate::transport::TransportEvent>,
736 ),
737 anyhow::Error,
738 > {
739 self.started
740 .send(())
741 .await
742 .map_err(|_| anyhow::anyhow!("transport-start receiver closed"))?;
743 self.release
744 .recv()
745 .await
746 .map_err(|_| anyhow::anyhow!("transport release closed"))?;
747 Err(anyhow::anyhow!("injected transport stop"))
748 }
749 }
750
751 #[cfg(feature = "client-lifecycle")]
752 impl ClientLifecycle for RunDuringInstallLifecycle {
753 fn install<'a>(
754 &'a self,
755 client: std::sync::Weak<Client>,
756 ) -> wacore::runtime::BoxFuture<'a, anyhow::Result<()>> {
757 Box::pin(async move {
758 let client = client
759 .upgrade()
760 .ok_or_else(|| anyhow::anyhow!("client unavailable during install"))?;
761 let run_client = client.clone();
762 let run_finished = self.run_finished.clone();
763 client
764 .runtime
765 .spawn(Box::pin(async move {
766 run_client.run().await;
767 let _ = run_finished.send(()).await;
768 }))
769 .detach();
770 self.client
771 .send(client)
772 .await
773 .map_err(|_| anyhow::anyhow!("test client receiver closed"))?;
774 self.release
775 .recv()
776 .await
777 .map_err(|_| anyhow::anyhow!("test install release closed"))?;
778 Ok(())
779 })
780 }
781 }
782
783 #[cfg(feature = "client-lifecycle")]
784 impl ClientLifecycle for ConnectDuringInstallLifecycle {
785 fn install<'a>(
786 &'a self,
787 client: std::sync::Weak<Client>,
788 ) -> wacore::runtime::BoxFuture<'a, anyhow::Result<()>> {
789 Box::pin(async move {
790 let client = client
791 .upgrade()
792 .ok_or_else(|| anyhow::anyhow!("client unavailable during install"))?;
793 let connect_client = client.clone();
794 let connect_invoked = self.connect_invoked.clone();
795 let connect_finished = self.connect_finished.clone();
796 client
797 .runtime
798 .spawn(Box::pin(async move {
799 let _ = connect_invoked.send(()).await;
800 let failed = connect_client.connect().await.is_err();
801 let _ = connect_finished.send(failed).await;
802 }))
803 .detach();
804 self.client
805 .send(client)
806 .await
807 .map_err(|_| anyhow::anyhow!("test client receiver closed"))?;
808 self.release
809 .recv()
810 .await
811 .map_err(|_| anyhow::anyhow!("test install release closed"))?;
812 Ok(())
813 })
814 }
815 }
816
817 #[cfg(feature = "client-lifecycle")]
818 impl ClientLifecycle for FailingLifecycle {
819 fn install<'a>(
820 &'a self,
821 client: std::sync::Weak<Client>,
822 ) -> wacore::runtime::BoxFuture<'a, anyhow::Result<()>> {
823 Box::pin(async move {
824 assert_eq!(self.spawns.load(Ordering::SeqCst), 0);
825 *self
826 .installed_client
827 .lock()
828 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(client);
829 Err(anyhow::anyhow!("injected install failure"))
830 })
831 }
832 }
833
834 struct CountingRuntime {
835 spawns: Arc<AtomicUsize>,
836 }
837
838 #[async_trait::async_trait]
839 impl Runtime for CountingRuntime {
840 fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> AbortHandle {
841 self.spawns.fetch_add(1, Ordering::SeqCst);
842 TokioRuntime.spawn(future)
843 }
844
845 fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>> {
846 TokioRuntime.sleep(duration)
847 }
848
849 fn spawn_blocking(
850 &self,
851 f: Box<dyn FnOnce() + Send + 'static>,
852 ) -> Pin<Box<dyn Future<Output = ()> + Send>> {
853 TokioRuntime.spawn_blocking(f)
854 }
855
856 fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>> {
857 TokioRuntime.yield_now()
858 }
859 }
860
861 async fn complete_builder() -> ClientBuilder {
862 let persistence_manager = Arc::new(
863 PersistenceManager::new(crate::test_utils::create_test_backend().await)
864 .await
865 .expect("persistence manager"),
866 );
867 ClientBuilder::new()
868 .with_runtime(TokioRuntime)
869 .with_persistence_manager(persistence_manager)
870 .with_transport_factory(MockTransportFactory::new())
871 .with_http_client(MockHttpClient)
872 }
873
874 #[tokio::test]
875 async fn validates_required_dependencies_before_assembly() {
876 assert!(matches!(
877 ClientBuilder::new().build().await,
878 Err(ClientBuilderError::MissingRuntime)
879 ));
880
881 assert!(matches!(
882 ClientBuilder::new()
883 .with_runtime(TokioRuntime)
884 .build()
885 .await,
886 Err(ClientBuilderError::MissingPersistenceManager)
887 ));
888
889 let persistence_manager = Arc::new(
890 PersistenceManager::new(crate::test_utils::create_test_backend().await)
891 .await
892 .expect("persistence manager"),
893 );
894 assert!(matches!(
895 ClientBuilder::new()
896 .with_runtime(TokioRuntime)
897 .with_persistence_manager(Arc::clone(&persistence_manager))
898 .build()
899 .await,
900 Err(ClientBuilderError::MissingTransportFactory)
901 ));
902
903 assert!(matches!(
904 ClientBuilder::new()
905 .with_runtime(TokioRuntime)
906 .with_persistence_manager(persistence_manager)
907 .with_transport_factory(MockTransportFactory::new())
908 .build()
909 .await,
910 Err(ClientBuilderError::MissingHttpClient)
911 ));
912 }
913
914 #[tokio::test]
915 async fn assembly_is_inert_until_started() {
916 let persistence_manager = Arc::new(
917 PersistenceManager::new(crate::test_utils::create_test_backend().await)
918 .await
919 .expect("persistence manager"),
920 );
921 let spawns = Arc::new(AtomicUsize::new(0));
922 let runtime = Arc::new(CountingRuntime {
923 spawns: Arc::clone(&spawns),
924 }) as Arc<dyn Runtime>;
925
926 let assembly = Client::assemble(
927 runtime,
928 persistence_manager,
929 Arc::new(MockTransportFactory::new()),
930 Arc::new(MockHttpClient),
931 None,
932 CacheConfig::default(),
933 ClientExtensions::default(),
934 );
935 assert_eq!(spawns.load(Ordering::SeqCst), 0);
936
937 let build = assembly.start();
938 assert_eq!(spawns.load(Ordering::SeqCst), 1);
939 build.into_client().signal_shutdown_sync();
940 }
941
942 #[tokio::test]
943 #[cfg(feature = "client-lifecycle")]
944 async fn run_leaked_during_install_waits_for_complete_construction() {
945 let (client_tx, client_rx) = async_channel::bounded(1);
946 let (release_tx, release_rx) = async_channel::bounded(1);
947 let (run_finished_tx, run_finished_rx) = async_channel::bounded(1);
948 let builder = complete_builder()
949 .await
950 .with_lifecycle(RunDuringInstallLifecycle {
951 client: client_tx,
952 release: release_rx,
953 run_finished: run_finished_tx,
954 });
955 let build = tokio::spawn(async move { builder.build().await });
956 let leaked_client = client_rx
957 .recv()
958 .await
959 .expect("client leaked during install");
960 tokio::task::yield_now().await;
961 assert!(!leaked_client.is_running.load(Ordering::Acquire));
962
963 release_tx.send(()).await.expect("release installation");
964 let client = build
965 .await
966 .expect("builder task")
967 .expect("successful build")
968 .into_client();
969 tokio::time::timeout(Duration::from_secs(1), async {
970 while !client.is_running.load(Ordering::Acquire) {
971 tokio::task::yield_now().await;
972 }
973 })
974 .await
975 .expect("run released after activation");
976 client.signal_shutdown_sync();
977 tokio::time::timeout(Duration::from_secs(5), run_finished_rx.recv())
978 .await
979 .expect("run stop timeout")
980 .expect("run stopped");
981 }
982
983 #[tokio::test]
984 #[cfg(feature = "client-lifecycle")]
985 async fn connect_leaked_during_install_waits_for_complete_construction() {
986 let (client_tx, client_rx) = async_channel::bounded(1);
987 let (install_release_tx, install_release_rx) = async_channel::bounded(1);
988 let (connect_invoked_tx, connect_invoked_rx) = async_channel::bounded(1);
989 let (connect_finished_tx, connect_finished_rx) = async_channel::bounded(1);
990 let (transport_started_tx, transport_started_rx) = async_channel::bounded(1);
991 let (transport_release_tx, transport_release_rx) = async_channel::bounded(1);
992 let builder = complete_builder()
993 .await
994 .with_transport_factory(BlockingTransportFactory {
995 started: transport_started_tx,
996 release: transport_release_rx,
997 })
998 .with_lifecycle(ConnectDuringInstallLifecycle {
999 client: client_tx,
1000 release: install_release_rx,
1001 connect_invoked: connect_invoked_tx,
1002 connect_finished: connect_finished_tx,
1003 });
1004 let build = tokio::spawn(async move { builder.build().await });
1005 let leaked_client = client_rx
1006 .recv()
1007 .await
1008 .expect("client leaked during install");
1009 connect_invoked_rx
1010 .recv()
1011 .await
1012 .expect("direct connect invoked");
1013
1014 assert!(
1015 tokio::time::timeout(Duration::from_millis(100), transport_started_rx.recv())
1016 .await
1017 .is_err(),
1018 "transport started before construction activation"
1019 );
1020 assert!(!leaked_client.is_connecting.load(Ordering::Acquire));
1021
1022 install_release_tx
1023 .send(())
1024 .await
1025 .expect("release installation");
1026 let client = build
1027 .await
1028 .expect("builder task")
1029 .expect("successful build")
1030 .into_client();
1031 tokio::time::timeout(Duration::from_secs(1), transport_started_rx.recv())
1032 .await
1033 .expect("connect remained gated after activation")
1034 .expect("transport-start sender closed");
1035 transport_release_tx
1036 .send(())
1037 .await
1038 .expect("release transport");
1039 assert!(
1040 tokio::time::timeout(Duration::from_secs(1), connect_finished_rx.recv())
1041 .await
1042 .expect("direct connect did not finish")
1043 .expect("connect-finished sender closed")
1044 );
1045 client.signal_shutdown_sync();
1046 }
1047
1048 #[tokio::test]
1049 #[cfg(feature = "client-lifecycle")]
1050 async fn shutdown_during_install_rejects_leaked_connect() {
1051 let (client_tx, client_rx) = async_channel::bounded(1);
1052 let (_install_release_tx, install_release_rx) = async_channel::bounded(1);
1053 let (connect_invoked_tx, connect_invoked_rx) = async_channel::bounded(1);
1054 let (connect_finished_tx, connect_finished_rx) = async_channel::bounded(1);
1055 let (transport_started_tx, transport_started_rx) = async_channel::bounded(1);
1056 let (_transport_release_tx, transport_release_rx) = async_channel::bounded(1);
1057 let builder = complete_builder()
1058 .await
1059 .with_transport_factory(BlockingTransportFactory {
1060 started: transport_started_tx,
1061 release: transport_release_rx,
1062 })
1063 .with_lifecycle(ConnectDuringInstallLifecycle {
1064 client: client_tx,
1065 release: install_release_rx,
1066 connect_invoked: connect_invoked_tx,
1067 connect_finished: connect_finished_tx,
1068 });
1069 let build = tokio::spawn(async move { builder.build().await });
1070 let leaked_client = client_rx
1071 .recv()
1072 .await
1073 .expect("client leaked during install");
1074 connect_invoked_rx
1075 .recv()
1076 .await
1077 .expect("direct connect invoked");
1078 leaked_client.signal_shutdown_sync();
1079
1080 assert!(matches!(
1081 tokio::time::timeout(Duration::from_secs(2), build)
1082 .await
1083 .expect("lifecycle install ignored terminal shutdown")
1084 .expect("builder task"),
1085 Err(ClientBuilderError::LifecycleInstall(_))
1086 ));
1087 assert!(
1088 tokio::time::timeout(Duration::from_secs(1), connect_finished_rx.recv())
1089 .await
1090 .expect("direct connect did not observe rejection")
1091 .expect("connect-finished sender closed")
1092 );
1093 assert!(transport_started_rx.try_recv().is_err());
1094 assert!(!leaked_client.is_connecting.load(Ordering::Acquire));
1095 }
1096
1097 #[tokio::test]
1098 #[cfg(feature = "client-lifecycle")]
1099 async fn shutdown_during_install_rejects_leaked_run_and_the_build() {
1100 let (client_tx, client_rx) = async_channel::bounded(1);
1101 let (_release_tx, release_rx) = async_channel::bounded(1);
1102 let (run_finished_tx, run_finished_rx) = async_channel::bounded(1);
1103 let builder = complete_builder()
1104 .await
1105 .with_lifecycle(RunDuringInstallLifecycle {
1106 client: client_tx,
1107 release: release_rx,
1108 run_finished: run_finished_tx,
1109 });
1110 let build = tokio::spawn(async move { builder.build().await });
1111 let leaked_client = client_rx
1112 .recv()
1113 .await
1114 .expect("client leaked during install");
1115 leaked_client.signal_shutdown_sync();
1116
1117 assert!(matches!(
1118 tokio::time::timeout(Duration::from_secs(2), build)
1119 .await
1120 .expect("lifecycle install ignored terminal shutdown")
1121 .expect("builder task"),
1122 Err(ClientBuilderError::LifecycleInstall(_))
1123 ));
1124 tokio::time::timeout(Duration::from_secs(5), run_finished_rx.recv())
1125 .await
1126 .expect("rejected run stop timeout")
1127 .expect("rejected run stopped");
1128 assert!(!leaked_client.is_running.load(Ordering::Acquire));
1129 }
1130
1131 #[tokio::test]
1132 async fn low_level_builder_installs_options_and_owned_services() {
1133 let persistence_manager = Arc::new(
1134 PersistenceManager::new(crate::test_utils::create_test_backend().await)
1135 .await
1136 .expect("persistence manager"),
1137 );
1138 let spawns = Arc::new(AtomicUsize::new(0));
1139 let meter = Arc::new(wacore::stats::AllocMeter::new());
1140
1141 let build = ClientBuilder::new()
1142 .with_runtime(CountingRuntime {
1143 spawns: Arc::clone(&spawns),
1144 })
1145 .with_persistence_manager(persistence_manager)
1146 .with_transport_factory(MockTransportFactory::new())
1147 .with_http_client(MockHttpClient)
1148 .with_skip_history_sync(true)
1149 .with_wanted_pre_key_count(123)
1150 .with_alloc_meter(Arc::clone(&meter))
1151 .with_background_saver_interval(Duration::from_secs(3600))
1152 .build()
1153 .await
1154 .expect("complete builder");
1155 let client = build.into_client();
1156
1157 assert!(client.skip_history_sync_enabled());
1158 assert_eq!(client.wanted_pre_key_count(), 123);
1159 assert!(
1160 client
1161 .alloc_meter
1162 .get()
1163 .is_some_and(|installed| Arc::ptr_eq(installed, &meter))
1164 );
1165 assert!(client.saver_handle.get().is_some());
1166 assert_eq!(spawns.load(Ordering::SeqCst), 3);
1167 client.signal_shutdown_sync();
1168 }
1169
1170 #[tokio::test]
1171 async fn consuming_build_as_client_keeps_major_sync_worker_alive() {
1172 let client = complete_builder()
1173 .await
1174 .build()
1175 .await
1176 .expect("complete builder")
1177 .into_client();
1178
1179 assert!(!client.major_sync_task_sender.is_closed());
1180 client.signal_shutdown_sync();
1181 }
1182
1183 #[tokio::test]
1184 async fn rejects_zero_background_saver_interval() {
1185 let result = complete_builder()
1186 .await
1187 .with_background_saver_interval(Duration::ZERO)
1188 .build()
1189 .await;
1190
1191 assert!(matches!(
1192 result,
1193 Err(ClientBuilderError::InvalidBackgroundSaverInterval)
1194 ));
1195 }
1196
1197 #[cfg(feature = "client-lifecycle")]
1198 struct PanickingInstallLifecycle {
1199 when_polled: bool,
1200 }
1201
1202 #[cfg(feature = "client-lifecycle")]
1203 impl ClientLifecycle for PanickingInstallLifecycle {
1204 fn install(
1205 &self,
1206 _client: std::sync::Weak<Client>,
1207 ) -> wacore::runtime::BoxFuture<'_, anyhow::Result<()>> {
1208 if !self.when_polled {
1209 panic!("injected synchronous install panic");
1210 }
1211 Box::pin(async { panic!("injected asynchronous install panic") })
1212 }
1213 }
1214
1215 #[tokio::test]
1216 #[cfg(feature = "client-lifecycle")]
1217 async fn lifecycle_install_panics_are_typed_build_errors() {
1218 for when_polled in [false, true] {
1219 let result = complete_builder()
1220 .await
1221 .with_lifecycle(PanickingInstallLifecycle { when_polled })
1222 .build()
1223 .await;
1224 assert!(matches!(
1225 result,
1226 Err(ClientBuilderError::LifecycleInstall(_))
1227 ));
1228 }
1229 }
1230
1231 #[tokio::test]
1232 #[cfg(feature = "client-lifecycle")]
1233 async fn lifecycle_install_failure_publishes_nothing_and_starts_no_tasks() {
1234 let persistence_manager = Arc::new(
1235 PersistenceManager::new(crate::test_utils::create_test_backend().await)
1236 .await
1237 .expect("persistence manager"),
1238 );
1239 let spawns = Arc::new(AtomicUsize::new(0));
1240 let lifecycle = Arc::new(FailingLifecycle {
1241 spawns: Arc::clone(&spawns),
1242 installed_client: std::sync::Mutex::new(None),
1243 });
1244
1245 let result = ClientBuilder::new()
1246 .with_runtime(CountingRuntime {
1247 spawns: Arc::clone(&spawns),
1248 })
1249 .with_persistence_manager(persistence_manager)
1250 .with_transport_factory(MockTransportFactory::new())
1251 .with_http_client(MockHttpClient)
1252 .with_lifecycle_arc(lifecycle.clone())
1253 .build()
1254 .await;
1255
1256 assert!(matches!(
1257 result,
1258 Err(ClientBuilderError::LifecycleInstall(_))
1259 ));
1260 assert_eq!(spawns.load(Ordering::SeqCst), 0);
1261 assert!(
1262 lifecycle
1263 .installed_client
1264 .lock()
1265 .unwrap_or_else(|poisoned| poisoned.into_inner())
1266 .as_ref()
1267 .is_some_and(|client| client.upgrade().is_none())
1268 );
1269 }
1270}