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::{Features, ResultExt, VirtualServerHandler};
24use perspective_client::{DescribeError, DescribeVerdict, Description, virtual_server};
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
77// This interface is the TypeScript contract for [`JsServerHandler`] below.
78// There is no codegen tying the two together - every method dispatched via
79// `Reflect::get` in this file MUST be declared here, with the exact argument
80// and return types the `Reflect` call sites accept. Keep them in sync when
81// editing either.
82#[wasm_bindgen(typescript_custom_section)]
83const TS_VIRTUAL_SERVER_HANDLER: &'static str = r#"
84/**
85 * A table hosted by a `VirtualServerHandler`, as returned by
86 * `getHostedTables()`. A plain `string` is shorthand for `{ name }`.
87 */
88export interface VirtualHostedTable {
89    name: string;
90    index?: string;
91    limit?: number;
92}
93
94/** One expression's compile error, as reported by `tableDescribe`. */
95export interface ExpressionError {
96    error_message: string;
97    line: number;
98    column: number;
99}
100
101/** The verdict of `VirtualServerHandler.tableDescribe`. */
102export type TableDescription =
103    | {
104          expression_schema: Record<string, ColumnType>;
105          view_schema: Record<string, ColumnType>;
106      }
107    | {
108          expression_schema?: Record<string, ColumnType>;
109          expression_errors: Record<string, ExpressionError>;
110      }
111    | { config_error: string };
112
113/**
114 * Handler interface that you implement to provide custom data sources.
115 *
116 * All methods will be called by the `VirtualServer` when handling protocol
117 * messages from Perspective clients. Methods can return values directly or
118 * return Promises for asynchronous operations (e.g., database queries).
119 * Optional methods fall back to defaults documented per-method.
120 */
121export interface VirtualServerHandler {
122    getHostedTables():
123        | (string | VirtualHostedTable)[]
124        | Promise<(string | VirtualHostedTable)[]>;
125    tableSchema(
126        tableId: string,
127    ): Record<string, ColumnType> | Promise<Record<string, ColumnType>>;
128    tableSize(tableId: string): number | Promise<number>;
129    tableMakeView(
130        tableId: string,
131        viewId: string,
132        config: ViewConfigUpdate,
133    ): void | Promise<void>;
134    viewDelete(viewId: string): void | Promise<void>;
135    viewGetData(
136        viewId: string,
137        config: ViewConfig,
138        schema: Record<string, ColumnType>,
139        viewport: ViewWindow,
140        dataSlice: VirtualDataSlice,
141    ): void | Promise<void>;
142
143    /** Defaults to `tableSchema(viewId)`. */
144    viewSchema?(
145        viewId: string,
146        config: ViewConfig,
147    ): Record<string, ColumnType> | Promise<Record<string, ColumnType>>;
148
149    /** Defaults to `tableSize(viewId)`. */
150    viewSize?(viewId: string): number | Promise<number>;
151
152    /** Defaults to the length of `tableSchema(tableId)`. */
153    tableColumnsSize?(tableId: string): number | Promise<number>;
154
155    /** Defaults to the length of `viewSchema(viewId, config)`. */
156    viewColumnSize?(
157        viewId: string,
158        config: ViewConfig,
159    ): number | Promise<number>;
160
161    /**
162     * Validate a complete view config against a table and report the schema
163     * a view built from it would have, without creating one.
164     */
165    tableDescribe(
166        tableId: string,
167        config: ViewConfig,
168    ): TableDescription | Promise<TableDescription>;
169
170    viewGetMinMax?(
171        viewId: string,
172        columnName: string,
173        config: ViewConfig,
174    ): { min: Scalar; max: Scalar } | Promise<{ min: Scalar; max: Scalar }>;
175
176    /** Defaults to no optional features. */
177    getFeatures?(): Features | Promise<Features>;
178
179    /** Defaults to port `0`. */
180    tableMakePort?(): number | Promise<number>;
181
182    makeTable?(
183        tableId: string,
184        data: string | Uint8Array,
185    ): void | Promise<void>;
186    viewMakeTable?(
187        viewId: string,
188        tableId: string,
189        config: ViewConfig,
190        schema?: Record<string, ColumnType>,
191    ): void | Promise<void>;
192}
193"#;
194
195#[wasm_bindgen]
196extern "C" {
197    #[wasm_bindgen(typescript_type = "VirtualServerHandler")]
198    pub type JsVirtualServerHandler;
199}
200
201pub struct JsServerHandler(Object);
202
203impl JsServerHandler {
204    fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
205        let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
206        let func = func
207            .dyn_ref::<js_sys::Function>()
208            .ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
209        Ok(func.apply(&self.0, args)?)
210    }
211
212    async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
213        let result = self.call_method_js(method, args)?;
214
215        // Check if result is a Promise
216        if result.is_instance_of::<js_sys::Promise>() {
217            let promise = js_sys::Promise::from(result);
218            JsFuture::from(promise).await.map_err(JsError)
219        } else {
220            Ok(result)
221        }
222    }
223}
224
225impl VirtualServerHandler for JsServerHandler {
226    type Error = JsError;
227
228    fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
229        let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
230            .map(|val| !val.is_undefined())
231            .unwrap_or(false);
232
233        if !has_method {
234            return Box::pin(async { Ok(Features::default()) });
235        }
236
237        let handler = self.0.clone();
238        Box::pin(async move {
239            let this = JsServerHandler(handler);
240            let args = Array::new();
241            let result = this.call_method_js_async("getFeatures", &args).await?;
242            Ok(serde_wasm_bindgen::from_value(result)?)
243        })
244    }
245
246    fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
247        let handler = self.0.clone();
248        Box::pin(async move {
249            let this = JsServerHandler(handler);
250            let args = Array::new();
251            let result = this.call_method_js_async("getHostedTables", &args).await?;
252            let array = result.dyn_ref::<Array>().ok_or_else(|| {
253                JsError(JsValue::from_str("getHostedTables must return an array"))
254            })?;
255
256            let mut tables = Vec::new();
257            for i in 0..array.length() {
258                let item = array.get(i);
259                if let Some(s) = item.as_string() {
260                    tables.push(HostedTable {
261                        entity_id: s,
262                        index: None,
263                        limit: None,
264                    });
265                } else if item.is_object() {
266                    let name = Reflect::get(&item, &JsValue::from_str("name"))?
267                        .as_string()
268                        .ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
269                    let index = Reflect::get(&item, &JsValue::from_str("index"))
270                        .ok()
271                        .and_then(|v| v.as_string());
272                    let limit = Reflect::get(&item, &JsValue::from_str("limit"))
273                        .ok()
274                        .and_then(|v| v.as_f64().map(|x| x as u32));
275                    tables.push(HostedTable {
276                        entity_id: name,
277                        index,
278                        limit,
279                    });
280                }
281            }
282            Ok(tables)
283        })
284    }
285
286    fn table_schema(
287        &self,
288        table_id: &str,
289    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
290        let handler = self.0.clone();
291        let table_id = table_id.to_string();
292        Box::pin(async move {
293            let this = JsServerHandler(handler);
294            let args = Array::new();
295            args.push(&JsValue::from_str(&table_id));
296            let result = this.call_method_js_async("tableSchema", &args).await?;
297            let obj = result
298                .dyn_ref::<Object>()
299                .ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
300
301            let mut schema = IndexMap::new();
302            let entries = Object::entries(obj);
303            for i in 0..entries.length() {
304                let entry = entries.get(i);
305                let entry_array = entry.dyn_ref::<Array>().unwrap();
306                let key = entry_array.get(0).as_string().unwrap();
307                let value = entry_array.get(1).as_string().unwrap();
308                schema.insert(key, ColumnType::from_str(&value).unwrap());
309            }
310            Ok(schema)
311        })
312    }
313
314    fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
315        let handler = self.0.clone();
316        let table_id = table_id.to_string();
317        Box::pin(async move {
318            let this = JsServerHandler(handler);
319            let args = Array::new();
320            args.push(&JsValue::from_str(&table_id));
321            let result = this.call_method_js_async("tableSize", &args).await?;
322            result
323                .as_f64()
324                .map(|x| x as u32)
325                .ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
326        })
327    }
328
329    fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
330        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
331            .map(|val| !val.is_undefined())
332            .unwrap_or(false);
333
334        let handler = self.0.clone();
335        let view_id = view_id.to_string();
336        Box::pin(async move {
337            let this = JsServerHandler(handler);
338            let args = Array::new();
339            args.push(&JsValue::from_str(&view_id));
340            if has_method {
341                let result = this.call_method_js_async("tableColumnsSize", &args).await?;
342                result.as_f64().map(|x| x as u32).ok_or_else(|| {
343                    JsError(JsValue::from_str(
344                        "tableColumnsSize must
345    return a number",
346                    ))
347                })
348            } else {
349                Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
350            }
351        })
352    }
353
354    fn table_describe(
355        &mut self,
356        table_id: &str,
357        config: &perspective_client::config::ViewConfig,
358    ) -> HandlerFuture<Result<Result<Description, DescribeError>, Self::Error>> {
359        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableDescribe"))
360            .map(|val| !val.is_undefined())
361            .unwrap_or(false);
362
363        let handler = self.0.clone();
364        let table_id = table_id.to_string();
365        let config_value = JsValue::from_serde_ext(config);
366        Box::pin(async move {
367            if !has_method {
368                return Err(JsError(JsValue::from_str(
369                    "`tableDescribe` is required of a `VirtualServerHandler`",
370                )));
371            }
372
373            let this = JsServerHandler(handler);
374            let args = Array::new();
375            args.push(&JsValue::from_str(&table_id));
376            args.push(&config_value?);
377            let result = this.call_method_js_async("tableDescribe", &args).await?;
378            let verdict: DescribeVerdict = result.into_serde_ext().map_err(|e| {
379                JsError(JsValue::from_str(&format!(
380                    "`tableDescribe` must return a `TableDescription`: {}",
381                    e
382                )))
383            })?;
384
385            Result::<Description, DescribeError>::try_from(verdict)
386                .map_err(|e| JsError(JsValue::from_str(&e.to_string())))
387        })
388    }
389
390    fn table_make_view(
391        &mut self,
392        table_id: &str,
393        view_id: &str,
394        config: &mut perspective_client::config::ViewConfigUpdate,
395    ) -> HandlerFuture<Result<String, Self::Error>> {
396        let handler = self.0.clone();
397        let table_id = table_id.to_string();
398        let view_id = view_id.to_string();
399        let config = config.clone();
400        Box::pin(async move {
401            let this = JsServerHandler(handler);
402            let args = Array::new();
403            args.push(&JsValue::from_str(&table_id));
404            args.push(&JsValue::from_str(&view_id));
405            args.push(&JsValue::from_serde_ext(&config)?);
406            let _ = this.call_method_js_async("tableMakeView", &args).await?;
407            Ok(view_id.to_string())
408        })
409    }
410
411    fn view_schema(
412        &self,
413        view_id: &str,
414        config: &perspective_client::config::ViewConfig,
415    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
416        let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
417            .is_ok_and(|v| !v.is_undefined());
418
419        let handler = self.0.clone();
420        let view_id = view_id.to_string();
421        let config_value = JsValue::from_serde_ext(config).ok();
422
423        Box::pin(async move {
424            let this = JsServerHandler(handler);
425            let args = Array::new();
426            args.push(&JsValue::from_str(&view_id));
427            if let Some(cv) = config_value {
428                args.push(&cv);
429            }
430
431            let result = this
432                .call_method_js_async(
433                    if has_view_schema {
434                        "viewSchema"
435                    } else {
436                        "tableSchema"
437                    },
438                    &args,
439                )
440                .await?;
441
442            let obj = result
443                .dyn_ref::<Object>()
444                .ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
445
446            let mut schema = IndexMap::new();
447            let entries = Object::entries(obj);
448            for i in 0..entries.length() {
449                let entry = entries.get(i);
450                let entry_array = entry.dyn_ref::<Array>().unwrap();
451                let key = entry_array.get(0).as_string().unwrap();
452                let value = entry_array.get(1).as_string().unwrap();
453                schema.insert(key, ColumnType::from_str(&value).unwrap());
454            }
455
456            Ok(schema)
457        })
458    }
459
460    fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
461        let handler = self.0.clone();
462        let view_id = view_id.to_string();
463        let has_view_size =
464            Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
465
466        Box::pin(async move {
467            let this = JsServerHandler(handler);
468            let args = Array::new();
469            args.push(&JsValue::from_str(&view_id));
470            let result = this
471                .call_method_js_async(
472                    if has_view_size {
473                        "viewSize"
474                    } else {
475                        "tableSize"
476                    },
477                    &args,
478                )
479                .await?;
480
481            result
482                .as_f64()
483                .map(|x| x as u32)
484                .ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
485        })
486    }
487
488    fn view_column_size(
489        &self,
490        view_id: &str,
491        config: &perspective_client::config::ViewConfig,
492    ) -> HandlerFuture<Result<u32, Self::Error>> {
493        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
494            .map(|val| !val.is_undefined())
495            .unwrap_or(false);
496
497        let handler = self.0.clone();
498        let view_id = view_id.to_string();
499        let config_value = JsValue::from_serde_ext(config).unwrap();
500        let config = config.clone();
501        Box::pin(async move {
502            let this = JsServerHandler(handler);
503            let args = Array::new();
504            args.push(&JsValue::from_str(&view_id));
505            args.push(&config_value);
506            if has_method {
507                let result = this.call_method_js_async("viewColumnSize", &args).await?;
508                result.as_f64().map(|x| x as u32).ok_or_else(|| {
509                    JsError(JsValue::from_str("viewColumnSize must return a number"))
510                })
511            } else {
512                Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
513            }
514        })
515    }
516
517    fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
518        let handler = self.0.clone();
519        let view_id = view_id.to_string();
520        Box::pin(async move {
521            let this = JsServerHandler(handler);
522            let args = Array::new();
523            args.push(&JsValue::from_str(&view_id));
524            this.call_method_js_async("viewDelete", &args).await?;
525            Ok(())
526        })
527    }
528
529    fn table_make_port(
530        &self,
531        _req: &perspective_client::proto::TableMakePortReq,
532    ) -> HandlerFuture<Result<u32, Self::Error>> {
533        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
534            .map(|val| !val.is_undefined())
535            .unwrap_or(false);
536
537        if !has_method {
538            return Box::pin(async { Ok(0) });
539        }
540
541        let handler = self.0.clone();
542        Box::pin(async move {
543            let this = JsServerHandler(handler);
544            let args = Array::new();
545            let result = this.call_method_js_async("tableMakePort", &args).await?;
546            result
547                .as_f64()
548                .map(|x| x as u32)
549                .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
550        })
551    }
552
553    fn view_make_table(
554        &mut self,
555        view_id: &str,
556        table_id: &str,
557        config: &perspective_client::config::ViewConfig,
558        schema: Option<&IndexMap<String, ColumnType>>,
559    ) -> HandlerFuture<Result<(), Self::Error>> {
560        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewMakeTable"))
561            .map(|val| !val.is_undefined())
562            .unwrap_or(false);
563
564        if !has_method {
565            return Box::pin(async {
566                Err(JsError(JsValue::from_str("viewMakeTable not implemented")))
567            });
568        }
569
570        let handler = self.0.clone();
571        let view_id = view_id.to_string();
572        let table_id = table_id.to_string();
573        let config_value = JsValue::from_serde_ext(config);
574        let schema_value = schema.map(|schema| {
575            let object = Object::new();
576            for (name, ty) in schema {
577                let _ = Reflect::set(
578                    &object,
579                    &JsValue::from_str(name),
580                    &JsValue::from_str(&ty.to_string()),
581                );
582            }
583
584            JsValue::from(object)
585        });
586
587        Box::pin(async move {
588            let this = JsServerHandler(handler);
589            let args = Array::new();
590            args.push(&JsValue::from_str(&view_id));
591            args.push(&JsValue::from_str(&table_id));
592            args.push(&config_value?);
593            if let Some(schema_value) = schema_value {
594                args.push(&schema_value);
595            }
596
597            this.call_method_js_async("viewMakeTable", &args).await?;
598            Ok(())
599        })
600    }
601
602    fn make_table(
603        &mut self,
604        table_id: &str,
605        data: &perspective_client::proto::MakeTableData,
606    ) -> HandlerFuture<Result<(), Self::Error>> {
607        let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
608            .map(|val| !val.is_undefined())
609            .unwrap_or(false);
610
611        if !has_method {
612            return Box::pin(async {
613                Err(JsError(JsValue::from_str("makeTable not implemented")))
614            });
615        }
616
617        let handler = self.0.clone();
618        let table_id = table_id.to_string();
619        use perspective_client::proto::make_table_data::Data;
620        let data_value = match &data.data {
621            Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
622            Some(Data::FromArrow(arrow)) => {
623                let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
624                JsValue::from(uint8array)
625            },
626            Some(Data::FromRows(rows)) => JsValue::from_str(rows),
627            Some(Data::FromCols(cols)) => JsValue::from_str(cols),
628            Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
629            _ => JsValue::from_str(""),
630        };
631
632        Box::pin(async move {
633            let this = JsServerHandler(handler);
634            let args = Array::new();
635            args.push(&JsValue::from_str(&table_id));
636            args.push(&data_value);
637            this.call_method_js_async("makeTable", &args).await?;
638            Ok(())
639        })
640    }
641
642    fn view_get_min_max(
643        &self,
644        view_id: &str,
645        column_name: &str,
646        config: &perspective_client::config::ViewConfig,
647    ) -> HandlerFuture<
648        Result<
649            (
650                perspective_client::config::Scalar,
651                perspective_client::config::Scalar,
652            ),
653            Self::Error,
654        >,
655    > {
656        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
657            .map(|val| !val.is_undefined())
658            .unwrap_or(false);
659
660        if !has_method {
661            return Box::pin(async {
662                Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
663            });
664        }
665
666        let handler = self.0.clone();
667        let view_id = view_id.to_string();
668        let column_name = column_name.to_string();
669        let config_js = JsValue::from_serde_ext(config).unwrap();
670        Box::pin(async move {
671            let this = JsServerHandler(handler);
672            let args = Array::new();
673            args.push(&JsValue::from_str(&view_id));
674            args.push(&JsValue::from_str(&column_name));
675            args.push(&config_js);
676            let result = this.call_method_js_async("viewGetMinMax", &args).await?;
677            let obj = result.dyn_ref::<Object>().unwrap();
678            let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
679            let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
680            Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
681        })
682    }
683
684    fn view_get_data(
685        &self,
686        view_id: &str,
687        config: &perspective_client::config::ViewConfig,
688        schema: &IndexMap<String, ColumnType>,
689        viewport: &perspective_client::proto::ViewPort,
690    ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
691        let handler = self.0.clone();
692        let view_id = view_id.to_string();
693        let window: JsViewPort = viewport.clone().into();
694        let config_value = JsValue::from_serde_ext(config).unwrap();
695        let window_value = JsValue::from_serde_ext(&window).unwrap();
696        let schema_value = JsValue::from_serde_ext(&schema).unwrap();
697
698        Box::pin(async move {
699            let this = JsServerHandler(handler);
700            let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
701
702            {
703                let args = Array::new();
704                args.push(&JsValue::from_str(&view_id));
705                args.push(&config_value);
706                args.push(&schema_value);
707                args.push(&window_value);
708                args.push(&JsValue::from(data.clone()));
709                this.call_method_js_async("viewGetData", &args).await?;
710            }
711
712            // Lock the mutex and take ownership of the inner data
713            // We can't unwrap the Arc because the JsValue might still hold a
714            // reference
715            let VirtualDataSlice(_obj, arc) = data;
716            let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
717            Ok(slice)
718        })
719    }
720}
721
722#[derive(Serialize, PartialEq)]
723pub struct JsViewPort {
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub start_row: ::core::option::Option<u32>,
726
727    #[serde(default, skip_serializing_if = "Option::is_none")]
728    pub start_col: ::core::option::Option<u32>,
729
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub end_row: ::core::option::Option<u32>,
732
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub end_col: ::core::option::Option<u32>,
735}
736
737impl From<perspective_client::proto::ViewPort> for JsViewPort {
738    fn from(value: perspective_client::proto::ViewPort) -> Self {
739        JsViewPort {
740            start_row: value.start_row,
741            start_col: value.start_col,
742            end_row: value.end_row,
743            end_col: value.end_col,
744        }
745    }
746}
747
748#[wasm_bindgen(js_name = "VirtualDataSlice")]
749#[derive(Clone)]
750pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
751
752#[wasm_bindgen]
753impl VirtualDataSlice {
754    #[wasm_bindgen(constructor)]
755    pub fn new(config: JsViewConfig) -> Self {
756        VirtualDataSlice(
757            Object::new(),
758            Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
759                config.into_serde_ext().unwrap(),
760            )))),
761        )
762    }
763
764    #[wasm_bindgen(js_name = "fromArrowIpc")]
765    pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
766        self.1
767            .lock()
768            .unwrap()
769            .as_mut()
770            .unwrap()
771            .from_arrow_ipc(&ipc.to_vec())
772            .map_err(|e| JsValue::from_str(&e.to_string()))
773    }
774
775    #[wasm_bindgen(js_name = "setCol")]
776    pub fn set_col(
777        &self,
778        dtype: &str,
779        name: &str,
780        index: u32,
781        val: JsValue,
782        group_by_index: Option<usize>,
783    ) -> Result<(), JsValue> {
784        match dtype {
785            "string" => self.set_string_col(name, index, val, group_by_index),
786            "integer" => self.set_integer_col(name, index, val, group_by_index),
787            "float" => self.set_float_col(name, index, val, group_by_index),
788            "date" => self.set_datetime_col(name, index, val, group_by_index),
789            "datetime" => self.set_datetime_col(name, index, val, group_by_index),
790            "boolean" => self.set_boolean_col(name, index, val, group_by_index),
791            _ => Err(JsValue::from_str("Unknown type")),
792        }
793    }
794
795    #[wasm_bindgen(js_name = "setStringCol")]
796    pub fn set_string_col(
797        &self,
798        name: &str,
799        index: u32,
800        val: JsValue,
801        group_by_index: Option<usize>,
802    ) -> Result<(), JsValue> {
803        if val.is_null() || val.is_undefined() {
804            self.1
805                .lock()
806                .unwrap()
807                .as_mut()
808                .unwrap()
809                .set_col(name, group_by_index, index as usize, None as Option<String>)
810                .unwrap();
811        } else if let Some(s) = val.as_string() {
812            self.1
813                .lock()
814                .unwrap()
815                .as_mut()
816                .unwrap()
817                .set_col(name, group_by_index, index as usize, Some(s))
818                .unwrap();
819        } else {
820            tracing::error!("Unhandled string value");
821        }
822        Ok(())
823    }
824
825    #[wasm_bindgen(js_name = "setIntegerCol")]
826    pub fn set_integer_col(
827        &self,
828        name: &str,
829        index: u32,
830        val: JsValue,
831        group_by_index: Option<usize>,
832    ) -> Result<(), JsValue> {
833        if val.is_null() || val.is_undefined() {
834            self.1
835                .lock()
836                .unwrap()
837                .as_mut()
838                .unwrap()
839                .set_col(name, group_by_index, index as usize, None as Option<i32>)
840                .unwrap();
841        } else if let Some(n) = val.as_f64() {
842            self.1
843                .lock()
844                .unwrap()
845                .as_mut()
846                .unwrap()
847                .set_col(name, group_by_index, index as usize, Some(n as i32))
848                .unwrap();
849        } else {
850            tracing::error!("Unhandled integer value");
851        }
852        Ok(())
853    }
854
855    #[wasm_bindgen(js_name = "setFloatCol")]
856    pub fn set_float_col(
857        &self,
858        name: &str,
859        index: u32,
860        val: JsValue,
861        group_by_index: Option<usize>,
862    ) -> Result<(), JsValue> {
863        if val.is_null() || val.is_undefined() {
864            self.1
865                .lock()
866                .unwrap()
867                .as_mut()
868                .unwrap()
869                .set_col(name, group_by_index, index as usize, None as Option<f64>)
870                .unwrap();
871        } else if let Some(n) = val.as_f64() {
872            self.1
873                .lock()
874                .unwrap()
875                .as_mut()
876                .unwrap()
877                .set_col(name, group_by_index, index as usize, Some(n))
878                .unwrap();
879        } else {
880            tracing::error!("Unhandled float value");
881        }
882        Ok(())
883    }
884
885    #[wasm_bindgen(js_name = "setBooleanCol")]
886    pub fn set_boolean_col(
887        &self,
888        name: &str,
889        index: u32,
890        val: JsValue,
891        group_by_index: Option<usize>,
892    ) -> Result<(), JsValue> {
893        if val.is_null() || val.is_undefined() {
894            self.1
895                .lock()
896                .unwrap()
897                .as_mut()
898                .unwrap()
899                .set_col(name, group_by_index, index as usize, None as Option<bool>)
900                .unwrap();
901        } else if let Some(b) = val.as_bool() {
902            self.1
903                .lock()
904                .unwrap()
905                .as_mut()
906                .unwrap()
907                .set_col(name, group_by_index, index as usize, Some(b))
908                .unwrap();
909        } else {
910            tracing::error!("Unhandled boolean value");
911        }
912        Ok(())
913    }
914
915    #[wasm_bindgen(js_name = "setDatetimeCol")]
916    pub fn set_datetime_col(
917        &self,
918        name: &str,
919        index: u32,
920        val: JsValue,
921        group_by_index: Option<usize>,
922    ) -> Result<(), JsValue> {
923        if val.is_null() || val.is_undefined() {
924            self.1
925                .lock()
926                .unwrap()
927                .as_mut()
928                .unwrap()
929                .set_col(name, group_by_index, index as usize, None as Option<i64>)
930                .unwrap();
931        } else if let Some(date) = val.dyn_ref::<Date>() {
932            let timestamp = date.get_time() as i64;
933            self.1
934                .lock()
935                .unwrap()
936                .as_mut()
937                .unwrap()
938                .set_col(name, group_by_index, index as usize, Some(timestamp))
939                .unwrap();
940        } else if let Some(n) = val.as_f64() {
941            self.1
942                .lock()
943                .unwrap()
944                .as_mut()
945                .unwrap()
946                .set_col(name, group_by_index, index as usize, Some(n as i64))
947                .unwrap();
948        } else {
949            tracing::error!("Unhandled datetime value");
950        }
951
952        Ok(())
953    }
954}
955
956#[wasm_bindgen]
957pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
958
959#[wasm_bindgen]
960impl VirtualServer {
961    #[wasm_bindgen(constructor)]
962    pub fn new(handler: JsVirtualServerHandler) -> Result<VirtualServer, JsValue> {
963        Ok(VirtualServer(Rc::new(UnsafeCell::new(
964            virtual_server::VirtualServer::new(JsServerHandler(handler.unchecked_into())),
965        ))))
966    }
967
968    #[wasm_bindgen(js_name = "handleRequest")]
969    pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
970        let bytes = bytes.to_vec();
971        let server = self.0.clone();
972
973        ApiFuture::new(async move {
974            // SAFETY:
975            // - WASM is single-threaded
976            // - JS re-entrancy is allowed by design
977            // - VirtualServer must tolerate re-entrant mutation
978            let result = unsafe {
979                (&mut *server.as_ref().get())
980                    .handle_request(bytes::Bytes::from(bytes))
981                    .await
982            };
983
984            match result.get_internal_error() {
985                Ok(x) => Ok(x.to_vec()),
986                Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
987                Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
988            }
989        })
990    }
991}