1use 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#[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 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 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 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 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}