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