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//!     #[cfg(not(target_arch = "wasm32"))]
37//!     cache_ttl: None,
38//! };
39//!
40//! let mut client = Client::new(config);
41//! client.start().await?;
42//!
43//! let namespace = client.namespace("application").await?;
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! ## Platform Support
49//!
50//! The library supports different behavior for wasm32 and non-wasm32 targets:
51//!
52//! - **Native Rust**: Full feature set with file caching, background tasks, and threading
53//! - **WebAssembly**: Memory-only caching, single-threaded execution, JavaScript interop
54
55use crate::namespace::Namespace;
56use async_std::sync::RwLock;
57use cache::Cache;
58use client_config::ClientConfig;
59use log::{debug, error, trace};
60use std::{collections::HashMap, sync::Arc, time::Duration};
61use wasm_bindgen::prelude::wasm_bindgen;
62
63cfg_if::cfg_if! {
64    if #[cfg(target_arch = "wasm32")] {
65        use wasm_bindgen_futures::spawn_local as spawn;
66    } else {
67        use async_std::task::spawn as spawn;
68    }
69}
70
71mod cache;
72
73pub mod client_config;
74pub mod namespace;
75
76/// Comprehensive error types that can occur when using the Apollo client.
77///
78/// This enum covers all possible error conditions that may arise during client operations,
79/// from initialization and configuration to runtime cache operations and namespace handling.
80///
81/// # Error Categories
82///
83/// - **Client State Errors**: Issues related to client lifecycle management
84/// - **Namespace Errors**: Problems with namespace format detection and processing
85/// - **Cache Errors**: Network, I/O, and caching-related failures
86///
87/// # Examples
88///
89/// ```rust,no_run
90/// use apollo_rust_client::{Client, Error};
91///
92/// # #[tokio::main]
93/// # async fn main() {
94/// # let client = Client::new(apollo_rust_client::client_config::ClientConfig {
95/// #     app_id: "test".to_string(),
96/// #     config_server: "http://localhost:8080".to_string(),
97/// #     cluster: "default".to_string(),
98/// #     secret: None,
99/// #     cache_dir: None,
100/// #     label: None,
101/// #     ip: 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/// ## Basic Usage
195///
196/// ```rust,no_run
197/// use apollo_rust_client::{Client, client_config::ClientConfig};
198///
199/// # #[tokio::main]
200/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
201/// let config = ClientConfig {
202///     app_id: "my-app".to_string(),
203///     config_server: "http://apollo-server:8080".to_string(),
204///     cluster: "default".to_string(),
205///     secret: None,
206///     cache_dir: None,
207///     label: None,
208///     ip: None,
209///     #[cfg(not(target_arch = "wasm32"))]
210///     cache_ttl: None,
211/// };
212///
213/// let mut client = Client::new(config);
214/// client.start().await?;
215///
216/// let namespace = client.namespace("application").await?;
217/// # Ok(())
218/// # }
219/// ```
220///
221/// ## With Event Listeners
222///
223/// ```rust,no_run
224/// use apollo_rust_client::{Client, client_config::ClientConfig};
225/// use std::sync::Arc;
226///
227/// # #[tokio::main]
228/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
229/// # let config = ClientConfig {
230/// #     app_id: "my-app".to_string(),
231/// #     config_server: "http://apollo-server:8080".to_string(),
232/// #     cluster: "default".to_string(),
233/// #     secret: None,
234/// #     cache_dir: None,
235/// #     label: None,
236/// #     ip: None,
237/// #     #[cfg(not(target_arch = "wasm32"))]
238/// #     cache_ttl: None,
239/// # };
240/// # let client = Client::new(config);
241/// client.add_listener("application", Arc::new(|result| {
242///     match result {
243///         Ok(namespace) => println!("Config updated: {:?}", namespace),
244///         Err(e) => eprintln!("Update error: {}", e),
245///     }
246/// })).await;
247/// # Ok(())
248/// # }
249/// ```
250///
251/// # Platform Differences
252///
253/// The client behaves differently on different platforms:
254///
255/// - **Native Rust**: Full feature set with file caching and background tasks
256/// - **WebAssembly**: Memory-only caching, single-threaded execution
257#[wasm_bindgen]
258pub struct Client {
259    /// The configuration settings for this Apollo client instance.
260    ///
261    /// Contains all necessary information to connect to Apollo servers,
262    /// including server URL, application ID, cluster, authentication, and caching settings.
263    client_config: ClientConfig,
264
265    /// Thread-safe storage for namespace-specific caches.
266    ///
267    /// Each namespace gets its own `Cache` instance, wrapped in `Arc` for shared ownership.
268    /// The `RwLock` provides thread-safe read/write access to the namespace map.
269    /// The outer `Arc` allows the background refresh task to safely access the namespaces.
270    namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
271
272    /// Handle to the background refresh task (native targets only).
273    ///
274    /// On non-wasm32 targets, this holds a `JoinHandle` to the spawned background task
275    /// that periodically refreshes all namespace caches. On wasm32 targets, this is
276    /// always `None` as task management differs in single-threaded environments.
277    handle: Option<async_std::task::JoinHandle<()>>,
278
279    /// Flag indicating whether the background refresh task is active.
280    ///
281    /// Wrapped in `Arc<RwLock<bool>>` for thread-safe shared access between the
282    /// client and its background task. Used to coordinate task lifecycle management.
283    running: Arc<RwLock<bool>>,
284}
285
286impl Client {
287    /// Get a cache for a given namespace.
288    ///
289    /// # Arguments
290    ///
291    /// * `namespace` - The namespace to get the cache for.
292    ///
293    /// # Returns
294    ///
295    /// A cache for the given namespace.
296    pub(crate) async fn cache(&self, namespace: &str) -> Arc<Cache> {
297        let mut namespaces = self.namespaces.write().await;
298        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
299            trace!("Cache miss, creating cache for namespace {namespace}");
300            Arc::new(Cache::new(self.client_config.clone(), namespace))
301        });
302        cache.clone()
303    }
304
305    pub async fn add_listener(&self, namespace: &str, listener: EventListener) {
306        let mut namespaces = self.namespaces.write().await;
307        let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
308            trace!("Cache miss, creating cache for namespace {namespace}");
309            Arc::new(Cache::new(self.client_config.clone(), namespace))
310        });
311        cache.add_listener(listener).await;
312    }
313
314    pub async fn namespace(&self, namespace: &str) -> Result<namespace::Namespace, Error> {
315        let cache = self.cache(namespace).await;
316        let value = cache.get_value().await?;
317        Ok(namespace::get_namespace(namespace, value)?)
318    }
319
320    /// Starts a background task that periodically refreshes all registered namespace caches.
321    ///
322    /// This method spawns an asynchronous task using `async_std::task::spawn` on native targets
323    /// or `wasm_bindgen_futures::spawn_local` on wasm32 targets. The task loops indefinitely
324    /// (until `stop` is called or the client is dropped) and performs the following actions
325    /// in each iteration:
326    ///
327    /// 1. Iterates through all namespaces currently managed by the client.
328    /// 2. Calls the `refresh` method on each namespace's `Cache` instance.
329    /// 3. Logs any errors encountered during the refresh process.
330    /// 4. Sleeps for a predefined interval (currently 30 seconds) before the next refresh cycle.
331    ///
332    /// # Returns
333    ///
334    /// * `Ok(())` if the background task was successfully started.
335    /// * `Err(Error::AlreadyRunning)` if the background task is already active.
336    pub async fn start(&mut self) -> Result<(), Error> {
337        let mut running = self.running.write().await;
338        if *running {
339            return Err(Error::AlreadyRunning);
340        }
341
342        *running = true;
343
344        let running = self.running.clone();
345        let namespaces = self.namespaces.clone();
346
347        // Spawn a background thread to refresh caches
348        let _handle = spawn(async move {
349            loop {
350                let running = running.read().await;
351                if !*running {
352                    break;
353                }
354
355                let namespaces = namespaces.read().await;
356                // Refresh each namespace's cache
357                for (namespace, cache) in namespaces.iter() {
358                    if let Err(err) = cache.refresh().await {
359                        error!("Failed to refresh cache for namespace {namespace}: {err:?}");
360                    } else {
361                        debug!("Successfully refreshed cache for namespace {namespace}");
362                    }
363                }
364
365                // Sleep for 30 seconds before the next refresh
366                async_std::task::sleep(Duration::from_secs(30)).await;
367            }
368        });
369
370        cfg_if::cfg_if! {
371            if #[cfg(target_arch = "wasm32")] {
372                self.handle = None;
373            } else {
374                self.handle = Some(_handle);
375            }
376        }
377
378        Ok(())
379    }
380
381    /// Stops the background cache refresh task.
382    ///
383    /// This method sets the `running` flag to `false`, signaling the background task
384    /// to terminate its refresh loop.
385    ///
386    /// On non-wasm32 targets, it also attempts to explicitly cancel the spawned task
387    /// by calling `cancel()` on its `JoinHandle` if it exists. This helps to ensure
388    /// that the task is properly cleaned up. On wasm32 targets, there is no direct
389    /// handle to cancel, so setting the `running` flag is the primary mechanism for stopping.
390    pub async fn stop(&mut self) {
391        let mut running = self.running.write().await;
392        *running = false;
393
394        cfg_if::cfg_if! {
395            if #[cfg(not(target_arch = "wasm32"))] {
396                if let Some(handle) = self.handle.take() {
397                    handle.cancel().await;
398                }
399            }
400        }
401    }
402}
403
404#[wasm_bindgen]
405impl Client {
406    /// Create a new Apollo client.
407    ///
408    /// # Arguments
409    ///
410    /// * `client_config` - The configuration for the Apollo client.
411    ///
412    /// # Returns
413    ///
414    /// A new Apollo client.
415    #[wasm_bindgen(constructor)]
416    pub fn new(client_config: ClientConfig) -> Self {
417        Self {
418            client_config,
419            namespaces: Arc::new(RwLock::new(HashMap::new())),
420            handle: None,
421            running: Arc::new(RwLock::new(false)),
422        }
423    }
424
425    /// Registers a JavaScript function as an event listener for this cache (WASM only).
426    ///
427    /// This method is exposed to JavaScript as `addListener`.
428    /// The provided JavaScript function will be called when the cache is refreshed.
429    ///
430    /// The JavaScript listener function is expected to have a signature like:
431    /// `function(data, error)`
432    /// - `data`: The JSON configuration object (if the refresh was successful and data
433    ///           could be serialized) or `null` if an error occurred or serialization failed.
434    /// - `error`: A string describing the error if one occurred during the configuration
435    ///            fetch or processing. If the operation was successful, this will be `null`.
436    ///
437    /// # Arguments
438    ///
439    /// * `js_listener` - A JavaScript `Function` to be called on cache events.
440    ///
441    /// # Example (JavaScript)
442    ///
443    /// ```javascript
444    /// // Assuming `cacheInstance` is an instance of the Rust `Cache` object in JS
445    /// cacheInstance.addListener((data, error) => {
446    ///   if (error) {
447    ///     console.error('Cache update error:', error);
448    ///   } else {
449    ///     console.log('Cache updated:', data);
450    ///   }
451    /// });
452    /// // ... later, when the cache refreshes, the callback will be invoked.
453    /// ```
454    #[cfg(target_arch = "wasm32")]
455    #[wasm_bindgen(js_name = "add_listener")]
456    pub async fn add_listener_wasm(&self, namespace: &str, js_listener: js_sys::Function) {
457        let js_listener_clone = js_listener.clone();
458
459        let event_listener: EventListener = Arc::new(move |result: Result<Namespace, Error>| {
460            let err_js_val: wasm_bindgen::JsValue;
461            let data_js_val: wasm_bindgen::JsValue;
462
463            match result {
464                Ok(value) => {
465                    data_js_val = value.into();
466                    err_js_val = wasm_bindgen::JsValue::UNDEFINED;
467                }
468                Err(cache_error) => {
469                    err_js_val = cache_error.into();
470                    data_js_val = wasm_bindgen::JsValue::UNDEFINED;
471                }
472            };
473
474            // Call the JavaScript listener: listener(data, error)
475            match js_listener_clone.call2(
476                &wasm_bindgen::JsValue::UNDEFINED,
477                &data_js_val,
478                &err_js_val,
479            ) {
480                Ok(_) => {
481                    // JS function called successfully
482                }
483                Err(e) => {
484                    // JS function threw an error or call failed
485                    log::error!("JavaScript listener threw an error: {:?}", e);
486                }
487            }
488        });
489
490        self.add_listener(namespace, event_listener).await; // Call the renamed Rust method
491    }
492
493    #[cfg(target_arch = "wasm32")]
494    #[wasm_bindgen(js_name = "namespace")]
495    pub async fn namespace_wasm(&self, namespace: &str) -> Result<wasm_bindgen::JsValue, Error> {
496        let cache = self.cache(namespace).await;
497        let value = cache.get_value().await?;
498        Ok(namespace::get_namespace(namespace, value)?.into())
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    #[cfg(not(target_arch = "wasm32"))]
506    use lazy_static::lazy_static;
507
508    cfg_if::cfg_if! {
509        if #[cfg(target_arch = "wasm32")] {
510            use std::sync::Mutex;
511        } else {
512            use async_std::sync::Mutex;
513            use async_std::task::block_on;
514        }
515    }
516
517    #[cfg(not(target_arch = "wasm32"))]
518    lazy_static! {
519        pub(crate) static ref CLIENT_NO_SECRET: Client = {
520            let config = ClientConfig {
521                app_id: String::from("101010101"),
522                cluster: String::from("default"),
523                config_server: String::from("http://81.68.181.139:8080"),
524                label: None,
525                secret: None,
526                cache_dir: Some(String::from("/tmp/apollo")),
527                ip: None,
528                #[cfg(not(target_arch = "wasm32"))]
529                cache_ttl: None,
530            };
531            Client::new(config)
532        };
533        pub(crate) static ref CLIENT_WITH_SECRET: Client = {
534            let config = ClientConfig {
535                app_id: String::from("101010102"),
536                cluster: String::from("default"),
537                config_server: String::from("http://81.68.181.139:8080"),
538                label: None,
539                secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
540                cache_dir: Some(String::from("/tmp/apollo")),
541                ip: None,
542                #[cfg(not(target_arch = "wasm32"))]
543                cache_ttl: None,
544            };
545            Client::new(config)
546        };
547        pub(crate) static ref CLIENT_WITH_GRAYSCALE_IP: Client = {
548            let config = ClientConfig {
549                app_id: String::from("101010101"),
550                cluster: String::from("default"),
551                config_server: String::from("http://81.68.181.139:8080"),
552                label: None,
553                secret: None,
554                cache_dir: Some(String::from("/tmp/apollo")),
555                ip: Some(String::from("1.2.3.4")),
556                #[cfg(not(target_arch = "wasm32"))]
557                cache_ttl: None,
558            };
559            Client::new(config)
560        };
561        pub(crate) static ref CLIENT_WITH_GRAYSCALE_LABEL: Client = {
562            let config = ClientConfig {
563                app_id: String::from("101010101"),
564                cluster: String::from("default"),
565                config_server: String::from("http://81.68.181.139:8080"),
566                label: Some(String::from("GrayScale")),
567                secret: None,
568                cache_dir: Some(String::from("/tmp/apollo")),
569                ip: None,
570                #[cfg(not(target_arch = "wasm32"))]
571                cache_ttl: None,
572            };
573            Client::new(config)
574        };
575    }
576
577    pub(crate) fn setup() {
578        cfg_if::cfg_if! {
579            if #[cfg(target_arch = "wasm32")] {
580                let _ = wasm_logger::init(wasm_logger::Config::default());
581                console_error_panic_hook::set_once();
582            } else {
583                let _ = env_logger::builder().is_test(true).try_init();
584            }
585        }
586    }
587
588    #[cfg(not(target_arch = "wasm32"))]
589    #[tokio::test]
590    async fn test_missing_value() {
591        setup();
592        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
593            namespace::Namespace::Properties(properties) => properties,
594            _ => panic!("Expected Properties namespace"),
595        };
596
597        assert_eq!(
598            properties.get_property::<String>("missingValue").await,
599            None
600        );
601    }
602
603    #[cfg(target_arch = "wasm32")]
604    #[wasm_bindgen_test::wasm_bindgen_test]
605    #[allow(dead_code)]
606    async fn test_missing_value_wasm() {
607        setup();
608        let client = create_client_no_secret();
609        let namespace = client.namespace("application").await;
610        match namespace {
611            Ok(namespace) => match namespace {
612                namespace::Namespace::Properties(properties) => {
613                    assert_eq!(properties.get_string("missingValue").await, None);
614                }
615                _ => panic!("Expected Properties namespace"),
616            },
617            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
618        }
619    }
620
621    #[cfg(not(target_arch = "wasm32"))]
622    #[tokio::test]
623    async fn test_string_value() {
624        setup();
625        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
626            namespace::Namespace::Properties(properties) => properties,
627            _ => panic!("Expected Properties namespace"),
628        };
629        assert_eq!(
630            properties.get_property::<String>("stringValue").await,
631            Some("string value".to_string())
632        );
633    }
634
635    #[cfg(target_arch = "wasm32")]
636    #[wasm_bindgen_test::wasm_bindgen_test]
637    #[allow(dead_code)]
638    async fn test_string_value_wasm() {
639        setup();
640        let client = create_client_no_secret();
641        let namespace = client.namespace("application").await;
642        match namespace {
643            Ok(namespace) => match namespace {
644                namespace::Namespace::Properties(properties) => {
645                    assert_eq!(
646                        properties.get_string("stringValue").await,
647                        Some("string value".to_string())
648                    );
649                }
650                _ => panic!("Expected Properties namespace"),
651            },
652            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
653        }
654    }
655
656    #[cfg(not(target_arch = "wasm32"))]
657    #[tokio::test]
658    async fn test_string_value_with_secret() {
659        setup();
660        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
661            namespace::Namespace::Properties(properties) => properties,
662            _ => panic!("Expected Properties namespace"),
663        };
664        assert_eq!(
665            properties.get_property::<String>("stringValue").await,
666            Some("string value".to_string())
667        );
668    }
669
670    #[cfg(target_arch = "wasm32")]
671    #[wasm_bindgen_test::wasm_bindgen_test]
672    #[allow(dead_code)]
673    async fn test_string_value_with_secret_wasm() {
674        setup();
675        let client = create_client_with_secret();
676        let namespace = client.namespace("application").await;
677        match namespace {
678            Ok(namespace) => match namespace {
679                namespace::Namespace::Properties(properties) => {
680                    assert_eq!(
681                        properties.get_string("stringValue").await,
682                        Some("string value".to_string())
683                    );
684                }
685                _ => panic!("Expected Properties namespace"),
686            },
687            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
688        }
689    }
690
691    #[cfg(not(target_arch = "wasm32"))]
692    #[tokio::test]
693    async fn test_int_value() {
694        setup();
695        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
696            namespace::Namespace::Properties(properties) => properties,
697            _ => panic!("Expected Properties namespace"),
698        };
699        assert_eq!(properties.get_property::<i32>("intValue").await, Some(42));
700    }
701
702    #[cfg(target_arch = "wasm32")]
703    #[wasm_bindgen_test::wasm_bindgen_test]
704    #[allow(dead_code)]
705    async fn test_int_value_wasm() {
706        setup();
707        let client = create_client_no_secret();
708        let namespace = client.namespace("application").await;
709        match namespace {
710            Ok(namespace) => match namespace {
711                namespace::Namespace::Properties(properties) => {
712                    assert_eq!(properties.get_int("intValue").await, Some(42));
713                }
714                _ => panic!("Expected Properties namespace"),
715            },
716            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
717        }
718    }
719
720    #[cfg(not(target_arch = "wasm32"))]
721    #[tokio::test]
722    async fn test_int_value_with_secret() {
723        setup();
724        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
725            namespace::Namespace::Properties(properties) => properties,
726            _ => panic!("Expected Properties namespace"),
727        };
728        assert_eq!(properties.get_property::<i32>("intValue").await, Some(42));
729    }
730
731    #[cfg(target_arch = "wasm32")]
732    #[wasm_bindgen_test::wasm_bindgen_test]
733    #[allow(dead_code)]
734    async fn test_int_value_with_secret_wasm() {
735        setup();
736        let client = create_client_with_secret();
737        let namespace = client.namespace("application").await;
738        match namespace {
739            Ok(namespace) => match namespace {
740                namespace::Namespace::Properties(properties) => {
741                    assert_eq!(properties.get_int("intValue").await, Some(42));
742                }
743                _ => panic!("Expected Properties namespace"),
744            },
745            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
746        }
747    }
748
749    #[cfg(not(target_arch = "wasm32"))]
750    #[tokio::test]
751    async fn test_float_value() {
752        setup();
753        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
754            namespace::Namespace::Properties(properties) => properties,
755            _ => panic!("Expected Properties namespace"),
756        };
757        assert_eq!(
758            properties.get_property::<f64>("floatValue").await,
759            Some(4.20)
760        );
761    }
762
763    #[cfg(target_arch = "wasm32")]
764    #[wasm_bindgen_test::wasm_bindgen_test]
765    #[allow(dead_code)]
766    async fn test_float_value_wasm() {
767        setup();
768        let client = create_client_no_secret();
769        let namespace = client.namespace("application").await;
770        match namespace {
771            Ok(namespace) => match namespace {
772                namespace::Namespace::Properties(properties) => {
773                    assert_eq!(properties.get_float("floatValue").await, Some(4.20));
774                }
775                _ => panic!("Expected Properties namespace"),
776            },
777            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
778        }
779    }
780
781    #[cfg(not(target_arch = "wasm32"))]
782    #[tokio::test]
783    async fn test_float_value_with_secret() {
784        setup();
785        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
786            namespace::Namespace::Properties(properties) => properties,
787            _ => panic!("Expected Properties namespace"),
788        };
789        assert_eq!(
790            properties.get_property::<f64>("floatValue").await,
791            Some(4.20)
792        );
793    }
794
795    #[cfg(target_arch = "wasm32")]
796    #[wasm_bindgen_test::wasm_bindgen_test]
797    #[allow(dead_code)]
798    async fn test_float_value_with_secret_wasm() {
799        setup();
800        let client = create_client_with_secret();
801        let namespace = client.namespace("application").await;
802        match namespace {
803            Ok(namespace) => match namespace {
804                namespace::Namespace::Properties(properties) => {
805                    assert_eq!(properties.get_float("floatValue").await, Some(4.20));
806                }
807                _ => panic!("Expected Properties namespace"),
808            },
809            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
810        }
811    }
812
813    #[cfg(not(target_arch = "wasm32"))]
814    #[tokio::test]
815    async fn test_bool_value() {
816        setup();
817        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
818            namespace::Namespace::Properties(properties) => properties,
819            _ => panic!("Expected Properties namespace"),
820        };
821        assert_eq!(
822            properties.get_property::<bool>("boolValue").await,
823            Some(false)
824        );
825    }
826
827    #[cfg(target_arch = "wasm32")]
828    #[wasm_bindgen_test::wasm_bindgen_test]
829    #[allow(dead_code)]
830    async fn test_bool_value_wasm() {
831        setup();
832        let client = create_client_no_secret();
833        let namespace = client.namespace("application").await;
834        match namespace {
835            Ok(namespace) => match namespace {
836                namespace::Namespace::Properties(properties) => {
837                    assert_eq!(properties.get_bool("boolValue").await, Some(false));
838                }
839                _ => panic!("Expected Properties namespace"),
840            },
841            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
842        }
843    }
844
845    #[cfg(not(target_arch = "wasm32"))]
846    #[tokio::test]
847    async fn test_bool_value_with_secret() {
848        setup();
849        let properties = match CLIENT_WITH_SECRET.namespace("application").await.unwrap() {
850            namespace::Namespace::Properties(properties) => properties,
851            _ => panic!("Expected Properties namespace"),
852        };
853        assert_eq!(
854            properties.get_property::<bool>("boolValue").await,
855            Some(false)
856        );
857    }
858
859    #[cfg(target_arch = "wasm32")]
860    #[wasm_bindgen_test::wasm_bindgen_test]
861    #[allow(dead_code)]
862    async fn test_bool_value_with_secret_wasm() {
863        setup();
864        let client = create_client_with_secret();
865        let namespace = client.namespace("application").await;
866        match namespace {
867            Ok(namespace) => match namespace {
868                namespace::Namespace::Properties(properties) => {
869                    assert_eq!(properties.get_bool("boolValue").await, Some(false));
870                }
871                _ => panic!("Expected Properties namespace"),
872            },
873            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
874        }
875    }
876
877    #[cfg(not(target_arch = "wasm32"))]
878    #[tokio::test]
879    async fn test_bool_value_with_grayscale_ip() {
880        setup();
881        let properties = match CLIENT_WITH_GRAYSCALE_IP
882            .namespace("application")
883            .await
884            .unwrap()
885        {
886            namespace::Namespace::Properties(properties) => properties,
887            _ => panic!("Expected Properties namespace"),
888        };
889        assert_eq!(
890            properties.get_property::<bool>("grayScaleValue").await,
891            Some(true)
892        );
893        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
894            namespace::Namespace::Properties(properties) => properties,
895            _ => panic!("Expected Properties namespace"),
896        };
897        assert_eq!(
898            properties.get_property::<bool>("grayScaleValue").await,
899            Some(false)
900        );
901    }
902
903    #[cfg(target_arch = "wasm32")]
904    #[wasm_bindgen_test::wasm_bindgen_test]
905    #[allow(dead_code)]
906    async fn test_bool_value_with_grayscale_ip_wasm() {
907        setup();
908        let client1 = create_client_with_grayscale_ip();
909        let namespace = client1.namespace("application").await;
910        match namespace {
911            Ok(namespace) => match namespace {
912                namespace::Namespace::Properties(properties) => {
913                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(true));
914                }
915                _ => panic!("Expected Properties namespace"),
916            },
917            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
918        }
919
920        let client2 = create_client_no_secret();
921        let namespace = client2.namespace("application").await;
922        match namespace {
923            Ok(namespace) => match namespace {
924                namespace::Namespace::Properties(properties) => {
925                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(false));
926                }
927                _ => panic!("Expected Properties namespace"),
928            },
929            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
930        }
931    }
932
933    #[cfg(not(target_arch = "wasm32"))]
934    #[tokio::test]
935    async fn test_bool_value_with_grayscale_label() {
936        setup();
937        let properties = match CLIENT_WITH_GRAYSCALE_LABEL
938            .namespace("application")
939            .await
940            .unwrap()
941        {
942            namespace::Namespace::Properties(properties) => properties,
943            _ => panic!("Expected Properties namespace"),
944        };
945        assert_eq!(
946            properties.get_property::<bool>("grayScaleValue").await,
947            Some(true)
948        );
949        let properties = match CLIENT_NO_SECRET.namespace("application").await.unwrap() {
950            namespace::Namespace::Properties(properties) => properties,
951            _ => panic!("Expected Properties namespace"),
952        };
953        assert_eq!(
954            properties.get_property::<bool>("grayScaleValue").await,
955            Some(false)
956        );
957    }
958
959    #[cfg(target_arch = "wasm32")]
960    #[wasm_bindgen_test::wasm_bindgen_test]
961    #[allow(dead_code)]
962    async fn test_bool_value_with_grayscale_label_wasm() {
963        setup();
964        let client1 = create_client_with_grayscale_label();
965        let namespace = client1.namespace("application").await;
966        match namespace {
967            Ok(namespace) => match namespace {
968                namespace::Namespace::Properties(properties) => {
969                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(true));
970                }
971                _ => panic!("Expected Properties namespace"),
972            },
973            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
974        }
975
976        let client2 = create_client_no_secret();
977        let namespace = client2.namespace("application").await;
978        match namespace {
979            Ok(namespace) => match namespace {
980                namespace::Namespace::Properties(properties) => {
981                    assert_eq!(properties.get_bool("grayScaleValue").await, Some(false));
982                }
983                _ => panic!("Expected Properties namespace"),
984            },
985            Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
986        }
987    }
988
989    #[cfg(target_arch = "wasm32")]
990    fn create_client_no_secret() -> Client {
991        let config = ClientConfig {
992            app_id: String::from("101010101"),
993            cluster: String::from("default"),
994            config_server: String::from("http://81.68.181.139:8080"),
995            label: None,
996            secret: None,
997            cache_dir: None,
998            ip: None,
999        };
1000        Client::new(config)
1001    }
1002
1003    #[cfg(target_arch = "wasm32")]
1004    fn create_client_with_secret() -> Client {
1005        let config = ClientConfig {
1006            app_id: String::from("101010102"),
1007            cluster: String::from("default"),
1008            config_server: String::from("http://81.68.181.139:8080"),
1009            label: None,
1010            secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
1011            cache_dir: None,
1012            ip: None,
1013        };
1014        Client::new(config)
1015    }
1016
1017    #[cfg(target_arch = "wasm32")]
1018    fn create_client_with_grayscale_ip() -> Client {
1019        let config = ClientConfig {
1020            app_id: String::from("101010101"),
1021            cluster: String::from("default"),
1022            config_server: String::from("http://81.68.181.139:8080"),
1023            label: None,
1024            secret: None,
1025            cache_dir: None,
1026            ip: Some(String::from("1.2.3.4")),
1027        };
1028        Client::new(config)
1029    }
1030
1031    #[cfg(target_arch = "wasm32")]
1032    fn create_client_with_grayscale_label() -> Client {
1033        let config = ClientConfig {
1034            app_id: String::from("101010101"),
1035            cluster: String::from("default"),
1036            config_server: String::from("http://81.68.181.139:8080"),
1037            label: Some(String::from("GrayScale")),
1038            secret: None,
1039            cache_dir: None,
1040            ip: None,
1041        };
1042        Client::new(config)
1043    }
1044
1045    #[cfg(not(target_arch = "wasm32"))]
1046    #[tokio::test] // Re-enable for WASM
1047    async fn test_add_listener_and_notify_on_refresh() {
1048        setup();
1049
1050        // Shared state to check if listener was called and what it received
1051        let listener_called_flag = Arc::new(Mutex::new(false));
1052        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1053
1054        // ClientConfig similar to CLIENT_NO_SECRET from lib.rs tests
1055        // Using the same external test server and app_id as tests in lib.rs
1056        let config = ClientConfig {
1057            config_server: "http://81.68.181.139:8080".to_string(), // Use external test server
1058            app_id: "101010101".to_string(), // Use existing app_id from lib.rs tests
1059            cluster: "default".to_string(),
1060            cache_dir: Some(String::from("/tmp/apollo")), // Use a writable directory
1061            secret: None,
1062            label: None,
1063            ip: None,
1064            #[cfg(not(target_arch = "wasm32"))]
1065            cache_ttl: None,
1066            // ..Default::default() // Be careful with Default if it doesn't set all needed fields for tests
1067        };
1068
1069        let client = Client::new(config);
1070
1071        let flag_clone = listener_called_flag.clone();
1072        let data_clone = received_config_data.clone();
1073
1074        let listener: EventListener = Arc::new(move |result| {
1075            let mut called_guard = block_on(flag_clone.lock());
1076            *called_guard = true;
1077            if let Ok(config_value) = result {
1078                match config_value {
1079                    Namespace::Properties(_) => {
1080                        let mut data_guard = block_on(data_clone.lock());
1081                        *data_guard = Some(config_value.clone());
1082                    }
1083                    _ => {
1084                        panic!("Expected Properties namespace, got {config_value:?}");
1085                    }
1086                }
1087            }
1088            // In a real scenario, avoid panicking in a listener.
1089            // For a test, this is acceptable to signal issues.
1090        });
1091
1092        client.add_listener("application", listener).await;
1093
1094        let cache = client.cache("application").await;
1095
1096        // Perform a refresh. This should trigger the listener.
1097        // The test Apollo server (localhost:8071) should have some known config for "SampleApp" "application" namespace.
1098        match cache.refresh().await {
1099            Ok(_) => log::debug!("Refresh successful for test_add_listener_and_notify_on_refresh"),
1100            Err(e) => panic!("Cache refresh failed during test: {e:?}"),
1101        }
1102
1103        // Check if the listener was called
1104        let called = *listener_called_flag.lock().await;
1105        assert!(called, "Listener was not called.");
1106
1107        // Check if config data was received
1108        let config_data_guard = received_config_data.lock().await;
1109        assert!(
1110            config_data_guard.is_some(),
1111            "Listener did not receive config data."
1112        );
1113
1114        // Optionally, assert specific content if known.
1115        // Assert based on known data for app_id "101010101", namespace "application"
1116        // from the external test server. Example: "stringValue"
1117        if let Some(value) = config_data_guard.as_ref() {
1118            match value {
1119                Namespace::Properties(properties) => {
1120                    assert_eq!(
1121                        properties.get_string("stringValue").await,
1122                        Some(String::from("string value")),
1123                        "Received config data does not match expected content for stringValue."
1124                    );
1125                }
1126                _ => {
1127                    panic!("Expected Properties namespace, got {value:?}");
1128                }
1129            }
1130        }
1131    }
1132
1133    #[cfg(target_arch = "wasm32")]
1134    #[wasm_bindgen_test::wasm_bindgen_test]
1135    async fn test_add_listener_wasm_and_notify() {
1136        setup(); // Existing test setup
1137
1138        // Shared state to check if listener was called and what it received
1139        let listener_called_flag = Arc::new(Mutex::new(false));
1140        let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1141
1142        let flag_clone = listener_called_flag.clone();
1143        let data_clone = received_config_data.clone();
1144
1145        // Create JS Listener Function that updates our shared state
1146        let js_listener_func_body = format!(
1147            r#"
1148            (data, error) => {{
1149                // We can't use window in Node.js, so we'll use a different approach
1150                // The Rust closure will handle the verification
1151                console.log('JS Listener called with error:', error);
1152                console.log('JS Listener called with data:', data);
1153            }}
1154        "#
1155        );
1156
1157        let js_listener = js_sys::Function::new_with_args("data, error", &js_listener_func_body);
1158
1159        let client = create_client_no_secret();
1160
1161        // Add a Rust listener to verify the functionality
1162        let rust_listener: EventListener = Arc::new(move |result| {
1163            let mut called_guard = flag_clone.lock().unwrap();
1164            *called_guard = true;
1165            if let Ok(config_value) = result {
1166                let mut data_guard = data_clone.lock().unwrap();
1167                *data_guard = Some(config_value);
1168            }
1169        });
1170
1171        client.add_listener("application", rust_listener).await;
1172
1173        // Add JS Listener
1174        client.add_listener_wasm("application", js_listener).await;
1175
1176        let cache = client.cache("application").await;
1177
1178        // Trigger Refresh
1179        match cache.refresh().await {
1180            Ok(_) => web_sys::console::log_1(&"WASM Test: Refresh successful".into()), // web_sys::console for logging
1181            Err(e) => panic!("WASM Test: Cache refresh failed: {:?}", e),
1182        }
1183
1184        // Verify Listener Was Called using our Rust listener
1185        let called = *listener_called_flag.lock().unwrap();
1186        assert!(called, "Listener was not called.");
1187
1188        // Check if config data was received
1189        let config_data_guard = received_config_data.lock().unwrap();
1190        assert!(
1191            config_data_guard.is_some(),
1192            "Listener did not receive config data."
1193        );
1194
1195        // Verify the content
1196        if let Some(value) = config_data_guard.as_ref() {
1197            match value {
1198                namespace::Namespace::Properties(properties) => {
1199                    assert_eq!(
1200                        properties.get_string("stringValue").await,
1201                        Some("string value".to_string())
1202                    );
1203                }
1204                _ => panic!("Expected Properties namespace"),
1205            }
1206        }
1207    }
1208}