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 | 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    /**
177     * Expand the row at `rowIndex` of a `"rollup"` mode `group_by` view to
178     * show its children, each collapsed, returning the number of rows shown.
179     * Required when `getFeatures()` declares `expand_collapse`.
180     */
181    viewExpand?(
182        viewId: string,
183        config: ViewConfig,
184        rowIndex: number,
185    ): number | Promise<number>;
186
187    /**
188     * Collapse the row at `rowIndex` of a `"rollup"` mode `group_by` view,
189     * discarding its descendants' expansion, returning the number of rows
190     * hidden. Required when `getFeatures()` declares `expand_collapse`.
191     */
192    viewCollapse?(
193        viewId: string,
194        config: ViewConfig,
195        rowIndex: number,
196    ): number | Promise<number>;
197
198    /**
199     * Expand every row of a `"rollup"` mode `group_by` view whose path is no
200     * longer than `depth` and collapse the rest. Required when
201     * `getFeatures()` declares `expand_collapse`.
202     */
203    viewSetDepth?(
204        viewId: string,
205        config: ViewConfig,
206        depth: number,
207    ): void | Promise<void>;
208
209    /** Defaults to no optional features. */
210    getFeatures?(): Features | Promise<Features>;
211
212    /** Defaults to port `0`. */
213    tableMakePort?(): number | Promise<number>;
214
215    makeTable?(
216        tableId: string,
217        data: string | Uint8Array,
218    ): void | Promise<void>;
219    viewMakeTable?(
220        viewId: string,
221        tableId: string,
222        config: ViewConfig,
223        schema?: Record<string, ColumnType>,
224    ): void | Promise<void>;
225}
226"#;
227
228#[wasm_bindgen]
229extern "C" {
230    #[wasm_bindgen(typescript_type = "VirtualServerHandler")]
231    pub type JsVirtualServerHandler;
232}
233
234pub struct JsServerHandler(Object);
235
236impl JsServerHandler {
237    fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
238        let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
239        let func = func
240            .dyn_ref::<js_sys::Function>()
241            .ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
242        Ok(func.apply(&self.0, args)?)
243    }
244
245    /// Calls an optional `(viewId, config, n)` tree method, returning its
246    /// numeric result (`0` when it returns nothing).
247    fn call_tree_method(
248        &self,
249        method: &'static str,
250        view_id: &str,
251        config: &perspective_client::config::ViewConfig,
252        arg: u32,
253    ) -> impl Future<Output = Result<u32, JsError>> + 'static {
254        let has_method = Reflect::get(&self.0, &JsValue::from_str(method))
255            .map(|val| !val.is_undefined())
256            .unwrap_or(false);
257
258        let handler = self.0.clone();
259        let view_id = view_id.to_string();
260        let config_value = JsValue::from_serde_ext(config);
261        async move {
262            if !has_method {
263                return Err(JsError(JsValue::from_str(&format!(
264                    "{} not implemented",
265                    method
266                ))));
267            }
268
269            let this = JsServerHandler(handler);
270            let args = Array::new();
271            args.push(&JsValue::from_str(&view_id));
272            args.push(&config_value?);
273            args.push(&JsValue::from(arg));
274            let result = this.call_method_js_async(method, &args).await?;
275            Ok(result.as_f64().map_or(0, |x| x as u32))
276        }
277    }
278
279    async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
280        let result = self.call_method_js(method, args)?;
281
282        // Check if result is a Promise
283        if result.is_instance_of::<js_sys::Promise>() {
284            let promise = js_sys::Promise::from(result);
285            JsFuture::from(promise).await.map_err(JsError)
286        } else {
287            Ok(result)
288        }
289    }
290}
291
292impl VirtualServerHandler for JsServerHandler {
293    type Error = JsError;
294
295    fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
296        let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
297            .map(|val| !val.is_undefined())
298            .unwrap_or(false);
299
300        if !has_method {
301            return Box::pin(async { Ok(Features::default()) });
302        }
303
304        let handler = self.0.clone();
305        Box::pin(async move {
306            let this = JsServerHandler(handler);
307            let args = Array::new();
308            let result = this.call_method_js_async("getFeatures", &args).await?;
309            Ok(serde_wasm_bindgen::from_value(result)?)
310        })
311    }
312
313    fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
314        let handler = self.0.clone();
315        Box::pin(async move {
316            let this = JsServerHandler(handler);
317            let args = Array::new();
318            let result = this.call_method_js_async("getHostedTables", &args).await?;
319            let array = result.dyn_ref::<Array>().ok_or_else(|| {
320                JsError(JsValue::from_str("getHostedTables must return an array"))
321            })?;
322
323            let mut tables = Vec::new();
324            for i in 0..array.length() {
325                let item = array.get(i);
326                if let Some(s) = item.as_string() {
327                    tables.push(HostedTable {
328                        entity_id: s,
329                        index: Vec::new(),
330                        limit: None,
331                    });
332                } else if item.is_object() {
333                    let name = Reflect::get(&item, &JsValue::from_str("name"))?
334                        .as_string()
335                        .ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
336                    let index = Reflect::get(&item, &JsValue::from_str("index"))
337                        .ok()
338                        .map(|v| {
339                            if let Some(s) = v.as_string() {
340                                vec![s]
341                            } else if Array::is_array(&v) {
342                                Array::from(&v)
343                                    .iter()
344                                    .filter_map(|x| x.as_string())
345                                    .collect()
346                            } else {
347                                Vec::new()
348                            }
349                        })
350                        .unwrap_or_default();
351                    let limit = Reflect::get(&item, &JsValue::from_str("limit"))
352                        .ok()
353                        .and_then(|v| v.as_f64().map(|x| x as u32));
354                    tables.push(HostedTable {
355                        entity_id: name,
356                        index,
357                        limit,
358                    });
359                }
360            }
361            Ok(tables)
362        })
363    }
364
365    fn table_schema(
366        &self,
367        table_id: &str,
368    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
369        let handler = self.0.clone();
370        let table_id = table_id.to_string();
371        Box::pin(async move {
372            let this = JsServerHandler(handler);
373            let args = Array::new();
374            args.push(&JsValue::from_str(&table_id));
375            let result = this.call_method_js_async("tableSchema", &args).await?;
376            let obj = result
377                .dyn_ref::<Object>()
378                .ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
379
380            let mut schema = IndexMap::new();
381            let entries = Object::entries(obj);
382            for i in 0..entries.length() {
383                let entry = entries.get(i);
384                let entry_array = entry.dyn_ref::<Array>().unwrap();
385                let key = entry_array.get(0).as_string().unwrap();
386                let value = entry_array.get(1).as_string().unwrap();
387                schema.insert(key, ColumnType::from_str(&value).unwrap());
388            }
389            Ok(schema)
390        })
391    }
392
393    fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
394        let handler = self.0.clone();
395        let table_id = table_id.to_string();
396        Box::pin(async move {
397            let this = JsServerHandler(handler);
398            let args = Array::new();
399            args.push(&JsValue::from_str(&table_id));
400            let result = this.call_method_js_async("tableSize", &args).await?;
401            result
402                .as_f64()
403                .map(|x| x as u32)
404                .ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
405        })
406    }
407
408    fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
409        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
410            .map(|val| !val.is_undefined())
411            .unwrap_or(false);
412
413        let handler = self.0.clone();
414        let view_id = view_id.to_string();
415        Box::pin(async move {
416            let this = JsServerHandler(handler);
417            let args = Array::new();
418            args.push(&JsValue::from_str(&view_id));
419            if has_method {
420                let result = this.call_method_js_async("tableColumnsSize", &args).await?;
421                result.as_f64().map(|x| x as u32).ok_or_else(|| {
422                    JsError(JsValue::from_str(
423                        "tableColumnsSize must
424    return a number",
425                    ))
426                })
427            } else {
428                Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
429            }
430        })
431    }
432
433    fn table_describe(
434        &mut self,
435        table_id: &str,
436        config: &perspective_client::config::ViewConfig,
437    ) -> HandlerFuture<Result<Result<Description, DescribeError>, Self::Error>> {
438        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableDescribe"))
439            .map(|val| !val.is_undefined())
440            .unwrap_or(false);
441
442        let handler = self.0.clone();
443        let table_id = table_id.to_string();
444        let config_value = JsValue::from_serde_ext(config);
445        Box::pin(async move {
446            if !has_method {
447                return Err(JsError(JsValue::from_str(
448                    "`tableDescribe` is required of a `VirtualServerHandler`",
449                )));
450            }
451
452            let this = JsServerHandler(handler);
453            let args = Array::new();
454            args.push(&JsValue::from_str(&table_id));
455            args.push(&config_value?);
456            let result = this.call_method_js_async("tableDescribe", &args).await?;
457            let verdict: DescribeVerdict = result.into_serde_ext().map_err(|e| {
458                JsError(JsValue::from_str(&format!(
459                    "`tableDescribe` must return a `TableDescription`: {}",
460                    e
461                )))
462            })?;
463
464            Result::<Description, DescribeError>::try_from(verdict)
465                .map_err(|e| JsError(JsValue::from_str(&e.to_string())))
466        })
467    }
468
469    fn table_make_view(
470        &mut self,
471        table_id: &str,
472        view_id: &str,
473        config: &mut perspective_client::config::ViewConfigUpdate,
474    ) -> HandlerFuture<Result<String, Self::Error>> {
475        let handler = self.0.clone();
476        let table_id = table_id.to_string();
477        let view_id = view_id.to_string();
478        let config = config.clone();
479        Box::pin(async move {
480            let this = JsServerHandler(handler);
481            let args = Array::new();
482            args.push(&JsValue::from_str(&table_id));
483            args.push(&JsValue::from_str(&view_id));
484            args.push(&JsValue::from_serde_ext(&config)?);
485            let _ = this.call_method_js_async("tableMakeView", &args).await?;
486            Ok(view_id.to_string())
487        })
488    }
489
490    fn view_schema(
491        &self,
492        view_id: &str,
493        config: &perspective_client::config::ViewConfig,
494    ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
495        let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
496            .is_ok_and(|v| !v.is_undefined());
497
498        let handler = self.0.clone();
499        let view_id = view_id.to_string();
500        let config_value = JsValue::from_serde_ext(config).ok();
501
502        Box::pin(async move {
503            let this = JsServerHandler(handler);
504            let args = Array::new();
505            args.push(&JsValue::from_str(&view_id));
506            if let Some(cv) = config_value {
507                args.push(&cv);
508            }
509
510            let result = this
511                .call_method_js_async(
512                    if has_view_schema {
513                        "viewSchema"
514                    } else {
515                        "tableSchema"
516                    },
517                    &args,
518                )
519                .await?;
520
521            let obj = result
522                .dyn_ref::<Object>()
523                .ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
524
525            let mut schema = IndexMap::new();
526            let entries = Object::entries(obj);
527            for i in 0..entries.length() {
528                let entry = entries.get(i);
529                let entry_array = entry.dyn_ref::<Array>().unwrap();
530                let key = entry_array.get(0).as_string().unwrap();
531                let value = entry_array.get(1).as_string().unwrap();
532                schema.insert(key, ColumnType::from_str(&value).unwrap());
533            }
534
535            Ok(schema)
536        })
537    }
538
539    fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
540        let handler = self.0.clone();
541        let view_id = view_id.to_string();
542        let has_view_size =
543            Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
544
545        Box::pin(async move {
546            let this = JsServerHandler(handler);
547            let args = Array::new();
548            args.push(&JsValue::from_str(&view_id));
549            let result = this
550                .call_method_js_async(
551                    if has_view_size {
552                        "viewSize"
553                    } else {
554                        "tableSize"
555                    },
556                    &args,
557                )
558                .await?;
559
560            result
561                .as_f64()
562                .map(|x| x as u32)
563                .ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
564        })
565    }
566
567    fn view_column_size(
568        &self,
569        view_id: &str,
570        config: &perspective_client::config::ViewConfig,
571    ) -> HandlerFuture<Result<u32, Self::Error>> {
572        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
573            .map(|val| !val.is_undefined())
574            .unwrap_or(false);
575
576        let handler = self.0.clone();
577        let view_id = view_id.to_string();
578        let config_value = JsValue::from_serde_ext(config).unwrap();
579        let config = config.clone();
580        Box::pin(async move {
581            let this = JsServerHandler(handler);
582            let args = Array::new();
583            args.push(&JsValue::from_str(&view_id));
584            args.push(&config_value);
585            if has_method {
586                let result = this.call_method_js_async("viewColumnSize", &args).await?;
587                result.as_f64().map(|x| x as u32).ok_or_else(|| {
588                    JsError(JsValue::from_str("viewColumnSize must return a number"))
589                })
590            } else {
591                Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
592            }
593        })
594    }
595
596    fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
597        let handler = self.0.clone();
598        let view_id = view_id.to_string();
599        Box::pin(async move {
600            let this = JsServerHandler(handler);
601            let args = Array::new();
602            args.push(&JsValue::from_str(&view_id));
603            this.call_method_js_async("viewDelete", &args).await?;
604            Ok(())
605        })
606    }
607
608    fn table_make_port(
609        &self,
610        _req: &perspective_client::proto::TableMakePortReq,
611    ) -> HandlerFuture<Result<u32, Self::Error>> {
612        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
613            .map(|val| !val.is_undefined())
614            .unwrap_or(false);
615
616        if !has_method {
617            return Box::pin(async { Ok(0) });
618        }
619
620        let handler = self.0.clone();
621        Box::pin(async move {
622            let this = JsServerHandler(handler);
623            let args = Array::new();
624            let result = this.call_method_js_async("tableMakePort", &args).await?;
625            result
626                .as_f64()
627                .map(|x| x as u32)
628                .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
629        })
630    }
631
632    fn view_make_table(
633        &mut self,
634        view_id: &str,
635        table_id: &str,
636        config: &perspective_client::config::ViewConfig,
637        schema: Option<&IndexMap<String, ColumnType>>,
638    ) -> HandlerFuture<Result<(), Self::Error>> {
639        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewMakeTable"))
640            .map(|val| !val.is_undefined())
641            .unwrap_or(false);
642
643        if !has_method {
644            return Box::pin(async {
645                Err(JsError(JsValue::from_str("viewMakeTable not implemented")))
646            });
647        }
648
649        let handler = self.0.clone();
650        let view_id = view_id.to_string();
651        let table_id = table_id.to_string();
652        let config_value = JsValue::from_serde_ext(config);
653        let schema_value = schema.map(|schema| {
654            let object = Object::new();
655            for (name, ty) in schema {
656                let _ = Reflect::set(
657                    &object,
658                    &JsValue::from_str(name),
659                    &JsValue::from_str(&ty.to_string()),
660                );
661            }
662
663            JsValue::from(object)
664        });
665
666        Box::pin(async move {
667            let this = JsServerHandler(handler);
668            let args = Array::new();
669            args.push(&JsValue::from_str(&view_id));
670            args.push(&JsValue::from_str(&table_id));
671            args.push(&config_value?);
672            if let Some(schema_value) = schema_value {
673                args.push(&schema_value);
674            }
675
676            this.call_method_js_async("viewMakeTable", &args).await?;
677            Ok(())
678        })
679    }
680
681    fn make_table(
682        &mut self,
683        table_id: &str,
684        data: &perspective_client::proto::MakeTableData,
685    ) -> HandlerFuture<Result<(), Self::Error>> {
686        let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
687            .map(|val| !val.is_undefined())
688            .unwrap_or(false);
689
690        if !has_method {
691            return Box::pin(async {
692                Err(JsError(JsValue::from_str("makeTable not implemented")))
693            });
694        }
695
696        let handler = self.0.clone();
697        let table_id = table_id.to_string();
698        use perspective_client::proto::make_table_data::Data;
699        let data_value = match &data.data {
700            Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
701            Some(Data::FromArrow(arrow)) => {
702                let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
703                JsValue::from(uint8array)
704            },
705            Some(Data::FromRows(rows)) => JsValue::from_str(rows),
706            Some(Data::FromCols(cols)) => JsValue::from_str(cols),
707            Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
708            _ => JsValue::from_str(""),
709        };
710
711        Box::pin(async move {
712            let this = JsServerHandler(handler);
713            let args = Array::new();
714            args.push(&JsValue::from_str(&table_id));
715            args.push(&data_value);
716            this.call_method_js_async("makeTable", &args).await?;
717            Ok(())
718        })
719    }
720
721    fn view_get_min_max(
722        &self,
723        view_id: &str,
724        column_name: &str,
725        config: &perspective_client::config::ViewConfig,
726    ) -> HandlerFuture<
727        Result<
728            (
729                perspective_client::config::Scalar,
730                perspective_client::config::Scalar,
731            ),
732            Self::Error,
733        >,
734    > {
735        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
736            .map(|val| !val.is_undefined())
737            .unwrap_or(false);
738
739        if !has_method {
740            return Box::pin(async {
741                Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
742            });
743        }
744
745        let handler = self.0.clone();
746        let view_id = view_id.to_string();
747        let column_name = column_name.to_string();
748        let config_js = JsValue::from_serde_ext(config).unwrap();
749        Box::pin(async move {
750            let this = JsServerHandler(handler);
751            let args = Array::new();
752            args.push(&JsValue::from_str(&view_id));
753            args.push(&JsValue::from_str(&column_name));
754            args.push(&config_js);
755            let result = this.call_method_js_async("viewGetMinMax", &args).await?;
756            let obj = result.dyn_ref::<Object>().unwrap();
757            let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
758            let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
759            Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
760        })
761    }
762
763    fn view_expand(
764        &mut self,
765        view_id: &str,
766        config: &perspective_client::config::ViewConfig,
767        row_index: u32,
768    ) -> HandlerFuture<Result<u32, Self::Error>> {
769        Box::pin(self.call_tree_method("viewExpand", view_id, config, row_index))
770    }
771
772    fn view_collapse(
773        &mut self,
774        view_id: &str,
775        config: &perspective_client::config::ViewConfig,
776        row_index: u32,
777    ) -> HandlerFuture<Result<u32, Self::Error>> {
778        Box::pin(self.call_tree_method("viewCollapse", view_id, config, row_index))
779    }
780
781    fn view_set_depth(
782        &mut self,
783        view_id: &str,
784        config: &perspective_client::config::ViewConfig,
785        depth: u32,
786    ) -> HandlerFuture<Result<(), Self::Error>> {
787        let fut = self.call_tree_method("viewSetDepth", view_id, config, depth);
788        Box::pin(async move {
789            fut.await?;
790            Ok(())
791        })
792    }
793
794    fn view_get_data(
795        &self,
796        view_id: &str,
797        config: &perspective_client::config::ViewConfig,
798        schema: &IndexMap<String, ColumnType>,
799        viewport: &perspective_client::proto::ViewPort,
800    ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
801        let handler = self.0.clone();
802        let view_id = view_id.to_string();
803        let window: JsViewPort = viewport.clone().into();
804        let config_value = JsValue::from_serde_ext(config).unwrap();
805        let window_value = JsValue::from_serde_ext(&window).unwrap();
806        let schema_value = JsValue::from_serde_ext(&schema).unwrap();
807
808        Box::pin(async move {
809            let this = JsServerHandler(handler);
810            let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
811
812            {
813                let args = Array::new();
814                args.push(&JsValue::from_str(&view_id));
815                args.push(&config_value);
816                args.push(&schema_value);
817                args.push(&window_value);
818                args.push(&JsValue::from(data.clone()));
819                this.call_method_js_async("viewGetData", &args).await?;
820            }
821
822            // Lock the mutex and take ownership of the inner data
823            // We can't unwrap the Arc because the JsValue might still hold a
824            // reference
825            let VirtualDataSlice(_obj, arc) = data;
826            let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
827            Ok(slice)
828        })
829    }
830}
831
832#[derive(Serialize, PartialEq)]
833pub struct JsViewPort {
834    #[serde(default, skip_serializing_if = "Option::is_none")]
835    pub start_row: ::core::option::Option<u32>,
836
837    #[serde(default, skip_serializing_if = "Option::is_none")]
838    pub start_col: ::core::option::Option<u32>,
839
840    #[serde(default, skip_serializing_if = "Option::is_none")]
841    pub end_row: ::core::option::Option<u32>,
842
843    #[serde(default, skip_serializing_if = "Option::is_none")]
844    pub end_col: ::core::option::Option<u32>,
845}
846
847impl From<perspective_client::proto::ViewPort> for JsViewPort {
848    fn from(value: perspective_client::proto::ViewPort) -> Self {
849        JsViewPort {
850            start_row: value.start_row,
851            start_col: value.start_col,
852            end_row: value.end_row,
853            end_col: value.end_col,
854        }
855    }
856}
857
858#[wasm_bindgen(js_name = "VirtualDataSlice")]
859#[derive(Clone)]
860pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
861
862#[wasm_bindgen]
863impl VirtualDataSlice {
864    #[wasm_bindgen(constructor)]
865    pub fn new(config: JsViewConfig) -> Self {
866        VirtualDataSlice(
867            Object::new(),
868            Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
869                config.into_serde_ext().unwrap(),
870            )))),
871        )
872    }
873
874    #[wasm_bindgen(js_name = "fromArrowIpc")]
875    pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
876        self.1
877            .lock()
878            .unwrap()
879            .as_mut()
880            .unwrap()
881            .from_arrow_ipc(&ipc.to_vec())
882            .map_err(|e| JsValue::from_str(&e.to_string()))
883    }
884
885    #[wasm_bindgen(js_name = "setCol")]
886    pub fn set_col(
887        &self,
888        dtype: &str,
889        name: &str,
890        index: u32,
891        val: JsValue,
892        group_by_index: Option<usize>,
893    ) -> Result<(), JsValue> {
894        match dtype {
895            "string" => self.set_string_col(name, index, val, group_by_index),
896            "integer" => self.set_integer_col(name, index, val, group_by_index),
897            "float" => self.set_float_col(name, index, val, group_by_index),
898            "date" => self.set_datetime_col(name, index, val, group_by_index),
899            "datetime" => self.set_datetime_col(name, index, val, group_by_index),
900            "boolean" => self.set_boolean_col(name, index, val, group_by_index),
901            _ => Err(JsValue::from_str("Unknown type")),
902        }
903    }
904
905    #[wasm_bindgen(js_name = "setStringCol")]
906    pub fn set_string_col(
907        &self,
908        name: &str,
909        index: u32,
910        val: JsValue,
911        group_by_index: Option<usize>,
912    ) -> Result<(), JsValue> {
913        if val.is_null() || val.is_undefined() {
914            self.1
915                .lock()
916                .unwrap()
917                .as_mut()
918                .unwrap()
919                .set_col(name, group_by_index, index as usize, None as Option<String>)
920                .unwrap();
921        } else if let Some(s) = val.as_string() {
922            self.1
923                .lock()
924                .unwrap()
925                .as_mut()
926                .unwrap()
927                .set_col(name, group_by_index, index as usize, Some(s))
928                .unwrap();
929        } else {
930            tracing::error!("Unhandled string value");
931        }
932        Ok(())
933    }
934
935    #[wasm_bindgen(js_name = "setIntegerCol")]
936    pub fn set_integer_col(
937        &self,
938        name: &str,
939        index: u32,
940        val: JsValue,
941        group_by_index: Option<usize>,
942    ) -> Result<(), JsValue> {
943        if val.is_null() || val.is_undefined() {
944            self.1
945                .lock()
946                .unwrap()
947                .as_mut()
948                .unwrap()
949                .set_col(name, group_by_index, index as usize, None as Option<i32>)
950                .unwrap();
951        } else if let Some(n) = val.as_f64() {
952            self.1
953                .lock()
954                .unwrap()
955                .as_mut()
956                .unwrap()
957                .set_col(name, group_by_index, index as usize, Some(n as i32))
958                .unwrap();
959        } else {
960            tracing::error!("Unhandled integer value");
961        }
962        Ok(())
963    }
964
965    #[wasm_bindgen(js_name = "setFloatCol")]
966    pub fn set_float_col(
967        &self,
968        name: &str,
969        index: u32,
970        val: JsValue,
971        group_by_index: Option<usize>,
972    ) -> Result<(), JsValue> {
973        if val.is_null() || val.is_undefined() {
974            self.1
975                .lock()
976                .unwrap()
977                .as_mut()
978                .unwrap()
979                .set_col(name, group_by_index, index as usize, None as Option<f64>)
980                .unwrap();
981        } else if let Some(n) = val.as_f64() {
982            self.1
983                .lock()
984                .unwrap()
985                .as_mut()
986                .unwrap()
987                .set_col(name, group_by_index, index as usize, Some(n))
988                .unwrap();
989        } else {
990            tracing::error!("Unhandled float value");
991        }
992        Ok(())
993    }
994
995    #[wasm_bindgen(js_name = "setBooleanCol")]
996    pub fn set_boolean_col(
997        &self,
998        name: &str,
999        index: u32,
1000        val: JsValue,
1001        group_by_index: Option<usize>,
1002    ) -> Result<(), JsValue> {
1003        if val.is_null() || val.is_undefined() {
1004            self.1
1005                .lock()
1006                .unwrap()
1007                .as_mut()
1008                .unwrap()
1009                .set_col(name, group_by_index, index as usize, None as Option<bool>)
1010                .unwrap();
1011        } else if let Some(b) = val.as_bool() {
1012            self.1
1013                .lock()
1014                .unwrap()
1015                .as_mut()
1016                .unwrap()
1017                .set_col(name, group_by_index, index as usize, Some(b))
1018                .unwrap();
1019        } else {
1020            tracing::error!("Unhandled boolean value");
1021        }
1022        Ok(())
1023    }
1024
1025    #[wasm_bindgen(js_name = "setDatetimeCol")]
1026    pub fn set_datetime_col(
1027        &self,
1028        name: &str,
1029        index: u32,
1030        val: JsValue,
1031        group_by_index: Option<usize>,
1032    ) -> Result<(), JsValue> {
1033        if val.is_null() || val.is_undefined() {
1034            self.1
1035                .lock()
1036                .unwrap()
1037                .as_mut()
1038                .unwrap()
1039                .set_col(name, group_by_index, index as usize, None as Option<i64>)
1040                .unwrap();
1041        } else if let Some(date) = val.dyn_ref::<Date>() {
1042            let timestamp = date.get_time() as i64;
1043            self.1
1044                .lock()
1045                .unwrap()
1046                .as_mut()
1047                .unwrap()
1048                .set_col(name, group_by_index, index as usize, Some(timestamp))
1049                .unwrap();
1050        } else if let Some(n) = val.as_f64() {
1051            self.1
1052                .lock()
1053                .unwrap()
1054                .as_mut()
1055                .unwrap()
1056                .set_col(name, group_by_index, index as usize, Some(n as i64))
1057                .unwrap();
1058        } else {
1059            tracing::error!("Unhandled datetime value");
1060        }
1061
1062        Ok(())
1063    }
1064}
1065
1066#[wasm_bindgen]
1067pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
1068
1069#[wasm_bindgen]
1070impl VirtualServer {
1071    #[wasm_bindgen(constructor)]
1072    pub fn new(handler: JsVirtualServerHandler) -> Result<VirtualServer, JsValue> {
1073        Ok(VirtualServer(Rc::new(UnsafeCell::new(
1074            virtual_server::VirtualServer::new(JsServerHandler(handler.unchecked_into())),
1075        ))))
1076    }
1077
1078    #[wasm_bindgen(js_name = "handleRequest")]
1079    pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
1080        let bytes = bytes.to_vec();
1081        let server = self.0.clone();
1082
1083        ApiFuture::new(async move {
1084            // SAFETY:
1085            // - WASM is single-threaded
1086            // - JS re-entrancy is allowed by design
1087            // - VirtualServer must tolerate re-entrant mutation
1088            let result = unsafe {
1089                (&mut *server.as_ref().get())
1090                    .handle_request(bytes::Bytes::from(bytes))
1091                    .await
1092            };
1093
1094            match result.get_internal_error() {
1095                Ok(x) => Ok(x.to_vec()),
1096                Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
1097                Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
1098            }
1099        })
1100    }
1101}