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};
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
63pub struct JsServerHandler(Object);
64
65impl JsServerHandler {
66 fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
67 let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
68 let func = func
69 .dyn_ref::<js_sys::Function>()
70 .ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
71 Ok(func.apply(&self.0, args)?)
72 }
73
74 async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
75 let result = self.call_method_js(method, args)?;
76
77 if result.is_instance_of::<js_sys::Promise>() {
79 let promise = js_sys::Promise::from(result);
80 JsFuture::from(promise).await.map_err(JsError)
81 } else {
82 Ok(result)
83 }
84 }
85}
86
87impl VirtualServerHandler for JsServerHandler {
88 type Error = JsError;
89
90 fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
91 let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
92 .map(|val| !val.is_undefined())
93 .unwrap_or(false);
94
95 if !has_method {
96 return Box::pin(async { Ok(Features::default()) });
97 }
98
99 let handler = self.0.clone();
100 Box::pin(async move {
101 let this = JsServerHandler(handler);
102 let args = Array::new();
103 let result = this.call_method_js_async("getFeatures", &args).await?;
104 Ok(serde_wasm_bindgen::from_value(result)?)
105 })
106 }
107
108 fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
109 let handler = self.0.clone();
110 Box::pin(async move {
111 let this = JsServerHandler(handler);
112 let args = Array::new();
113 let result = this.call_method_js_async("getHostedTables", &args).await?;
114 let array = result.dyn_ref::<Array>().ok_or_else(|| {
115 JsError(JsValue::from_str("getHostedTables must return an array"))
116 })?;
117
118 let mut tables = Vec::new();
119 for i in 0..array.length() {
120 let item = array.get(i);
121 if let Some(s) = item.as_string() {
122 tables.push(HostedTable {
123 entity_id: s,
124 index: None,
125 limit: None,
126 });
127 } else if item.is_object() {
128 let name = Reflect::get(&item, &JsValue::from_str("name"))?
129 .as_string()
130 .ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
131 let index = Reflect::get(&item, &JsValue::from_str("index"))
132 .ok()
133 .and_then(|v| v.as_string());
134 let limit = Reflect::get(&item, &JsValue::from_str("limit"))
135 .ok()
136 .and_then(|v| v.as_f64().map(|x| x as u32));
137 tables.push(HostedTable {
138 entity_id: name,
139 index,
140 limit,
141 });
142 }
143 }
144 Ok(tables)
145 })
146 }
147
148 fn table_schema(
149 &self,
150 table_id: &str,
151 ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
152 let handler = self.0.clone();
153 let table_id = table_id.to_string();
154 Box::pin(async move {
155 let this = JsServerHandler(handler);
156 let args = Array::new();
157 args.push(&JsValue::from_str(&table_id));
158 let result = this.call_method_js_async("tableSchema", &args).await?;
159 let obj = result
160 .dyn_ref::<Object>()
161 .ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
162
163 let mut schema = IndexMap::new();
164 let entries = Object::entries(obj);
165 for i in 0..entries.length() {
166 let entry = entries.get(i);
167 let entry_array = entry.dyn_ref::<Array>().unwrap();
168 let key = entry_array.get(0).as_string().unwrap();
169 let value = entry_array.get(1).as_string().unwrap();
170 schema.insert(key, ColumnType::from_str(&value).unwrap());
171 }
172 Ok(schema)
173 })
174 }
175
176 fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
177 let handler = self.0.clone();
178 let table_id = table_id.to_string();
179 Box::pin(async move {
180 let this = JsServerHandler(handler);
181 let args = Array::new();
182 args.push(&JsValue::from_str(&table_id));
183 let result = this.call_method_js_async("tableSize", &args).await?;
184 result
185 .as_f64()
186 .map(|x| x as u32)
187 .ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
188 })
189 }
190
191 fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
192 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
193 .map(|val| !val.is_undefined())
194 .unwrap_or(false);
195
196 let handler = self.0.clone();
197 let view_id = view_id.to_string();
198 Box::pin(async move {
199 let this = JsServerHandler(handler);
200 let args = Array::new();
201 args.push(&JsValue::from_str(&view_id));
202 if has_method {
203 let result = this.call_method_js_async("tableColumnsSize", &args).await?;
204 result.as_f64().map(|x| x as u32).ok_or_else(|| {
205 JsError(JsValue::from_str(
206 "tableColumnsSize must
207 return a number",
208 ))
209 })
210 } else {
211 Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
212 }
213 })
214 }
215
216 fn table_validate_expression(
217 &self,
218 table_id: &str,
219 expression: &str,
220 ) -> HandlerFuture<Result<ColumnType, Self::Error>> {
221 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableValidateExpression"))
223 .map(|val| !val.is_undefined())
224 .unwrap_or(false);
225
226 let handler = self.0.clone();
227 let table_id = table_id.to_string();
228 let expression = expression.to_string();
229 Box::pin(async move {
230 if !has_method {
231 return Err(JsError(JsValue::from_str(
232 "feature `table_validate_expression` not implemented",
233 )));
234 }
235
236 let this = JsServerHandler(handler);
237 let args = Array::new();
238 args.push(&JsValue::from_str(&table_id));
239 args.push(&JsValue::from_str(&expression));
240 let result = this
241 .call_method_js_async("tableValidateExpression", &args)
242 .await?;
243
244 let type_str = result
245 .as_string()
246 .ok_or_else(|| JsError(JsValue::from_str("Must return a string")))?;
247
248 Ok(ColumnType::from_str(&type_str).unwrap())
249 })
250 }
251
252 fn table_make_view(
253 &mut self,
254 table_id: &str,
255 view_id: &str,
256 config: &mut perspective_client::config::ViewConfigUpdate,
257 ) -> HandlerFuture<Result<String, Self::Error>> {
258 let handler = self.0.clone();
259 let table_id = table_id.to_string();
260 let view_id = view_id.to_string();
261 let config = config.clone();
262 Box::pin(async move {
263 let this = JsServerHandler(handler);
264 let args = Array::new();
265 args.push(&JsValue::from_str(&table_id));
266 args.push(&JsValue::from_str(&view_id));
267 args.push(&JsValue::from_serde_ext(&config)?);
268 let _ = this.call_method_js_async("tableMakeView", &args).await?;
269 Ok(view_id.to_string())
270 })
271 }
272
273 fn view_schema(
274 &self,
275 view_id: &str,
276 config: &perspective_client::config::ViewConfig,
277 ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
278 let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
279 .is_ok_and(|v| !v.is_undefined());
280
281 let handler = self.0.clone();
282 let view_id = view_id.to_string();
283 let config_value = JsValue::from_serde_ext(config).ok();
284
285 Box::pin(async move {
286 let this = JsServerHandler(handler);
287 let args = Array::new();
288 args.push(&JsValue::from_str(&view_id));
289 if let Some(cv) = config_value {
290 args.push(&cv);
291 }
292
293 let result = this
294 .call_method_js_async(
295 if has_view_schema {
296 "viewSchema"
297 } else {
298 "tableSchema"
299 },
300 &args,
301 )
302 .await?;
303
304 let obj = result
305 .dyn_ref::<Object>()
306 .ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
307
308 let mut schema = IndexMap::new();
309 let entries = Object::entries(obj);
310 for i in 0..entries.length() {
311 let entry = entries.get(i);
312 let entry_array = entry.dyn_ref::<Array>().unwrap();
313 let key = entry_array.get(0).as_string().unwrap();
314 let value = entry_array.get(1).as_string().unwrap();
315 schema.insert(key, ColumnType::from_str(&value).unwrap());
316 }
317
318 Ok(schema)
319 })
320 }
321
322 fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
323 let handler = self.0.clone();
324 let view_id = view_id.to_string();
325 let has_view_size =
326 Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
327
328 Box::pin(async move {
329 let this = JsServerHandler(handler);
330 let args = Array::new();
331 args.push(&JsValue::from_str(&view_id));
332 let result = this
333 .call_method_js_async(
334 if has_view_size {
335 "viewSize"
336 } else {
337 "tableSize"
338 },
339 &args,
340 )
341 .await?;
342
343 result
344 .as_f64()
345 .map(|x| x as u32)
346 .ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
347 })
348 }
349
350 fn view_column_size(
351 &self,
352 view_id: &str,
353 config: &perspective_client::config::ViewConfig,
354 ) -> HandlerFuture<Result<u32, Self::Error>> {
355 let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
356 .map(|val| !val.is_undefined())
357 .unwrap_or(false);
358
359 let handler = self.0.clone();
360 let view_id = view_id.to_string();
361 let config_value = serde_wasm_bindgen::to_value(config).unwrap();
362 let config = config.clone();
363 Box::pin(async move {
364 let this = JsServerHandler(handler);
365 let args = Array::new();
366 args.push(&JsValue::from_str(&view_id));
367 args.push(&config_value);
368 if has_method {
369 let result = this.call_method_js_async("viewColumnSize", &args).await?;
370 result.as_f64().map(|x| x as u32).ok_or_else(|| {
371 JsError(JsValue::from_str("viewColumnSize must return a number"))
372 })
373 } else {
374 Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
375 }
376 })
377 }
378
379 fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
380 let handler = self.0.clone();
381 let view_id = view_id.to_string();
382 Box::pin(async move {
383 let this = JsServerHandler(handler);
384 let args = Array::new();
385 args.push(&JsValue::from_str(&view_id));
386 this.call_method_js_async("viewDelete", &args).await?;
387 Ok(())
388 })
389 }
390
391 fn table_make_port(
392 &self,
393 _req: &perspective_client::proto::TableMakePortReq,
394 ) -> HandlerFuture<Result<u32, Self::Error>> {
395 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
396 .map(|val| !val.is_undefined())
397 .unwrap_or(false);
398
399 if !has_method {
400 return Box::pin(async { Ok(0) });
401 }
402
403 let handler = self.0.clone();
404 Box::pin(async move {
405 let this = JsServerHandler(handler);
406 let args = Array::new();
407 let result = this.call_method_js_async("tableMakePort", &args).await?;
408 result
409 .as_f64()
410 .map(|x| x as u32)
411 .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
412 })
413 }
414
415 fn make_table(
416 &mut self,
417 table_id: &str,
418 data: &perspective_client::proto::MakeTableData,
419 ) -> HandlerFuture<Result<(), Self::Error>> {
420 let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
421 .map(|val| !val.is_undefined())
422 .unwrap_or(false);
423
424 if !has_method {
425 return Box::pin(async {
426 Err(JsError(JsValue::from_str("makeTable not implemented")))
427 });
428 }
429
430 let handler = self.0.clone();
431 let table_id = table_id.to_string();
432 use perspective_client::proto::make_table_data::Data;
433 let data_value = match &data.data {
434 Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
435 Some(Data::FromArrow(arrow)) => {
436 let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
437 JsValue::from(uint8array)
438 },
439 Some(Data::FromRows(rows)) => JsValue::from_str(rows),
440 Some(Data::FromCols(cols)) => JsValue::from_str(cols),
441 Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
442 _ => JsValue::from_str(""),
443 };
444
445 Box::pin(async move {
446 let this = JsServerHandler(handler);
447 let args = Array::new();
448 args.push(&JsValue::from_str(&table_id));
449 args.push(&data_value);
450 this.call_method_js_async("makeTable", &args).await?;
451 Ok(())
452 })
453 }
454
455 fn view_get_data(
456 &self,
457 view_id: &str,
458 config: &perspective_client::config::ViewConfig,
459 schema: &IndexMap<String, ColumnType>,
460 viewport: &perspective_client::proto::ViewPort,
461 ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
462 let handler = self.0.clone();
463 let view_id = view_id.to_string();
464 let window: JsViewPort = viewport.clone().into();
465 let config_value = serde_wasm_bindgen::to_value(config).unwrap();
466 let window_value = serde_wasm_bindgen::to_value(&window).unwrap();
467 let schema_value = JsValue::from_serde_ext(&schema).unwrap();
468
469 Box::pin(async move {
470 let this = JsServerHandler(handler);
471 let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
472
473 {
474 let args = Array::new();
475 args.push(&JsValue::from_str(&view_id));
476 args.push(&config_value);
477 args.push(&schema_value);
478 args.push(&window_value);
479 args.push(&JsValue::from(data.clone()));
480 this.call_method_js_async("viewGetData", &args).await?;
481 }
482
483 let VirtualDataSlice(_obj, arc) = data;
486 let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
487 Ok(slice)
488 })
489 }
490}
491
492#[derive(Serialize, PartialEq)]
493pub struct JsViewPort {
494 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub start_row: ::core::option::Option<u32>,
496
497 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub start_col: ::core::option::Option<u32>,
499
500 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub end_row: ::core::option::Option<u32>,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub end_col: ::core::option::Option<u32>,
505}
506
507impl From<perspective_client::proto::ViewPort> for JsViewPort {
508 fn from(value: perspective_client::proto::ViewPort) -> Self {
509 JsViewPort {
510 start_row: value.start_row,
511 start_col: value.start_col,
512 end_row: value.end_row,
513 end_col: value.end_col,
514 }
515 }
516}
517
518#[wasm_bindgen(js_name = "VirtualDataSlice")]
519#[derive(Clone)]
520pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
521
522#[wasm_bindgen]
523impl VirtualDataSlice {
524 #[wasm_bindgen(constructor)]
525 pub fn new(config: JsViewConfig) -> Self {
526 VirtualDataSlice(
527 Object::new(),
528 Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
529 config.into_serde_ext().unwrap(),
530 )))),
531 )
532 }
533
534 #[wasm_bindgen(js_name = "setCol")]
535 pub fn set_col(
536 &self,
537 dtype: &str,
538 name: &str,
539 index: u32,
540 val: JsValue,
541 group_by_index: Option<usize>,
542 ) -> Result<(), JsValue> {
543 match dtype {
544 "string" => self.set_string_col(name, index, val, group_by_index),
545 "integer" => self.set_integer_col(name, index, val, group_by_index),
546 "float" => self.set_float_col(name, index, val, group_by_index),
547 "date" => self.set_datetime_col(name, index, val, group_by_index),
548 "datetime" => self.set_datetime_col(name, index, val, group_by_index),
549 "boolean" => self.set_boolean_col(name, index, val, group_by_index),
550 _ => Err(JsValue::from_str("Unknown type")),
551 }
552 }
553
554 #[wasm_bindgen(js_name = "setStringCol")]
555 pub fn set_string_col(
556 &self,
557 name: &str,
558 index: u32,
559 val: JsValue,
560 group_by_index: Option<usize>,
561 ) -> Result<(), JsValue> {
562 if val.is_null() || val.is_undefined() {
563 self.1
564 .lock()
565 .unwrap()
566 .as_mut()
567 .unwrap()
568 .set_col(name, group_by_index, index as usize, None as Option<String>)
569 .unwrap();
570 } else if let Some(s) = val.as_string() {
571 self.1
572 .lock()
573 .unwrap()
574 .as_mut()
575 .unwrap()
576 .set_col(name, group_by_index, index as usize, Some(s))
577 .unwrap();
578 } else {
579 tracing::error!("Unhandled string value");
580 }
581 Ok(())
582 }
583
584 #[wasm_bindgen(js_name = "setIntegerCol")]
585 pub fn set_integer_col(
586 &self,
587 name: &str,
588 index: u32,
589 val: JsValue,
590 group_by_index: Option<usize>,
591 ) -> Result<(), JsValue> {
592 if val.is_null() || val.is_undefined() {
593 self.1
594 .lock()
595 .unwrap()
596 .as_mut()
597 .unwrap()
598 .set_col(name, group_by_index, index as usize, None as Option<i32>)
599 .unwrap();
600 } else if let Some(n) = val.as_f64() {
601 self.1
602 .lock()
603 .unwrap()
604 .as_mut()
605 .unwrap()
606 .set_col(name, group_by_index, index as usize, Some(n as i32))
607 .unwrap();
608 } else {
609 tracing::error!("Unhandled integer value");
610 }
611 Ok(())
612 }
613
614 #[wasm_bindgen(js_name = "setFloatCol")]
615 pub fn set_float_col(
616 &self,
617 name: &str,
618 index: u32,
619 val: JsValue,
620 group_by_index: Option<usize>,
621 ) -> Result<(), JsValue> {
622 if val.is_null() || val.is_undefined() {
623 self.1
624 .lock()
625 .unwrap()
626 .as_mut()
627 .unwrap()
628 .set_col(name, group_by_index, index as usize, None as Option<f64>)
629 .unwrap();
630 } else if let Some(n) = val.as_f64() {
631 self.1
632 .lock()
633 .unwrap()
634 .as_mut()
635 .unwrap()
636 .set_col(name, group_by_index, index as usize, Some(n))
637 .unwrap();
638 } else {
639 tracing::error!("Unhandled float value");
640 }
641 Ok(())
642 }
643
644 #[wasm_bindgen(js_name = "setBooleanCol")]
645 pub fn set_boolean_col(
646 &self,
647 name: &str,
648 index: u32,
649 val: JsValue,
650 group_by_index: Option<usize>,
651 ) -> Result<(), JsValue> {
652 if val.is_null() || val.is_undefined() {
653 self.1
654 .lock()
655 .unwrap()
656 .as_mut()
657 .unwrap()
658 .set_col(name, group_by_index, index as usize, None as Option<bool>)
659 .unwrap();
660 } else if let Some(b) = val.as_bool() {
661 self.1
662 .lock()
663 .unwrap()
664 .as_mut()
665 .unwrap()
666 .set_col(name, group_by_index, index as usize, Some(b))
667 .unwrap();
668 } else {
669 tracing::error!("Unhandled boolean value");
670 }
671 Ok(())
672 }
673
674 #[wasm_bindgen(js_name = "setDatetimeCol")]
675 pub fn set_datetime_col(
676 &self,
677 name: &str,
678 index: u32,
679 val: JsValue,
680 group_by_index: Option<usize>,
681 ) -> Result<(), JsValue> {
682 if val.is_null() || val.is_undefined() {
683 self.1
684 .lock()
685 .unwrap()
686 .as_mut()
687 .unwrap()
688 .set_col(name, group_by_index, index as usize, None as Option<i64>)
689 .unwrap();
690 } else if let Some(date) = val.dyn_ref::<Date>() {
691 let timestamp = date.get_time() as i64;
692 self.1
693 .lock()
694 .unwrap()
695 .as_mut()
696 .unwrap()
697 .set_col(name, group_by_index, index as usize, Some(timestamp))
698 .unwrap();
699 } else if let Some(n) = val.as_f64() {
700 self.1
701 .lock()
702 .unwrap()
703 .as_mut()
704 .unwrap()
705 .set_col(name, group_by_index, index as usize, Some(n as i64))
706 .unwrap();
707 } else {
708 tracing::error!("Unhandled datetime value");
709 }
710
711 Ok(())
712 }
713}
714
715#[wasm_bindgen]
716pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
717
718#[wasm_bindgen]
719impl VirtualServer {
720 #[wasm_bindgen(constructor)]
721 pub fn new(handler: Object) -> Result<VirtualServer, JsValue> {
722 Ok(VirtualServer(Rc::new(UnsafeCell::new(
723 virtual_server::VirtualServer::new(JsServerHandler(handler)),
724 ))))
725 }
726
727 #[wasm_bindgen(js_name = "handleRequest")]
728 pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
729 let bytes = bytes.to_vec();
730 let server = self.0.clone();
731
732 ApiFuture::new(async move {
733 let result = unsafe {
738 (&mut *server.as_ref().get())
739 .handle_request(bytes::Bytes::from(bytes))
740 .await
741 };
742
743 match result.get_internal_error() {
744 Ok(x) => Ok(x.to_vec()),
745 Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
746 Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
747 }
748 })
749 }
750}