1use js_sys::Reflect;
7use wasm_bindgen::prelude::*;
8
9use libfw_core::{CHUNK_SIZE, DEFAULT_CONCURRENCY, MAX_RETRIES};
10
11pub const DEFAULT_BASE_RETRY_MS: u32 = 500;
13pub const DEFAULT_MAX_RETRY_MS: u32 = 30_000;
15pub const DEFAULT_TIMEOUT_MS: u32 = 60_000;
17
18#[derive(Debug, Clone)]
20pub struct ClientConfig {
21 pub concurrency: usize,
23 pub compress: bool,
25 pub chunk_size: u64,
27 pub max_retries: u32,
29 pub base_retry_delay_ms: u32,
31 pub max_retry_delay_ms: u32,
33 pub timeout_ms: u32,
35}
36
37impl Default for ClientConfig {
38 fn default() -> Self {
39 ClientConfig {
40 concurrency: DEFAULT_CONCURRENCY,
41 compress: true,
42 chunk_size: CHUNK_SIZE,
43 max_retries: MAX_RETRIES,
44 base_retry_delay_ms: DEFAULT_BASE_RETRY_MS,
45 max_retry_delay_ms: DEFAULT_MAX_RETRY_MS,
46 timeout_ms: DEFAULT_TIMEOUT_MS,
47 }
48 }
49}
50
51fn opt_u64(obj: &JsValue, key: &str) -> Option<u64> {
53 Reflect::get(obj, &JsValue::from_str(key))
54 .ok()
55 .and_then(|v| v.as_f64())
56 .map(|f| f as u64)
57}
58
59fn opt_usize(obj: &JsValue, key: &str) -> Option<usize> {
61 Reflect::get(obj, &JsValue::from_str(key))
62 .ok()
63 .and_then(|v| v.as_f64())
64 .map(|f| f as usize)
65}
66
67fn opt_u32(obj: &JsValue, key: &str) -> Option<u32> {
69 Reflect::get(obj, &JsValue::from_str(key))
70 .ok()
71 .and_then(|v| v.as_f64())
72 .map(|f| f as u32)
73}
74
75fn opt_bool(obj: &JsValue, key: &str) -> Option<bool> {
77 Reflect::get(obj, &JsValue::from_str(key))
78 .ok()
79 .and_then(|v| v.as_bool())
80}
81
82impl ClientConfig {
83 pub fn from_js(opts: &JsValue) -> ClientConfig {
89 let mut cfg = ClientConfig::default();
90 if !opts.is_object() {
91 return cfg;
92 }
93 if let Some(v) = opt_usize(opts, "concurrency") {
94 if v > 0 {
95 cfg.concurrency = v;
96 }
97 }
98 if let Some(v) = opt_bool(opts, "compress") {
99 cfg.compress = v;
100 }
101 if let Some(v) = opt_u64(opts, "chunkSize") {
102 if v > 0 {
103 cfg.chunk_size = v;
104 }
105 }
106 if let Some(v) = opt_u32(opts, "maxRetries") {
107 cfg.max_retries = v;
108 }
109 if let Some(v) = opt_u32(opts, "baseRetryDelayMs") {
110 cfg.base_retry_delay_ms = v;
111 }
112 if let Some(v) = opt_u32(opts, "maxRetryDelayMs") {
113 cfg.max_retry_delay_ms = v;
114 }
115 if let Some(v) = opt_u32(opts, "timeoutMs") {
116 cfg.timeout_ms = v;
117 }
118 cfg
119 }
120
121 pub fn backoff_ms(&self, attempt: u32) -> u32 {
126 backoff_ms(attempt, self.base_retry_delay_ms, self.max_retry_delay_ms)
127 }
128}
129
130pub fn backoff_ms(attempt: u32, base: u32, max: u32) -> u32 {
132 if attempt == 0 {
133 return base.min(max);
134 }
135 let shift = attempt.min(31);
136 let doubled = (base as u64) << shift;
137 doubled.min(max as u64) as u32
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn defaults_are_sane() {
146 let cfg = ClientConfig::default();
147 assert_eq!(cfg.concurrency, 4);
148 assert_eq!(cfg.chunk_size, CHUNK_SIZE);
149 assert_eq!(cfg.max_retries, MAX_RETRIES);
150 assert!(cfg.compress);
151 }
152
153 #[test]
154 fn backoff_grows_exponentially_then_clamps() {
155 let cfg = ClientConfig {
156 base_retry_delay_ms: 100,
157 max_retry_delay_ms: 1000,
158 ..ClientConfig::default()
159 };
160 assert_eq!(cfg.backoff_ms(0), 100);
161 assert_eq!(cfg.backoff_ms(1), 200);
162 assert_eq!(cfg.backoff_ms(2), 400);
163 assert_eq!(cfg.backoff_ms(3), 800);
164 assert_eq!(cfg.backoff_ms(4), 1000); assert_eq!(cfg.backoff_ms(99), 1000);
166 }
167
168 #[test]
169 fn pure_backoff_saturates() {
170 assert_eq!(backoff_ms(0, 500, 30_000), 500);
171 assert_eq!(backoff_ms(6, 500, 30_000), 30_000);
172 }
173
174 #[test]
175 #[cfg(target_arch = "wasm32")]
176 fn parses_js_options() {
177 let obj = js_sys::Object::new();
178 js_sys::Reflect::set(&obj, &JsValue::from_str("concurrency"), &JsValue::from_f64(8.0))
179 .unwrap();
180 js_sys::Reflect::set(&obj, &JsValue::from_str("compress"), &JsValue::FALSE).unwrap();
181 js_sys::Reflect::set(
182 &obj,
183 &JsValue::from_str("chunkSize"),
184 &JsValue::from_f64(1024.0),
185 )
186 .unwrap();
187 let cfg = ClientConfig::from_js(&obj);
188 assert_eq!(cfg.concurrency, 8);
189 assert!(!cfg.compress);
190 assert_eq!(cfg.chunk_size, 1024);
191 }
192
193 #[test]
194 #[cfg(target_arch = "wasm32")]
195 fn ignores_non_object_options() {
196 let cfg = ClientConfig::from_js(&JsValue::NULL);
197 assert_eq!(cfg.concurrency, DEFAULT_CONCURRENCY);
198 let cfg = ClientConfig::from_js(&JsValue::UNDEFINED);
199 assert_eq!(cfg.chunk_size, CHUNK_SIZE);
200 }
201}