#![cfg(target_arch = "wasm32")]
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Headers, Request, RequestInit, Response};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteRange {
pub start: u64,
pub end: u64,
}
impl ByteRange {
pub fn new(start: u64, end: u64) -> Self {
assert!(start <= end, "start must be <= end");
Self { start, end }
}
pub fn size(&self) -> u64 {
self.end - self.start + 1
}
pub fn last_n_bytes(n: u64) -> Self {
Self {
start: u64::MAX - n + 1, end: u64::MAX,
}
}
}
impl std::fmt::Display for ByteRange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "bytes={}-{}", self.start, self.end)
}
}
pub struct RangeClient {
base_url: String,
retry_attempts: u32,
}
impl RangeClient {
pub fn new(url: impl Into<String>) -> Self {
Self { base_url: url.into(), retry_attempts: 3 }
}
pub async fn fetch_range(&self, range: ByteRange) -> Result<Vec<u8>, JsValue> {
let mut opts = RequestInit::new();
opts.method("GET");
let headers = Headers::new()?;
headers.set("Range", &range.to_string())?;
let request = Request::new_with_str_and_init(&self.base_url, &opts)?;
request.headers().set("Range", &range.to_string())?;
for attempt in 0..self.retry_attempts {
match self.try_fetch(&request).await {
Ok(bytes) => return Ok(bytes),
Err(e) if attempt < self.retry_attempts - 1 => {
let delay_ms = 100 * 2_u32.pow(attempt);
sleep_ms(delay_ms).await;
continue;
}
Err(e) => return Err(e),
}
}
Err(JsValue::from_str("Max retries exceeded"))
}
async fn try_fetch(&self, request: &Request) -> Result<Vec<u8>, JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
let response_promise = window.fetch_with_request(request);
let response_value = JsFuture::from(response_promise).await?;
let response: Response = response_value.dyn_into()?;
let status = response.status();
if status != 206 && status != 200 {
return Err(JsValue::from_str(&format!("HTTP error: {}", status)));
}
let array_buffer_promise = response.array_buffer()?;
let array_buffer = JsFuture::from(array_buffer_promise).await?;
let uint8_array = Uint8Array::new(&array_buffer);
Ok(uint8_array.to_vec())
}
pub async fn get_file_size(&self) -> Result<u64, JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("No window object"))?;
let mut opts = RequestInit::new();
opts.method("HEAD");
let request = Request::new_with_str_and_init(&self.base_url, &opts)?;
let response_promise = window.fetch_with_request(&request);
let response_value = JsFuture::from(response_promise).await?;
let response: Response = response_value.dyn_into()?;
let headers = response.headers();
let content_length_js = headers.get("content-length")?;
match content_length_js {
Some(length_str) => length_str
.parse::<u64>()
.map_err(|e| JsValue::from_str(&format!("Invalid content-length: {}", e))),
None => Err(JsValue::from_str("No content-length header")),
}
}
}
async fn sleep_ms(ms: u32) {
let promise = js_sys::Promise::new(&mut |resolve, _reject| {
let window = web_sys::window().unwrap();
window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, ms as i32).unwrap();
});
let _ = JsFuture::from(promise).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_byte_range_new() {
let range = ByteRange::new(0, 1023);
assert_eq!(range.start, 0);
assert_eq!(range.end, 1023);
assert_eq!(range.size(), 1024);
}
#[test]
fn test_byte_range_display() {
let range = ByteRange::new(100, 199);
assert_eq!(range.to_string(), "bytes=100-199");
}
#[test]
fn test_byte_range_size() {
let range = ByteRange::new(0, 0);
assert_eq!(range.size(), 1);
let range = ByteRange::new(0, 1023);
assert_eq!(range.size(), 1024);
}
#[test]
#[should_panic(expected = "start must be <= end")]
fn test_byte_range_invalid() {
ByteRange::new(100, 50);
}
}