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