use js_sys::Reflect;
use wasm_bindgen::prelude::*;
use libfw_core::{
CHUNK_SIZE, DEFAULT_CONCURRENCY, DEFAULT_DOWNLOAD_CHUNK_SIZE, DEFAULT_DOWNLOAD_WINDOW,
DEFAULT_UPLOAD_WINDOW, MAX_RETRIES,
};
pub const DEFAULT_BASE_RETRY_MS: u32 = 500;
pub const DEFAULT_MAX_RETRY_MS: u32 = 30_000;
pub const DEFAULT_TIMEOUT_MS: u32 = 60_000;
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub concurrency: usize,
pub upload_window: usize,
pub download_window: usize,
pub download_chunk_size: u64,
pub compress: bool,
pub chunk_size: u64,
pub max_retries: u32,
pub base_retry_delay_ms: u32,
pub max_retry_delay_ms: u32,
pub timeout_ms: u32,
}
impl Default for ClientConfig {
fn default() -> Self {
ClientConfig {
concurrency: DEFAULT_CONCURRENCY,
upload_window: DEFAULT_UPLOAD_WINDOW,
download_window: DEFAULT_DOWNLOAD_WINDOW,
download_chunk_size: DEFAULT_DOWNLOAD_CHUNK_SIZE,
compress: true,
chunk_size: CHUNK_SIZE,
max_retries: MAX_RETRIES,
base_retry_delay_ms: DEFAULT_BASE_RETRY_MS,
max_retry_delay_ms: DEFAULT_MAX_RETRY_MS,
timeout_ms: DEFAULT_TIMEOUT_MS,
}
}
}
fn opt_u64(obj: &JsValue, key: &str) -> Option<u64> {
Reflect::get(obj, &JsValue::from_str(key))
.ok()
.and_then(|v| v.as_f64())
.map(|f| f as u64)
}
fn opt_usize(obj: &JsValue, key: &str) -> Option<usize> {
Reflect::get(obj, &JsValue::from_str(key))
.ok()
.and_then(|v| v.as_f64())
.map(|f| f as usize)
}
fn opt_u32(obj: &JsValue, key: &str) -> Option<u32> {
Reflect::get(obj, &JsValue::from_str(key))
.ok()
.and_then(|v| v.as_f64())
.map(|f| f as u32)
}
fn opt_bool(obj: &JsValue, key: &str) -> Option<bool> {
Reflect::get(obj, &JsValue::from_str(key))
.ok()
.and_then(|v| v.as_bool())
}
impl ClientConfig {
pub fn from_js(opts: &JsValue) -> ClientConfig {
let mut cfg = ClientConfig::default();
if !opts.is_object() {
return cfg;
}
if let Some(v) = opt_usize(opts, "concurrency") {
if v > 0 {
cfg.concurrency = v;
}
}
if let Some(v) = opt_usize(opts, "uploadWindow") {
if v > 0 {
cfg.upload_window = v;
}
}
if let Some(v) = opt_usize(opts, "downloadWindow") {
if v > 0 {
cfg.download_window = v;
}
}
if let Some(v) = opt_u64(opts, "downloadChunkSize") {
if v > 0 {
cfg.download_chunk_size = v;
}
}
if let Some(v) = opt_bool(opts, "compress") {
cfg.compress = v;
}
if let Some(v) = opt_u64(opts, "chunkSize") {
if v > 0 {
cfg.chunk_size = v;
}
}
if let Some(v) = opt_u32(opts, "maxRetries") {
cfg.max_retries = v;
}
if let Some(v) = opt_u32(opts, "baseRetryDelayMs") {
cfg.base_retry_delay_ms = v;
}
if let Some(v) = opt_u32(opts, "maxRetryDelayMs") {
cfg.max_retry_delay_ms = v;
}
if let Some(v) = opt_u32(opts, "timeoutMs") {
cfg.timeout_ms = v;
}
cfg
}
pub fn backoff_ms(&self, attempt: u32) -> u32 {
backoff_ms(attempt, self.base_retry_delay_ms, self.max_retry_delay_ms)
}
}
pub fn backoff_ms(attempt: u32, base: u32, max: u32) -> u32 {
if attempt == 0 {
return base.min(max);
}
let shift = attempt.min(31);
let doubled = (base as u64) << shift;
doubled.min(max as u64) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_sane() {
let cfg = ClientConfig::default();
assert_eq!(cfg.concurrency, 4);
assert_eq!(cfg.upload_window, DEFAULT_UPLOAD_WINDOW);
assert_eq!(cfg.download_window, DEFAULT_DOWNLOAD_WINDOW);
assert_eq!(cfg.download_chunk_size, DEFAULT_DOWNLOAD_CHUNK_SIZE);
assert_eq!(cfg.chunk_size, CHUNK_SIZE);
assert_eq!(cfg.max_retries, MAX_RETRIES);
assert!(cfg.compress);
}
#[test]
fn backoff_grows_exponentially_then_clamps() {
let cfg = ClientConfig {
base_retry_delay_ms: 100,
max_retry_delay_ms: 1000,
..ClientConfig::default()
};
assert_eq!(cfg.backoff_ms(0), 100);
assert_eq!(cfg.backoff_ms(1), 200);
assert_eq!(cfg.backoff_ms(2), 400);
assert_eq!(cfg.backoff_ms(3), 800);
assert_eq!(cfg.backoff_ms(4), 1000); assert_eq!(cfg.backoff_ms(99), 1000);
}
#[test]
fn pure_backoff_saturates() {
assert_eq!(backoff_ms(0, 500, 30_000), 500);
assert_eq!(backoff_ms(6, 500, 30_000), 30_000);
}
#[test]
#[cfg(target_arch = "wasm32")]
fn parses_js_options() {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &JsValue::from_str("concurrency"), &JsValue::from_f64(8.0))
.unwrap();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("uploadWindow"),
&JsValue::from_f64(16.0),
)
.unwrap();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("downloadWindow"),
&JsValue::from_f64(6.0),
)
.unwrap();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("downloadChunkSize"),
&JsValue::from_f64(131072.0),
)
.unwrap();
js_sys::Reflect::set(&obj, &JsValue::from_str("compress"), &JsValue::FALSE).unwrap();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("chunkSize"),
&JsValue::from_f64(1024.0),
)
.unwrap();
let cfg = ClientConfig::from_js(&obj);
assert_eq!(cfg.concurrency, 8);
assert_eq!(cfg.upload_window, 16);
assert_eq!(cfg.download_window, 6);
assert_eq!(cfg.download_chunk_size, 131072);
assert!(!cfg.compress);
assert_eq!(cfg.chunk_size, 1024);
}
#[test]
#[cfg(target_arch = "wasm32")]
fn ignores_non_object_options() {
let cfg = ClientConfig::from_js(&JsValue::NULL);
assert_eq!(cfg.concurrency, DEFAULT_CONCURRENCY);
let cfg = ClientConfig::from_js(&JsValue::UNDEFINED);
assert_eq!(cfg.chunk_size, CHUNK_SIZE);
}
}