apollo_rust_client/
lib.rs

1//! # Apollo Rust Client
2//!
3//! A robust Rust client for the Apollo Configuration Centre, with support for WebAssembly
4//! for browser and Node.js environments.
5//!
6//! This crate provides a comprehensive client for interacting with Apollo configuration services.
7//! The client manages configurations for different namespaces, supports multiple configuration
8//! formats (Properties, JSON, Text), provides caching mechanisms, and offers real-time updates
9//! through background polling and event listeners.
10//!
11//! ## Key Features
12//!
13//! - **Multiple Configuration Formats**: Support for Properties, JSON, Text formats with automatic detection
14//! - **Cross-Platform**: Native Rust and WebAssembly targets with platform-specific optimizations
15//! - **Real-Time Updates**: Background polling with configurable intervals and event listeners
16//! - **Comprehensive Caching**: Multi-level caching with file persistence (native) and memory-only (WASM)
17//! - **Type-Safe API**: Compile-time guarantees and runtime type conversion
18//! - **Error Handling**: Detailed error diagnostics with comprehensive error types
19//! - **Grayscale Release Support**: IP and label-based configuration targeting
20//!
21//! ## Quick Start
22//!
23//! ```rust,no_run
24//! use apollo_rust_client::{Client, client_config::ClientConfig};
25//!
26//! # #[tokio::main]
27//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
28//! let config = ClientConfig {
29//!     app_id: "my-app".to_string(),
30//!     config_server: "http://apollo-server:8080".to_string(),
31//!     cluster: "default".to_string(),
32//!     secret: None,
33//!     cache_dir: None,
34//!     label: None,
35//!     ip: None,
36//!     allow_insecure_https: None,
37//!     #[cfg(not(target_arch = "wasm32"))]
38//!     cache_ttl: None,
39//! };
40//!
41//! let mut client = Client::new(config);
42//! client.start().await?;
43//!
44//! let namespace = client.namespace("application").await?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! ## Platform Support
50//!
51//! The library supports different behavior for wasm32 and non-wasm32 targets:
52//!
53//! - **Native Rust**: Full feature set with file caching, background tasks, and threading
54//! - **WebAssembly**: Memory-only caching, single-threaded execution, JavaScript interop
55
56use crate::namespace::Namespace;
57use async_std::sync::RwLock;
58use cache::Cache;
59use client_config::ClientConfig;
60use log::{error, trace};
61use std::{collections::HashMap, sync::Arc};
62use wasm_bindgen::prelude::wasm_bindgen;
63
64cfg_if::cfg_if! {
65    if #[cfg(not(target_arch = "wasm32"))] {
66        use async_std::task::spawn as spawn;
67    }
68}
69
70mod cache;
71
72pub mod client_config;
73pub mod namespace;
74
75/// Comprehensive error types that can occur when using the Apollo client.
76///
77/// This enum covers all possible error conditions that may arise during client operations,
78/// from initialization and configuration to runtime cache operations and namespace handling.
79///
80/// # Error Categories
81///
82/// - **Client State Errors**: Issues related to client lifecycle management
83/// - **Namespace Errors**: Problems with namespace format detection and processing
84/// - **Cache Errors**: Network, I/O, and caching-related failures
85///
86/// # Examples
87///
88/// ```rust,no_run
89/// use apollo_rust_client::{Client, Error};
90///
91/// # #[tokio::main]
92/// # async fn main() {
93/// # let client = Client::new(apollo_rust_client::client_config::ClientConfig {
94/// #     app_id: "test".to_string(),
95/// #     config_server: "http://localhost:8080".to_string(),
96/// #     cluster: "default".to_string(),
97/// #     secret: None,
98/// #     cache_dir: None,
99/// #     label: None,
100/// #     ip: None,
101/// #     allow_insecure_https: None,
102/// #     #[cfg(not(target_arch = "wasm32"))]
103/// #     cache_ttl: None,
104/// # });
105/// match client.namespace("application").await {
106///     Ok(namespace) => {
107///         // Handle successful namespace retrieval
108///     }
109///     Err(Error::Cache(cache_error)) => {
110///         // Handle cache-related errors (network, parsing, etc.)
111///         eprintln!("Cache error: {}", cache_error);
112///     }
113///     Err(Error::Namespace(namespace_error)) => {
114///         // Handle namespace-related errors (format detection, etc.)
115///         eprintln!("Namespace error: {}", namespace_error);
116///     }
117///     Err(e) => {
118///         // Handle other errors
119///         eprintln!("Error: {}", e);
120///     }
121/// }
122/// # }
123/// ```
124#[derive(Debug, thiserror::Error)]
125pub enum Error {
126    /// The client background task is already running.
127    ///
128    /// This error occurs when attempting to start a client that is already
129    /// running its background refresh task. Each client instance can only
130    /// have one active background task at a time.
131    #[error("Client is already running")]
132    AlreadyRunning,
133
134    /// An error occurred during namespace processing.
135    ///
136    /// This includes errors from format detection, parsing, or type conversion
137    /// operations specific to namespace handling.
138    #[error("Namespace error: {0}")]
139    Namespace(#[from] namespace::Error),
140
141    /// An error occurred during cache operations.
142    ///
143    /// This encompasses network errors, I/O failures, serialization issues,
144    /// and other cache-related problems during configuration retrieval or storage.
145    #[error("Cache error: {0}")]
146    Cache(#[from] cache::Error),
147}
148
149impl From<Error> for wasm_bindgen::JsValue {
150    fn from(error: Error) -> Self {
151        error.to_string().into()
152    }
153}
154
155// Type alias for event listeners that can be registered with the cache.
156// For WASM targets, listeners don't need to be Send + Sync since WASM is single-threaded.
157// Listeners are functions that take a `Result<Value, Error>` as an argument.
158// `Value` is the `serde_json::Value` representing the configuration.
159// `Error` is the cache's error enum.
160cfg_if::cfg_if! {
161    if #[cfg(target_arch = "wasm32")] {
162        /// Type alias for event listeners that can be registered with the cache.
163        /// For WASM targets, listeners don't need to be Send + Sync since WASM is single-threaded.
164        /// Listeners are functions that take a `Result<Value, Error>` as an argument.
165        pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>)>;
166    } else {
167        /// Type alias for event listeners that can be registered with the client.
168        /// For native targets, listeners need to be `Send` and `Sync` to be safely
169        /// shared across threads.
170        ///
171        /// Listeners are functions that take a `Result<Namespace<T>, Error>` as an
172        /// argument, where `Namespace<T>` is a fresh copy of the updated namespace,`
173        /// and `Error` is the cache's error enum.
174        pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>) + Send + Sync>;
175    }
176}
177
178/// The main Apollo configuration client.
179///
180/// This struct provides the primary interface for interacting with Apollo configuration services.
181/// It manages multiple namespace caches, handles background refresh tasks, and provides
182/// event listener functionality for real-time configuration updates.
183///
184/// # Features
185///
186/// - **Namespace Management**: Automatically creates and manages caches for different namespaces
187/// - **Background Refresh**: Optional background task that periodically refreshes all namespaces
188/// - **Event Listeners**: Support for registering callbacks on configuration changes
189/// - **Cross-Platform**: Works on both native Rust and WebAssembly targets
190/// - **Thread Safety**: All operations are thread-safe and async-friendly
191///
192/// # Examples
193///
194/// ```rust,no_run
195/// use apollo_rust_client::{Client, client_config::ClientConfig};
196///
197/// # #[tokio::main]
198/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
199/// #     // Create a client instance
200/// #     let client = Client::new(ClientConfig {
201/// #         app_id: "test_app".to_string(),
202/// #         config_server: "http://localhost:8080".to_string(),
203/// #         cluster: "default".to_string(),
204/// #         secret: None,
205/// #         cache_dir: None,
206/// #         label: None,
207/// #         ip: None,
208/// #         allow_insecure_https: None,
209/// #         #[cfg(not(target_arch = "wasm32"))]
210/// #         cache_ttl: None,
211/// #     });
212/// #
213/// #     // Get properties namespace (default format)
214/// #     let props_namespace = client.namespace("application").await?;
215/// #
216/// #     // Get JSON namespace
217/// #     let json_namespace = client.namespace("config.json").await?;
218/// #
219/// #     // Get YAML namespace
220/// #     let yaml_namespace = client.namespace("settings.yaml").await?;
221/// #
222/// #     Ok(())
223/// # }
224/// ```
225#[wasm_bindgen]
226pub struct Client {
227    /// The configuration settings for this Apollo client instance.
228    ///
229    /// Contains all necessary information to connect to Apollo servers,
230    /// including server URL, application ID, cluster, authentication, and caching settings.
231    config: ClientConfig,
232
233    /// Thread-safe storage for namespace-specific caches.
234    ///
235    /// Each namespace gets its own `Cache` instance, wrapped in `Arc` for shared ownership.
236    /// The `RwLock` provides thread-safe read/write access to the namespace map.
237    /// The outer `Arc` allows the background refresh task to safely access the namespaces.
238    namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
239
240    /// Handle to the background refresh task (native targets only).
241    ///
242    /// On non-wasm32 targets, this holds a `JoinHandle` to the spawned background task
243    /// that periodically refreshes all namespace caches. On wasm32 targets, this is
244    /// always `None` as task management differs in single-threaded environments.
245    handle: Option<async_std::task::JoinHandle<()>>,
246
247    /// Flag indicating whether the background refresh task is active.
248    ///
249    /// Wrapped in `Arc<RwLock<bool>>` for thread-safe shared access between the
250    /// client and its background task. Used to coordinate task lifecycle management.
251    running: Arc<RwLock<bool>>,
252}
253
254impl Client {
255    /// Get a cache for a given namespace.
256    ///
257    /// # Arguments
258    ///
259    /// * `namespace` - The namespace to get the cache for.
260    ///
261    /// # Returns
262    ///
263    /// A cache for the given namespace.
264    pub(crate) async fn cache(&self, namespace: &str) -> Arc<Cache> {
265        let mut namespaces = self.namespaces.write().await;
266        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
267            trace!("Cache miss, creating cache for namespace {namespace}");
268            Arc::new(Cache::new(self.config.clone(), namespace))
269        });
270        cache.clone()
271    }
272
273    pub async fn add_listener(&self, namespace: &str, listener: EventListener) {
274        let mut namespaces = self.namespaces.write().await;
275        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
276            trace!("Cache miss, creating cache for namespace {namespace}");
277            Arc::new(Cache::new(self.config.clone(), namespace))
278        });
279        cache.add_listener(listener).await;
280    }
281
282    /// Retrieves a namespace configuration from the Apollo server.
283    ///
284    /// This method fetches the configuration for the specified namespace and
285    /// automatically detects the format based on the namespace name. The format
286    /// detection follows these rules:
287    ///
288    /// - **Properties format** (default): No file extension
289    /// - **JSON format**: `.json` extension
290    /// - **YAML format**: `.yaml` or `.yml` extension
291    /// - **Text format**: `.txt` extension
292    /// - **XML format**: `.xml` extension (not yet supported)
293    ///
294    /// # Arguments
295    ///
296    /// * `namespace` - The namespace identifier string (e.g., "application", "config.json")
297    ///
298    /// # Returns
299    ///
300    /// * `Ok(Namespace)` - The configuration data in the appropriate format
301    /// * `Err(Error::Cache)` - If cache operations fail (network, I/O, etc.)
302    /// * `Err(Error::Namespace)` - If namespace format detection or processing fails
303    ///
304    /// # Errors
305    ///
306    /// This method will return an error if:
307    /// - Network requests to the Apollo server fail
308    /// - Cache file operations fail (native targets only)
309    /// - JSON parsing fails during configuration retrieval
310    /// - Namespace format detection fails
311    /// - The requested namespace format is not supported (e.g., XML)
312    ///
313    /// # Examples
314    ///
315    /// ```rust,no_run
316    /// use apollo_rust_client::{Client, client_config::ClientConfig};
317    ///
318    /// # #[tokio::main]
319    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
320    /// #     // Create a client instance
321    /// #     let client = Client::new(ClientConfig {
322    /// #         app_id: "test_app".to_string(),
323    /// #         config_server: "http://localhost:8080".to_string(),
324    /// #         cluster: "default".to_string(),
325    /// #         secret: None,
326    /// #         cache_dir: None,
327    /// #         label: None,
328    /// #         ip: None,
329    /// #         allow_insecure_https: None,
330    /// #         #[cfg(not(target_arch = "wasm32"))]
331    /// #         cache_ttl: None,
332    /// #     });
333    /// #
334    /// #     // Get properties namespace (default format)
335    /// #     let props_namespace = client.namespace("application").await?;
336    /// #
337    /// #     // Get JSON namespace
338    /// #     let json_namespace = client.namespace("config.json").await?;
339    /// #
340    /// #     // Get YAML namespace
341    /// #     let yaml_namespace = client.namespace("settings.yaml").await?;
342    /// #
343    /// #     Ok(())
344    /// # }
345    /// ```
346    pub async fn namespace(&self, namespace: &str) -> Result<namespace::Namespace, Error> {
347        let cache = self.cache(namespace).await;
348        let value = cache.get_value().await?;
349        Ok(namespace::get_namespace(namespace, value)?)
350    }
351
352    /// Starts a background task that periodically refreshes all registered namespace caches.
353    ///
354    /// This method spawns an asynchronous task using `async_std::task::spawn` on native targets
355    /// or `wasm_bindgen_futures::spawn_local` on wasm32 targets. The task loops indefinitely
356    /// (until `stop` is called or the client is dropped) and performs the following actions
357    /// in each iteration:
358    ///
359    /// 1. Iterates through all namespaces currently managed by the client.
360    /// 2. Calls the `refresh` method on each namespace's `Cache` instance.
361    /// 3. Logs any errors encountered during the refresh process.
362    /// 4. Sleeps for a predefined interval (currently 30 seconds) before the next refresh cycle.
363    ///
364    /// # Returns
365    ///
366    /// * `Ok(())` if the background task was successfully started.
367    /// * `Err(Error::AlreadyRunning)` if the background task is already active.
368    ///
369    /// # Errors
370    ///
371    /// This method will return an error if:
372    /// - The background task is already running (`Error::AlreadyRunning`)
373    /// - Task spawning fails (though this is rare and typically indicates system resource issues)
374    pub async fn start(&mut self) -> Result<(), Error> {
375        let mut running = self.running.write().await;
376        if *running {
377            return Err(Error::AlreadyRunning);
378        }
379
380        *running = true;
381
382        cfg_if::cfg_if! {
383            if #[cfg(target_arch = "wasm32")] {
384                self.handle = None;
385            } else {
386                let running = self.running.clone();
387                let namespaces = self.namespaces.clone();
388                // Spawn a background thread to refresh caches
389                let handle = spawn(async move {
390                    loop {
391                        let running = running.read().await;
392                        if !*running {
393                            break;
394                        }
395
396                        // Clone cache references before releasing the lock to prevent long-held locks
397                        let cache_refs: Vec<_> = {
398                            let namespaces = namespaces.read().await;
399                            namespaces.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
400                        }; // Lock released here
401
402                        // Refresh each namespace's cache without holding the lock
403                        for (namespace, cache) in cache_refs {
404                            if let Err(err) = cache.refresh().await {
405                                error!("Failed to refresh cache for namespace {namespace}: {err:?}");
406                            } else {
407                                log::debug!("Successfully refreshed cache for namespace {namespace}");
408                            }
409                        }
410
411                        // Sleep for 30 seconds before the next refresh
412                        async_std::task::sleep(std::time::Duration::from_secs(30)).await;
413                    }
414                });
415                self.handle = Some(handle);
416            }
417        }
418
419        Ok(())
420    }
421
422    /// Stops the background cache refresh task.
423    ///
424    /// This method sets the `running` flag to `false`, signaling the background task
425    /// to terminate its refresh loop.
426    ///
427    /// On non-wasm32 targets, it also attempts to explicitly cancel the spawned task
428    /// by calling `cancel()` on its `JoinHandle` if it exists. This helps to ensure
429    /// that the task is properly cleaned up. On wasm32 targets, there is no direct
430    /// handle to cancel, so setting the `running` flag is the primary mechanism for stopping.
431    pub async fn stop(&mut self) {
432        let mut running = self.running.write().await;
433        *running = false;
434
435        cfg_if::cfg_if! {
436            if #[cfg(not(target_arch = "wasm32"))] {
437                if let Some(handle) = self.handle.take() {
438                    handle.cancel().await;
439                }
440            }
441        }
442    }
443
444    /// Preloads critical namespaces during client initialization to reduce startup delays.
445    ///
446    /// This method fetches configuration for the specified namespaces in parallel,
447    /// which can significantly reduce the perceived startup time when these namespaces
448    /// are accessed later.
449    ///
450    /// # Arguments
451    ///
452    /// * `namespaces` - A slice of namespace names to preload
453    ///
454    /// # Returns
455    ///
456    /// * `Ok(())` if all namespaces were successfully preloaded
457    /// * `Err(Error)` if any namespace failed to load
458    ///
459    /// # Examples
460    ///
461    /// ```rust,no_run
462    /// use apollo_rust_client::{Client, client_config::ClientConfig};
463    ///
464    /// # #[tokio::main]
465    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
466    /// let config = ClientConfig {
467    ///     app_id: "my-app".to_string(),
468    ///     config_server: "http://apollo-server:8080".to_string(),
469    ///     cluster: "default".to_string(),
470    ///     secret: None,
471    ///     cache_dir: None,
472    ///     label: None,
473    ///     ip: None,
474    ///     allow_insecure_https: None,
475    ///     #[cfg(not(target_arch = "wasm32"))]
476    ///     cache_ttl: None,
477    /// };
478    ///
479    /// let mut client = Client::new(config);
480    ///
481    /// // Preload critical namespaces
482    /// client.preload(&["application", "database", "redis"]).await?;
483    ///
484    /// // Now accessing these namespaces will be much faster
485    /// let app_config = client.namespace("application").await?;
486    /// # Ok(())
487    /// # }
488    /// ```
489    ///
490    /// # Errors
491    ///
492    /// This method will return an error if:
493    /// - Any of the specified namespaces fail to load
494    /// - Cache operations fail during preloading
495    pub async fn preload(&self, namespaces: &[impl AsRef<str>]) -> Result<(), Error> {
496        #[cfg(not(target_arch = "wasm32"))]
497        let mut tasks = Vec::new();
498
499        #[cfg(target_arch = "wasm32")]
500        {
501            for namespace in namespaces {
502                let cache = self.cache(namespace.as_ref()).await;
503                cache.get_value().await?;
504            }
505        }
506
507        #[cfg(not(target_arch = "wasm32"))]
508        {
509            for namespace in namespaces {
510                let cache = self.cache(namespace.as_ref()).await;
511                let task = tokio::spawn(async move { cache.get_value().await });
512                tasks.push(task);
513            }
514
515            // Wait for all preload tasks to complete
516            for task in tasks {
517                let result = task.await.map_err(|e| {
518                    Error::Cache(cache::Error::Io(std::io::Error::other(format!(
519                        "Preload task failed: {e}"
520                    ))))
521                })?;
522                result?;
523            }
524        }
525
526        Ok(())
527    }
528}
529
530#[wasm_bindgen]
531impl Client {
532    /// Create a new Apollo client.
533    ///
534    /// # Arguments
535    ///
536    /// * `client_config` - The configuration for the Apollo client.
537    ///
538    /// # Returns
539    ///
540    /// A new Apollo client.
541    #[wasm_bindgen(constructor)]
542    #[must_use]
543    pub fn new(config: ClientConfig) -> Self {
544        Self {
545            config,
546            namespaces: Arc::new(RwLock::new(HashMap::new())),
547            handle: None,
548            running: Arc::new(RwLock::new(false)),
549        }
550    }
551
552    /// Registers a JavaScript function as an event listener for this cache (WASM only).
553    ///
554    /// This method is exposed to JavaScript as `addListener`.
555    /// The provided JavaScript function will be called when the cache is refreshed.
556    ///
557    /// The JavaScript listener function is expected to have a signature like:
558    /// `function(data, error)`
559    /// - `data`: The JSON configuration object (if the refresh was successful and data
560    ///           could be serialized) or `null` if an error occurred or serialization failed.
561    /// - `error`: A string describing the error if one occurred during the configuration
562    ///            fetch or processing. If the operation was successful, this will be `null`.
563    ///
564    /// # Arguments
565    ///
566    /// * `js_listener` - A JavaScript `Function` to be called on cache events.
567    ///
568    /// # Example (JavaScript)
569    ///
570    /// ```javascript
571    /// // Assuming `cacheInstance` is an instance of the Rust `Cache` object in JS
572    /// cacheInstance.addListener((data, error) => {
573    ///   if (error) {
574    ///     console.error('Cache update error:', error);
575    ///   } else {
576    ///     console.log('Cache updated:', data);
577    ///   }
578    /// });
579    /// // ... later, when the cache refreshes, the callback will be invoked.
580    /// ```
581    #[cfg(target_arch = "wasm32")]
582    #[wasm_bindgen(js_name = "add_listener")]
583    pub async fn add_listener_wasm(&self, namespace: &str, js_listener: js_sys::Function) {
584        let js_listener_clone = js_listener.clone();
585
586        let event_listener: EventListener = Arc::new(move |result: Result<Namespace, Error>| {
587            let err_js_val: wasm_bindgen::JsValue;
588            let data_js_val: wasm_bindgen::JsValue;
589
590            match result {
591                Ok(value) => {
592                    data_js_val = value.into();
593                    err_js_val = wasm_bindgen::JsValue::UNDEFINED;
594                }
595                Err(cache_error) => {
596                    err_js_val = cache_error.into();
597                    data_js_val = wasm_bindgen::JsValue::UNDEFINED;
598                }
599            };
600
601            // Call the JavaScript listener: listener(data, error)
602            match js_listener_clone.call2(
603                &wasm_bindgen::JsValue::UNDEFINED,
604                &data_js_val,
605                &err_js_val,
606            ) {
607                Ok(_) => {
608                    // JS function called successfully
609                }
610                Err(e) => {
611                    // JS function threw an error or call failed
612                    log::error!("JavaScript listener threw an error: {:?}", e);
613                }
614            }
615        });
616
617        self.add_listener(namespace, event_listener).await; // Call the renamed Rust method
618    }
619
620    #[cfg(target_arch = "wasm32")]
621    #[wasm_bindgen(js_name = "namespace")]
622    pub async fn namespace_wasm(&self, namespace: &str) -> Result<wasm_bindgen::JsValue, Error> {
623        let cache = self.cache(namespace).await;
624        let value = cache.get_value().await?;
625        Ok(namespace::get_namespace(namespace, value)?.into())
626    }
627}
628
629#[cfg(test)]
630pub(crate) struct TempDir {
631    path: std::path::PathBuf,
632}
633
634#[cfg(test)]
635impl TempDir {
636    pub(crate) fn new(name: &str) -> Self {
637        let path = std::env::temp_dir().join(name);
638        // Ignore errors if the directory already exists
639        let _ = std::fs::create_dir_all(&path);
640        Self { path }
641    }
642
643    pub(crate) fn path(&self) -> &std::path::Path {
644        &self.path
645    }
646}
647
648#[cfg(test)]
649impl Drop for TempDir {
650    fn drop(&mut self) {
651        // Ignore errors, e.g. if the directory was already removed
652        let _ = std::fs::remove_dir_all(&self.path);
653    }
654}
655
656#[cfg(test)]
657pub(crate) fn setup() {
658    cfg_if::cfg_if! {
659        if #[cfg(target_arch = "wasm32")] {
660            let _ = wasm_logger::init(wasm_logger::Config::default());
661            console_error_panic_hook::set_once();
662        } else {
663            let _ = env_logger::builder().is_test(true).try_init();
664        }
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use super::*;
671
672    cfg_if::cfg_if! {
673        if #[cfg(target_arch = "wasm32")] {
674            use std::sync::Mutex;
675        } else {
676            use async_std::sync::Mutex;
677            use async_std::task::block_on;
678        }
679    }
680
681    #[cfg(not(target_arch = "wasm32"))]
682    pub(crate) static CLIENT_NO_SECRET: std::sync::LazyLock<Client> =
683        std::sync::LazyLock::new(|| {
684            let config = ClientConfig {
685                app_id: String::from("101010101"),
686                cluster: String::from("default"),
687                config_server: String::from("http://81.68.181.139:8080"),
688                label: None,
689                secret: None,
690                cache_dir: Some(String::from("/tmp/apollo")),
691                ip: None,
692                allow_insecure_https: None,
693                #[cfg(not(target_arch = "wasm32"))]
694                cache_ttl: None,
695            };
696            Client::new(config)
697        });
698
699    #[cfg(not(target_arch = "wasm32"))]
700    pub(crate) static CLIENT_WITH_SECRET: std::sync::LazyLock<Client> =
701        std::sync::LazyLock::new(|| {
702            let config = ClientConfig {
703                app_id: String::from("101010102"),
704                cluster: String::from("default"),
705                config_server: String::from("http://81.68.181.139:8080"),
706                label: None,
707                secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
708                cache_dir: Some(String::from("/tmp/apollo")),
709                ip: None,
710                allow_insecure_https: None,
711                #[cfg(not(target_arch = "wasm32"))]
712                cache_ttl: None,
713            };
714            Client::new(config)
715        });
716
717    #[cfg(not(target_arch = "wasm32"))]
718    pub(crate) static CLIENT_WITH_GRAYSCALE_IP: std::sync::LazyLock<Client> =
719        std::sync::LazyLock::new(|| {
720            let config = ClientConfig {
721                app_id: String::from("101010101"),
722                cluster: String::from("default"),
723                config_server: String::from("http://81.68.181.139:8080"),
724                label: None,
725                secret: None,
726                cache_dir: Some(String::from("/tmp/apollo")),
727                ip: Some(String::from("1.2.3.4")),
728                allow_insecure_https: None,
729                #[cfg(not(target_arch = "wasm32"))]
730                cache_ttl: None,
731            };
732            Client::new(config)
733        });
734
735    #[cfg(not(target_arch = "wasm32"))]
736    pub(crate) static CLIENT_WITH_GRAYSCALE_LABEL: std::sync::LazyLock<Client> =
737        std::sync::LazyLock::new(|| {
738            let config = ClientConfig {
739                app_id: String::from("101010101"),
740                cluster: String::from("default"),
741                config_server: String::from("http://81.68.181.139:8080"),
742                label: Some(String::from("GrayScale")),
743                secret: None,
744                cache_dir: Some(String::from("/tmp/apollo")),
745                ip: None,
746                allow_insecure_https: None,
747                #[cfg(not(target_arch = "wasm32"))]
748                cache_ttl: None,
749            };
750            Client::new(config)
751        });
752
753    #[cfg(not(target_arch = "wasm32"))]
754    #[tokio::test]
755    async fn test_missing_value() {
756        setup();
757        let namespace::Namespace::Properties(properties) =
758            CLIENT_NO_SECRET.namespace("application").await.unwrap()
759        else {
760            panic!("Expected Properties namespace");
761        };
762
763        assert_eq!(properties.get_property::<String>("missingValue"), None);
764    }
765
766    #[cfg(target_arch = "wasm32")]
767    #[wasm_bindgen_test::wasm_bindgen_test]
768    #[allow(dead_code)]
769    async fn test_missing_value_wasm() {
770        setup();
771        let client = create_client_no_secret();
772        let namespace = client.namespace("application").await;
773        match namespace {
774            Ok(namespace) => match namespace {
775                namespace::Namespace::Properties(properties) => {
776                    assert_eq!(properties.get_string("missingValue"), None);
777                }
778                _ => panic!("Expected Properties namespace"),
779            },
780            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
781        }
782    }
783
784    #[cfg(not(target_arch = "wasm32"))]
785    #[tokio::test]
786    async fn test_string_value() {
787        setup();
788        let namespace::Namespace::Properties(properties) =
789            CLIENT_NO_SECRET.namespace("application").await.unwrap()
790        else {
791            panic!("Expected Properties namespace");
792        };
793
794        assert_eq!(
795            properties.get_property::<String>("stringValue"),
796            Some("string value".to_string())
797        );
798    }
799
800    #[cfg(target_arch = "wasm32")]
801    #[wasm_bindgen_test::wasm_bindgen_test]
802    #[allow(dead_code)]
803    async fn test_string_value_wasm() {
804        setup();
805        let client = create_client_no_secret();
806        let namespace = client.namespace("application").await;
807        match namespace {
808            Ok(namespace) => match namespace {
809                namespace::Namespace::Properties(properties) => {
810                    assert_eq!(
811                        properties.get_string("stringValue"),
812                        Some("string value".to_string())
813                    );
814                }
815                _ => panic!("Expected Properties namespace"),
816            },
817            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
818        }
819    }
820
821    #[cfg(not(target_arch = "wasm32"))]
822    #[tokio::test]
823    async fn test_string_value_with_secret() {
824        setup();
825        let namespace::Namespace::Properties(properties) =
826            CLIENT_WITH_SECRET.namespace("application").await.unwrap()
827        else {
828            panic!("Expected Properties namespace");
829        };
830        assert_eq!(
831            properties.get_property::<String>("stringValue"),
832            Some("string value".to_string())
833        );
834    }
835
836    #[cfg(target_arch = "wasm32")]
837    #[wasm_bindgen_test::wasm_bindgen_test]
838    #[allow(dead_code)]
839    async fn test_string_value_with_secret_wasm() {
840        setup();
841        let client = create_client_with_secret();
842        let namespace = client.namespace("application").await;
843        match namespace {
844            Ok(namespace) => match namespace {
845                namespace::Namespace::Properties(properties) => {
846                    assert_eq!(
847                        properties.get_string("stringValue"),
848                        Some("string value".to_string())
849                    );
850                }
851                _ => panic!("Expected Properties namespace"),
852            },
853            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
854        }
855    }
856
857    #[cfg(not(target_arch = "wasm32"))]
858    #[tokio::test]
859    async fn test_int_value() {
860        setup();
861        let namespace::Namespace::Properties(properties) =
862            CLIENT_NO_SECRET.namespace("application").await.unwrap()
863        else {
864            panic!("Expected Properties namespace");
865        };
866        assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
867    }
868
869    #[cfg(target_arch = "wasm32")]
870    #[wasm_bindgen_test::wasm_bindgen_test]
871    #[allow(dead_code)]
872    async fn test_int_value_wasm() {
873        setup();
874        let client = create_client_no_secret();
875        let namespace = client.namespace("application").await;
876        match namespace {
877            Ok(namespace) => match namespace {
878                namespace::Namespace::Properties(properties) => {
879                    assert_eq!(properties.get_int("intValue"), Some(42));
880                }
881                _ => panic!("Expected Properties namespace"),
882            },
883            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
884        }
885    }
886
887    #[cfg(not(target_arch = "wasm32"))]
888    #[tokio::test]
889    async fn test_int_value_with_secret() {
890        setup();
891        let namespace::Namespace::Properties(properties) =
892            CLIENT_WITH_SECRET.namespace("application").await.unwrap()
893        else {
894            panic!("Expected Properties namespace");
895        };
896        assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
897    }
898
899    #[cfg(target_arch = "wasm32")]
900    #[wasm_bindgen_test::wasm_bindgen_test]
901    #[allow(dead_code)]
902    async fn test_int_value_with_secret_wasm() {
903        setup();
904        let client = create_client_with_secret();
905        let namespace = client.namespace("application").await;
906        match namespace {
907            Ok(namespace) => match namespace {
908                namespace::Namespace::Properties(properties) => {
909                    assert_eq!(properties.get_int("intValue"), Some(42));
910                }
911                _ => panic!("Expected Properties namespace"),
912            },
913            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
914        }
915    }
916
917    #[cfg(not(target_arch = "wasm32"))]
918    #[tokio::test]
919    async fn test_float_value() {
920        setup();
921        let namespace::Namespace::Properties(properties) =
922            CLIENT_NO_SECRET.namespace("application").await.unwrap()
923        else {
924            panic!("Expected Properties namespace");
925        };
926        assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
927    }
928
929    #[cfg(target_arch = "wasm32")]
930    #[wasm_bindgen_test::wasm_bindgen_test]
931    #[allow(dead_code)]
932    async fn test_float_value_wasm() {
933        setup();
934        let client = create_client_no_secret();
935        let namespace = client.namespace("application").await;
936        match namespace {
937            Ok(namespace) => match namespace {
938                namespace::Namespace::Properties(properties) => {
939                    assert_eq!(properties.get_float("floatValue"), Some(4.20));
940                }
941                _ => panic!("Expected Properties namespace"),
942            },
943            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
944        }
945    }
946
947    #[cfg(not(target_arch = "wasm32"))]
948    #[tokio::test]
949    async fn test_float_value_with_secret() {
950        setup();
951        let namespace::Namespace::Properties(properties) =
952            CLIENT_WITH_SECRET.namespace("application").await.unwrap()
953        else {
954            panic!("Expected Properties namespace");
955        };
956        assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
957    }
958
959    #[cfg(target_arch = "wasm32")]
960    #[wasm_bindgen_test::wasm_bindgen_test]
961    #[allow(dead_code)]
962    async fn test_float_value_with_secret_wasm() {
963        setup();
964        let client = create_client_with_secret();
965        let namespace = client.namespace("application").await;
966        match namespace {
967            Ok(namespace) => match namespace {
968                namespace::Namespace::Properties(properties) => {
969                    assert_eq!(properties.get_float("floatValue"), Some(4.20));
970                }
971                _ => panic!("Expected Properties namespace"),
972            },
973            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
974        }
975    }
976
977    #[cfg(not(target_arch = "wasm32"))]
978    #[tokio::test]
979    async fn test_bool_value() {
980        setup();
981        let namespace::Namespace::Properties(properties) =
982            CLIENT_NO_SECRET.namespace("application").await.unwrap()
983        else {
984            panic!("Expected Properties namespace");
985        };
986        assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
987    }
988
989    #[cfg(target_arch = "wasm32")]
990    #[wasm_bindgen_test::wasm_bindgen_test]
991    #[allow(dead_code)]
992    async fn test_bool_value_wasm() {
993        setup();
994        let client = create_client_no_secret();
995        let namespace = client.namespace("application").await;
996        match namespace {
997            Ok(namespace) => match namespace {
998                namespace::Namespace::Properties(properties) => {
999                    assert_eq!(properties.get_bool("boolValue"), Some(false));
1000                }
1001                _ => panic!("Expected Properties namespace"),
1002            },
1003            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1004        }
1005    }
1006
1007    #[cfg(not(target_arch = "wasm32"))]
1008    #[tokio::test]
1009    async fn test_bool_value_with_secret() {
1010        setup();
1011        let namespace::Namespace::Properties(properties) =
1012            CLIENT_WITH_SECRET.namespace("application").await.unwrap()
1013        else {
1014            panic!("Expected Properties namespace");
1015        };
1016        assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
1017    }
1018
1019    #[cfg(target_arch = "wasm32")]
1020    #[wasm_bindgen_test::wasm_bindgen_test]
1021    #[allow(dead_code)]
1022    async fn test_bool_value_with_secret_wasm() {
1023        setup();
1024        let client = create_client_with_secret();
1025        let namespace = client.namespace("application").await;
1026        match namespace {
1027            Ok(namespace) => match namespace {
1028                namespace::Namespace::Properties(properties) => {
1029                    assert_eq!(properties.get_bool("boolValue"), Some(false));
1030                }
1031                _ => panic!("Expected Properties namespace"),
1032            },
1033            Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1034        }
1035    }
1036
1037    #[cfg(not(target_arch = "wasm32"))]
1038    #[tokio::test]
1039    async fn test_bool_value_with_grayscale_ip() {
1040        setup();
1041        let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_IP
1042            .namespace("application")
1043            .await
1044            .unwrap()
1045        else {
1046            panic!("Expected Properties namespace");
1047        };
1048        assert_eq!(
1049            properties.get_property::<bool>("grayScaleValue"),
1050            Some(true)
1051        );
1052        let namespace::Namespace::Properties(properties) =
1053            CLIENT_NO_SECRET.namespace("application").await.unwrap()
1054        else {
1055            panic!("Expected Properties namespace");
1056        };
1057        assert_eq!(
1058            properties.get_property::<bool>("grayScaleValue"),
1059            Some(false)
1060        );
1061    }
1062
1063    #[cfg(target_arch = "wasm32")]
1064    #[wasm_bindgen_test::wasm_bindgen_test]
1065    #[allow(dead_code)]
1066    async fn test_bool_value_with_grayscale_ip_wasm() {
1067        setup();
1068        let client1 = create_client_with_grayscale_ip();
1069        let namespace = client1.namespace("application").await;
1070        match namespace {
1071            Ok(namespace) => match namespace {
1072                namespace::Namespace::Properties(properties) => {
1073                    assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1074                }
1075                _ => panic!("Expected Properties namespace"),
1076            },
1077            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1078        }
1079
1080        let client2 = create_client_no_secret();
1081        let namespace = client2.namespace("application").await;
1082        match namespace {
1083            Ok(namespace) => match namespace {
1084                namespace::Namespace::Properties(properties) => {
1085                    assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1086                }
1087                _ => panic!("Expected Properties namespace"),
1088            },
1089            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1090        }
1091    }
1092
1093    #[cfg(not(target_arch = "wasm32"))]
1094    #[tokio::test]
1095    async fn test_bool_value_with_grayscale_label() {
1096        setup();
1097        let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_LABEL
1098            .namespace("application")
1099            .await
1100            .unwrap()
1101        else {
1102            panic!("Expected Properties namespace");
1103        };
1104        assert_eq!(
1105            properties.get_property::<bool>("grayScaleValue"),
1106            Some(true)
1107        );
1108        let namespace::Namespace::Properties(properties) =
1109            CLIENT_NO_SECRET.namespace("application").await.unwrap()
1110        else {
1111            panic!("Expected Properties namespace");
1112        };
1113        assert_eq!(
1114            properties.get_property::<bool>("grayScaleValue"),
1115            Some(false)
1116        );
1117    }
1118
1119    #[cfg(target_arch = "wasm32")]
1120    #[wasm_bindgen_test::wasm_bindgen_test]
1121    #[allow(dead_code)]
1122    async fn test_bool_value_with_grayscale_label_wasm() {
1123        setup();
1124        let client1 = create_client_with_grayscale_label();
1125        let namespace = client1.namespace("application").await;
1126        match namespace {
1127            Ok(namespace) => match namespace {
1128                namespace::Namespace::Properties(properties) => {
1129                    assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1130                }
1131                _ => panic!("Expected Properties namespace"),
1132            },
1133            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1134        }
1135
1136        let client2 = create_client_no_secret();
1137        let namespace = client2.namespace("application").await;
1138        match namespace {
1139            Ok(namespace) => match namespace {
1140                namespace::Namespace::Properties(properties) => {
1141                    assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1142                }
1143                _ => panic!("Expected Properties namespace"),
1144            },
1145            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1146        }
1147    }
1148
1149    #[cfg(target_arch = "wasm32")]
1150    fn create_client_no_secret() -> Client {
1151        let config = ClientConfig {
1152            app_id: String::from("101010101"),
1153            cluster: String::from("default"),
1154            config_server: String::from("http://81.68.181.139:8080"),
1155            label: None,
1156            secret: None,
1157            cache_dir: None,
1158            ip: None,
1159            allow_insecure_https: None,
1160        };
1161        Client::new(config)
1162    }
1163
1164    #[cfg(target_arch = "wasm32")]
1165    fn create_client_with_secret() -> Client {
1166        let config = ClientConfig {
1167            app_id: String::from("101010102"),
1168            cluster: String::from("default"),
1169            config_server: String::from("http://81.68.181.139:8080"),
1170            label: None,
1171            secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
1172            cache_dir: None,
1173            ip: None,
1174            allow_insecure_https: None,
1175        };
1176        Client::new(config)
1177    }
1178
1179    #[cfg(target_arch = "wasm32")]
1180    fn create_client_with_grayscale_ip() -> Client {
1181        let config = ClientConfig {
1182            app_id: String::from("101010101"),
1183            cluster: String::from("default"),
1184            config_server: String::from("http://81.68.181.139:8080"),
1185            label: None,
1186            secret: None,
1187            cache_dir: None,
1188            ip: Some(String::from("1.2.3.4")),
1189            allow_insecure_https: None,
1190        };
1191        Client::new(config)
1192    }
1193
1194    #[cfg(target_arch = "wasm32")]
1195    fn create_client_with_grayscale_label() -> Client {
1196        let config = ClientConfig {
1197            app_id: String::from("101010101"),
1198            cluster: String::from("default"),
1199            config_server: String::from("http://81.68.181.139:8080"),
1200            label: Some(String::from("GrayScale")),
1201            secret: None,
1202            cache_dir: None,
1203            ip: None,
1204            allow_insecure_https: None,
1205        };
1206        Client::new(config)
1207    }
1208
1209    #[cfg(not(target_arch = "wasm32"))]
1210    #[tokio::test] // Re-enable for WASM
1211    async fn test_add_listener_and_notify_on_refresh() {
1212        setup();
1213
1214        // Shared state to check if listener was called and what it received
1215        let listener_called_flag = Arc::new(Mutex::new(false));
1216        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1217
1218        // ClientConfig similar to CLIENT_NO_SECRET from lib.rs tests
1219        // Using the same external test server and app_id as tests in lib.rs
1220        let config = ClientConfig {
1221            config_server: "http://81.68.181.139:8080".to_string(), // Use external test server
1222            app_id: "101010101".to_string(), // Use existing app_id from lib.rs tests
1223            cluster: "default".to_string(),
1224            cache_dir: Some(String::from("/tmp/apollo")), // Use a writable directory
1225            secret: None,
1226            label: None,
1227            ip: None,
1228            allow_insecure_https: None,
1229            #[cfg(not(target_arch = "wasm32"))]
1230            cache_ttl: None,
1231            // ..Default::default() // Be careful with Default if it doesn't set all needed fields for tests
1232        };
1233
1234        let client = Client::new(config);
1235
1236        let flag_clone = listener_called_flag.clone();
1237        let data_clone = received_config_data.clone();
1238
1239        let listener: EventListener = Arc::new(move |result| {
1240            let mut called_guard = block_on(flag_clone.lock());
1241            *called_guard = true;
1242            if let Ok(config_value) = result {
1243                match config_value {
1244                    Namespace::Properties(_) => {
1245                        let mut data_guard = block_on(data_clone.lock());
1246                        *data_guard = Some(config_value.clone());
1247                    }
1248                    _ => {
1249                        panic!("Expected Properties namespace, got {config_value:?}");
1250                    }
1251                }
1252            }
1253            // In a real scenario, avoid panicking in a listener.
1254            // For a test, this is acceptable to signal issues.
1255        });
1256
1257        client.add_listener("application", listener).await;
1258
1259        let cache = client.cache("application").await;
1260
1261        // Perform a refresh. This should trigger the listener.
1262        // The test Apollo server (localhost:8071) should have some known config for "SampleApp" "application" namespace.
1263        match cache.refresh().await {
1264            Ok(()) => log::debug!("Refresh successful for test_add_listener_and_notify_on_refresh"),
1265            Err(e) => panic!("Cache refresh failed during test: {e:?}"),
1266        }
1267
1268        // Give the async listener task time to complete
1269        cfg_if::cfg_if! {
1270            if #[cfg(target_arch = "wasm32")] {
1271                // For WASM, listeners are synchronous so no wait needed
1272            } else {
1273                // For native targets, use tokio sleep
1274                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1275            }
1276        }
1277
1278        // Check if the listener was called
1279        let called = *listener_called_flag.lock().await;
1280        assert!(called, "Listener was not called.");
1281
1282        // Check if config data was received
1283        let config_data_guard = received_config_data.lock().await;
1284        assert!(
1285            config_data_guard.is_some(),
1286            "Listener did not receive config data."
1287        );
1288
1289        // Optionally, assert specific content if known.
1290        // Assert based on known data for app_id "101010101", namespace "application"
1291        // from the external test server. Example: "stringValue"
1292        if let Some(value) = config_data_guard.as_ref() {
1293            match value {
1294                Namespace::Properties(properties) => {
1295                    assert_eq!(
1296                        properties.get_string("stringValue"),
1297                        Some(String::from("string value")),
1298                        "Received config data does not match expected content for stringValue."
1299                    );
1300                }
1301                _ => {
1302                    panic!("Expected Properties namespace, got {value:?}");
1303                }
1304            }
1305        }
1306    }
1307
1308    #[cfg(target_arch = "wasm32")]
1309    #[wasm_bindgen_test::wasm_bindgen_test]
1310    async fn test_add_listener_wasm_and_notify() {
1311        setup(); // Existing test setup
1312
1313        // Shared state to check if listener was called and what it received
1314        let listener_called_flag = Arc::new(Mutex::new(false));
1315        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1316
1317        let flag_clone = listener_called_flag.clone();
1318        let data_clone = received_config_data.clone();
1319
1320        // Create JS Listener Function that updates our shared state
1321        let js_listener_func_body = format!(
1322            r#"
1323            (data, error) => {{
1324                // We can't use window in Node.js, so we'll use a different approach
1325                // The Rust closure will handle the verification
1326                console.log('JS Listener called with error:', error);
1327                console.log('JS Listener called with data:', data);
1328            }}
1329        "#
1330        );
1331
1332        let js_listener = js_sys::Function::new_with_args("data, error", &js_listener_func_body);
1333
1334        let client = create_client_no_secret();
1335
1336        // Add a Rust listener to verify the functionality
1337        let rust_listener: EventListener = Arc::new(move |result| {
1338            let mut called_guard = flag_clone.lock().unwrap();
1339            *called_guard = true;
1340            if let Ok(config_value) = result {
1341                let mut data_guard = data_clone.lock().unwrap();
1342                *data_guard = Some(config_value);
1343            }
1344        });
1345
1346        client.add_listener("application", rust_listener).await;
1347
1348        // Add JS Listener
1349        client.add_listener_wasm("application", js_listener).await;
1350
1351        let cache = client.cache("application").await;
1352
1353        // Trigger Refresh
1354        match cache.refresh().await {
1355            Ok(_) => web_sys::console::log_1(&"WASM Test: Refresh successful".into()), // web_sys::console for logging
1356            Err(e) => panic!("WASM Test: Cache refresh failed: {:?}", e),
1357        }
1358
1359        // Give the async listener task time to complete
1360        cfg_if::cfg_if! {
1361            if #[cfg(target_arch = "wasm32")] {
1362                // For WASM, listeners are synchronous so no wait needed
1363            } else {
1364                // For native targets, use tokio sleep
1365                tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1366            }
1367        }
1368
1369        // Verify Listener Was Called using our Rust listener
1370        let called = *listener_called_flag.lock().unwrap();
1371        assert!(called, "Listener was not called.");
1372
1373        // Check if config data was received
1374        let config_data_guard = received_config_data.lock().unwrap();
1375        assert!(
1376            config_data_guard.is_some(),
1377            "Listener did not receive config data."
1378        );
1379
1380        // Verify the content
1381        if let Some(value) = config_data_guard.as_ref() {
1382            match value {
1383                namespace::Namespace::Properties(properties) => {
1384                    assert_eq!(
1385                        properties.get_string("stringValue"),
1386                        Some("string value".to_string())
1387                    );
1388                }
1389                _ => panic!("Expected Properties namespace"),
1390            }
1391        }
1392    }
1393
1394    #[cfg(not(target_arch = "wasm32"))]
1395    #[tokio::test]
1396    async fn test_concurrent_namespace_hang_repro() {
1397        use async_std::task;
1398        setup();
1399
1400        let temp_dir = TempDir::new("apollo_hang_test");
1401
1402        let config = ClientConfig {
1403            app_id: String::from("101010101"),
1404            cluster: String::from("default"),
1405            config_server: String::from("http://81.68.181.139:8080"),
1406            secret: None,
1407            cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
1408            label: None,
1409            ip: None,
1410            allow_insecure_https: None,
1411            cache_ttl: None,
1412        };
1413
1414        let client = Arc::new(Client::new(config));
1415        let client_in_listener = client.clone();
1416
1417        let listener_triggered = Arc::new(Mutex::new(false));
1418        let listener_triggered_in_listener = listener_triggered.clone();
1419
1420        let listener: EventListener = Arc::new(move |_| {
1421            let client_in_listener = client_in_listener.clone();
1422            let listener_triggered_in_listener = listener_triggered_in_listener.clone();
1423            task::spawn(async move {
1424                let mut triggered = listener_triggered_in_listener.lock().await;
1425                if *triggered {
1426                    // Avoid infinite loops if the listener is called more than once.
1427                    return;
1428                }
1429                *triggered = true;
1430                drop(triggered);
1431
1432                // This is the recursive call that should no longer trigger a deadlock.
1433                let _ = client_in_listener.namespace("application").await;
1434            });
1435        });
1436
1437        client.add_listener("application", listener).await;
1438
1439        let test_body = async {
1440            let _ = client.namespace("application").await;
1441        };
1442
1443        // The test should not hang. If it does, timeout will fail it.
1444        let res = tokio::time::timeout(std::time::Duration::from_secs(10), test_body).await;
1445        assert!(res.is_ok(), "Test timed out, which indicates a deadlock.");
1446    }
1447}