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;
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 /** Defaults to no optional features. */
177 getFeatures?(): Features | Promise<Features>;
178
179 /** Defaults to port `0`. */
180 tableMakePort?(): number | Promise<number>;
181
182 makeTable?(
183 tableId: string,
184 data: string | Uint8Array,
185 ): void | Promise<void>;
186 viewMakeTable?(
187 viewId: string,
188 tableId: string,
189 config: ViewConfig,
190 schema?: Record<string, ColumnType>,
191 ): void | Promise<void>;
192}
193"#;
194
195#[wasm_bindgen]
196extern "C" {
197 #[wasm_bindgen(typescript_type = "VirtualServerHandler")]
198 pub type JsVirtualServerHandler;
199}
200
201pub struct JsServerHandler(Object);
202
203impl JsServerHandler {
204 fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
205 let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
206 let func = func
207 .dyn_ref::<js_sys::Function>()
208 .ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
209 Ok(func.apply(&self.0, args)?)
210 }
211
212 async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
213 let result = self.call_method_js(method, args)?;
214
215 if result.is_instance_of::<js_sys::Promise>() {
217 let promise = js_sys::Promise::from(result);
218 JsFuture::from(promise).await.map_err(JsError)
219 } else {
220 Ok(result)
221 }
222 }
223}
224
225impl VirtualServerHandler for JsServerHandler {
226 type Error = JsError;
227
228 fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
229 let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
230 .map(|val| !val.is_undefined())
231 .unwrap_or(false);
232
233 if !has_method {
234 return Box::pin(async { Ok(Features::default()) });
235 }
236
237 let handler = self.0.clone();
238 Box::pin(async move {
239 let this = JsServerHandler(handler);
240 let args = Array::new();
241 let result = this.call_method_js_async("getFeatures", &args).await?;
242 Ok(serde_wasm_bindgen::from_value(result)?)
243 })
244 }
245
246 fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
247 let handler = self.0.clone();
248 Box::pin(async move {
249 let this = JsServerHandler(handler);
250 let args = Array::new();
251 let result = this.call_method_js_async("getHostedTables", &args).await?;
252 let array = result.dyn_ref::<Array>().ok_or_else(|| {
253 JsError(JsValue::from_str("getHostedTables must return an array"))
254 })?;
255
256 let mut tables = Vec::new();
257 for i in 0..array.length() {
258 let item = array.get(i);
259 if let Some(s) = item.as_string() {
260 tables.push(HostedTable {
261 entity_id: s,
262 index: None,
263 limit: None,
264 });
265 } else if item.is_object() {
266 let name = Reflect::get(&item, &JsValue::from_str("name"))?
267 .as_string()
268 .ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
269 let index = Reflect::get(&item, &JsValue::from_str("index"))
270 .ok()
271 .and_then(|v| v.as_string());
272 let limit = Reflect::get(&item, &JsValue::from_str("limit"))
273 .ok()
274 .and_then(|v| v.as_f64().map(|x| x as u32));
275 tables.push(HostedTable {
276 entity_id: name,
277 index,
278 limit,
279 });
280 }
281 }
282 Ok(tables)
283 })
284 }
285
286 fn table_schema(
287 &self,
288 table_id: &str,
289 ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
290 let handler = self.0.clone();
291 let table_id = table_id.to_string();
292 Box::pin(async move {
293 let this = JsServerHandler(handler);
294 let args = Array::new();
295 args.push(&JsValue::from_str(&table_id));
296 let result = this.call_method_js_async("tableSchema", &args).await?;
297 let obj = result
298 .dyn_ref::<Object>()
299 .ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
300
301 let mut schema = IndexMap::new();
302 let entries = Object::entries(obj);
303 for i in 0..entries.length() {
304 let entry = entries.get(i);
305 let entry_array = entry.dyn_ref::<Array>().unwrap();
306 let key = entry_array.get(0).as_string().unwrap();
307 let value = entry_array.get(1).as_string().unwrap();
308 schema.insert(key, ColumnType::from_str(&value).unwrap());
309 }
310 Ok(schema)
311 })
312 }
313
314 fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
315 let handler = self.0.clone();
316 let table_id = table_id.to_string();
317 Box::pin(async move {
318 let this = JsServerHandler(handler);
319 let args = Array::new();
320 args.push(&JsValue::from_str(&table_id));
321 let result = this.call_method_js_async("tableSize", &args).await?;
322 result
323 .as_f64()
324 .map(|x| x as u32)
325 .ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
326 })
327 }
328
329 fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
330 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
331 .map(|val| !val.is_undefined())
332 .unwrap_or(false);
333
334 let handler = self.0.clone();
335 let view_id = view_id.to_string();
336 Box::pin(async move {
337 let this = JsServerHandler(handler);
338 let args = Array::new();
339 args.push(&JsValue::from_str(&view_id));
340 if has_method {
341 let result = this.call_method_js_async("tableColumnsSize", &args).await?;
342 result.as_f64().map(|x| x as u32).ok_or_else(|| {
343 JsError(JsValue::from_str(
344 "tableColumnsSize must
345 return a number",
346 ))
347 })
348 } else {
349 Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
350 }
351 })
352 }
353
354 fn table_describe(
355 &mut self,
356 table_id: &str,
357 config: &perspective_client::config::ViewConfig,
358 ) -> HandlerFuture<Result<Result<Description, DescribeError>, Self::Error>> {
359 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableDescribe"))
360 .map(|val| !val.is_undefined())
361 .unwrap_or(false);
362
363 let handler = self.0.clone();
364 let table_id = table_id.to_string();
365 let config_value = JsValue::from_serde_ext(config);
366 Box::pin(async move {
367 if !has_method {
368 return Err(JsError(JsValue::from_str(
369 "`tableDescribe` is required of a `VirtualServerHandler`",
370 )));
371 }
372
373 let this = JsServerHandler(handler);
374 let args = Array::new();
375 args.push(&JsValue::from_str(&table_id));
376 args.push(&config_value?);
377 let result = this.call_method_js_async("tableDescribe", &args).await?;
378 let verdict: DescribeVerdict = result.into_serde_ext().map_err(|e| {
379 JsError(JsValue::from_str(&format!(
380 "`tableDescribe` must return a `TableDescription`: {}",
381 e
382 )))
383 })?;
384
385 Result::<Description, DescribeError>::try_from(verdict)
386 .map_err(|e| JsError(JsValue::from_str(&e.to_string())))
387 })
388 }
389
390 fn table_make_view(
391 &mut self,
392 table_id: &str,
393 view_id: &str,
394 config: &mut perspective_client::config::ViewConfigUpdate,
395 ) -> HandlerFuture<Result<String, Self::Error>> {
396 let handler = self.0.clone();
397 let table_id = table_id.to_string();
398 let view_id = view_id.to_string();
399 let config = config.clone();
400 Box::pin(async move {
401 let this = JsServerHandler(handler);
402 let args = Array::new();
403 args.push(&JsValue::from_str(&table_id));
404 args.push(&JsValue::from_str(&view_id));
405 args.push(&JsValue::from_serde_ext(&config)?);
406 let _ = this.call_method_js_async("tableMakeView", &args).await?;
407 Ok(view_id.to_string())
408 })
409 }
410
411 fn view_schema(
412 &self,
413 view_id: &str,
414 config: &perspective_client::config::ViewConfig,
415 ) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
416 let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
417 .is_ok_and(|v| !v.is_undefined());
418
419 let handler = self.0.clone();
420 let view_id = view_id.to_string();
421 let config_value = JsValue::from_serde_ext(config).ok();
422
423 Box::pin(async move {
424 let this = JsServerHandler(handler);
425 let args = Array::new();
426 args.push(&JsValue::from_str(&view_id));
427 if let Some(cv) = config_value {
428 args.push(&cv);
429 }
430
431 let result = this
432 .call_method_js_async(
433 if has_view_schema {
434 "viewSchema"
435 } else {
436 "tableSchema"
437 },
438 &args,
439 )
440 .await?;
441
442 let obj = result
443 .dyn_ref::<Object>()
444 .ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
445
446 let mut schema = IndexMap::new();
447 let entries = Object::entries(obj);
448 for i in 0..entries.length() {
449 let entry = entries.get(i);
450 let entry_array = entry.dyn_ref::<Array>().unwrap();
451 let key = entry_array.get(0).as_string().unwrap();
452 let value = entry_array.get(1).as_string().unwrap();
453 schema.insert(key, ColumnType::from_str(&value).unwrap());
454 }
455
456 Ok(schema)
457 })
458 }
459
460 fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
461 let handler = self.0.clone();
462 let view_id = view_id.to_string();
463 let has_view_size =
464 Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
465
466 Box::pin(async move {
467 let this = JsServerHandler(handler);
468 let args = Array::new();
469 args.push(&JsValue::from_str(&view_id));
470 let result = this
471 .call_method_js_async(
472 if has_view_size {
473 "viewSize"
474 } else {
475 "tableSize"
476 },
477 &args,
478 )
479 .await?;
480
481 result
482 .as_f64()
483 .map(|x| x as u32)
484 .ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
485 })
486 }
487
488 fn view_column_size(
489 &self,
490 view_id: &str,
491 config: &perspective_client::config::ViewConfig,
492 ) -> HandlerFuture<Result<u32, Self::Error>> {
493 let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
494 .map(|val| !val.is_undefined())
495 .unwrap_or(false);
496
497 let handler = self.0.clone();
498 let view_id = view_id.to_string();
499 let config_value = JsValue::from_serde_ext(config).unwrap();
500 let config = config.clone();
501 Box::pin(async move {
502 let this = JsServerHandler(handler);
503 let args = Array::new();
504 args.push(&JsValue::from_str(&view_id));
505 args.push(&config_value);
506 if has_method {
507 let result = this.call_method_js_async("viewColumnSize", &args).await?;
508 result.as_f64().map(|x| x as u32).ok_or_else(|| {
509 JsError(JsValue::from_str("viewColumnSize must return a number"))
510 })
511 } else {
512 Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
513 }
514 })
515 }
516
517 fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
518 let handler = self.0.clone();
519 let view_id = view_id.to_string();
520 Box::pin(async move {
521 let this = JsServerHandler(handler);
522 let args = Array::new();
523 args.push(&JsValue::from_str(&view_id));
524 this.call_method_js_async("viewDelete", &args).await?;
525 Ok(())
526 })
527 }
528
529 fn table_make_port(
530 &self,
531 _req: &perspective_client::proto::TableMakePortReq,
532 ) -> HandlerFuture<Result<u32, Self::Error>> {
533 let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
534 .map(|val| !val.is_undefined())
535 .unwrap_or(false);
536
537 if !has_method {
538 return Box::pin(async { Ok(0) });
539 }
540
541 let handler = self.0.clone();
542 Box::pin(async move {
543 let this = JsServerHandler(handler);
544 let args = Array::new();
545 let result = this.call_method_js_async("tableMakePort", &args).await?;
546 result
547 .as_f64()
548 .map(|x| x as u32)
549 .ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
550 })
551 }
552
553 fn view_make_table(
554 &mut self,
555 view_id: &str,
556 table_id: &str,
557 config: &perspective_client::config::ViewConfig,
558 schema: Option<&IndexMap<String, ColumnType>>,
559 ) -> HandlerFuture<Result<(), Self::Error>> {
560 let has_method = Reflect::get(&self.0, &JsValue::from_str("viewMakeTable"))
561 .map(|val| !val.is_undefined())
562 .unwrap_or(false);
563
564 if !has_method {
565 return Box::pin(async {
566 Err(JsError(JsValue::from_str("viewMakeTable not implemented")))
567 });
568 }
569
570 let handler = self.0.clone();
571 let view_id = view_id.to_string();
572 let table_id = table_id.to_string();
573 let config_value = JsValue::from_serde_ext(config);
574 let schema_value = schema.map(|schema| {
575 let object = Object::new();
576 for (name, ty) in schema {
577 let _ = Reflect::set(
578 &object,
579 &JsValue::from_str(name),
580 &JsValue::from_str(&ty.to_string()),
581 );
582 }
583
584 JsValue::from(object)
585 });
586
587 Box::pin(async move {
588 let this = JsServerHandler(handler);
589 let args = Array::new();
590 args.push(&JsValue::from_str(&view_id));
591 args.push(&JsValue::from_str(&table_id));
592 args.push(&config_value?);
593 if let Some(schema_value) = schema_value {
594 args.push(&schema_value);
595 }
596
597 this.call_method_js_async("viewMakeTable", &args).await?;
598 Ok(())
599 })
600 }
601
602 fn make_table(
603 &mut self,
604 table_id: &str,
605 data: &perspective_client::proto::MakeTableData,
606 ) -> HandlerFuture<Result<(), Self::Error>> {
607 let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
608 .map(|val| !val.is_undefined())
609 .unwrap_or(false);
610
611 if !has_method {
612 return Box::pin(async {
613 Err(JsError(JsValue::from_str("makeTable not implemented")))
614 });
615 }
616
617 let handler = self.0.clone();
618 let table_id = table_id.to_string();
619 use perspective_client::proto::make_table_data::Data;
620 let data_value = match &data.data {
621 Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
622 Some(Data::FromArrow(arrow)) => {
623 let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
624 JsValue::from(uint8array)
625 },
626 Some(Data::FromRows(rows)) => JsValue::from_str(rows),
627 Some(Data::FromCols(cols)) => JsValue::from_str(cols),
628 Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
629 _ => JsValue::from_str(""),
630 };
631
632 Box::pin(async move {
633 let this = JsServerHandler(handler);
634 let args = Array::new();
635 args.push(&JsValue::from_str(&table_id));
636 args.push(&data_value);
637 this.call_method_js_async("makeTable", &args).await?;
638 Ok(())
639 })
640 }
641
642 fn view_get_min_max(
643 &self,
644 view_id: &str,
645 column_name: &str,
646 config: &perspective_client::config::ViewConfig,
647 ) -> HandlerFuture<
648 Result<
649 (
650 perspective_client::config::Scalar,
651 perspective_client::config::Scalar,
652 ),
653 Self::Error,
654 >,
655 > {
656 let has_method = Reflect::get(&self.0, &JsValue::from_str("viewGetMinMax"))
657 .map(|val| !val.is_undefined())
658 .unwrap_or(false);
659
660 if !has_method {
661 return Box::pin(async {
662 Err(JsError(JsValue::from_str("viewGetMinMax not implemented")))
663 });
664 }
665
666 let handler = self.0.clone();
667 let view_id = view_id.to_string();
668 let column_name = column_name.to_string();
669 let config_js = JsValue::from_serde_ext(config).unwrap();
670 Box::pin(async move {
671 let this = JsServerHandler(handler);
672 let args = Array::new();
673 args.push(&JsValue::from_str(&view_id));
674 args.push(&JsValue::from_str(&column_name));
675 args.push(&config_js);
676 let result = this.call_method_js_async("viewGetMinMax", &args).await?;
677 let obj = result.dyn_ref::<Object>().unwrap();
678 let min_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("min")))?;
679 let max_val = Reflect::get(obj, &JsValue::from_str(wasm_bindgen::intern("max")))?;
680 Ok((jsvalue_to_scalar(&min_val), jsvalue_to_scalar(&max_val)))
681 })
682 }
683
684 fn view_get_data(
685 &self,
686 view_id: &str,
687 config: &perspective_client::config::ViewConfig,
688 schema: &IndexMap<String, ColumnType>,
689 viewport: &perspective_client::proto::ViewPort,
690 ) -> HandlerFuture<Result<virtual_server::VirtualDataSlice, Self::Error>> {
691 let handler = self.0.clone();
692 let view_id = view_id.to_string();
693 let window: JsViewPort = viewport.clone().into();
694 let config_value = JsValue::from_serde_ext(config).unwrap();
695 let window_value = JsValue::from_serde_ext(&window).unwrap();
696 let schema_value = JsValue::from_serde_ext(&schema).unwrap();
697
698 Box::pin(async move {
699 let this = JsServerHandler(handler);
700 let data = VirtualDataSlice::new(config_value.clone().unchecked_into());
701
702 {
703 let args = Array::new();
704 args.push(&JsValue::from_str(&view_id));
705 args.push(&config_value);
706 args.push(&schema_value);
707 args.push(&window_value);
708 args.push(&JsValue::from(data.clone()));
709 this.call_method_js_async("viewGetData", &args).await?;
710 }
711
712 let VirtualDataSlice(_obj, arc) = data;
716 let slice = std::mem::take(&mut *arc.lock().unwrap()).unwrap();
717 Ok(slice)
718 })
719 }
720}
721
722#[derive(Serialize, PartialEq)]
723pub struct JsViewPort {
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub start_row: ::core::option::Option<u32>,
726
727 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub start_col: ::core::option::Option<u32>,
729
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub end_row: ::core::option::Option<u32>,
732
733 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub end_col: ::core::option::Option<u32>,
735}
736
737impl From<perspective_client::proto::ViewPort> for JsViewPort {
738 fn from(value: perspective_client::proto::ViewPort) -> Self {
739 JsViewPort {
740 start_row: value.start_row,
741 start_col: value.start_col,
742 end_row: value.end_row,
743 end_col: value.end_col,
744 }
745 }
746}
747
748#[wasm_bindgen(js_name = "VirtualDataSlice")]
749#[derive(Clone)]
750pub struct VirtualDataSlice(Object, Arc<Mutex<Option<virtual_server::VirtualDataSlice>>>);
751
752#[wasm_bindgen]
753impl VirtualDataSlice {
754 #[wasm_bindgen(constructor)]
755 pub fn new(config: JsViewConfig) -> Self {
756 VirtualDataSlice(
757 Object::new(),
758 Arc::new(Mutex::new(Some(virtual_server::VirtualDataSlice::new(
759 config.into_serde_ext().unwrap(),
760 )))),
761 )
762 }
763
764 #[wasm_bindgen(js_name = "fromArrowIpc")]
765 pub fn from_arrow_ipc(&self, ipc: Uint8Array) -> Result<(), JsValue> {
766 self.1
767 .lock()
768 .unwrap()
769 .as_mut()
770 .unwrap()
771 .from_arrow_ipc(&ipc.to_vec())
772 .map_err(|e| JsValue::from_str(&e.to_string()))
773 }
774
775 #[wasm_bindgen(js_name = "setCol")]
776 pub fn set_col(
777 &self,
778 dtype: &str,
779 name: &str,
780 index: u32,
781 val: JsValue,
782 group_by_index: Option<usize>,
783 ) -> Result<(), JsValue> {
784 match dtype {
785 "string" => self.set_string_col(name, index, val, group_by_index),
786 "integer" => self.set_integer_col(name, index, val, group_by_index),
787 "float" => self.set_float_col(name, index, val, group_by_index),
788 "date" => self.set_datetime_col(name, index, val, group_by_index),
789 "datetime" => self.set_datetime_col(name, index, val, group_by_index),
790 "boolean" => self.set_boolean_col(name, index, val, group_by_index),
791 _ => Err(JsValue::from_str("Unknown type")),
792 }
793 }
794
795 #[wasm_bindgen(js_name = "setStringCol")]
796 pub fn set_string_col(
797 &self,
798 name: &str,
799 index: u32,
800 val: JsValue,
801 group_by_index: Option<usize>,
802 ) -> Result<(), JsValue> {
803 if val.is_null() || val.is_undefined() {
804 self.1
805 .lock()
806 .unwrap()
807 .as_mut()
808 .unwrap()
809 .set_col(name, group_by_index, index as usize, None as Option<String>)
810 .unwrap();
811 } else if let Some(s) = val.as_string() {
812 self.1
813 .lock()
814 .unwrap()
815 .as_mut()
816 .unwrap()
817 .set_col(name, group_by_index, index as usize, Some(s))
818 .unwrap();
819 } else {
820 tracing::error!("Unhandled string value");
821 }
822 Ok(())
823 }
824
825 #[wasm_bindgen(js_name = "setIntegerCol")]
826 pub fn set_integer_col(
827 &self,
828 name: &str,
829 index: u32,
830 val: JsValue,
831 group_by_index: Option<usize>,
832 ) -> Result<(), JsValue> {
833 if val.is_null() || val.is_undefined() {
834 self.1
835 .lock()
836 .unwrap()
837 .as_mut()
838 .unwrap()
839 .set_col(name, group_by_index, index as usize, None as Option<i32>)
840 .unwrap();
841 } else if let Some(n) = val.as_f64() {
842 self.1
843 .lock()
844 .unwrap()
845 .as_mut()
846 .unwrap()
847 .set_col(name, group_by_index, index as usize, Some(n as i32))
848 .unwrap();
849 } else {
850 tracing::error!("Unhandled integer value");
851 }
852 Ok(())
853 }
854
855 #[wasm_bindgen(js_name = "setFloatCol")]
856 pub fn set_float_col(
857 &self,
858 name: &str,
859 index: u32,
860 val: JsValue,
861 group_by_index: Option<usize>,
862 ) -> Result<(), JsValue> {
863 if val.is_null() || val.is_undefined() {
864 self.1
865 .lock()
866 .unwrap()
867 .as_mut()
868 .unwrap()
869 .set_col(name, group_by_index, index as usize, None as Option<f64>)
870 .unwrap();
871 } else if let Some(n) = val.as_f64() {
872 self.1
873 .lock()
874 .unwrap()
875 .as_mut()
876 .unwrap()
877 .set_col(name, group_by_index, index as usize, Some(n))
878 .unwrap();
879 } else {
880 tracing::error!("Unhandled float value");
881 }
882 Ok(())
883 }
884
885 #[wasm_bindgen(js_name = "setBooleanCol")]
886 pub fn set_boolean_col(
887 &self,
888 name: &str,
889 index: u32,
890 val: JsValue,
891 group_by_index: Option<usize>,
892 ) -> Result<(), JsValue> {
893 if val.is_null() || val.is_undefined() {
894 self.1
895 .lock()
896 .unwrap()
897 .as_mut()
898 .unwrap()
899 .set_col(name, group_by_index, index as usize, None as Option<bool>)
900 .unwrap();
901 } else if let Some(b) = val.as_bool() {
902 self.1
903 .lock()
904 .unwrap()
905 .as_mut()
906 .unwrap()
907 .set_col(name, group_by_index, index as usize, Some(b))
908 .unwrap();
909 } else {
910 tracing::error!("Unhandled boolean value");
911 }
912 Ok(())
913 }
914
915 #[wasm_bindgen(js_name = "setDatetimeCol")]
916 pub fn set_datetime_col(
917 &self,
918 name: &str,
919 index: u32,
920 val: JsValue,
921 group_by_index: Option<usize>,
922 ) -> Result<(), JsValue> {
923 if val.is_null() || val.is_undefined() {
924 self.1
925 .lock()
926 .unwrap()
927 .as_mut()
928 .unwrap()
929 .set_col(name, group_by_index, index as usize, None as Option<i64>)
930 .unwrap();
931 } else if let Some(date) = val.dyn_ref::<Date>() {
932 let timestamp = date.get_time() as i64;
933 self.1
934 .lock()
935 .unwrap()
936 .as_mut()
937 .unwrap()
938 .set_col(name, group_by_index, index as usize, Some(timestamp))
939 .unwrap();
940 } else if let Some(n) = val.as_f64() {
941 self.1
942 .lock()
943 .unwrap()
944 .as_mut()
945 .unwrap()
946 .set_col(name, group_by_index, index as usize, Some(n as i64))
947 .unwrap();
948 } else {
949 tracing::error!("Unhandled datetime value");
950 }
951
952 Ok(())
953 }
954}
955
956#[wasm_bindgen]
957pub struct VirtualServer(Rc<UnsafeCell<virtual_server::VirtualServer<JsServerHandler>>>);
958
959#[wasm_bindgen]
960impl VirtualServer {
961 #[wasm_bindgen(constructor)]
962 pub fn new(handler: JsVirtualServerHandler) -> Result<VirtualServer, JsValue> {
963 Ok(VirtualServer(Rc::new(UnsafeCell::new(
964 virtual_server::VirtualServer::new(JsServerHandler(handler.unchecked_into())),
965 ))))
966 }
967
968 #[wasm_bindgen(js_name = "handleRequest")]
969 pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
970 let bytes = bytes.to_vec();
971 let server = self.0.clone();
972
973 ApiFuture::new(async move {
974 let result = unsafe {
979 (&mut *server.as_ref().get())
980 .handle_request(bytes::Bytes::from(bytes))
981 .await
982 };
983
984 match result.get_internal_error() {
985 Ok(x) => Ok(x.to_vec()),
986 Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
987 Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
988 }
989 })
990 }
991}