1use std::io::Cursor;
14
15use arrow_array::cast::AsArray;
16use arrow_array::types::*;
17use arrow_array::{
18 Array as _, ArrayRef, ArrowPrimitiveType, DictionaryArray, PrimitiveArray, StringArray,
19};
20use arrow_ipc::reader::StreamReader;
21use arrow_schema::{DataType, TimeUnit};
22use js_sys::{Array, Function, JsString, Uint8Array};
23use perspective_client::ViewWindow;
24use ts_rs::TS;
25use wasm_bindgen::JsCast;
26use wasm_bindgen::prelude::*;
27
28#[wasm_bindgen]
29unsafe extern "C" {
30 #[wasm_bindgen(typescript_type = "TypedArrayWindow")]
31 #[derive(Clone)]
32 pub type JsTypedArrayWindow;
33}
34
35#[derive(Default, serde::Deserialize, TS)]
38pub struct TypedArrayWindow {
39 #[serde(flatten)]
40 pub view_window: ViewWindow,
41
42 #[serde(default)]
45 pub float32: bool,
46}
47
48impl From<TypedArrayWindow> for ViewWindow {
49 fn from(w: TypedArrayWindow) -> Self {
50 w.view_window
51 }
52}
53
54fn zero_invalid_slots<T: ArrowPrimitiveType>(arr: &PrimitiveArray<T>) {
55 let Some(nulls) = arr.nulls() else { return };
56 let ptr = arr.values().as_ptr() as *mut T::Native;
57 let chunks = nulls.inner().bit_chunks();
58 let mut base = 0;
59 for chunk in chunks.iter() {
60 if chunk != u64::MAX {
61 for bit in 0..64 {
62 if chunk & (1 << bit) == 0 {
63 unsafe { ptr.add(base + bit).write(T::default_value()) };
64 }
65 }
66 }
67
68 base += 64;
69 }
70
71 let rem = chunks.remainder_bits();
72 for bit in 0..chunks.remainder_len() {
73 if rem & (1 << bit) == 0 {
74 unsafe { ptr.add(base + bit).write(T::default_value()) };
75 }
76 }
77}
78
79fn set_widened_i32<T>(
87 col: &ArrayRef,
88 col_idx: usize,
89 js_values: &Array,
90 js_dicts: &Array,
91 storage: &mut Vec<Box<[i32]>>,
92) where
93 T: ArrowPrimitiveType,
94 T::Native: Into<i32>,
95{
96 let typed = col.as_primitive::<T>();
97 zero_invalid_slots(typed);
98 let vals: Box<[i32]> = typed.values().iter().map(|&v| v.into()).collect();
99
100 let arr = unsafe { js_sys::Int32Array::view(&vals) };
101 storage.push(vals);
102 js_values.set(col_idx as u32, arr.into());
103 js_dicts.set(col_idx as u32, JsValue::NULL);
104}
105
106pub(crate) async fn decode_and_call(
118 arrow: &[u8],
119 float32: bool,
120 callback: &Function,
121) -> Result<(), JsValue> {
122 let cursor = Cursor::new(arrow);
123 let reader = StreamReader::try_new(cursor, None)
124 .map_err(|e| JsValue::from_str(&format!("Arrow decode error: {e}")))?;
125
126 let batch = reader
127 .into_iter()
128 .next()
129 .ok_or_else(|| JsValue::from_str("Arrow IPC contained no record batches"))?
130 .map_err(|e| JsValue::from_str(&format!("Arrow batch error: {e}")))?;
131
132 let schema = batch.schema();
133 let num_cols = batch.num_columns();
134
135 let js_names = Array::new_with_length(num_cols as u32);
136 let js_values = Array::new_with_length(num_cols as u32);
137 let js_validities = Array::new_with_length(num_cols as u32);
138 let js_dicts = Array::new_with_length(num_cols as u32);
139
140 let mut i32_storage: Vec<Box<[i32]>> = Vec::new();
148 let mut f32_storage: Vec<Box<[f32]>> = Vec::new();
149 let mut f64_storage: Vec<Box<[f64]>> = Vec::new();
150
151 for col_idx in 0..num_cols {
160 let field = schema.field(col_idx);
161 let col = batch.column(col_idx);
162 let validity = col.nulls().map(|nulls| nulls.validity());
163
164 js_names.set(col_idx as u32, JsString::from(field.name().as_str()).into());
165
166 match col.data_type() {
167 DataType::Int8 => {
168 set_widened_i32::<Int8Type>(col, col_idx, &js_values, &js_dicts, &mut i32_storage);
169 },
170 DataType::UInt8 => {
171 set_widened_i32::<UInt8Type>(col, col_idx, &js_values, &js_dicts, &mut i32_storage);
172 },
173 DataType::Int16 => {
174 set_widened_i32::<Int16Type>(col, col_idx, &js_values, &js_dicts, &mut i32_storage);
175 },
176 DataType::UInt16 => {
177 set_widened_i32::<UInt16Type>(
178 col,
179 col_idx,
180 &js_values,
181 &js_dicts,
182 &mut i32_storage,
183 );
184 },
185 DataType::UInt32 => {
186 let typed = col.as_primitive::<UInt32Type>();
187 zero_invalid_slots(typed);
188 let arr = unsafe { js_sys::Uint32Array::view(typed.values().as_ref()) };
189 js_values.set(col_idx as u32, arr.into());
190 js_dicts.set(col_idx as u32, JsValue::NULL);
191 },
192 DataType::Int32 => {
193 let typed = col.as_primitive::<Int32Type>();
194 zero_invalid_slots(typed);
195 let arr = unsafe { js_sys::Int32Array::view(typed.values().as_ref()) };
196 js_values.set(col_idx as u32, arr.into());
197 js_dicts.set(col_idx as u32, JsValue::NULL);
198 },
199 DataType::Float32 => {
200 let typed = col.as_primitive::<Float32Type>();
201 zero_invalid_slots(typed);
202 let arr = unsafe { js_sys::Float32Array::view(typed.values().as_ref()) };
203 js_values.set(col_idx as u32, arr.into());
204 js_dicts.set(col_idx as u32, JsValue::NULL);
205 },
206 DataType::Float64 => {
207 let typed = col.as_primitive::<Float64Type>();
208 zero_invalid_slots(typed);
209 if float32 {
210 let vals: Box<[f32]> = typed.values().iter().map(|&v| v as f32).collect();
211
212 let arr = unsafe { js_sys::Float32Array::view(&vals) };
213 f32_storage.push(vals);
214 js_values.set(col_idx as u32, arr.into());
215 } else {
216 let arr = unsafe { js_sys::Float64Array::view(typed.values().as_ref()) };
217
218 js_values.set(col_idx as u32, arr.into());
219 }
220
221 js_dicts.set(col_idx as u32, JsValue::NULL);
222 },
223 DataType::Date32 => {
224 let typed = col.as_primitive::<Date32Type>();
229 zero_invalid_slots(typed);
230 let vals: Box<[f64]> = typed
231 .values()
232 .iter()
233 .map(|&v| v as f64 * 86_400_000.0)
234 .collect();
235
236 let arr = unsafe { js_sys::Float64Array::view(&vals) };
237 f64_storage.push(vals);
238 js_values.set(col_idx as u32, arr.into());
239 js_dicts.set(col_idx as u32, JsValue::NULL);
240 },
241 DataType::Timestamp(TimeUnit::Millisecond, _) => {
242 let typed = col.as_primitive::<TimestampMillisecondType>();
243 zero_invalid_slots(typed);
244 let vals: Box<[f64]> = typed.values().iter().map(|&v| v as f64).collect();
245
246 let arr = unsafe { js_sys::Float64Array::view(&vals) };
247 f64_storage.push(vals);
248 js_values.set(col_idx as u32, arr.into());
249 js_dicts.set(col_idx as u32, JsValue::NULL);
250 },
251 DataType::Int64 => {
252 let typed = col.as_primitive::<Int64Type>();
253 zero_invalid_slots(typed);
254 if float32 {
255 let vals: Box<[f32]> = typed.values().iter().map(|&v| v as f32).collect();
256
257 let arr = unsafe { js_sys::Float32Array::view(&vals) };
258 f32_storage.push(vals);
259 js_values.set(col_idx as u32, arr.into());
260 } else {
261 let vals: Box<[f64]> = typed.values().iter().map(|&v| v as f64).collect();
262
263 let arr = unsafe { js_sys::Float64Array::view(&vals) };
264 f64_storage.push(vals);
265 js_values.set(col_idx as u32, arr.into());
266 }
267
268 js_dicts.set(col_idx as u32, JsValue::NULL);
269 },
270 DataType::UInt64 => {
277 let typed = col.as_primitive::<UInt64Type>();
278 zero_invalid_slots(typed);
279 if float32 {
280 let vals: Box<[f32]> = typed.values().iter().map(|&v| v as f32).collect();
281
282 let arr = unsafe { js_sys::Float32Array::view(&vals) };
283 f32_storage.push(vals);
284 js_values.set(col_idx as u32, arr.into());
285 } else {
286 let vals: Box<[f64]> = typed.values().iter().map(|&v| v as f64).collect();
287
288 let arr = unsafe { js_sys::Float64Array::view(&vals) };
289 f64_storage.push(vals);
290 js_values.set(col_idx as u32, arr.into());
291 }
292
293 js_dicts.set(col_idx as u32, JsValue::NULL);
294 },
295 DataType::Boolean => {
296 let typed = col.as_boolean();
301 let vals: Box<[i32]> = (0..typed.len())
302 .map(|i| i32::from(typed.is_valid(i) && typed.value(i)))
303 .collect();
304
305 let arr = unsafe { js_sys::Int32Array::view(&vals) };
306 i32_storage.push(vals);
307 js_values.set(col_idx as u32, arr.into());
308 js_dicts.set(col_idx as u32, JsValue::NULL);
309 },
310 DataType::Dictionary(..) => {
311 let dict = col
312 .as_any()
313 .downcast_ref::<DictionaryArray<Int32Type>>()
314 .ok_or_else(|| {
315 JsValue::from_str(&format!(
316 "Unsupported dictionary key type for typed array: {}",
317 col.data_type()
318 ))
319 })?;
320
321 let keys = dict.keys();
322 zero_invalid_slots(keys);
323 let arr = unsafe { js_sys::Int32Array::view(keys.values().as_ref()) };
324 js_values.set(col_idx as u32, arr.into());
325
326 let values = dict
327 .values()
328 .as_any()
329 .downcast_ref::<StringArray>()
330 .ok_or_else(|| {
331 JsValue::from_str(&format!(
332 "Unsupported dictionary value type for typed array: {}",
333 dict.values().data_type()
334 ))
335 })?;
336
337 let js_dict = Array::new_with_length(values.len() as u32);
338 for i in 0..values.len() {
339 js_dict.set(i as u32, JsValue::from_str(values.value(i)));
340 }
341 js_dicts.set(col_idx as u32, js_dict.into());
342 },
343 dt => {
344 return Err(JsValue::from_str(&format!(
345 "Unsupported column type for typed array: {dt}"
346 )));
347 },
348 }
349
350 let js_validity = validity.map(|v| unsafe { Uint8Array::view(v) });
353 js_validities.set(
354 col_idx as u32,
355 js_validity
356 .as_ref()
357 .map(JsValue::from)
358 .unwrap_or(JsValue::NULL),
359 );
360 }
361
362 let ret = callback.call4(
363 &JsValue::UNDEFINED,
364 &js_names.into(),
365 &js_values.into(),
366 &js_validities.into(),
367 &js_dicts.into(),
368 )?;
369
370 if ret.is_instance_of::<js_sys::Promise>() {
375 let promise: js_sys::Promise = ret.unchecked_into();
376 wasm_bindgen_futures::JsFuture::from(promise).await?;
377 }
378
379 drop(i32_storage);
382 drop(f32_storage);
383 drop(f64_storage);
384
385 Ok(())
386}