1use crate::namespace::Namespace;
57use async_std::sync::RwLock;
58use cache::Cache;
59use client_config::ClientConfig;
60use log::{error, trace};
61use std::{collections::HashMap, sync::Arc};
62use wasm_bindgen::prelude::wasm_bindgen;
63
64#[cfg(all(feature = "native-tls", feature = "rustls", not(target_arch = "wasm32")))]
65compile_error!(
66 "Features 'native-tls' and 'rustls' are mutually exclusive on non-WASM targets. \
67 Please disable default features and enable only one."
68);
69
70#[cfg(all(feature = "rustls", target_arch = "wasm32"))]
71compile_error!("Feature 'rustls' is not supported on WASM targets. Only native-tls (browser) is supported.");
72
73cfg_if::cfg_if! {
74 if #[cfg(not(target_arch = "wasm32"))] {
75 use async_std::task::spawn as spawn;
76 }
77}
78
79mod cache;
80
81pub mod client_config;
82pub mod namespace;
83
84#[derive(Debug, thiserror::Error)]
134pub enum Error {
135 #[error("Client is already running")]
141 AlreadyRunning,
142
143 #[error("Namespace error: {0}")]
148 Namespace(#[from] namespace::Error),
149
150 #[error("Cache error: {0}")]
155 Cache(#[from] cache::Error),
156}
157
158impl From<Error> for wasm_bindgen::JsValue {
159 fn from(error: Error) -> Self {
160 error.to_string().into()
161 }
162}
163
164cfg_if::cfg_if! {
170 if #[cfg(target_arch = "wasm32")] {
171 pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>)>;
175 } else {
176 pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>) + Send + Sync>;
184 }
185}
186
187#[wasm_bindgen]
235pub struct Client {
236 config: ClientConfig,
241
242 namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
248
249 handle: Option<async_std::task::JoinHandle<()>>,
255
256 running: Arc<RwLock<bool>>,
261}
262
263impl Client {
264 pub(crate) async fn cache(&self, namespace: &str) -> Arc<Cache> {
274 let mut namespaces = self.namespaces.write().await;
275 let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
276 trace!("Cache miss, creating cache for namespace {namespace}");
277 Arc::new(Cache::new(self.config.clone(), namespace))
278 });
279 cache.clone()
280 }
281
282 pub async fn add_listener(&self, namespace: &str, listener: EventListener) {
283 let mut namespaces = self.namespaces.write().await;
284 let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
285 trace!("Cache miss, creating cache for namespace {namespace}");
286 Arc::new(Cache::new(self.config.clone(), namespace))
287 });
288 cache.add_listener(listener).await;
289 }
290
291 pub async fn namespace(&self, namespace: &str) -> Result<namespace::Namespace, Error> {
356 let cache = self.cache(namespace).await;
357 let value = cache.get_value().await?;
358 Ok(namespace::get_namespace(namespace, value)?)
359 }
360
361 pub async fn start(&mut self) -> Result<(), Error> {
384 let mut running = self.running.write().await;
385 if *running {
386 return Err(Error::AlreadyRunning);
387 }
388
389 *running = true;
390
391 cfg_if::cfg_if! {
392 if #[cfg(target_arch = "wasm32")] {
393 self.handle = None;
394 } else {
395 let running = self.running.clone();
396 let namespaces = self.namespaces.clone();
397 let handle = spawn(async move {
399 loop {
400 let running = running.read().await;
401 if !*running {
402 break;
403 }
404
405 let cache_refs: Vec<_> = {
407 let namespaces = namespaces.read().await;
408 namespaces.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
409 }; for (namespace, cache) in cache_refs {
413 if let Err(err) = cache.refresh().await {
414 error!("Failed to refresh cache for namespace {namespace}: {err:?}");
415 } else {
416 log::debug!("Successfully refreshed cache for namespace {namespace}");
417 }
418 }
419
420 async_std::task::sleep(std::time::Duration::from_secs(30)).await;
422 }
423 });
424 self.handle = Some(handle);
425 }
426 }
427
428 Ok(())
429 }
430
431 pub async fn stop(&mut self) {
441 let mut running = self.running.write().await;
442 *running = false;
443
444 cfg_if::cfg_if! {
445 if #[cfg(not(target_arch = "wasm32"))] {
446 if let Some(handle) = self.handle.take() {
447 handle.cancel().await;
448 }
449 }
450 }
451 }
452
453 pub async fn preload(&self, namespaces: &[impl AsRef<str>]) -> Result<(), Error> {
505 #[cfg(not(target_arch = "wasm32"))]
506 let mut tasks = Vec::new();
507
508 #[cfg(target_arch = "wasm32")]
509 {
510 for namespace in namespaces {
511 let cache = self.cache(namespace.as_ref()).await;
512 cache.get_value().await?;
513 }
514 }
515
516 #[cfg(not(target_arch = "wasm32"))]
517 {
518 for namespace in namespaces {
519 let cache = self.cache(namespace.as_ref()).await;
520 let task = tokio::spawn(async move { cache.get_value().await });
521 tasks.push(task);
522 }
523
524 for task in tasks {
526 let result = task.await.map_err(|e| {
527 Error::Cache(cache::Error::Io(std::io::Error::other(format!(
528 "Preload task failed: {e}"
529 ))))
530 })?;
531 result?;
532 }
533 }
534
535 Ok(())
536 }
537}
538
539#[wasm_bindgen]
540impl Client {
541 #[wasm_bindgen(constructor)]
551 #[must_use]
552 pub fn new(config: ClientConfig) -> Self {
553 Self {
554 config,
555 namespaces: Arc::new(RwLock::new(HashMap::new())),
556 handle: None,
557 running: Arc::new(RwLock::new(false)),
558 }
559 }
560
561 #[cfg(target_arch = "wasm32")]
591 #[wasm_bindgen(js_name = "add_listener")]
592 pub async fn add_listener_wasm(&self, namespace: &str, js_listener: js_sys::Function) {
593 let js_listener_clone = js_listener.clone();
594
595 let event_listener: EventListener = Arc::new(move |result: Result<Namespace, Error>| {
596 let err_js_val: wasm_bindgen::JsValue;
597 let data_js_val: wasm_bindgen::JsValue;
598
599 match result {
600 Ok(value) => {
601 data_js_val = value.into();
602 err_js_val = wasm_bindgen::JsValue::UNDEFINED;
603 }
604 Err(cache_error) => {
605 err_js_val = cache_error.into();
606 data_js_val = wasm_bindgen::JsValue::UNDEFINED;
607 }
608 };
609
610 match js_listener_clone.call2(
612 &wasm_bindgen::JsValue::UNDEFINED,
613 &data_js_val,
614 &err_js_val,
615 ) {
616 Ok(_) => {
617 }
619 Err(e) => {
620 log::error!("JavaScript listener threw an error: {:?}", e);
622 }
623 }
624 });
625
626 self.add_listener(namespace, event_listener).await; }
628
629 #[cfg(target_arch = "wasm32")]
630 #[wasm_bindgen(js_name = "namespace")]
631 pub async fn namespace_wasm(&self, namespace: &str) -> Result<wasm_bindgen::JsValue, Error> {
632 let cache = self.cache(namespace).await;
633 let value = cache.get_value().await?;
634 Ok(namespace::get_namespace(namespace, value)?.into())
635 }
636}
637
638#[cfg(test)]
639pub(crate) struct TempDir {
640 path: std::path::PathBuf,
641}
642
643#[cfg(test)]
644impl TempDir {
645 pub(crate) fn new(name: &str) -> Self {
646 let path = std::env::temp_dir().join(name);
647 let _ = std::fs::create_dir_all(&path);
649 Self { path }
650 }
651
652 pub(crate) fn path(&self) -> &std::path::Path {
653 &self.path
654 }
655}
656
657#[cfg(test)]
658impl Drop for TempDir {
659 fn drop(&mut self) {
660 let _ = std::fs::remove_dir_all(&self.path);
662 }
663}
664
665#[cfg(test)]
666pub(crate) fn setup() {
667 cfg_if::cfg_if! {
668 if #[cfg(target_arch = "wasm32")] {
669 let _ = wasm_logger::init(wasm_logger::Config::default());
670 console_error_panic_hook::set_once();
671 } else {
672 let _ = env_logger::builder().is_test(true).try_init();
673 }
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 cfg_if::cfg_if! {
682 if #[cfg(target_arch = "wasm32")] {
683 use std::sync::Mutex;
684 } else {
685 use async_std::sync::Mutex;
686 use async_std::task::block_on;
687 }
688 }
689
690 #[cfg(not(target_arch = "wasm32"))]
691 pub(crate) static CLIENT_NO_SECRET: std::sync::LazyLock<Client> =
692 std::sync::LazyLock::new(|| {
693 let config = ClientConfig {
694 app_id: String::from("101010101"),
695 cluster: String::from("default"),
696 config_server: String::from("http://81.68.181.139:8080"),
697 label: None,
698 secret: None,
699 cache_dir: Some(String::from("/tmp/apollo")),
700 ip: None,
701 allow_insecure_https: None,
702 #[cfg(not(target_arch = "wasm32"))]
703 cache_ttl: None,
704 };
705 Client::new(config)
706 });
707
708 #[cfg(not(target_arch = "wasm32"))]
709 pub(crate) static CLIENT_WITH_SECRET: std::sync::LazyLock<Client> =
710 std::sync::LazyLock::new(|| {
711 let config = ClientConfig {
712 app_id: String::from("101010102"),
713 cluster: String::from("default"),
714 config_server: String::from("http://81.68.181.139:8080"),
715 label: None,
716 secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
717 cache_dir: Some(String::from("/tmp/apollo")),
718 ip: None,
719 allow_insecure_https: None,
720 #[cfg(not(target_arch = "wasm32"))]
721 cache_ttl: None,
722 };
723 Client::new(config)
724 });
725
726 #[cfg(not(target_arch = "wasm32"))]
727 pub(crate) static CLIENT_WITH_GRAYSCALE_IP: std::sync::LazyLock<Client> =
728 std::sync::LazyLock::new(|| {
729 let config = ClientConfig {
730 app_id: String::from("101010101"),
731 cluster: String::from("default"),
732 config_server: String::from("http://81.68.181.139:8080"),
733 label: None,
734 secret: None,
735 cache_dir: Some(String::from("/tmp/apollo")),
736 ip: Some(String::from("1.2.3.4")),
737 allow_insecure_https: None,
738 #[cfg(not(target_arch = "wasm32"))]
739 cache_ttl: None,
740 };
741 Client::new(config)
742 });
743
744 #[cfg(not(target_arch = "wasm32"))]
745 pub(crate) static CLIENT_WITH_GRAYSCALE_LABEL: std::sync::LazyLock<Client> =
746 std::sync::LazyLock::new(|| {
747 let config = ClientConfig {
748 app_id: String::from("101010101"),
749 cluster: String::from("default"),
750 config_server: String::from("http://81.68.181.139:8080"),
751 label: Some(String::from("GrayScale")),
752 secret: None,
753 cache_dir: Some(String::from("/tmp/apollo")),
754 ip: None,
755 allow_insecure_https: None,
756 #[cfg(not(target_arch = "wasm32"))]
757 cache_ttl: None,
758 };
759 Client::new(config)
760 });
761
762 #[cfg(not(target_arch = "wasm32"))]
763 #[tokio::test]
764 async fn test_missing_value() {
765 setup();
766 let namespace::Namespace::Properties(properties) =
767 CLIENT_NO_SECRET.namespace("application").await.unwrap()
768 else {
769 panic!("Expected Properties namespace");
770 };
771
772 assert_eq!(properties.get_property::<String>("missingValue"), None);
773 }
774
775 #[cfg(target_arch = "wasm32")]
776 #[wasm_bindgen_test::wasm_bindgen_test]
777 #[allow(dead_code)]
778 async fn test_missing_value_wasm() {
779 setup();
780 let client = create_client_no_secret();
781 let namespace = client.namespace("application").await;
782 match namespace {
783 Ok(namespace) => match namespace {
784 namespace::Namespace::Properties(properties) => {
785 assert_eq!(properties.get_string("missingValue"), None);
786 }
787 _ => panic!("Expected Properties namespace"),
788 },
789 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
790 }
791 }
792
793 #[cfg(not(target_arch = "wasm32"))]
794 #[tokio::test]
795 async fn test_string_value() {
796 setup();
797 let namespace::Namespace::Properties(properties) =
798 CLIENT_NO_SECRET.namespace("application").await.unwrap()
799 else {
800 panic!("Expected Properties namespace");
801 };
802
803 assert_eq!(
804 properties.get_property::<String>("stringValue"),
805 Some("string value".to_string())
806 );
807 }
808
809 #[cfg(target_arch = "wasm32")]
810 #[wasm_bindgen_test::wasm_bindgen_test]
811 #[allow(dead_code)]
812 async fn test_string_value_wasm() {
813 setup();
814 let client = create_client_no_secret();
815 let namespace = client.namespace("application").await;
816 match namespace {
817 Ok(namespace) => match namespace {
818 namespace::Namespace::Properties(properties) => {
819 assert_eq!(
820 properties.get_string("stringValue"),
821 Some("string value".to_string())
822 );
823 }
824 _ => panic!("Expected Properties namespace"),
825 },
826 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
827 }
828 }
829
830 #[cfg(not(target_arch = "wasm32"))]
831 #[tokio::test]
832 async fn test_string_value_with_secret() {
833 setup();
834 let namespace::Namespace::Properties(properties) =
835 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
836 else {
837 panic!("Expected Properties namespace");
838 };
839 assert_eq!(
840 properties.get_property::<String>("stringValue"),
841 Some("string value".to_string())
842 );
843 }
844
845 #[cfg(target_arch = "wasm32")]
846 #[wasm_bindgen_test::wasm_bindgen_test]
847 #[allow(dead_code)]
848 async fn test_string_value_with_secret_wasm() {
849 setup();
850 let client = create_client_with_secret();
851 let namespace = client.namespace("application").await;
852 match namespace {
853 Ok(namespace) => match namespace {
854 namespace::Namespace::Properties(properties) => {
855 assert_eq!(
856 properties.get_string("stringValue"),
857 Some("string value".to_string())
858 );
859 }
860 _ => panic!("Expected Properties namespace"),
861 },
862 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
863 }
864 }
865
866 #[cfg(not(target_arch = "wasm32"))]
867 #[tokio::test]
868 async fn test_int_value() {
869 setup();
870 let namespace::Namespace::Properties(properties) =
871 CLIENT_NO_SECRET.namespace("application").await.unwrap()
872 else {
873 panic!("Expected Properties namespace");
874 };
875 assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
876 }
877
878 #[cfg(target_arch = "wasm32")]
879 #[wasm_bindgen_test::wasm_bindgen_test]
880 #[allow(dead_code)]
881 async fn test_int_value_wasm() {
882 setup();
883 let client = create_client_no_secret();
884 let namespace = client.namespace("application").await;
885 match namespace {
886 Ok(namespace) => match namespace {
887 namespace::Namespace::Properties(properties) => {
888 assert_eq!(properties.get_int("intValue"), Some(42));
889 }
890 _ => panic!("Expected Properties namespace"),
891 },
892 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
893 }
894 }
895
896 #[cfg(not(target_arch = "wasm32"))]
897 #[tokio::test]
898 async fn test_int_value_with_secret() {
899 setup();
900 let namespace::Namespace::Properties(properties) =
901 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
902 else {
903 panic!("Expected Properties namespace");
904 };
905 assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
906 }
907
908 #[cfg(target_arch = "wasm32")]
909 #[wasm_bindgen_test::wasm_bindgen_test]
910 #[allow(dead_code)]
911 async fn test_int_value_with_secret_wasm() {
912 setup();
913 let client = create_client_with_secret();
914 let namespace = client.namespace("application").await;
915 match namespace {
916 Ok(namespace) => match namespace {
917 namespace::Namespace::Properties(properties) => {
918 assert_eq!(properties.get_int("intValue"), Some(42));
919 }
920 _ => panic!("Expected Properties namespace"),
921 },
922 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
923 }
924 }
925
926 #[cfg(not(target_arch = "wasm32"))]
927 #[tokio::test]
928 async fn test_float_value() {
929 setup();
930 let namespace::Namespace::Properties(properties) =
931 CLIENT_NO_SECRET.namespace("application").await.unwrap()
932 else {
933 panic!("Expected Properties namespace");
934 };
935 assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
936 }
937
938 #[cfg(target_arch = "wasm32")]
939 #[wasm_bindgen_test::wasm_bindgen_test]
940 #[allow(dead_code)]
941 async fn test_float_value_wasm() {
942 setup();
943 let client = create_client_no_secret();
944 let namespace = client.namespace("application").await;
945 match namespace {
946 Ok(namespace) => match namespace {
947 namespace::Namespace::Properties(properties) => {
948 assert_eq!(properties.get_float("floatValue"), Some(4.20));
949 }
950 _ => panic!("Expected Properties namespace"),
951 },
952 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
953 }
954 }
955
956 #[cfg(not(target_arch = "wasm32"))]
957 #[tokio::test]
958 async fn test_float_value_with_secret() {
959 setup();
960 let namespace::Namespace::Properties(properties) =
961 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
962 else {
963 panic!("Expected Properties namespace");
964 };
965 assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
966 }
967
968 #[cfg(target_arch = "wasm32")]
969 #[wasm_bindgen_test::wasm_bindgen_test]
970 #[allow(dead_code)]
971 async fn test_float_value_with_secret_wasm() {
972 setup();
973 let client = create_client_with_secret();
974 let namespace = client.namespace("application").await;
975 match namespace {
976 Ok(namespace) => match namespace {
977 namespace::Namespace::Properties(properties) => {
978 assert_eq!(properties.get_float("floatValue"), Some(4.20));
979 }
980 _ => panic!("Expected Properties namespace"),
981 },
982 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
983 }
984 }
985
986 #[cfg(not(target_arch = "wasm32"))]
987 #[tokio::test]
988 async fn test_bool_value() {
989 setup();
990 let namespace::Namespace::Properties(properties) =
991 CLIENT_NO_SECRET.namespace("application").await.unwrap()
992 else {
993 panic!("Expected Properties namespace");
994 };
995 assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
996 }
997
998 #[cfg(target_arch = "wasm32")]
999 #[wasm_bindgen_test::wasm_bindgen_test]
1000 #[allow(dead_code)]
1001 async fn test_bool_value_wasm() {
1002 setup();
1003 let client = create_client_no_secret();
1004 let namespace = client.namespace("application").await;
1005 match namespace {
1006 Ok(namespace) => match namespace {
1007 namespace::Namespace::Properties(properties) => {
1008 assert_eq!(properties.get_bool("boolValue"), Some(false));
1009 }
1010 _ => panic!("Expected Properties namespace"),
1011 },
1012 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1013 }
1014 }
1015
1016 #[cfg(not(target_arch = "wasm32"))]
1017 #[tokio::test]
1018 async fn test_bool_value_with_secret() {
1019 setup();
1020 let namespace::Namespace::Properties(properties) =
1021 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
1022 else {
1023 panic!("Expected Properties namespace");
1024 };
1025 assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
1026 }
1027
1028 #[cfg(target_arch = "wasm32")]
1029 #[wasm_bindgen_test::wasm_bindgen_test]
1030 #[allow(dead_code)]
1031 async fn test_bool_value_with_secret_wasm() {
1032 setup();
1033 let client = create_client_with_secret();
1034 let namespace = client.namespace("application").await;
1035 match namespace {
1036 Ok(namespace) => match namespace {
1037 namespace::Namespace::Properties(properties) => {
1038 assert_eq!(properties.get_bool("boolValue"), Some(false));
1039 }
1040 _ => panic!("Expected Properties namespace"),
1041 },
1042 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1043 }
1044 }
1045
1046 #[cfg(not(target_arch = "wasm32"))]
1047 #[tokio::test]
1048 async fn test_bool_value_with_grayscale_ip() {
1049 setup();
1050 let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_IP
1051 .namespace("application")
1052 .await
1053 .unwrap()
1054 else {
1055 panic!("Expected Properties namespace");
1056 };
1057 assert_eq!(
1058 properties.get_property::<bool>("grayScaleValue"),
1059 Some(true)
1060 );
1061 let namespace::Namespace::Properties(properties) =
1062 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1063 else {
1064 panic!("Expected Properties namespace");
1065 };
1066 assert_eq!(
1067 properties.get_property::<bool>("grayScaleValue"),
1068 Some(false)
1069 );
1070 }
1071
1072 #[cfg(target_arch = "wasm32")]
1073 #[wasm_bindgen_test::wasm_bindgen_test]
1074 #[allow(dead_code)]
1075 async fn test_bool_value_with_grayscale_ip_wasm() {
1076 setup();
1077 let client1 = create_client_with_grayscale_ip();
1078 let namespace = client1.namespace("application").await;
1079 match namespace {
1080 Ok(namespace) => match namespace {
1081 namespace::Namespace::Properties(properties) => {
1082 assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1083 }
1084 _ => panic!("Expected Properties namespace"),
1085 },
1086 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1087 }
1088
1089 let client2 = create_client_no_secret();
1090 let namespace = client2.namespace("application").await;
1091 match namespace {
1092 Ok(namespace) => match namespace {
1093 namespace::Namespace::Properties(properties) => {
1094 assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1095 }
1096 _ => panic!("Expected Properties namespace"),
1097 },
1098 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1099 }
1100 }
1101
1102 #[cfg(not(target_arch = "wasm32"))]
1103 #[tokio::test]
1104 async fn test_bool_value_with_grayscale_label() {
1105 setup();
1106 let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_LABEL
1107 .namespace("application")
1108 .await
1109 .unwrap()
1110 else {
1111 panic!("Expected Properties namespace");
1112 };
1113 assert_eq!(
1114 properties.get_property::<bool>("grayScaleValue"),
1115 Some(true)
1116 );
1117 let namespace::Namespace::Properties(properties) =
1118 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1119 else {
1120 panic!("Expected Properties namespace");
1121 };
1122 assert_eq!(
1123 properties.get_property::<bool>("grayScaleValue"),
1124 Some(false)
1125 );
1126 }
1127
1128 #[cfg(target_arch = "wasm32")]
1129 #[wasm_bindgen_test::wasm_bindgen_test]
1130 #[allow(dead_code)]
1131 async fn test_bool_value_with_grayscale_label_wasm() {
1132 setup();
1133 let client1 = create_client_with_grayscale_label();
1134 let namespace = client1.namespace("application").await;
1135 match namespace {
1136 Ok(namespace) => match namespace {
1137 namespace::Namespace::Properties(properties) => {
1138 assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1139 }
1140 _ => panic!("Expected Properties namespace"),
1141 },
1142 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1143 }
1144
1145 let client2 = create_client_no_secret();
1146 let namespace = client2.namespace("application").await;
1147 match namespace {
1148 Ok(namespace) => match namespace {
1149 namespace::Namespace::Properties(properties) => {
1150 assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1151 }
1152 _ => panic!("Expected Properties namespace"),
1153 },
1154 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1155 }
1156 }
1157
1158 #[cfg(target_arch = "wasm32")]
1159 fn create_client_no_secret() -> Client {
1160 let config = ClientConfig {
1161 app_id: String::from("101010101"),
1162 cluster: String::from("default"),
1163 config_server: String::from("http://81.68.181.139:8080"),
1164 label: None,
1165 secret: None,
1166 cache_dir: None,
1167 ip: None,
1168 allow_insecure_https: None,
1169 };
1170 Client::new(config)
1171 }
1172
1173 #[cfg(target_arch = "wasm32")]
1174 fn create_client_with_secret() -> Client {
1175 let config = ClientConfig {
1176 app_id: String::from("101010102"),
1177 cluster: String::from("default"),
1178 config_server: String::from("http://81.68.181.139:8080"),
1179 label: None,
1180 secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
1181 cache_dir: None,
1182 ip: None,
1183 allow_insecure_https: None,
1184 };
1185 Client::new(config)
1186 }
1187
1188 #[cfg(target_arch = "wasm32")]
1189 fn create_client_with_grayscale_ip() -> Client {
1190 let config = ClientConfig {
1191 app_id: String::from("101010101"),
1192 cluster: String::from("default"),
1193 config_server: String::from("http://81.68.181.139:8080"),
1194 label: None,
1195 secret: None,
1196 cache_dir: None,
1197 ip: Some(String::from("1.2.3.4")),
1198 allow_insecure_https: None,
1199 };
1200 Client::new(config)
1201 }
1202
1203 #[cfg(target_arch = "wasm32")]
1204 fn create_client_with_grayscale_label() -> Client {
1205 let config = ClientConfig {
1206 app_id: String::from("101010101"),
1207 cluster: String::from("default"),
1208 config_server: String::from("http://81.68.181.139:8080"),
1209 label: Some(String::from("GrayScale")),
1210 secret: None,
1211 cache_dir: None,
1212 ip: None,
1213 allow_insecure_https: None,
1214 };
1215 Client::new(config)
1216 }
1217
1218 #[cfg(not(target_arch = "wasm32"))]
1219 #[tokio::test] async fn test_add_listener_and_notify_on_refresh() {
1221 setup();
1222
1223 let listener_called_flag = Arc::new(Mutex::new(false));
1225 let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1226
1227 let config = ClientConfig {
1230 config_server: "http://81.68.181.139:8080".to_string(), app_id: "101010101".to_string(), cluster: "default".to_string(),
1233 cache_dir: Some(String::from("/tmp/apollo")), secret: None,
1235 label: None,
1236 ip: None,
1237 allow_insecure_https: None,
1238 #[cfg(not(target_arch = "wasm32"))]
1239 cache_ttl: None,
1240 };
1242
1243 let client = Client::new(config);
1244
1245 let flag_clone = listener_called_flag.clone();
1246 let data_clone = received_config_data.clone();
1247
1248 let listener: EventListener = Arc::new(move |result| {
1249 let mut called_guard = block_on(flag_clone.lock());
1250 *called_guard = true;
1251 if let Ok(config_value) = result {
1252 match config_value {
1253 Namespace::Properties(_) => {
1254 let mut data_guard = block_on(data_clone.lock());
1255 *data_guard = Some(config_value.clone());
1256 }
1257 _ => {
1258 panic!("Expected Properties namespace, got {config_value:?}");
1259 }
1260 }
1261 }
1262 });
1265
1266 client.add_listener("application", listener).await;
1267
1268 let cache = client.cache("application").await;
1269
1270 match cache.refresh().await {
1273 Ok(()) => log::debug!("Refresh successful for test_add_listener_and_notify_on_refresh"),
1274 Err(e) => panic!("Cache refresh failed during test: {e:?}"),
1275 }
1276
1277 cfg_if::cfg_if! {
1279 if #[cfg(target_arch = "wasm32")] {
1280 } else {
1282 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1284 }
1285 }
1286
1287 let called = *listener_called_flag.lock().await;
1289 assert!(called, "Listener was not called.");
1290
1291 let config_data_guard = received_config_data.lock().await;
1293 assert!(
1294 config_data_guard.is_some(),
1295 "Listener did not receive config data."
1296 );
1297
1298 if let Some(value) = config_data_guard.as_ref() {
1302 match value {
1303 Namespace::Properties(properties) => {
1304 assert_eq!(
1305 properties.get_string("stringValue"),
1306 Some(String::from("string value")),
1307 "Received config data does not match expected content for stringValue."
1308 );
1309 }
1310 _ => {
1311 panic!("Expected Properties namespace, got {value:?}");
1312 }
1313 }
1314 }
1315 }
1316
1317 #[cfg(target_arch = "wasm32")]
1318 #[wasm_bindgen_test::wasm_bindgen_test]
1319 async fn test_add_listener_wasm_and_notify() {
1320 setup(); let listener_called_flag = Arc::new(Mutex::new(false));
1324 let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1325
1326 let flag_clone = listener_called_flag.clone();
1327 let data_clone = received_config_data.clone();
1328
1329 let js_listener_func_body = format!(
1331 r#"
1332 (data, error) => {{
1333 // We can't use window in Node.js, so we'll use a different approach
1334 // The Rust closure will handle the verification
1335 console.log('JS Listener called with error:', error);
1336 console.log('JS Listener called with data:', data);
1337 }}
1338 "#
1339 );
1340
1341 let js_listener = js_sys::Function::new_with_args("data, error", &js_listener_func_body);
1342
1343 let client = create_client_no_secret();
1344
1345 let rust_listener: EventListener = Arc::new(move |result| {
1347 let mut called_guard = flag_clone.lock().unwrap();
1348 *called_guard = true;
1349 if let Ok(config_value) = result {
1350 let mut data_guard = data_clone.lock().unwrap();
1351 *data_guard = Some(config_value);
1352 }
1353 });
1354
1355 client.add_listener("application", rust_listener).await;
1356
1357 client.add_listener_wasm("application", js_listener).await;
1359
1360 let cache = client.cache("application").await;
1361
1362 match cache.refresh().await {
1364 Ok(_) => web_sys::console::log_1(&"WASM Test: Refresh successful".into()), Err(e) => panic!("WASM Test: Cache refresh failed: {:?}", e),
1366 }
1367
1368 cfg_if::cfg_if! {
1370 if #[cfg(target_arch = "wasm32")] {
1371 } else {
1373 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1375 }
1376 }
1377
1378 let called = *listener_called_flag.lock().unwrap();
1380 assert!(called, "Listener was not called.");
1381
1382 let config_data_guard = received_config_data.lock().unwrap();
1384 assert!(
1385 config_data_guard.is_some(),
1386 "Listener did not receive config data."
1387 );
1388
1389 if let Some(value) = config_data_guard.as_ref() {
1391 match value {
1392 namespace::Namespace::Properties(properties) => {
1393 assert_eq!(
1394 properties.get_string("stringValue"),
1395 Some("string value".to_string())
1396 );
1397 }
1398 _ => panic!("Expected Properties namespace"),
1399 }
1400 }
1401 }
1402
1403 #[cfg(not(target_arch = "wasm32"))]
1404 #[tokio::test]
1405 async fn test_concurrent_namespace_hang_repro() {
1406 use async_std::task;
1407 setup();
1408
1409 let temp_dir = TempDir::new("apollo_hang_test");
1410
1411 let config = ClientConfig {
1412 app_id: String::from("101010101"),
1413 cluster: String::from("default"),
1414 config_server: String::from("http://81.68.181.139:8080"),
1415 secret: None,
1416 cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
1417 label: None,
1418 ip: None,
1419 allow_insecure_https: None,
1420 cache_ttl: None,
1421 };
1422
1423 let client = Arc::new(Client::new(config));
1424 let client_in_listener = client.clone();
1425
1426 let listener_triggered = Arc::new(Mutex::new(false));
1427 let listener_triggered_in_listener = listener_triggered.clone();
1428
1429 let listener: EventListener = Arc::new(move |_| {
1430 let client_in_listener = client_in_listener.clone();
1431 let listener_triggered_in_listener = listener_triggered_in_listener.clone();
1432 task::spawn(async move {
1433 let mut triggered = listener_triggered_in_listener.lock().await;
1434 if *triggered {
1435 return;
1437 }
1438 *triggered = true;
1439 drop(triggered);
1440
1441 let _ = client_in_listener.namespace("application").await;
1443 });
1444 });
1445
1446 client.add_listener("application", listener).await;
1447
1448 let test_body = async {
1449 let _ = client.namespace("application").await;
1450 };
1451
1452 let res = tokio::time::timeout(std::time::Duration::from_secs(10), test_body).await;
1454 assert!(res.is_ok(), "Test timed out, which indicates a deadlock.");
1455 }
1456}