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_delete(&self, table_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
609        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableDelete"))
610            .map(|val| !val.is_undefined())
611            .unwrap_or(false);
612
613        let method = if has_method {
614            "tableDelete"
615        } else {
616            "viewDelete"
617        };
618
619        let handler = self.0.clone();
620        let table_id = table_id.to_string();
621        Box::pin(async move {
622            let this = JsServerHandler(handler);
623            let args = Array::new();
624            args.push(&JsValue::from_str(&table_id));
625            this.call_method_js_async(method, &args).await?;
626            Ok(())
627        })
628    }
629
630    fn table_make_port(
631        &self,
632        _req: &perspective_client::proto::TableMakePortReq,
633    ) -> HandlerFuture<Result<u32, Self::Error>> {
634        let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
635            .map(|val| !val.is_undefined())
636            .unwrap_or(false);
637
638        if !has_method {
639            return Box::pin(async { Ok(0) });
640        }
641
642        let handler = self.0.clone();
643        Box::pin(async move {
644            let this = JsServerHandler(handler);
645            let args = Array::new();
646            let result = this.call_method_js_async("tableMakePort", &args).await?;
647            result
648                .as_f64()
649                .map(|x| x as u32)
650                .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
651        })
652    }
653
654    fn view_make_table(
655        &mut self,
656        view_id: &str,
657        table_id: &str,
658        config: &perspective_client::config::ViewConfig,
659        schema: Option<&IndexMap<String, ColumnType>>,
660    ) -> HandlerFuture<Result<(), Self::Error>> {
661        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewMakeTable"))
662            .map(|val| !val.is_undefined())
663            .unwrap_or(false);
664
665        if !has_method {
666            return Box::pin(async {
667                Err(JsError(JsValue::from_str("viewMakeTable not implemented")))
668            });
669        }
670
671        let handler = self.0.clone();
672        let view_id = view_id.to_string();
673        let table_id = table_id.to_string();
674        let config_value = JsValue::from_serde_ext(config);
675        let schema_value = schema.map(|schema| {
676            let object = Object::new();
677            for (name, ty) in schema {
678                let _ = Reflect::set(
679                    &object,
680                    &JsValue::from_str(name),
681                    &JsValue::from_str(&ty.to_string()),
682                );
683            }
684
685            JsValue::from(object)
686        });
687
688        Box::pin(async move {
689            let this = JsServerHandler(handler);
690            let args = Array::new();
691            args.push(&JsValue::from_str(&view_id));
692            args.push(&JsValue::from_str(&table_id));
693            args.push(&config_value?);
694            if let Some(schema_value) = schema_value {
695                args.push(&schema_value);
696            }
697
698            this.call_method_js_async("viewMakeTable", &args).await?;
699            Ok(())
700        })
701    }
702
703    fn make_table(
704        &mut self,
705        table_id: &str,
706        data: &perspective_client::proto::MakeTableData,
707    ) -> HandlerFuture<Result<(), Self::Error>> {
708        let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
709            .map(|val| !val.is_undefined())
710            .unwrap_or(false);
711
712        if !has_method {
713            return Box::pin(async {
714                Err(JsError(JsValue::from_str("makeTable not implemented")))
715            });
716        }
717
718        let handler = self.0.clone();
719        let table_id = table_id.to_string();
720        use perspective_client::proto::make_table_data::Data;
721        let data_value = match &data.data {
722            Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
723            Some(Data::FromArrow(arrow)) => {
724                let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
725                JsValue::from(uint8array)
726            },
727            Some(Data::FromRows(rows)) => JsValue::from_str(rows),
728            Some(Data::FromCols(cols)) => JsValue::from_str(cols),
729            Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
730            _ => JsValue::from_str(""),
731        };
732
733        Box::pin(async move {
734            let this = JsServerHandler(handler);
735            let args = Array::new();
736            args.push(&JsValue::from_str(&table_id));
737            args.push(&data_value);
738            this.call_method_js_async("makeTable", &args).await?;
739            Ok(())
740        })
741    }
742
743    fn view_get_min_max(
744        &self,
745        view_id: &str,
746        column_name: &str,
747        config: &perspective_client::config::ViewConfig,
748    ) -> HandlerFuture<
749        Result<
750            (
751                perspective_client::config::Scalar,
752                perspective_client::config::Scalar,
753            ),
754            Self::Error,
755        >,
756    > {
757        let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
758            .map(|val| !val.is_undefined())
759            .unwrap_or(false);
760
761        if !has_method {
762            return Box::pin(async {
763                Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
764            });
765        }
766
767        let handler = self.0.clone();
768        let view_id = view_id.to_string();
769        let column_name = column_name.to_string();
770        let config_js = JsValue::from_serde_ext(config).unwrap();
771        Box::pin(async move {
772            let this = JsServerHandler(handler);
773            let args = Array::new();
774            args.push(&JsValue::from_str(&view_id));
775            args.push(&JsValue::from_str(&column_name));
776            args.push(&config_js);
777            let result = this.call_method_js_async("viewGetMinMax", &args).await?;
778            let obj = result.dyn_ref::<Object>().unwrap();
779            let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
780            let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
781            Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
782        })
783    }
784
785    fn view_expand(
786        &mut self,
787        view_id: &str,
788        config: &perspective_client::config::ViewConfig,
789        row_index: u32,
790    ) -> HandlerFuture<Result<u32, Self::Error>> {
791        Box::pin(self.call_tree_method("viewExpand", view_id, config, row_index))
792    }
793
794    fn view_collapse(
795        &mut self,
796        view_id: &str,
797        config: &perspective_client::config::ViewConfig,
798        row_index: u32,
799    ) -> HandlerFuture<Result<u32, Self::Error>> {
800        Box::pin(self.call_tree_method("viewCollapse", view_id, config, row_index))
801    }
802
803    fn view_set_depth(
804        &mut self,
805        view_id: &str,
806        config: &perspective_client::config::ViewConfig,
807        depth: u32,
808    ) -> HandlerFuture<Result<(), Self::Error>> {
809        let fut = self.call_tree_method("viewSetDepth", view_id, config, depth);
810        Box::pin(async move {
811            fut.await?;
812            Ok(())
813        })
814    }
815
816    fn view_get_data(
817        &self,
818        view_id: &str,
819        config: &perspective_client::config::ViewConfig,
820        schema: &IndexMap<String, ColumnType>,
821        viewport: &perspective_client::proto::ViewPort,
822    ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
823        let handler = self.0.clone();
824        let view_id = view_id.to_string();
825        let window: JsViewPort = viewport.clone().into();
826        let config_value = JsValue::from_serde_ext(config).unwrap();
827        let window_value = JsValue::from_serde_ext(&window).unwrap();
828        let schema_value = JsValue::from_serde_ext(&schema).unwrap();
829
830        Box::pin(async move {
831            let this = JsServerHandler(handler);
832            let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
833
834            {
835                let args = Array::new();
836                args.push(&JsValue::from_str(&view_id));
837                args.push(&config_value);
838                args.push(&schema_value);
839                args.push(&window_value);
840                args.push(&JsValue::from(data.clone()));
841                this.call_method_js_async("viewGetData", &args).await?;
842            }
843
844            // Lock the mutex and take ownership of the inner data
845            // We can't unwrap the Arc because the JsValue might still hold a
846            // reference
847            let VirtualDataSlice(_obj, arc) = data;
848            let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
849            Ok(slice)
850        })
851    }
852}
853
854#[derive(Serialize, PartialEq)]
855pub struct JsViewPort {
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub start_row: ::core::option::Option<u32>,
858
859    #[serde(default, skip_serializing_if = "Option::is_none")]
860    pub start_col: ::core::option::Option<u32>,
861
862    #[serde(default, skip_serializing_if = "Option::is_none")]
863    pub end_row: ::core::option::Option<u32>,
864
865    #[serde(default, skip_serializing_if = "Option::is_none")]
866    pub end_col: ::core::option::Option<u32>,
867}
868
869impl From<perspective_client::proto::ViewPort> for JsViewPort {
870    fn from(value: perspective_client::proto::ViewPort) -> Self {
871        JsViewPort {
872            start_row: value.start_row,
873            start_col: value.start_col,
874            end_row: value.end_row,
875            end_col: value.end_col,
876        }
877    }
878}
879
880#[wasm_bindgen(js_name = "VirtualDataSlice")]
881#[derive(Clone)]
882pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
883
884#[wasm_bindgen]
885impl VirtualDataSlice {
886    #[wasm_bindgen(constructor)]
887    pub fn new(config: JsViewConfig) -> Self {
888        VirtualDataSlice(
889            Object::new(),
890            Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
891                config.into_serde_ext().unwrap(),
892            )))),
893        )
894    }
895
896    #[wasm_bindgen(js_name = "fromArrowIpc")]
897    pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
898        self.1
899            .lock()
900            .unwrap()
901            .as_mut()
902            .unwrap()
903            .from_arrow_ipc(&ipc.to_vec())
904            .map_err(|e| JsValue::from_str(&e.to_string()))
905    }
906
907    #[wasm_bindgen(js_name = "setCol")]
908    pub fn set_col(
909        &self,
910        dtype: &str,
911        name: &str,
912        index: u32,
913        val: JsValue,
914        group_by_index: Option<usize>,
915    ) -> Result<(), JsValue> {
916        match dtype {
917            "string" => self.set_string_col(name, index, val, group_by_index),
918            "integer" => self.set_integer_col(name, index, val, group_by_index),
919            "float" => self.set_float_col(name, index, val, group_by_index),
920            "date" => self.set_datetime_col(name, index, val, group_by_index),
921            "datetime" => self.set_datetime_col(name, index, val, group_by_index),
922            "boolean" => self.set_boolean_col(name, index, val, group_by_index),
923            _ => Err(JsValue::from_str("Unknown type")),
924        }
925    }
926
927    #[wasm_bindgen(js_name = "setStringCol")]
928    pub fn set_string_col(
929        &self,
930        name: &str,
931        index: u32,
932        val: JsValue,
933        group_by_index: Option<usize>,
934    ) -> Result<(), JsValue> {
935        if val.is_null() || val.is_undefined() {
936            self.1
937                .lock()
938                .unwrap()
939                .as_mut()
940                .unwrap()
941                .set_col(name, group_by_index, index as usize, None as Option<String>)
942                .unwrap();
943        } else if let Some(s) = val.as_string() {
944            self.1
945                .lock()
946                .unwrap()
947                .as_mut()
948                .unwrap()
949                .set_col(name, group_by_index, index as usize, Some(s))
950                .unwrap();
951        } else {
952            tracing::error!("Unhandled string value");
953        }
954        Ok(())
955    }
956
957    #[wasm_bindgen(js_name = "setIntegerCol")]
958    pub fn set_integer_col(
959        &self,
960        name: &str,
961        index: u32,
962        val: JsValue,
963        group_by_index: Option<usize>,
964    ) -> Result<(), JsValue> {
965        if val.is_null() || val.is_undefined() {
966            self.1
967                .lock()
968                .unwrap()
969                .as_mut()
970                .unwrap()
971                .set_col(name, group_by_index, index as usize, None as Option<i32>)
972                .unwrap();
973        } else if let Some(n) = val.as_f64() {
974            self.1
975                .lock()
976                .unwrap()
977                .as_mut()
978                .unwrap()
979                .set_col(name, group_by_index, index as usize, Some(n as i32))
980                .unwrap();
981        } else {
982            tracing::error!("Unhandled integer value");
983        }
984        Ok(())
985    }
986
987    #[wasm_bindgen(js_name = "setFloatCol")]
988    pub fn set_float_col(
989        &self,
990        name: &str,
991        index: u32,
992        val: JsValue,
993        group_by_index: Option<usize>,
994    ) -> Result<(), JsValue> {
995        if val.is_null() || val.is_undefined() {
996            self.1
997                .lock()
998                .unwrap()
999                .as_mut()
1000                .unwrap()
1001                .set_col(name, group_by_index, index as usize, None as Option<f64>)
1002                .unwrap();
1003        } else if let Some(n) = val.as_f64() {
1004            self.1
1005                .lock()
1006                .unwrap()
1007                .as_mut()
1008                .unwrap()
1009                .set_col(name, group_by_index, index as usize, Some(n))
1010                .unwrap();
1011        } else {
1012            tracing::error!("Unhandled float value");
1013        }
1014        Ok(())
1015    }
1016
1017    #[wasm_bindgen(js_name = "setBooleanCol")]
1018    pub fn set_boolean_col(
1019        &self,
1020        name: &str,
1021        index: u32,
1022        val: JsValue,
1023        group_by_index: Option<usize>,
1024    ) -> Result<(), JsValue> {
1025        if val.is_null() || val.is_undefined() {
1026            self.1
1027                .lock()
1028                .unwrap()
1029                .as_mut()
1030                .unwrap()
1031                .set_col(name, group_by_index, index as usize, None as Option<bool>)
1032                .unwrap();
1033        } else if let Some(b) = val.as_bool() {
1034            self.1
1035                .lock()
1036                .unwrap()
1037                .as_mut()
1038                .unwrap()
1039                .set_col(name, group_by_index, index as usize, Some(b))
1040                .unwrap();
1041        } else {
1042            tracing::error!("Unhandled boolean value");
1043        }
1044        Ok(())
1045    }
1046
1047    #[wasm_bindgen(js_name = "setDatetimeCol")]
1048    pub fn set_datetime_col(
1049        &self,
1050        name: &str,
1051        index: u32,
1052        val: JsValue,
1053        group_by_index: Option<usize>,
1054    ) -> Result<(), JsValue> {
1055        if val.is_null() || val.is_undefined() {
1056            self.1
1057                .lock()
1058                .unwrap()
1059                .as_mut()
1060                .unwrap()
1061                .set_col(name, group_by_index, index as usize, None as Option<i64>)
1062                .unwrap();
1063        } else if let Some(date) = val.dyn_ref::<Date>() {
1064            let timestamp = date.get_time() as i64;
1065            self.1
1066                .lock()
1067                .unwrap()
1068                .as_mut()
1069                .unwrap()
1070                .set_col(name, group_by_index, index as usize, Some(timestamp))
1071                .unwrap();
1072        } else if let Some(n) = val.as_f64() {
1073            self.1
1074                .lock()
1075                .unwrap()
1076                .as_mut()
1077                .unwrap()
1078                .set_col(name, group_by_index, index as usize, Some(n as i64))
1079                .unwrap();
1080        } else {
1081            tracing::error!("Unhandled datetime value");
1082        }
1083
1084        Ok(())
1085    }
1086}
1087
1088#[wasm_bindgen]
1089pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
1090
1091#[wasm_bindgen]
1092impl VirtualServer {
1093    #[wasm_bindgen(constructor)]
1094    pub fn new(handler: JsVirtualServerHandler) -> Result<VirtualServer, JsValue> {
1095        Ok(VirtualServer(Rc::new(UnsafeCell::new(
1096            virtual_server::VirtualServer::new(JsServerHandler(handler.unchecked_into())),
1097        ))))
1098    }
1099
1100    #[wasm_bindgen(js_name = "handleRequest")]
1101    pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
1102        let bytes = bytes.to_vec();
1103        let server = self.0.clone();
1104
1105        ApiFuture::new(async move {
1106            // SAFETY:
1107            // - WASM is single-threaded
1108            // - JS re-entrancy is allowed by design
1109            // - VirtualServer must tolerate re-entrant mutation
1110            let result = unsafe {
1111                (&mut *server.as_ref().get())
1112                    .handle_request(bytes::Bytes::from(bytes))
1113                    .await
1114            };
1115
1116            match result.get_internal_error() {
1117                Ok(x) => Ok(x.to_vec()),
1118                Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
1119                Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
1120            }
1121        })
1122    }
1123}