Skip to main content

perspective_js/
virtual_server.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::cell::UnsafeCell;
14use std::future::Future;
15use std::pin::Pin;
16use std::rc::Rc;
17use std::str::FromStr;
18use std::sync::{Arc, Mutex};
19
20use indexmap::IndexMap;
21use js_sys::{Array, Date, Object, Reflect, Uint8Array};
22use perspective_client::proto::{ColumnType, HostedTable};
23use perspective_client::virtual_server;
24use perspective_client::virtual_server::{Features, ResultExt, VirtualServerHandler};
25use serde::Serialize;
26use wasm_bindgen::prelude::*;
27use wasm_bindgen_futures::JsFuture;
28
29use crate::JsViewConfig;
30use crate::utils::{ApiError, ApiFuture, *};
31
32type HandlerFuture<T> = Pin<Box<dyn Future<Output = T>>>;
33
34#[derive(Debug)]
35pub struct JsError(JsValue);
36
37impl std::fmt::Display for JsError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        write!(f, "{:?}", self.0)
40    }
41}
42
43impl std::error::Error for JsError {}
44
45impl From<JsValue> for JsError {
46    fn from(value: JsValue) -> Self {
47        JsError(value)
48    }
49}
50
51impl From<JsError> for JsValue {
52    fn from(error: JsError) -> Self {
53        error.0
54    }
55}
56
57impl From<serde_wasm_bindgen::Error> for JsError {
58    fn from(error: serde_wasm_bindgen::Error) -> Self {
59        JsError(error.into())
60    }
61}
62
63fn jsvalue_to_scalar(val: &JsValue) -> perspective_client::config::Scalar {
64    if val.is_null() || val.is_undefined() {
65        perspective_client::config::Scalar::Null
66    } else if let Some(b) = val.as_bool() {
67        perspective_client::config::Scalar::Bool(b)
68    } else if let Some(n) = val.as_f64() {
69        perspective_client::config::Scalar::Float(n)
70    } else if let Some(s) = val.as_string() {
71        perspective_client::config::Scalar::String(s)
72    } else {
73        perspective_client::config::Scalar::Null
74    }
75}
76
77pub struct JsServerHandler(Object);
78
79impl JsServerHandler {
80    fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
81        let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
82        let func = func
83            .dyn_ref::<js_sys::Function>()
84            .ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
85        Ok(func.apply(&self.0, args)?)
86    }
87
88    async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
89        let result = self.call_method_js(method, args)?;
90
91        // Check if result is a Promise
92        if result.is_instance_of::<js_sys::Promise>() {
93            let promise = js_sys::Promise::from(result);
94            JsFuture::from(promise).await.map_err(JsError)
95        } else {
96            Ok(result)
97        }
98    }
99}
100
101impl VirtualServerHandler for JsServerHandler {
102    type Error = JsError;
103
104    fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
105        let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
106            .map(|val| !val.is_undefined())
107            .unwrap_or(false);
108
109        if !has_method {
110            return Box::pin(async { Ok(Features::default()) });
111        }
112
113        let handler = self.0.clone();
114        Box::pin(async move {
115            let this = JsServerHandler(handler);
116            let args = Array::new();
117            let result = this.call_method_js_async("getFeatures", &args).await?;
118            Ok(serde_wasm_bindgen::from_value(result)?)
119        })
120    }
121
122    fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
123        let handler = self.0.clone();
124        Box::pin(async move {
125            let this = JsServerHandler(handler);
126            let args = Array::new();
127            let result = this.call_method_js_async("getHostedTables", &args).await?;
128            let array = result.dyn_ref::<Array>().ok_or_else(|| {
129                JsError(JsValue::from_str("getHostedTables must return an array"))
130            })?;
131
132            let mut tables = Vec::new();
133            for i in 0..array.length() {
134                let item = array.get(i);
135                if let Some(s) = item.as_string() {
136                    tables.push(HostedTable {
137                        entity_id: s,
138                        index: None,
139                        limit: None,
140                    });
141                } else if item.is_object() {
142                    let name = Reflect::get(&item, &JsValue::from_str("name"))?
143                        .as_string()
144                        .ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
145                    let index = Reflect::get(&item, &JsValue::from_str("index"))
146                        .ok()
147                        .and_then(|v| v.as_string());
148                    let limit = Reflect::get(&item, &JsValue::from_str("limit"))
149                        .ok()
150                        .and_then(|v| v.as_f64().map(|x| x as u32));
151                    tables.push(HostedTable {
152                        entity_id: name,
153                        index,
154                        limit,
155                    });
156                }
157            }
158            Ok(tables)
159        })
160    }
161
162    fn table_schema(
163        &self,
164        table_id: &str,
165    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
166        let handler = self.0.clone();
167        let table_id = table_id.to_string();
168        Box::pin(async move {
169            let this = JsServerHandler(handler);
170            let args = Array::new();
171            args.push(&JsValue::from_str(&table_id));
172            let result = this.call_method_js_async("tableSchema", &args).await?;
173            let obj = result
174                .dyn_ref::<Object>()
175                .ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
176
177            let mut schema = IndexMap::new();
178            let entries = Object::entries(obj);
179            for i in 0..entries.length() {
180                let entry = entries.get(i);
181                let entry_array = entry.dyn_ref::<Array>().unwrap();
182                let key = entry_array.get(0).as_string().unwrap();
183                let value = entry_array.get(1).as_string().unwrap();
184                schema.insert(key, ColumnType::from_str(&value).unwrap());
185            }
186            Ok(schema)
187        })
188    }
189
190    fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
191        let handler = self.0.clone();
192        let table_id = table_id.to_string();
193        Box::pin(async move {
194            let this = JsServerHandler(handler);
195            let args = Array::new();
196            args.push(&JsValue::from_str(&table_id));
197            let result = this.call_method_js_async("tableSize", &args).await?;
198            result
199                .as_f64()
200                .map(|x| x as u32)
201                .ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
202        })
203    }
204
205    fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
206        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
207            .map(|val| !val.is_undefined())
208            .unwrap_or(false);
209
210        let handler = self.0.clone();
211        let view_id = view_id.to_string();
212        Box::pin(async move {
213            let this = JsServerHandler(handler);
214            let args = Array::new();
215            args.push(&JsValue::from_str(&view_id));
216            if has_method {
217                let result = this.call_method_js_async("tableColumnsSize", &args).await?;
218                result.as_f64().map(|x| x as u32).ok_or_else(|| {
219                    JsError(JsValue::from_str(
220                        "tableColumnsSize must
221    return a number",
222                    ))
223                })
224            } else {
225                Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
226            }
227        })
228    }
229
230    fn table_validate_expression(
231        &self,
232        table_id: &str,
233        expression: &str,
234    ) -> HandlerFuture<Result<ColumnType, Self::Error>> {
235        // TODO Cache these inspection calls
236        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableValidateExpression"))
237            .map(|val| !val.is_undefined())
238            .unwrap_or(false);
239
240        let handler = self.0.clone();
241        let table_id = table_id.to_string();
242        let expression = expression.to_string();
243        Box::pin(async move {
244            if !has_method {
245                return Err(JsError(JsValue::from_str(
246                    "feature `table_validate_expression` not implemented",
247                )));
248            }
249
250            let this = JsServerHandler(handler);
251            let args = Array::new();
252            args.push(&JsValue::from_str(&table_id));
253            args.push(&JsValue::from_str(&expression));
254            let result = this
255                .call_method_js_async("tableValidateExpression", &args)
256                .await?;
257
258            let type_str = result
259                .as_string()
260                .ok_or_else(|| JsError(JsValue::from_str("Must return a string")))?;
261
262            Ok(ColumnType::from_str(&type_str).unwrap())
263        })
264    }
265
266    fn table_make_view(
267        &mut self,
268        table_id: &str,
269        view_id: &str,
270        config: &mut perspective_client::config::ViewConfigUpdate,
271    ) -> HandlerFuture<Result<String, Self::Error>> {
272        let handler = self.0.clone();
273        let table_id = table_id.to_string();
274        let view_id = view_id.to_string();
275        let config = config.clone();
276        Box::pin(async move {
277            let this = JsServerHandler(handler);
278            let args = Array::new();
279            args.push(&JsValue::from_str(&table_id));
280            args.push(&JsValue::from_str(&view_id));
281            args.push(&JsValue::from_serde_ext(&config)?);
282            let _ = this.call_method_js_async("tableMakeView", &args).await?;
283            Ok(view_id.to_string())
284        })
285    }
286
287    fn view_schema(
288        &self,
289        view_id: &str,
290        config: &perspective_client::config::ViewConfig,
291    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
292        let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
293            .is_ok_and(|v| !v.is_undefined());
294
295        let handler = self.0.clone();
296        let view_id = view_id.to_string();
297        let config_value = JsValue::from_serde_ext(config).ok();
298
299        Box::pin(async move {
300            let this = JsServerHandler(handler);
301            let args = Array::new();
302            args.push(&JsValue::from_str(&view_id));
303            if let Some(cv) = config_value {
304                args.push(&cv);
305            }
306
307            let result = this
308                .call_method_js_async(
309                    if has_view_schema {
310                        "viewSchema"
311                    } else {
312                        "tableSchema"
313                    },
314                    &args,
315                )
316                .await?;
317
318            let obj = result
319                .dyn_ref::<Object>()
320                .ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
321
322            let mut schema = IndexMap::new();
323            let entries = Object::entries(obj);
324            for i in 0..entries.length() {
325                let entry = entries.get(i);
326                let entry_array = entry.dyn_ref::<Array>().unwrap();
327                let key = entry_array.get(0).as_string().unwrap();
328                let value = entry_array.get(1).as_string().unwrap();
329                schema.insert(key, ColumnType::from_str(&value).unwrap());
330            }
331
332            Ok(schema)
333        })
334    }
335
336    fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
337        let handler = self.0.clone();
338        let view_id = view_id.to_string();
339        let has_view_size =
340            Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
341
342        Box::pin(async move {
343            let this = JsServerHandler(handler);
344            let args = Array::new();
345            args.push(&JsValue::from_str(&view_id));
346            let result = this
347                .call_method_js_async(
348                    if has_view_size {
349                        "viewSize"
350                    } else {
351                        "tableSize"
352                    },
353                    &args,
354                )
355                .await?;
356
357            result
358                .as_f64()
359                .map(|x| x as u32)
360                .ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
361        })
362    }
363
364    fn view_column_size(
365        &self,
366        view_id: &str,
367        config: &perspective_client::config::ViewConfig,
368    ) -> HandlerFuture<Result<u32, Self::Error>> {
369        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
370            .map(|val| !val.is_undefined())
371            .unwrap_or(false);
372
373        let handler = self.0.clone();
374        let view_id = view_id.to_string();
375        let config_value = serde_wasm_bindgen::to_value(config).unwrap();
376        let config = config.clone();
377        Box::pin(async move {
378            let this = JsServerHandler(handler);
379            let args = Array::new();
380            args.push(&JsValue::from_str(&view_id));
381            args.push(&config_value);
382            if has_method {
383                let result = this.call_method_js_async("viewColumnSize", &args).await?;
384                result.as_f64().map(|x| x as u32).ok_or_else(|| {
385                    JsError(JsValue::from_str("viewColumnSize must return a number"))
386                })
387            } else {
388                Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
389            }
390        })
391    }
392
393    fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
394        let handler = self.0.clone();
395        let view_id = view_id.to_string();
396        Box::pin(async move {
397            let this = JsServerHandler(handler);
398            let args = Array::new();
399            args.push(&JsValue::from_str(&view_id));
400            this.call_method_js_async("viewDelete", &args).await?;
401            Ok(())
402        })
403    }
404
405    fn table_make_port(
406        &self,
407        _req: &perspective_client::proto::TableMakePortReq,
408    ) -> HandlerFuture<Result<u32, Self::Error>> {
409        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
410            .map(|val| !val.is_undefined())
411            .unwrap_or(false);
412
413        if !has_method {
414            return Box::pin(async { Ok(0) });
415        }
416
417        let handler = self.0.clone();
418        Box::pin(async move {
419            let this = JsServerHandler(handler);
420            let args = Array::new();
421            let result = this.call_method_js_async("tableMakePort", &args).await?;
422            result
423                .as_f64()
424                .map(|x| x as u32)
425                .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
426        })
427    }
428
429    fn make_table(
430        &mut self,
431        table_id: &str,
432        data: &perspective_client::proto::MakeTableData,
433    ) -> HandlerFuture<Result<(), Self::Error>> {
434        let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
435            .map(|val| !val.is_undefined())
436            .unwrap_or(false);
437
438        if !has_method {
439            return Box::pin(async {
440                Err(JsError(JsValue::from_str("makeTable not implemented")))
441            });
442        }
443
444        let handler = self.0.clone();
445        let table_id = table_id.to_string();
446        use perspective_client::proto::make_table_data::Data;
447        let data_value = match &data.data {
448            Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
449            Some(Data::FromArrow(arrow)) => {
450                let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
451                JsValue::from(uint8array)
452            },
453            Some(Data::FromRows(rows)) => JsValue::from_str(rows),
454            Some(Data::FromCols(cols)) => JsValue::from_str(cols),
455            Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
456            _ => JsValue::from_str(""),
457        };
458
459        Box::pin(async move {
460            let this = JsServerHandler(handler);
461            let args = Array::new();
462            args.push(&JsValue::from_str(&table_id));
463            args.push(&data_value);
464            this.call_method_js_async("makeTable", &args).await?;
465            Ok(())
466        })
467    }
468
469    fn view_get_min_max(
470        &self,
471        view_id: &str,
472        column_name: &str,
473        config: &perspective_client::config::ViewConfig,
474    ) -> HandlerFuture<
475        Result<
476            (
477                perspective_client::config::Scalar,
478                perspective_client::config::Scalar,
479            ),
480            Self::Error,
481        >,
482    > {
483        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
484            .map(|val| !val.is_undefined())
485            .unwrap_or(false);
486
487        if !has_method {
488            return Box::pin(async {
489                Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
490            });
491        }
492
493        let handler = self.0.clone();
494        let view_id = view_id.to_string();
495        let column_name = column_name.to_string();
496        let config_js = serde_wasm_bindgen::to_value(config).unwrap();
497        Box::pin(async move {
498            let this = JsServerHandler(handler);
499            let args = Array::new();
500            args.push(&JsValue::from_str(&view_id));
501            args.push(&JsValue::from_str(&column_name));
502            args.push(&config_js);
503            let result = this.call_method_js_async("viewGetMinMax", &args).await?;
504            let obj = result.dyn_ref::<Object>().unwrap();
505            let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
506            let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
507            Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
508        })
509    }
510
511    fn view_get_data(
512        &self,
513        view_id: &str,
514        config: &perspective_client::config::ViewConfig,
515        schema: &IndexMap<String, ColumnType>,
516        viewport: &perspective_client::proto::ViewPort,
517    ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
518        let handler = self.0.clone();
519        let view_id = view_id.to_string();
520        let window: JsViewPort = viewport.clone().into();
521        let config_value = serde_wasm_bindgen::to_value(config).unwrap();
522        let window_value = serde_wasm_bindgen::to_value(&window).unwrap();
523        let schema_value = JsValue::from_serde_ext(&schema).unwrap();
524
525        Box::pin(async move {
526            let this = JsServerHandler(handler);
527            let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
528
529            {
530                let args = Array::new();
531                args.push(&JsValue::from_str(&view_id));
532                args.push(&config_value);
533                args.push(&schema_value);
534                args.push(&window_value);
535                args.push(&JsValue::from(data.clone()));
536                this.call_method_js_async("viewGetData", &args).await?;
537            }
538
539            // Lock the mutex and take ownership of the inner data
540            // We can't unwrap the Arc because the JsValue might still hold a reference
541            let VirtualDataSlice(_obj, arc) = data;
542            let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
543            Ok(slice)
544        })
545    }
546}
547
548#[derive(Serialize, PartialEq)]
549pub struct JsViewPort {
550    #[serde(default, skip_serializing_if = "Option::is_none")]
551    pub start_row: ::core::option::Option<u32>,
552
553    #[serde(default, skip_serializing_if = "Option::is_none")]
554    pub start_col: ::core::option::Option<u32>,
555
556    #[serde(default, skip_serializing_if = "Option::is_none")]
557    pub end_row: ::core::option::Option<u32>,
558
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub end_col: ::core::option::Option<u32>,
561}
562
563impl From<perspective_client::proto::ViewPort> for JsViewPort {
564    fn from(value: perspective_client::proto::ViewPort) -> Self {
565        JsViewPort {
566            start_row: value.start_row,
567            start_col: value.start_col,
568            end_row: value.end_row,
569            end_col: value.end_col,
570        }
571    }
572}
573
574#[wasm_bindgen(js_name = "VirtualDataSlice")]
575#[derive(Clone)]
576pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
577
578#[wasm_bindgen]
579impl VirtualDataSlice {
580    #[wasm_bindgen(constructor)]
581    pub fn new(config: JsViewConfig) -> Self {
582        VirtualDataSlice(
583            Object::new(),
584            Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
585                config.into_serde_ext().unwrap(),
586            )))),
587        )
588    }
589
590    #[wasm_bindgen(js_name = "fromArrowIpc")]
591    pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
592        self.1
593            .lock()
594            .unwrap()
595            .as_mut()
596            .unwrap()
597            .from_arrow_ipc(&ipc.to_vec())
598            .map_err(|e| JsValue::from_str(&e.to_string()))
599    }
600
601    #[wasm_bindgen(js_name = "setCol")]
602    pub fn set_col(
603        &self,
604        dtype: &str,
605        name: &str,
606        index: u32,
607        val: JsValue,
608        group_by_index: Option<usize>,
609    ) -> Result<(), JsValue> {
610        match dtype {
611            "string" => self.set_string_col(name, index, val, group_by_index),
612            "integer" => self.set_integer_col(name, index, val, group_by_index),
613            "float" => self.set_float_col(name, index, val, group_by_index),
614            "date" => self.set_datetime_col(name, index, val, group_by_index),
615            "datetime" => self.set_datetime_col(name, index, val, group_by_index),
616            "boolean" => self.set_boolean_col(name, index, val, group_by_index),
617            _ => Err(JsValue::from_str("Unknown type")),
618        }
619    }
620
621    #[wasm_bindgen(js_name = "setStringCol")]
622    pub fn set_string_col(
623        &self,
624        name: &str,
625        index: u32,
626        val: JsValue,
627        group_by_index: Option<usize>,
628    ) -> Result<(), JsValue> {
629        if val.is_null() || val.is_undefined() {
630            self.1
631                .lock()
632                .unwrap()
633                .as_mut()
634                .unwrap()
635                .set_col(name, group_by_index, index as usize, None as Option<String>)
636                .unwrap();
637        } else if let Some(s) = val.as_string() {
638            self.1
639                .lock()
640                .unwrap()
641                .as_mut()
642                .unwrap()
643                .set_col(name, group_by_index, index as usize, Some(s))
644                .unwrap();
645        } else {
646            tracing::error!("Unhandled string value");
647        }
648        Ok(())
649    }
650
651    #[wasm_bindgen(js_name = "setIntegerCol")]
652    pub fn set_integer_col(
653        &self,
654        name: &str,
655        index: u32,
656        val: JsValue,
657        group_by_index: Option<usize>,
658    ) -> Result<(), JsValue> {
659        if val.is_null() || val.is_undefined() {
660            self.1
661                .lock()
662                .unwrap()
663                .as_mut()
664                .unwrap()
665                .set_col(name, group_by_index, index as usize, None as Option<i32>)
666                .unwrap();
667        } else if let Some(n) = val.as_f64() {
668            self.1
669                .lock()
670                .unwrap()
671                .as_mut()
672                .unwrap()
673                .set_col(name, group_by_index, index as usize, Some(n as i32))
674                .unwrap();
675        } else {
676            tracing::error!("Unhandled integer value");
677        }
678        Ok(())
679    }
680
681    #[wasm_bindgen(js_name = "setFloatCol")]
682    pub fn set_float_col(
683        &self,
684        name: &str,
685        index: u32,
686        val: JsValue,
687        group_by_index: Option<usize>,
688    ) -> Result<(), JsValue> {
689        if val.is_null() || val.is_undefined() {
690            self.1
691                .lock()
692                .unwrap()
693                .as_mut()
694                .unwrap()
695                .set_col(name, group_by_index, index as usize, None as Option<f64>)
696                .unwrap();
697        } else if let Some(n) = val.as_f64() {
698            self.1
699                .lock()
700                .unwrap()
701                .as_mut()
702                .unwrap()
703                .set_col(name, group_by_index, index as usize, Some(n))
704                .unwrap();
705        } else {
706            tracing::error!("Unhandled float value");
707        }
708        Ok(())
709    }
710
711    #[wasm_bindgen(js_name = "setBooleanCol")]
712    pub fn set_boolean_col(
713        &self,
714        name: &str,
715        index: u32,
716        val: JsValue,
717        group_by_index: Option<usize>,
718    ) -> Result<(), JsValue> {
719        if val.is_null() || val.is_undefined() {
720            self.1
721                .lock()
722                .unwrap()
723                .as_mut()
724                .unwrap()
725                .set_col(name, group_by_index, index as usize, None as Option<bool>)
726                .unwrap();
727        } else if let Some(b) = val.as_bool() {
728            self.1
729                .lock()
730                .unwrap()
731                .as_mut()
732                .unwrap()
733                .set_col(name, group_by_index, index as usize, Some(b))
734                .unwrap();
735        } else {
736            tracing::error!("Unhandled boolean value");
737        }
738        Ok(())
739    }
740
741    #[wasm_bindgen(js_name = "setDatetimeCol")]
742    pub fn set_datetime_col(
743        &self,
744        name: &str,
745        index: u32,
746        val: JsValue,
747        group_by_index: Option<usize>,
748    ) -> Result<(), JsValue> {
749        if val.is_null() || val.is_undefined() {
750            self.1
751                .lock()
752                .unwrap()
753                .as_mut()
754                .unwrap()
755                .set_col(name, group_by_index, index as usize, None as Option<i64>)
756                .unwrap();
757        } else if let Some(date) = val.dyn_ref::<Date>() {
758            let timestamp = date.get_time() as i64;
759            self.1
760                .lock()
761                .unwrap()
762                .as_mut()
763                .unwrap()
764                .set_col(name, group_by_index, index as usize, Some(timestamp))
765                .unwrap();
766        } else if let Some(n) = val.as_f64() {
767            self.1
768                .lock()
769                .unwrap()
770                .as_mut()
771                .unwrap()
772                .set_col(name, group_by_index, index as usize, Some(n as i64))
773                .unwrap();
774        } else {
775            tracing::error!("Unhandled datetime value");
776        }
777
778        Ok(())
779    }
780}
781
782#[wasm_bindgen]
783pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
784
785#[wasm_bindgen]
786impl VirtualServer {
787    #[wasm_bindgen(constructor)]
788    pub fn new(handler: Object) -> Result<VirtualServer, JsValue> {
789        Ok(VirtualServer(Rc::new(UnsafeCell::new(
790            virtual_server::VirtualServer::new(JsServerHandler(handler)),
791        ))))
792    }
793
794    #[wasm_bindgen(js_name = "handleRequest")]
795    pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
796        let bytes = bytes.to_vec();
797        let server = self.0.clone();
798
799        ApiFuture::new(async move {
800            // SAFETY:
801            // - WASM is single-threaded
802            // - JS re-entrancy is allowed by design
803            // - VirtualServer must tolerate re-entrant mutation
804            let result = unsafe {
805                (&mut *server.as_ref().get())
806                    .handle_request(bytes::Bytes::from(bytes))
807                    .await
808            };
809
810            match result.get_internal_error() {
811                Ok(x) => Ok(x.to_vec()),
812                Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
813                Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
814            }
815        })
816    }
817}