Skip to main content

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