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_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 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 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}