Skip to main content

perspective_js/
virtual_server.rs

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