1use js_sys::{ArrayBuffer, Uint8Array};
2use wasm_bindgen::{prelude::*, JsCast};
3use wasm_bindgen_futures::JsFuture;
4use web_sys::{AbortController, Request, RequestInit, RequestMode, Response};
5
6use spatialrust_math::Vec3;
7use spatialrust_viewer::{ViewerState, ViewportSize};
8use spatialrust_viz::{Camera, Projection};
9
10use crate::{BrowserInput, ByteRange, RangeBudget, RangeCache, RangePlanner, WebViewerState};
11
12fn js_error(error: impl core::fmt::Display) -> JsValue {
13 JsValue::from_str(&error.to_string())
14}
15
16#[wasm_bindgen]
18pub struct BrowserRangeAbort {
19 controller: AbortController,
20}
21
22#[wasm_bindgen]
23impl BrowserRangeAbort {
24 #[wasm_bindgen(constructor)]
26 pub fn new() -> Result<BrowserRangeAbort, JsValue> {
27 Ok(Self { controller: AbortController::new()? })
28 }
29
30 pub fn abort(&self) {
32 self.controller.abort();
33 }
34
35 #[wasm_bindgen(getter)]
37 pub fn aborted(&self) -> bool {
38 self.controller.signal().aborted()
39 }
40}
41
42#[wasm_bindgen]
47pub async fn bounded_fetch_range(
48 url: String,
49 start: u64,
50 end_exclusive: u64,
51 max_response_bytes: u64,
52 cancellation: &BrowserRangeAbort,
53) -> Result<Uint8Array, JsValue> {
54 let range = ByteRange::try_new(start, end_exclusive).map_err(js_error)?;
55 if max_response_bytes == 0 || range.len() > max_response_bytes {
56 return Err(js_error("requested range exceeds max_response_bytes"));
57 }
58 if cancellation.aborted() {
59 return Err(js_error("range fetch cancelled before request"));
60 }
61 let options = RequestInit::new();
62 options.set_method("GET");
63 options.set_mode(RequestMode::Cors);
64 options.set_signal(Some(&cancellation.controller.signal()));
65 let request = Request::new_with_str_and_init(&url, &options)?;
66 request.headers().set("Range", &range.http_header())?;
67 let window = web_sys::window().ok_or_else(|| js_error("browser Window is unavailable"))?;
68 let response =
69 JsFuture::from(window.fetch_with_request(&request)).await?.dyn_into::<Response>()?;
70 if response.status() != 206 {
71 return Err(js_error(format!(
72 "range server returned status {}, expected 206",
73 response.status()
74 )));
75 }
76 let length_header = response
77 .headers()
78 .get("Content-Length")?
79 .ok_or_else(|| js_error("range response omitted Content-Length"))?;
80 let declared: u64 = length_header.parse().map_err(|_| js_error("invalid Content-Length"))?;
81 if declared != range.len() || declared > max_response_bytes {
82 return Err(js_error(format!(
83 "range Content-Length {declared} does not match requested {}",
84 range.len()
85 )));
86 }
87 let buffer = JsFuture::from(response.array_buffer()?).await?.dyn_into::<ArrayBuffer>()?;
88 let bytes = Uint8Array::new(&buffer);
89 if u64::from(bytes.length()) != declared {
90 return Err(js_error("range body length differs from Content-Length"));
91 }
92 Ok(bytes)
93}
94
95#[wasm_bindgen]
97pub struct BrowserViewer {
98 state: WebViewerState,
99 planner: RangePlanner,
100 cache: RangeCache,
101}
102
103#[wasm_bindgen]
104impl BrowserViewer {
105 pub fn new_default(
107 width: u32,
108 height: u32,
109 range_budget_json: &str,
110 ) -> Result<BrowserViewer, JsValue> {
111 let camera = Camera::try_new(
112 Vec3::new(0.0, 0.0, 5.0),
113 Vec3::new(0.0, 0.0, 0.0),
114 Vec3::new(0.0, 1.0, 0.0),
115 Projection::Perspective { vertical_fov_radians: 1.0, near: 0.1, far: 100.0 },
116 )
117 .map_err(js_error)?;
118 let viewer =
119 ViewerState::try_new(camera, ViewportSize::try_new(width, height).map_err(js_error)?)
120 .map_err(js_error)?;
121 let state = WebViewerState::try_new(viewer).map_err(js_error)?;
122 let budget: RangeBudget = serde_json::from_str(range_budget_json).map_err(js_error)?;
123 Ok(Self {
124 state,
125 planner: RangePlanner::try_new(budget).map_err(js_error)?,
126 cache: RangeCache::try_new(budget).map_err(js_error)?,
127 })
128 }
129
130 #[wasm_bindgen(constructor)]
132 pub fn new(state_json: &str, range_budget_json: &str) -> Result<BrowserViewer, JsValue> {
133 let state = WebViewerState::from_json(state_json).map_err(js_error)?;
134 let budget: RangeBudget = serde_json::from_str(range_budget_json).map_err(js_error)?;
135 Ok(Self {
136 state,
137 planner: RangePlanner::try_new(budget).map_err(js_error)?,
138 cache: RangeCache::try_new(budget).map_err(js_error)?,
139 })
140 }
141
142 pub fn state_json(&self) -> Result<String, JsValue> {
144 self.state.to_json().map_err(js_error)
145 }
146
147 pub fn apply_input_json(&mut self, input_json: &str) -> Result<(), JsValue> {
149 let input: BrowserInput = serde_json::from_str(input_json).map_err(js_error)?;
150 self.state.apply(input).map_err(js_error)
151 }
152
153 pub fn plan_ranges_json(
155 &mut self,
156 ranges_json: &str,
157 cancelled: bool,
158 ) -> Result<String, JsValue> {
159 let ranges: Vec<ByteRange> = serde_json::from_str(ranges_json).map_err(js_error)?;
160 let plan = self.planner.plan(ranges, &self.cache, cancelled).map_err(js_error)?;
161 serde_json::to_string(&plan).map_err(js_error)
162 }
163
164 pub fn admit_range(
168 &mut self,
169 start: u64,
170 end_exclusive: u64,
171 bytes: &Uint8Array,
172 ) -> Result<String, JsValue> {
173 let range = ByteRange::try_new(start, end_exclusive).map_err(js_error)?;
174 let mut owned = vec![0_u8; bytes.length() as usize];
175 bytes.copy_to(&mut owned);
176 let receipt = self.cache.admit(range, owned).map_err(js_error)?;
177 serde_json::to_string(&receipt).map_err(js_error)
178 }
179
180 pub fn cached_range(
182 &mut self,
183 start: u64,
184 end_exclusive: u64,
185 ) -> Result<Option<Uint8Array>, JsValue> {
186 let range = ByteRange::try_new(start, end_exclusive).map_err(js_error)?;
187 Ok(self.cache.get(range).map(Uint8Array::from))
188 }
189}