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