@perspective-dev/client 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-server.worker.js +1 -1
- package/dist/cdn/perspective-server.worker.js.map +3 -3
- package/dist/cdn/perspective.js +2 -2
- package/dist/cdn/perspective.js.map +4 -4
- package/dist/esm/perspective.browser.d.ts +5 -1
- package/dist/esm/perspective.inline.js +2 -2
- package/dist/esm/perspective.inline.js.map +4 -4
- package/dist/esm/perspective.js +2 -2
- package/dist/esm/perspective.js.map +4 -4
- package/dist/esm/perspective.node.d.ts +6 -1
- package/dist/esm/perspective.node.js +722 -353
- package/dist/esm/perspective.node.js.map +4 -4
- package/dist/esm/ts-rs/ColumnType.d.ts +4 -0
- package/dist/esm/ts-rs/ViewConfig.d.ts +18 -0
- package/dist/esm/ts-rs/ViewWindow.d.ts +9 -9
- package/dist/esm/virtual_server.d.ts +47 -0
- package/dist/esm/virtual_servers/duckdb.d.ts +39 -0
- package/dist/esm/virtual_servers/duckdb.js +12 -0
- package/dist/esm/virtual_servers/duckdb.js.map +7 -0
- package/dist/esm/wasm/browser.d.ts +1 -1
- package/dist/wasm/perspective-js.d.ts +52 -13
- package/dist/wasm/perspective-js.js +680 -399
- package/dist/wasm/perspective-js.wasm +0 -0
- package/dist/wasm/perspective-js.wasm.d.ts +20 -8
- package/package.json +4 -1
- package/src/rust/client.rs +14 -5
- package/src/rust/lib.rs +11 -1
- package/src/rust/table.rs +3 -2
- package/src/rust/table_data.rs +19 -14
- package/src/rust/utils/browser.rs +0 -4
- package/src/rust/utils/console_logger.rs +3 -2
- package/src/rust/utils/errors.rs +10 -28
- package/src/rust/utils/futures.rs +3 -3
- package/src/rust/utils/json.rs +4 -4
- package/src/rust/virtual_server.rs +746 -0
- package/src/ts/perspective-server.worker.ts +33 -23
- package/src/ts/perspective.browser.ts +17 -2
- package/src/ts/perspective.node.ts +46 -11
- package/src/ts/ts-rs/ColumnType.ts +6 -0
- package/src/ts/ts-rs/ViewConfig.ts +8 -0
- package/src/ts/ts-rs/ViewWindow.ts +3 -3
- package/src/ts/virtual_server.ts +126 -0
- package/src/ts/virtual_servers/duckdb.ts +511 -0
- package/src/ts/wasm/browser.ts +17 -9
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
use std::cell::UnsafeCell;
|
|
14
|
+
use std::future::Future;
|
|
15
|
+
use std::pin::Pin;
|
|
16
|
+
use std::rc::Rc;
|
|
17
|
+
use std::str::FromStr;
|
|
18
|
+
use std::sync::{Arc, Mutex};
|
|
19
|
+
|
|
20
|
+
use indexmap::IndexMap;
|
|
21
|
+
use js_sys::{Array, Date, Object, Reflect};
|
|
22
|
+
use perspective_client::proto::{ColumnType, HostedTable};
|
|
23
|
+
use perspective_client::virtual_server::{
|
|
24
|
+
Features, ResultExt, VirtualDataSlice, VirtualServer, VirtualServerHandler,
|
|
25
|
+
};
|
|
26
|
+
use serde::Serialize;
|
|
27
|
+
use wasm_bindgen::prelude::*;
|
|
28
|
+
use wasm_bindgen_futures::JsFuture;
|
|
29
|
+
|
|
30
|
+
use crate::utils::{ApiError, ApiFuture, *};
|
|
31
|
+
|
|
32
|
+
// Conditional type alias matching the trait definition
|
|
33
|
+
#[cfg(target_arch = "wasm32")]
|
|
34
|
+
type HandlerFuture<T> = Pin<Box<dyn Future<Output = T>>>;
|
|
35
|
+
|
|
36
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
37
|
+
type HandlerFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
|
|
38
|
+
|
|
39
|
+
#[derive(Debug)]
|
|
40
|
+
pub struct JsError(JsValue);
|
|
41
|
+
|
|
42
|
+
impl std::fmt::Display for JsError {
|
|
43
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
44
|
+
write!(f, "{:?}", self.0)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl std::error::Error for JsError {}
|
|
49
|
+
|
|
50
|
+
impl From<JsValue> for JsError {
|
|
51
|
+
fn from(value: JsValue) -> Self {
|
|
52
|
+
JsError(value)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl From<JsError> for JsValue {
|
|
57
|
+
fn from(error: JsError) -> Self {
|
|
58
|
+
error.0
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl From<serde_wasm_bindgen::Error> for JsError {
|
|
63
|
+
fn from(error: serde_wasm_bindgen::Error) -> Self {
|
|
64
|
+
JsError(error.into())
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// SAFETY: In WASM, we're always single-threaded, so JsError can safely be Send
|
|
69
|
+
// + Sync
|
|
70
|
+
unsafe impl Send for JsError {}
|
|
71
|
+
unsafe impl Sync for JsError {}
|
|
72
|
+
|
|
73
|
+
pub struct JsServerHandler(Object);
|
|
74
|
+
|
|
75
|
+
unsafe impl Send for JsServerHandler {}
|
|
76
|
+
unsafe impl Sync for JsServerHandler {}
|
|
77
|
+
|
|
78
|
+
impl JsServerHandler {
|
|
79
|
+
fn call_method_js(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
|
|
80
|
+
let func = Reflect::get(&self.0, &JsValue::from_str(method))?;
|
|
81
|
+
let func = func
|
|
82
|
+
.dyn_ref::<js_sys::Function>()
|
|
83
|
+
.ok_or_else(|| JsError(JsValue::from_str(&format!("{} is not a function", method))))?;
|
|
84
|
+
Ok(func.apply(&self.0, args)?)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async fn call_method_js_async(&self, method: &str, args: &Array) -> Result<JsValue, JsError> {
|
|
88
|
+
let result = self.call_method_js(method, args)?;
|
|
89
|
+
|
|
90
|
+
// Check if result is a Promise
|
|
91
|
+
if result.is_instance_of::<js_sys::Promise>() {
|
|
92
|
+
let promise = js_sys::Promise::from(result);
|
|
93
|
+
JsFuture::from(promise).await.map_err(|e| JsError(e))
|
|
94
|
+
} else {
|
|
95
|
+
Ok(result)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl VirtualServerHandler for JsServerHandler {
|
|
101
|
+
type Error = JsError;
|
|
102
|
+
|
|
103
|
+
fn get_features(&self) -> HandlerFuture<Result<Features<'_>, Self::Error>> {
|
|
104
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("getFeatures"))
|
|
105
|
+
.map(|val| !val.is_undefined())
|
|
106
|
+
.unwrap_or(false);
|
|
107
|
+
|
|
108
|
+
if !has_method {
|
|
109
|
+
return Box::pin(async { Ok(Features::default()) });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let handler = self.0.clone();
|
|
113
|
+
Box::pin(async move {
|
|
114
|
+
let this = JsServerHandler(handler);
|
|
115
|
+
let args = Array::new();
|
|
116
|
+
let result = this.call_method_js_async("getFeatures", &args).await?;
|
|
117
|
+
Ok(serde_wasm_bindgen::from_value(result)?)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn get_hosted_tables(&self) -> HandlerFuture<Result<Vec<HostedTable>, Self::Error>> {
|
|
122
|
+
let handler = self.0.clone();
|
|
123
|
+
Box::pin(async move {
|
|
124
|
+
let this = JsServerHandler(handler);
|
|
125
|
+
let args = Array::new();
|
|
126
|
+
let result = this.call_method_js_async("getHostedTables", &args).await?;
|
|
127
|
+
let array = result.dyn_ref::<Array>().ok_or_else(|| {
|
|
128
|
+
JsError(JsValue::from_str("getHostedTables must return an array"))
|
|
129
|
+
})?;
|
|
130
|
+
|
|
131
|
+
let mut tables = Vec::new();
|
|
132
|
+
for i in 0..array.length() {
|
|
133
|
+
let item = array.get(i);
|
|
134
|
+
if let Some(s) = item.as_string() {
|
|
135
|
+
tables.push(HostedTable {
|
|
136
|
+
entity_id: s,
|
|
137
|
+
index: None,
|
|
138
|
+
limit: None,
|
|
139
|
+
});
|
|
140
|
+
} else if item.is_object() {
|
|
141
|
+
let name = Reflect::get(&item, &JsValue::from_str("name"))?
|
|
142
|
+
.as_string()
|
|
143
|
+
.ok_or_else(|| JsError(JsValue::from_str("name must be a string")))?;
|
|
144
|
+
let index = Reflect::get(&item, &JsValue::from_str("index"))
|
|
145
|
+
.ok()
|
|
146
|
+
.and_then(|v| v.as_string());
|
|
147
|
+
let limit = Reflect::get(&item, &JsValue::from_str("limit"))
|
|
148
|
+
.ok()
|
|
149
|
+
.and_then(|v| v.as_f64().map(|x| x as u32));
|
|
150
|
+
tables.push(HostedTable {
|
|
151
|
+
entity_id: name,
|
|
152
|
+
index,
|
|
153
|
+
limit,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
Ok(tables)
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn table_schema(
|
|
162
|
+
&self,
|
|
163
|
+
table_id: &str,
|
|
164
|
+
) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
|
|
165
|
+
let handler = self.0.clone();
|
|
166
|
+
let table_id = table_id.to_string();
|
|
167
|
+
Box::pin(async move {
|
|
168
|
+
let this = JsServerHandler(handler);
|
|
169
|
+
let args = Array::new();
|
|
170
|
+
args.push(&JsValue::from_str(&table_id));
|
|
171
|
+
let result = this.call_method_js_async("tableSchema", &args).await?;
|
|
172
|
+
let obj = result
|
|
173
|
+
.dyn_ref::<Object>()
|
|
174
|
+
.ok_or_else(|| JsError(JsValue::from_str("tableSchema must return an object")))?;
|
|
175
|
+
|
|
176
|
+
let mut schema = IndexMap::new();
|
|
177
|
+
let entries = Object::entries(obj);
|
|
178
|
+
for i in 0..entries.length() {
|
|
179
|
+
let entry = entries.get(i);
|
|
180
|
+
let entry_array = entry.dyn_ref::<Array>().unwrap();
|
|
181
|
+
let key = entry_array.get(0).as_string().unwrap();
|
|
182
|
+
let value = entry_array.get(1).as_string().unwrap();
|
|
183
|
+
schema.insert(key, ColumnType::from_str(&value).unwrap());
|
|
184
|
+
}
|
|
185
|
+
Ok(schema)
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn table_size(&self, table_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
|
|
190
|
+
let handler = self.0.clone();
|
|
191
|
+
let table_id = table_id.to_string();
|
|
192
|
+
Box::pin(async move {
|
|
193
|
+
let this = JsServerHandler(handler);
|
|
194
|
+
let args = Array::new();
|
|
195
|
+
args.push(&JsValue::from_str(&table_id));
|
|
196
|
+
let result = this.call_method_js_async("tableSize", &args).await?;
|
|
197
|
+
result
|
|
198
|
+
.as_f64()
|
|
199
|
+
.map(|x| x as u32)
|
|
200
|
+
.ok_or_else(|| JsError(JsValue::from_str("tableSize must return a number")))
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn table_column_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
|
|
205
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("tableColumnsSize"))
|
|
206
|
+
.map(|val| !val.is_undefined())
|
|
207
|
+
.unwrap_or(false);
|
|
208
|
+
|
|
209
|
+
let handler = self.0.clone();
|
|
210
|
+
let view_id = view_id.to_string();
|
|
211
|
+
Box::pin(async move {
|
|
212
|
+
let this = JsServerHandler(handler);
|
|
213
|
+
let args = Array::new();
|
|
214
|
+
args.push(&JsValue::from_str(&view_id));
|
|
215
|
+
if has_method {
|
|
216
|
+
let result = this.call_method_js_async("tableColumnsSize", &args).await?;
|
|
217
|
+
result.as_f64().map(|x| x as u32).ok_or_else(|| {
|
|
218
|
+
JsError(JsValue::from_str(
|
|
219
|
+
"tableColumnsSize must
|
|
220
|
+
return a number",
|
|
221
|
+
))
|
|
222
|
+
})
|
|
223
|
+
} else {
|
|
224
|
+
Ok(this.table_schema(view_id.as_str()).await?.len() as u32)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn table_validate_expression(
|
|
230
|
+
&self,
|
|
231
|
+
table_id: &str,
|
|
232
|
+
expression: &str,
|
|
233
|
+
) -> HandlerFuture<Result<ColumnType, Self::Error>> {
|
|
234
|
+
// TODO Cache these inspection calls
|
|
235
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("tableValidateExpression"))
|
|
236
|
+
.map(|val| !val.is_undefined())
|
|
237
|
+
.unwrap_or(false);
|
|
238
|
+
|
|
239
|
+
let handler = self.0.clone();
|
|
240
|
+
let table_id = table_id.to_string();
|
|
241
|
+
let expression = expression.to_string();
|
|
242
|
+
Box::pin(async move {
|
|
243
|
+
if !has_method {
|
|
244
|
+
return Err(JsError(JsValue::from_str(
|
|
245
|
+
"feature `table_validate_expression` not implemented",
|
|
246
|
+
)));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let this = JsServerHandler(handler);
|
|
250
|
+
let args = Array::new();
|
|
251
|
+
args.push(&JsValue::from_str(&table_id));
|
|
252
|
+
args.push(&JsValue::from_str(&expression));
|
|
253
|
+
let result = this
|
|
254
|
+
.call_method_js_async("tableValidateExpression", &args)
|
|
255
|
+
.await?;
|
|
256
|
+
|
|
257
|
+
let type_str = result
|
|
258
|
+
.as_string()
|
|
259
|
+
.ok_or_else(|| JsError(JsValue::from_str("Must return a string")))?;
|
|
260
|
+
|
|
261
|
+
Ok(ColumnType::from_str(&type_str).unwrap())
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn table_make_view(
|
|
266
|
+
&mut self,
|
|
267
|
+
table_id: &str,
|
|
268
|
+
view_id: &str,
|
|
269
|
+
config: &mut perspective_client::config::ViewConfigUpdate,
|
|
270
|
+
) -> HandlerFuture<Result<String, Self::Error>> {
|
|
271
|
+
let handler = self.0.clone();
|
|
272
|
+
let table_id = table_id.to_string();
|
|
273
|
+
let view_id = view_id.to_string();
|
|
274
|
+
let config = config.clone();
|
|
275
|
+
Box::pin(async move {
|
|
276
|
+
let this = JsServerHandler(handler);
|
|
277
|
+
let args = Array::new();
|
|
278
|
+
args.push(&JsValue::from_str(&table_id));
|
|
279
|
+
args.push(&JsValue::from_str(&view_id));
|
|
280
|
+
args.push(&JsValue::from_serde_ext(&config)?);
|
|
281
|
+
let _ = this.call_method_js_async("tableMakeView", &args).await?;
|
|
282
|
+
Ok(view_id.to_string())
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn view_schema(
|
|
287
|
+
&self,
|
|
288
|
+
view_id: &str,
|
|
289
|
+
config: &perspective_client::config::ViewConfig,
|
|
290
|
+
) -> HandlerFuture<Result<IndexMap<String, ColumnType>, Self::Error>> {
|
|
291
|
+
let has_view_schema = Reflect::get(&self.0, &JsValue::from_str("viewSchema"))
|
|
292
|
+
.is_ok_and(|v| !v.is_undefined());
|
|
293
|
+
|
|
294
|
+
let handler = self.0.clone();
|
|
295
|
+
let view_id = view_id.to_string();
|
|
296
|
+
let config_value = if has_view_schema {
|
|
297
|
+
serde_wasm_bindgen::to_value(config).ok()
|
|
298
|
+
} else {
|
|
299
|
+
None
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
Box::pin(async move {
|
|
303
|
+
let this = JsServerHandler(handler);
|
|
304
|
+
let args = Array::new();
|
|
305
|
+
args.push(&JsValue::from_str(&view_id));
|
|
306
|
+
if let Some(cv) = config_value {
|
|
307
|
+
args.push(&cv);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let result = this
|
|
311
|
+
.call_method_js_async(
|
|
312
|
+
if has_view_schema {
|
|
313
|
+
"viewSchema"
|
|
314
|
+
} else {
|
|
315
|
+
"tableSchema"
|
|
316
|
+
},
|
|
317
|
+
&args,
|
|
318
|
+
)
|
|
319
|
+
.await?;
|
|
320
|
+
|
|
321
|
+
let obj = result
|
|
322
|
+
.dyn_ref::<Object>()
|
|
323
|
+
.ok_or_else(|| JsError(JsValue::from_str("viewSchema must return an object")))?;
|
|
324
|
+
|
|
325
|
+
let mut schema = IndexMap::new();
|
|
326
|
+
let entries = Object::entries(obj);
|
|
327
|
+
for i in 0..entries.length() {
|
|
328
|
+
let entry = entries.get(i);
|
|
329
|
+
let entry_array = entry.dyn_ref::<Array>().unwrap();
|
|
330
|
+
let key = entry_array.get(0).as_string().unwrap();
|
|
331
|
+
let value = entry_array.get(1).as_string().unwrap();
|
|
332
|
+
schema.insert(key, ColumnType::from_str(&value).unwrap());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Ok(schema)
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn view_size(&self, view_id: &str) -> HandlerFuture<Result<u32, Self::Error>> {
|
|
340
|
+
let handler = self.0.clone();
|
|
341
|
+
let view_id = view_id.to_string();
|
|
342
|
+
let has_view_size =
|
|
343
|
+
Reflect::get(&self.0, &JsValue::from_str("viewSize")).is_ok_and(|v| !v.is_undefined());
|
|
344
|
+
|
|
345
|
+
Box::pin(async move {
|
|
346
|
+
let this = JsServerHandler(handler);
|
|
347
|
+
let args = Array::new();
|
|
348
|
+
args.push(&JsValue::from_str(&view_id));
|
|
349
|
+
let result = this
|
|
350
|
+
.call_method_js_async(
|
|
351
|
+
if has_view_size {
|
|
352
|
+
"viewSize"
|
|
353
|
+
} else {
|
|
354
|
+
"tableSize"
|
|
355
|
+
},
|
|
356
|
+
&args,
|
|
357
|
+
)
|
|
358
|
+
.await?;
|
|
359
|
+
|
|
360
|
+
result
|
|
361
|
+
.as_f64()
|
|
362
|
+
.map(|x| x as u32)
|
|
363
|
+
.ok_or_else(|| JsError(JsValue::from_str("viewSize must return a number")))
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
fn view_column_size(
|
|
368
|
+
&self,
|
|
369
|
+
view_id: &str,
|
|
370
|
+
config: &perspective_client::config::ViewConfig,
|
|
371
|
+
) -> HandlerFuture<Result<u32, Self::Error>> {
|
|
372
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("viewColumnSize"))
|
|
373
|
+
.map(|val| !val.is_undefined())
|
|
374
|
+
.unwrap_or(false);
|
|
375
|
+
|
|
376
|
+
let handler = self.0.clone();
|
|
377
|
+
let view_id = view_id.to_string();
|
|
378
|
+
let config_value = serde_wasm_bindgen::to_value(config).unwrap();
|
|
379
|
+
let config = config.clone();
|
|
380
|
+
Box::pin(async move {
|
|
381
|
+
let this = JsServerHandler(handler);
|
|
382
|
+
let args = Array::new();
|
|
383
|
+
args.push(&JsValue::from_str(&view_id));
|
|
384
|
+
args.push(&config_value);
|
|
385
|
+
if has_method {
|
|
386
|
+
let result = this.call_method_js_async("viewColumnSize", &args).await?;
|
|
387
|
+
result.as_f64().map(|x| x as u32).ok_or_else(|| {
|
|
388
|
+
JsError(JsValue::from_str("viewColumnSize must return a number"))
|
|
389
|
+
})
|
|
390
|
+
} else {
|
|
391
|
+
Ok(this.view_schema(view_id.as_str(), &config).await?.len() as u32)
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn view_delete(&self, view_id: &str) -> HandlerFuture<Result<(), Self::Error>> {
|
|
397
|
+
let handler = self.0.clone();
|
|
398
|
+
let view_id = view_id.to_string();
|
|
399
|
+
Box::pin(async move {
|
|
400
|
+
let this = JsServerHandler(handler);
|
|
401
|
+
let args = Array::new();
|
|
402
|
+
args.push(&JsValue::from_str(&view_id));
|
|
403
|
+
this.call_method_js_async("viewDelete", &args).await?;
|
|
404
|
+
Ok(())
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
fn table_make_port(
|
|
409
|
+
&self,
|
|
410
|
+
_req: &perspective_client::proto::TableMakePortReq,
|
|
411
|
+
) -> HandlerFuture<Result<u32, Self::Error>> {
|
|
412
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("tableMakePort"))
|
|
413
|
+
.map(|val| !val.is_undefined())
|
|
414
|
+
.unwrap_or(false);
|
|
415
|
+
|
|
416
|
+
if !has_method {
|
|
417
|
+
return Box::pin(async { Ok(0) });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let handler = self.0.clone();
|
|
421
|
+
Box::pin(async move {
|
|
422
|
+
let this = JsServerHandler(handler);
|
|
423
|
+
let args = Array::new();
|
|
424
|
+
let result = this.call_method_js_async("tableMakePort", &args).await?;
|
|
425
|
+
result
|
|
426
|
+
.as_f64()
|
|
427
|
+
.map(|x| x as u32)
|
|
428
|
+
.ok_or_else(|| JsError(JsValue::from_str("tableMakePort must return a number")))
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
fn make_table(
|
|
433
|
+
&mut self,
|
|
434
|
+
table_id: &str,
|
|
435
|
+
data: &perspective_client::proto::MakeTableData,
|
|
436
|
+
) -> HandlerFuture<Result<(), Self::Error>> {
|
|
437
|
+
let has_method = Reflect::get(&self.0, &JsValue::from_str("makeTable"))
|
|
438
|
+
.map(|val| !val.is_undefined())
|
|
439
|
+
.unwrap_or(false);
|
|
440
|
+
|
|
441
|
+
if !has_method {
|
|
442
|
+
return Box::pin(async {
|
|
443
|
+
Err(JsError(JsValue::from_str("makeTable not implemented")))
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let handler = self.0.clone();
|
|
448
|
+
let table_id = table_id.to_string();
|
|
449
|
+
|
|
450
|
+
use perspective_client::proto::make_table_data::Data;
|
|
451
|
+
let data_value = match &data.data {
|
|
452
|
+
Some(Data::FromCsv(csv)) => JsValue::from_str(csv),
|
|
453
|
+
Some(Data::FromArrow(arrow)) => {
|
|
454
|
+
let uint8array = js_sys::Uint8Array::from(arrow.as_slice());
|
|
455
|
+
JsValue::from(uint8array)
|
|
456
|
+
},
|
|
457
|
+
Some(Data::FromRows(rows)) => JsValue::from_str(rows),
|
|
458
|
+
Some(Data::FromCols(cols)) => JsValue::from_str(cols),
|
|
459
|
+
Some(Data::FromNdjson(ndjson)) => JsValue::from_str(ndjson),
|
|
460
|
+
_ => JsValue::from_str(""),
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
Box::pin(async move {
|
|
464
|
+
let this = JsServerHandler(handler);
|
|
465
|
+
let args = Array::new();
|
|
466
|
+
args.push(&JsValue::from_str(&table_id));
|
|
467
|
+
args.push(&data_value);
|
|
468
|
+
this.call_method_js_async("makeTable", &args).await?;
|
|
469
|
+
Ok(())
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fn view_get_data(
|
|
474
|
+
&self,
|
|
475
|
+
view_id: &str,
|
|
476
|
+
config: &perspective_client::config::ViewConfig,
|
|
477
|
+
viewport: &perspective_client::proto::ViewPort,
|
|
478
|
+
) -> HandlerFuture<Result<VirtualDataSlice, Self::Error>> {
|
|
479
|
+
let handler = self.0.clone();
|
|
480
|
+
let view_id = view_id.to_string();
|
|
481
|
+
let window: JsViewPort = viewport.clone().into();
|
|
482
|
+
let config_value = serde_wasm_bindgen::to_value(config).unwrap();
|
|
483
|
+
let window_value = serde_wasm_bindgen::to_value(&window).unwrap();
|
|
484
|
+
|
|
485
|
+
Box::pin(async move {
|
|
486
|
+
let this = JsServerHandler(handler);
|
|
487
|
+
let data = JsVirtualDataSlice::default();
|
|
488
|
+
|
|
489
|
+
{
|
|
490
|
+
let args = Array::new();
|
|
491
|
+
args.push(&JsValue::from_str(&view_id));
|
|
492
|
+
args.push(&config_value);
|
|
493
|
+
args.push(&window_value);
|
|
494
|
+
args.push(&JsValue::from(data.clone()));
|
|
495
|
+
this.call_method_js_async("viewGetData", &args).await?;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Lock the mutex and take ownership of the inner data
|
|
499
|
+
// We can't unwrap the Arc because the JsValue might still hold a reference
|
|
500
|
+
let JsVirtualDataSlice(_obj, arc) = data;
|
|
501
|
+
let slice = std::mem::take(&mut *arc.lock().unwrap());
|
|
502
|
+
Ok(slice)
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#[derive(Serialize, PartialEq)]
|
|
508
|
+
pub struct JsViewPort {
|
|
509
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
510
|
+
pub start_row: ::core::option::Option<u32>,
|
|
511
|
+
|
|
512
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
513
|
+
pub start_col: ::core::option::Option<u32>,
|
|
514
|
+
|
|
515
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
516
|
+
pub end_row: ::core::option::Option<u32>,
|
|
517
|
+
|
|
518
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
519
|
+
pub end_col: ::core::option::Option<u32>,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
impl From<perspective_client::proto::ViewPort> for JsViewPort {
|
|
523
|
+
fn from(value: perspective_client::proto::ViewPort) -> Self {
|
|
524
|
+
JsViewPort {
|
|
525
|
+
start_row: value.start_row,
|
|
526
|
+
start_col: value.start_col,
|
|
527
|
+
end_row: value.end_row,
|
|
528
|
+
end_col: value.end_col,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#[wasm_bindgen]
|
|
534
|
+
#[derive(Clone)]
|
|
535
|
+
pub struct JsVirtualDataSlice(Object, Arc<Mutex<VirtualDataSlice>>);
|
|
536
|
+
|
|
537
|
+
impl Default for JsVirtualDataSlice {
|
|
538
|
+
fn default() -> Self {
|
|
539
|
+
JsVirtualDataSlice(
|
|
540
|
+
Object::new(),
|
|
541
|
+
Arc::new(Mutex::new(VirtualDataSlice::default())),
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#[wasm_bindgen]
|
|
547
|
+
impl JsVirtualDataSlice {
|
|
548
|
+
#[wasm_bindgen(constructor)]
|
|
549
|
+
pub fn new() -> Self {
|
|
550
|
+
Self::default()
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[wasm_bindgen(js_name = "setCol")]
|
|
554
|
+
pub fn set_col(
|
|
555
|
+
&self,
|
|
556
|
+
dtype: &str,
|
|
557
|
+
name: &str,
|
|
558
|
+
index: u32,
|
|
559
|
+
val: JsValue,
|
|
560
|
+
group_by_index: Option<usize>,
|
|
561
|
+
) -> Result<(), JsValue> {
|
|
562
|
+
match dtype {
|
|
563
|
+
"string" => self.set_string_col(name, index, val, group_by_index),
|
|
564
|
+
"integer" => self.set_integer_col(name, index, val, group_by_index),
|
|
565
|
+
"float" => self.set_float_col(name, index, val, group_by_index),
|
|
566
|
+
"date" => self.set_datetime_col(name, index, val, group_by_index),
|
|
567
|
+
"datetime" => self.set_datetime_col(name, index, val, group_by_index),
|
|
568
|
+
"boolean" => self.set_boolean_col(name, index, val, group_by_index),
|
|
569
|
+
_ => Err(JsValue::from_str("Unknown type")),
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
#[wasm_bindgen(js_name = "setStringCol")]
|
|
574
|
+
pub fn set_string_col(
|
|
575
|
+
&self,
|
|
576
|
+
name: &str,
|
|
577
|
+
index: u32,
|
|
578
|
+
val: JsValue,
|
|
579
|
+
group_by_index: Option<usize>,
|
|
580
|
+
) -> Result<(), JsValue> {
|
|
581
|
+
if val.is_null() || val.is_undefined() {
|
|
582
|
+
self.1
|
|
583
|
+
.lock()
|
|
584
|
+
.unwrap()
|
|
585
|
+
.set_col(name, group_by_index, index as usize, None as Option<String>)
|
|
586
|
+
.unwrap();
|
|
587
|
+
} else if let Some(s) = val.as_string() {
|
|
588
|
+
self.1
|
|
589
|
+
.lock()
|
|
590
|
+
.unwrap()
|
|
591
|
+
.set_col(name, group_by_index, index as usize, Some(s))
|
|
592
|
+
.unwrap();
|
|
593
|
+
} else {
|
|
594
|
+
tracing::error!("Unhandled string value");
|
|
595
|
+
}
|
|
596
|
+
Ok(())
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#[wasm_bindgen(js_name = "setIntegerCol")]
|
|
600
|
+
pub fn set_integer_col(
|
|
601
|
+
&self,
|
|
602
|
+
name: &str,
|
|
603
|
+
index: u32,
|
|
604
|
+
val: JsValue,
|
|
605
|
+
group_by_index: Option<usize>,
|
|
606
|
+
) -> Result<(), JsValue> {
|
|
607
|
+
if val.is_null() || val.is_undefined() {
|
|
608
|
+
self.1
|
|
609
|
+
.lock()
|
|
610
|
+
.unwrap()
|
|
611
|
+
.set_col(name, group_by_index, index as usize, None as Option<i32>)
|
|
612
|
+
.unwrap();
|
|
613
|
+
} else if let Some(n) = val.as_f64() {
|
|
614
|
+
self.1
|
|
615
|
+
.lock()
|
|
616
|
+
.unwrap()
|
|
617
|
+
.set_col(name, group_by_index, index as usize, Some(n as i32))
|
|
618
|
+
.unwrap();
|
|
619
|
+
} else {
|
|
620
|
+
tracing::error!("Unhandled integer value");
|
|
621
|
+
}
|
|
622
|
+
Ok(())
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[wasm_bindgen(js_name = "setFloatCol")]
|
|
626
|
+
pub fn set_float_col(
|
|
627
|
+
&self,
|
|
628
|
+
name: &str,
|
|
629
|
+
index: u32,
|
|
630
|
+
val: JsValue,
|
|
631
|
+
group_by_index: Option<usize>,
|
|
632
|
+
) -> Result<(), JsValue> {
|
|
633
|
+
if val.is_null() || val.is_undefined() {
|
|
634
|
+
self.1
|
|
635
|
+
.lock()
|
|
636
|
+
.unwrap()
|
|
637
|
+
.set_col(name, group_by_index, index as usize, None as Option<f64>)
|
|
638
|
+
.unwrap();
|
|
639
|
+
} else if let Some(n) = val.as_f64() {
|
|
640
|
+
self.1
|
|
641
|
+
.lock()
|
|
642
|
+
.unwrap()
|
|
643
|
+
.set_col(name, group_by_index, index as usize, Some(n))
|
|
644
|
+
.unwrap();
|
|
645
|
+
} else {
|
|
646
|
+
tracing::error!("Unhandled float value");
|
|
647
|
+
}
|
|
648
|
+
Ok(())
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#[wasm_bindgen(js_name = "setBooleanCol")]
|
|
652
|
+
pub fn set_boolean_col(
|
|
653
|
+
&self,
|
|
654
|
+
name: &str,
|
|
655
|
+
index: u32,
|
|
656
|
+
val: JsValue,
|
|
657
|
+
group_by_index: Option<usize>,
|
|
658
|
+
) -> Result<(), JsValue> {
|
|
659
|
+
if val.is_null() || val.is_undefined() {
|
|
660
|
+
self.1
|
|
661
|
+
.lock()
|
|
662
|
+
.unwrap()
|
|
663
|
+
.set_col(name, group_by_index, index as usize, None as Option<bool>)
|
|
664
|
+
.unwrap();
|
|
665
|
+
} else if let Some(b) = val.as_bool() {
|
|
666
|
+
self.1
|
|
667
|
+
.lock()
|
|
668
|
+
.unwrap()
|
|
669
|
+
.set_col(name, group_by_index, index as usize, Some(b))
|
|
670
|
+
.unwrap();
|
|
671
|
+
} else {
|
|
672
|
+
tracing::error!("Unhandled boolean value");
|
|
673
|
+
}
|
|
674
|
+
Ok(())
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
#[wasm_bindgen(js_name = "setDatetimeCol")]
|
|
678
|
+
pub fn set_datetime_col(
|
|
679
|
+
&self,
|
|
680
|
+
name: &str,
|
|
681
|
+
index: u32,
|
|
682
|
+
val: JsValue,
|
|
683
|
+
group_by_index: Option<usize>,
|
|
684
|
+
) -> Result<(), JsValue> {
|
|
685
|
+
if val.is_null() || val.is_undefined() {
|
|
686
|
+
self.1
|
|
687
|
+
.lock()
|
|
688
|
+
.unwrap()
|
|
689
|
+
.set_col(name, group_by_index, index as usize, None as Option<i64>)
|
|
690
|
+
.unwrap();
|
|
691
|
+
} else if let Some(date) = val.dyn_ref::<Date>() {
|
|
692
|
+
let timestamp = date.get_time() as i64;
|
|
693
|
+
self.1
|
|
694
|
+
.lock()
|
|
695
|
+
.unwrap()
|
|
696
|
+
.set_col(name, group_by_index, index as usize, Some(timestamp))
|
|
697
|
+
.unwrap();
|
|
698
|
+
} else if let Some(n) = val.as_f64() {
|
|
699
|
+
self.1
|
|
700
|
+
.lock()
|
|
701
|
+
.unwrap()
|
|
702
|
+
.set_col(name, group_by_index, index as usize, Some(n as i64))
|
|
703
|
+
.unwrap();
|
|
704
|
+
} else {
|
|
705
|
+
tracing::error!("Unhandled datetime value");
|
|
706
|
+
}
|
|
707
|
+
Ok(())
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
#[wasm_bindgen]
|
|
712
|
+
pub struct JsVirtualServer(Rc<UnsafeCell<VirtualServer<JsServerHandler>>>);
|
|
713
|
+
|
|
714
|
+
#[wasm_bindgen]
|
|
715
|
+
impl JsVirtualServer {
|
|
716
|
+
#[wasm_bindgen(constructor)]
|
|
717
|
+
pub fn new(handler: Object) -> Result<JsVirtualServer, JsValue> {
|
|
718
|
+
Ok(JsVirtualServer(Rc::new(UnsafeCell::new(
|
|
719
|
+
VirtualServer::new(JsServerHandler(handler)),
|
|
720
|
+
))))
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
#[wasm_bindgen(js_name = "handleRequest")]
|
|
724
|
+
pub fn handle_request(&self, bytes: &[u8]) -> ApiFuture<Vec<u8>> {
|
|
725
|
+
let bytes = bytes.to_vec();
|
|
726
|
+
let server = self.0.clone();
|
|
727
|
+
|
|
728
|
+
ApiFuture::new(async move {
|
|
729
|
+
// SAFETY:
|
|
730
|
+
// - WASM is single-threaded
|
|
731
|
+
// - JS re-entrancy is allowed by design
|
|
732
|
+
// - VirtualServer must tolerate re-entrant mutation
|
|
733
|
+
let result = unsafe {
|
|
734
|
+
(&mut *server.as_ref().get())
|
|
735
|
+
.handle_request(bytes::Bytes::from(bytes))
|
|
736
|
+
.await
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
match result.get_internal_error() {
|
|
740
|
+
Ok(x) => Ok(x.to_vec()),
|
|
741
|
+
Err(Ok(x)) => Err(ApiError::from(JsValue::from(x))),
|
|
742
|
+
Err(Err(x)) => Err(ApiError::from(JsValue::from_str(&x))),
|
|
743
|
+
}
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
}
|