apollo_rust_client/
lib.rs

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