@perspective-dev/client 4.4.1 → 4.5.1
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 +2 -2
- package/dist/cdn/perspective.js +2 -2
- package/dist/cdn/perspective.js.map +3 -3
- package/dist/esm/perspective.browser.d.ts +19 -0
- package/dist/esm/perspective.inline.js +2 -2
- package/dist/esm/perspective.inline.js.map +3 -3
- package/dist/esm/perspective.js +2 -2
- package/dist/esm/perspective.js.map +3 -3
- package/dist/esm/perspective.node.d.ts +12 -0
- package/dist/esm/perspective.node.js +160 -15
- package/dist/esm/perspective.node.js.map +2 -2
- package/dist/esm/ts-rs/TypedArrayWindow.d.ts +31 -0
- package/dist/esm/ts-rs/ViewWindow.d.ts +6 -0
- package/dist/esm/virtual_servers/duckdb.js +1 -1
- package/dist/esm/virtual_servers/duckdb.js.map +2 -2
- package/dist/wasm/perspective-js.d.ts +51 -5
- package/dist/wasm/perspective-js.js +167 -14
- package/dist/wasm/perspective-js.wasm +0 -0
- package/dist/wasm/perspective-js.wasm.d.ts +8 -5
- package/package.json +27 -3
- package/src/rust/client.rs +28 -0
- package/src/rust/lib.rs +4 -0
- package/src/rust/typed_array.rs +243 -0
- package/src/rust/view.rs +38 -0
- package/src/rust/virtual_server.rs +4 -4
- package/src/ts/perspective.browser.ts +89 -7
- package/src/ts/perspective.node.ts +15 -1
- package/src/ts/ts-rs/TypedArrayWindow.ts +26 -0
- package/src/ts/ts-rs/ViewWindow.ts +7 -1
- package/src/ts/virtual_servers/duckdb.ts +4 -0
- package/src/ts/wasm/engine.ts +2 -2
- package/src/ts/wasm/perspective-server.poly.ts +1 -1
package/package.json
CHANGED
|
@@ -1,29 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perspective-dev/client",
|
|
3
|
-
"version": "4.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "4.5.1",
|
|
4
|
+
"description": "Client for Perspective, a high-performance WASM engine with reactive Tables, Views, and joins.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"perspective",
|
|
7
|
+
"data",
|
|
8
|
+
"analytics",
|
|
9
|
+
"streaming",
|
|
10
|
+
"wasm",
|
|
11
|
+
"arrow",
|
|
12
|
+
"duckdb",
|
|
13
|
+
"clickhouse"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://perspective-dev.github.io",
|
|
5
16
|
"repository": {
|
|
6
17
|
"type": "git",
|
|
7
18
|
"url": "https://github.com/perspective-dev/perspective"
|
|
8
19
|
},
|
|
9
20
|
"type": "module",
|
|
10
21
|
"license": "Apache-2.0",
|
|
22
|
+
"sideEffects": false,
|
|
11
23
|
"unpkg": "dist/cdn/perspective.js",
|
|
12
24
|
"jsdelivr": "dist/cdn/perspective.js",
|
|
13
25
|
"exports": {
|
|
14
26
|
".": {
|
|
15
27
|
"node": {
|
|
16
28
|
"types": "./dist/esm/perspective.node.d.ts",
|
|
29
|
+
"import": "./dist/esm/perspective.node.js",
|
|
17
30
|
"default": "./dist/esm/perspective.node.js"
|
|
18
31
|
},
|
|
19
32
|
"types": "./dist/esm/perspective.browser.d.ts",
|
|
33
|
+
"import": "./dist/esm/perspective.js",
|
|
20
34
|
"default": "./dist/esm/perspective.js"
|
|
21
35
|
},
|
|
22
36
|
"./node": {
|
|
23
37
|
"types": "./dist/esm/perspective.node.d.ts",
|
|
38
|
+
"import": "./dist/esm/perspective.node.js",
|
|
24
39
|
"default": "./dist/esm/perspective.node.js"
|
|
25
40
|
},
|
|
26
|
-
"./
|
|
41
|
+
"./inline": {
|
|
42
|
+
"types": "./dist/esm/perspective.browser.d.ts",
|
|
43
|
+
"import": "./dist/esm/perspective.inline.js",
|
|
44
|
+
"default": "./dist/esm/perspective.inline.js"
|
|
45
|
+
},
|
|
46
|
+
"./virtual_servers/*": {
|
|
47
|
+
"types": "./dist/esm/virtual_servers/*.d.ts",
|
|
48
|
+
"import": "./dist/esm/virtual_servers/*.js",
|
|
49
|
+
"default": "./dist/esm/virtual_servers/*.js"
|
|
50
|
+
},
|
|
27
51
|
"./dist/*": "./dist/*",
|
|
28
52
|
"./src/*": "./src/*",
|
|
29
53
|
"./test/*": "./test/*",
|
package/src/rust/client.rs
CHANGED
|
@@ -481,6 +481,34 @@ impl Client {
|
|
|
481
481
|
Ok(Table(self.client.open_table(entity_id).await?))
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
/// Unsafely gets a [`View`] by raw ID, useful for JavaScript multi-threaded
|
|
485
|
+
/// (via Web Worker) context where a standard `View` cannot otherwise be
|
|
486
|
+
/// shared because its wrapper is not serializable.
|
|
487
|
+
///
|
|
488
|
+
/// # Safety
|
|
489
|
+
///
|
|
490
|
+
/// This method is unsafe because the lifetime of a [`View`] is bound to
|
|
491
|
+
/// the [`Client`] which created it.
|
|
492
|
+
///
|
|
493
|
+
/// The caller must guarantee that `entity_id` corresponds to a live
|
|
494
|
+
/// [`crate::View`] on the connected server (obtained from another
|
|
495
|
+
/// [`Client`]'s [`crate::View::get_name`] and forwarded across the
|
|
496
|
+
/// serialization boundary).
|
|
497
|
+
///
|
|
498
|
+
/// # JavaScript Examples
|
|
499
|
+
///
|
|
500
|
+
/// ```javascript
|
|
501
|
+
/// const view = client.__unsafe_open_view(name_from_main_thread);
|
|
502
|
+
/// const cols = await view.to_columns();
|
|
503
|
+
/// ```
|
|
504
|
+
#[wasm_bindgen]
|
|
505
|
+
pub fn __unsafe_open_view(&self, entity_id: String) -> crate::view::View {
|
|
506
|
+
crate::view::View(perspective_client::View::new(
|
|
507
|
+
entity_id,
|
|
508
|
+
self.client.clone(),
|
|
509
|
+
))
|
|
510
|
+
}
|
|
511
|
+
|
|
484
512
|
/// Retrieves the names of all tables that this client has access to.
|
|
485
513
|
///
|
|
486
514
|
/// `name` is a string identifier unique to the [`Table`] (per [`Client`]),
|
package/src/rust/lib.rs
CHANGED
|
@@ -29,6 +29,7 @@ mod client;
|
|
|
29
29
|
mod generic_sql_model;
|
|
30
30
|
mod table;
|
|
31
31
|
mod table_data;
|
|
32
|
+
mod typed_array;
|
|
32
33
|
pub mod utils;
|
|
33
34
|
mod view;
|
|
34
35
|
mod virtual_server;
|
|
@@ -40,6 +41,7 @@ pub use crate::client::Client;
|
|
|
40
41
|
pub use crate::generic_sql_model::*;
|
|
41
42
|
pub use crate::table::*;
|
|
42
43
|
pub use crate::table_data::*;
|
|
44
|
+
pub use crate::typed_array::*;
|
|
43
45
|
pub use crate::virtual_server::*;
|
|
44
46
|
|
|
45
47
|
#[cfg(feature = "export-init")]
|
|
@@ -61,10 +63,12 @@ export type * from "../../src/ts/ts-rs/Filter.d.ts";
|
|
|
61
63
|
export type * from "../../src/ts/ts-rs/ViewConfig.d.ts";
|
|
62
64
|
export type * from "../../src/ts/ts-rs/JoinOptions.ts";
|
|
63
65
|
export type * from "../../src/ts/ts-rs/JoinType.ts";
|
|
66
|
+
export type * from "../../src/ts/ts-rs/TypedArrayWindow.ts";
|
|
64
67
|
|
|
65
68
|
import type {ColumnWindow} from "../../src/ts/ts-rs/ColumnWindow.d.ts";
|
|
66
69
|
import type {ColumnType} from "../../src/ts/ts-rs/ColumnType.d.ts";
|
|
67
70
|
import type {ViewWindow} from "../../src/ts/ts-rs/ViewWindow.d.ts";
|
|
71
|
+
import type {TypedArrayWindow} from "../../src/ts/ts-rs/TypedArrayWindow.ts";
|
|
68
72
|
import type {TableInitOptions} from "../../src/ts/ts-rs/TableInitOptions.d.ts";
|
|
69
73
|
import type {JoinOptions} from "../../src/ts/ts-rs/JoinOptions.ts";
|
|
70
74
|
import type {JoinType} from "../../src/ts/ts-rs/JoinType.ts";
|
|
@@ -0,0 +1,243 @@
|
|
|
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::io::Cursor;
|
|
14
|
+
|
|
15
|
+
use arrow_array::Array as _;
|
|
16
|
+
use arrow_array::cast::AsArray;
|
|
17
|
+
use arrow_array::types::*;
|
|
18
|
+
use arrow_ipc::reader::StreamReader;
|
|
19
|
+
use arrow_schema::{DataType, TimeUnit};
|
|
20
|
+
use js_sys::{Array, Function, JsString, Uint8Array};
|
|
21
|
+
use perspective_client::ViewWindow;
|
|
22
|
+
use ts_rs::TS;
|
|
23
|
+
use wasm_bindgen::JsCast;
|
|
24
|
+
use wasm_bindgen::prelude::*;
|
|
25
|
+
|
|
26
|
+
#[wasm_bindgen]
|
|
27
|
+
unsafe extern "C" {
|
|
28
|
+
#[wasm_bindgen(typescript_type = "TypedArrayWindow")]
|
|
29
|
+
#[derive(Clone)]
|
|
30
|
+
pub type JsTypedArrayWindow;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Options for `with_typed_arrays`, extending `ViewWindow` with
|
|
34
|
+
/// typed-array-specific options.
|
|
35
|
+
#[derive(Default, serde::Deserialize, TS)]
|
|
36
|
+
pub struct TypedArrayWindow {
|
|
37
|
+
#[serde(flatten)]
|
|
38
|
+
pub view_window: ViewWindow,
|
|
39
|
+
|
|
40
|
+
/// When `true`, Float64/Date32/Timestamp columns are output as
|
|
41
|
+
/// `Float32Array` instead of `Float64Array`.
|
|
42
|
+
#[serde(default)]
|
|
43
|
+
pub float32: bool,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl From<TypedArrayWindow> for ViewWindow {
|
|
47
|
+
fn from(w: TypedArrayWindow) -> Self {
|
|
48
|
+
w.view_window
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Decode an Arrow IPC batch and call `callback` once with all columns.
|
|
53
|
+
///
|
|
54
|
+
/// Callback signature:
|
|
55
|
+
/// ```js
|
|
56
|
+
/// callback(names: string[], values: TypedArray[], validities: (Uint8Array|null)[], dictionaries: (string[]|null)[]) => void | Promise<void>
|
|
57
|
+
/// ```
|
|
58
|
+
///
|
|
59
|
+
/// If the callback returns a `Promise`, it is awaited before the Arrow
|
|
60
|
+
/// batch (and therefore the zero-copy typed-array views into it) is
|
|
61
|
+
/// dropped. A synchronous callback returning `undefined` is supported
|
|
62
|
+
/// with no promise-handling overhead.
|
|
63
|
+
pub(crate) async fn decode_and_call(
|
|
64
|
+
arrow: &[u8],
|
|
65
|
+
float32: bool,
|
|
66
|
+
callback: &Function,
|
|
67
|
+
) -> Result<(), JsValue> {
|
|
68
|
+
let cursor = Cursor::new(arrow);
|
|
69
|
+
let reader = StreamReader::try_new(cursor, None)
|
|
70
|
+
.map_err(|e| JsValue::from_str(&format!("Arrow decode error: {e}")))?;
|
|
71
|
+
|
|
72
|
+
let batch = reader
|
|
73
|
+
.into_iter()
|
|
74
|
+
.next()
|
|
75
|
+
.ok_or_else(|| JsValue::from_str("Arrow IPC contained no record batches"))?
|
|
76
|
+
.map_err(|e| JsValue::from_str(&format!("Arrow batch error: {e}")))?;
|
|
77
|
+
|
|
78
|
+
let schema = batch.schema();
|
|
79
|
+
let num_cols = batch.num_columns();
|
|
80
|
+
|
|
81
|
+
let js_names = Array::new_with_length(num_cols as u32);
|
|
82
|
+
let js_values = Array::new_with_length(num_cols as u32);
|
|
83
|
+
let js_validities = Array::new_with_length(num_cols as u32);
|
|
84
|
+
let js_dicts = Array::new_with_length(num_cols as u32);
|
|
85
|
+
|
|
86
|
+
// Storage for allocated conversion buffers. These MUST outlive the
|
|
87
|
+
// callback because `js_sys::*Array::view()` creates zero-copy views
|
|
88
|
+
// into their heap memory. Using `Box<[T]>` (rather than `Vec<T>`)
|
|
89
|
+
// yields a stable pointer that won't move when the outer Vec grows.
|
|
90
|
+
let mut f32_storage: Vec<Box<[f32]>> = Vec::new();
|
|
91
|
+
let mut f64_storage: Vec<Box<[f64]>> = Vec::new();
|
|
92
|
+
|
|
93
|
+
for col_idx in 0..num_cols {
|
|
94
|
+
let field = schema.field(col_idx);
|
|
95
|
+
let col = batch.column(col_idx);
|
|
96
|
+
let validity = col.nulls().map(|nulls| nulls.validity());
|
|
97
|
+
|
|
98
|
+
js_names.set(col_idx as u32, JsString::from(field.name().as_str()).into());
|
|
99
|
+
|
|
100
|
+
match col.data_type() {
|
|
101
|
+
DataType::UInt32 => {
|
|
102
|
+
let vals = col.as_primitive::<UInt32Type>().values();
|
|
103
|
+
let arr = unsafe { js_sys::Uint32Array::view(vals.as_ref()) };
|
|
104
|
+
js_values.set(col_idx as u32, arr.into());
|
|
105
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
106
|
+
},
|
|
107
|
+
DataType::Int32 => {
|
|
108
|
+
let vals = col.as_primitive::<Int32Type>().values();
|
|
109
|
+
let arr = unsafe { js_sys::Int32Array::view(vals.as_ref()) };
|
|
110
|
+
js_values.set(col_idx as u32, arr.into());
|
|
111
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
112
|
+
},
|
|
113
|
+
DataType::Float32 => {
|
|
114
|
+
let vals = col.as_primitive::<Float32Type>().values();
|
|
115
|
+
let arr = unsafe { js_sys::Float32Array::view(vals.as_ref()) };
|
|
116
|
+
js_values.set(col_idx as u32, arr.into());
|
|
117
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
118
|
+
},
|
|
119
|
+
DataType::Float64 => {
|
|
120
|
+
if float32 {
|
|
121
|
+
let vals = col.as_primitive::<Float64Type>().values();
|
|
122
|
+
f32_storage.push(vals.iter().map(|&v| v as f32).collect());
|
|
123
|
+
} else {
|
|
124
|
+
let vals = col.as_primitive::<Float64Type>().values();
|
|
125
|
+
let arr = unsafe { js_sys::Float64Array::view(vals.as_ref()) };
|
|
126
|
+
js_values.set(col_idx as u32, arr.into());
|
|
127
|
+
}
|
|
128
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
129
|
+
},
|
|
130
|
+
DataType::Date32 => {
|
|
131
|
+
// Datetime values are always emitted as Float64 — narrowing
|
|
132
|
+
// epoch-ms to f32 collapses ~256 ms of resolution at modern
|
|
133
|
+
// timestamps, so the `float32` flag is intentionally ignored
|
|
134
|
+
// for date/timestamp columns.
|
|
135
|
+
let typed = col.as_primitive::<Date32Type>();
|
|
136
|
+
f64_storage.push(
|
|
137
|
+
typed
|
|
138
|
+
.values()
|
|
139
|
+
.iter()
|
|
140
|
+
.map(|&v| v as f64 * 86_400_000.0)
|
|
141
|
+
.collect(),
|
|
142
|
+
);
|
|
143
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
144
|
+
},
|
|
145
|
+
DataType::Timestamp(TimeUnit::Millisecond, _) => {
|
|
146
|
+
let typed = col.as_primitive::<TimestampMillisecondType>();
|
|
147
|
+
f64_storage.push(typed.values().iter().map(|&v| v as f64).collect());
|
|
148
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
149
|
+
},
|
|
150
|
+
DataType::Int64 => {
|
|
151
|
+
let typed = col.as_primitive::<Int64Type>();
|
|
152
|
+
if float32 {
|
|
153
|
+
f32_storage.push(typed.values().iter().map(|&v| v as f32).collect());
|
|
154
|
+
} else {
|
|
155
|
+
f64_storage.push(typed.values().iter().map(|&v| v as f64).collect());
|
|
156
|
+
}
|
|
157
|
+
js_dicts.set(col_idx as u32, JsValue::NULL);
|
|
158
|
+
},
|
|
159
|
+
DataType::Dictionary(..) => {
|
|
160
|
+
let dict = col.as_dictionary::<Int32Type>();
|
|
161
|
+
let keys = dict.keys();
|
|
162
|
+
let arr = unsafe { js_sys::Int32Array::view(keys.values().as_ref()) };
|
|
163
|
+
js_values.set(col_idx as u32, arr.into());
|
|
164
|
+
|
|
165
|
+
let values = dict.values().as_string::<i32>();
|
|
166
|
+
let js_dict = Array::new_with_length(values.len() as u32);
|
|
167
|
+
for i in 0..values.len() {
|
|
168
|
+
js_dict.set(i as u32, JsValue::from_str(values.value(i)));
|
|
169
|
+
}
|
|
170
|
+
js_dicts.set(col_idx as u32, js_dict.into());
|
|
171
|
+
},
|
|
172
|
+
dt => {
|
|
173
|
+
return Err(JsValue::from_str(&format!(
|
|
174
|
+
"Unsupported column type for typed array: {dt}"
|
|
175
|
+
)));
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// SAFETY: Validity bitmap is owned by `batch` which outlives the
|
|
180
|
+
// callback — safe to view zero-copy.
|
|
181
|
+
let js_validity = validity.map(|v| unsafe { Uint8Array::view(v) });
|
|
182
|
+
js_validities.set(
|
|
183
|
+
col_idx as u32,
|
|
184
|
+
js_validity
|
|
185
|
+
.as_ref()
|
|
186
|
+
.map(JsValue::from)
|
|
187
|
+
.unwrap_or(JsValue::NULL),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Second pass: fill in value views for columns backed by f32_storage /
|
|
192
|
+
// f64_storage. The Box<[T]> buffers are heap-allocated and stable; their
|
|
193
|
+
// data pointers remain valid even as the outer Vec grows.
|
|
194
|
+
let mut f32_idx = 0;
|
|
195
|
+
let mut f64_idx = 0;
|
|
196
|
+
for col_idx in 0..num_cols {
|
|
197
|
+
let col = batch.column(col_idx);
|
|
198
|
+
let uses_f32_storage = matches!(
|
|
199
|
+
(col.data_type(), float32),
|
|
200
|
+
(DataType::Float64, true) | (DataType::Int64, true),
|
|
201
|
+
);
|
|
202
|
+
let uses_f64_storage = matches!(
|
|
203
|
+
(col.data_type(), float32),
|
|
204
|
+
(DataType::Date32, _)
|
|
205
|
+
| (DataType::Timestamp(TimeUnit::Millisecond, _), _)
|
|
206
|
+
| (DataType::Int64, false),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if uses_f32_storage {
|
|
210
|
+
let arr = unsafe { js_sys::Float32Array::view(&f32_storage[f32_idx]) };
|
|
211
|
+
js_values.set(col_idx as u32, arr.into());
|
|
212
|
+
f32_idx += 1;
|
|
213
|
+
} else if uses_f64_storage {
|
|
214
|
+
let arr = unsafe { js_sys::Float64Array::view(&f64_storage[f64_idx]) };
|
|
215
|
+
js_values.set(col_idx as u32, arr.into());
|
|
216
|
+
f64_idx += 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let ret = callback.call4(
|
|
221
|
+
&JsValue::UNDEFINED,
|
|
222
|
+
&js_names.into(),
|
|
223
|
+
&js_values.into(),
|
|
224
|
+
&js_validities.into(),
|
|
225
|
+
&js_dicts.into(),
|
|
226
|
+
)?;
|
|
227
|
+
|
|
228
|
+
// If the callback returned a Promise, await it before releasing the
|
|
229
|
+
// batch — zero-copy TypedArray views into `batch`/`f32_storage`/
|
|
230
|
+
// `f64_storage` must remain valid for the full lifetime of the
|
|
231
|
+
// awaited work.
|
|
232
|
+
if ret.is_instance_of::<js_sys::Promise>() {
|
|
233
|
+
let promise: js_sys::Promise = ret.unchecked_into();
|
|
234
|
+
wasm_bindgen_futures::JsFuture::from(promise).await?;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Keep storage alive until after the callback (and its awaited
|
|
238
|
+
// promise, if any) returns.
|
|
239
|
+
drop(f32_storage);
|
|
240
|
+
drop(f64_storage);
|
|
241
|
+
|
|
242
|
+
Ok(())
|
|
243
|
+
}
|
package/src/rust/view.rs
CHANGED
|
@@ -88,6 +88,12 @@ impl View {
|
|
|
88
88
|
self.clone()
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
#[wasm_bindgen]
|
|
92
|
+
#[doc(hidden)]
|
|
93
|
+
pub fn __unsafe_get_name(&self) -> String {
|
|
94
|
+
self.0.name.clone()
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
/// Returns an array of strings containing the column paths of the [`View`]
|
|
92
98
|
/// without any of the source columns.
|
|
93
99
|
///
|
|
@@ -247,6 +253,38 @@ impl View {
|
|
|
247
253
|
Ok(self.0.to_csv(window.unwrap_or_default()).await?)
|
|
248
254
|
}
|
|
249
255
|
|
|
256
|
+
/// Fetches columns from the [`View`] in Arrow format, decodes them, and
|
|
257
|
+
/// passes typed array views to `callback`. All arrays are only valid for
|
|
258
|
+
/// the duration of the callback — if `callback` returns a `Promise`, it
|
|
259
|
+
/// is awaited before the backing Arrow buffer is released, so async
|
|
260
|
+
/// callbacks may use the views for the full duration of the awaited
|
|
261
|
+
/// work (e.g. across an `await requestAnimationFrame`-backed promise).
|
|
262
|
+
///
|
|
263
|
+
/// # Arguments
|
|
264
|
+
///
|
|
265
|
+
/// - `window` - Optional [`TypedArrayWindow`] controlling row/column
|
|
266
|
+
/// windowing and output options (e.g., `float32` mode).
|
|
267
|
+
/// - `callback` - A JS function called with `(names: string[], values:
|
|
268
|
+
/// TypedArray[], validities: (Uint8Array|null)[], dictionaries:
|
|
269
|
+
/// (string[]|null)[]) => void | Promise<void>`.
|
|
270
|
+
#[wasm_bindgen]
|
|
271
|
+
pub async fn with_typed_arrays(
|
|
272
|
+
&self,
|
|
273
|
+
window: Option<crate::typed_array::JsTypedArrayWindow>,
|
|
274
|
+
callback: Function,
|
|
275
|
+
) -> ApiResult<()> {
|
|
276
|
+
let opts: crate::typed_array::TypedArrayWindow = window
|
|
277
|
+
.into_serde_ext::<Option<crate::typed_array::TypedArrayWindow>>()?
|
|
278
|
+
.unwrap_or_default();
|
|
279
|
+
|
|
280
|
+
let float32 = opts.float32;
|
|
281
|
+
let mut view_window: ViewWindow = opts.into();
|
|
282
|
+
view_window.emit_legacy_row_path_names = Some(false);
|
|
283
|
+
let arrow = self.0.to_arrow(view_window).await?;
|
|
284
|
+
crate::typed_array::decode_and_call(&arrow, float32, &callback).await?;
|
|
285
|
+
Ok(())
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
/// Register a callback with this [`View`]. Whenever the view's underlying
|
|
251
289
|
/// table emits an update, this callback will be invoked with an object
|
|
252
290
|
/// containing `port_id`, indicating which port the update fired on, and
|
|
@@ -372,7 +372,7 @@ impl VirtualServerHandler for JsServerHandler {
|
|
|
372
372
|
|
|
373
373
|
let handler = self.0.clone();
|
|
374
374
|
let view_id = view_id.to_string();
|
|
375
|
-
let config_value =
|
|
375
|
+
let config_value = JsValue::from_serde_ext(config).unwrap();
|
|
376
376
|
let config = config.clone();
|
|
377
377
|
Box::pin(async move {
|
|
378
378
|
let this = JsServerHandler(handler);
|
|
@@ -493,7 +493,7 @@ impl VirtualServerHandler for JsServerHandler {
|
|
|
493
493
|
let handler = self.0.clone();
|
|
494
494
|
let view_id = view_id.to_string();
|
|
495
495
|
let column_name = column_name.to_string();
|
|
496
|
-
let config_js =
|
|
496
|
+
let config_js = JsValue::from_serde_ext(config).unwrap();
|
|
497
497
|
Box::pin(async move {
|
|
498
498
|
let this = JsServerHandler(handler);
|
|
499
499
|
let args = Array::new();
|
|
@@ -518,8 +518,8 @@ impl VirtualServerHandler for JsServerHandler {
|
|
|
518
518
|
let handler = self.0.clone();
|
|
519
519
|
let view_id = view_id.to_string();
|
|
520
520
|
let window: JsViewPort = viewport.clone().into();
|
|
521
|
-
let config_value =
|
|
522
|
-
let window_value =
|
|
521
|
+
let config_value = JsValue::from_serde_ext(config).unwrap();
|
|
522
|
+
let window_value = JsValue::from_serde_ext(&window).unwrap();
|
|
523
523
|
let schema_value = JsValue::from_serde_ext(&schema).unwrap();
|
|
524
524
|
|
|
525
525
|
Box::pin(async move {
|
|
@@ -65,13 +65,32 @@ export function init_server(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
let GLOBAL_CLIENT_WASM: Promise<typeof psp>;
|
|
68
|
+
let GLOBAL_CLIENT_MODULE: Promise<WebAssembly.Module> | undefined;
|
|
69
|
+
|
|
70
|
+
async function compile_module(wasm: any): Promise<WebAssembly.Module> {
|
|
71
|
+
if (wasm instanceof WebAssembly.Module) {
|
|
72
|
+
return wasm;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof Response !== "undefined" && wasm instanceof Response) {
|
|
76
|
+
return WebAssembly.compileStreaming(wasm);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return WebAssembly.compile(wasm);
|
|
80
|
+
}
|
|
68
81
|
|
|
69
82
|
async function compilerize(
|
|
70
83
|
wasm: PerspectiveWasm,
|
|
71
84
|
disable_stage_0: boolean = false,
|
|
72
85
|
) {
|
|
73
86
|
const wasm_buff = disable_stage_0 ? wasm : await load_wasm_stage_0(wasm);
|
|
74
|
-
|
|
87
|
+
// Compile to a `WebAssembly.Module` once so it can be both instantiated
|
|
88
|
+
// locally and forwarded to other workers via `getCompiledClientWasm()`.
|
|
89
|
+
// `WebAssembly.Module` is structured-cloneable across workers, so the
|
|
90
|
+
// recipient can instantiate without re-fetching or re-compiling.
|
|
91
|
+
const compiled = await compile_module(wasm_buff);
|
|
92
|
+
GLOBAL_CLIENT_MODULE = Promise.resolve(compiled);
|
|
93
|
+
await wasm_module.default({ module_or_path: compiled });
|
|
75
94
|
await wasm_module.init();
|
|
76
95
|
return wasm_module;
|
|
77
96
|
}
|
|
@@ -103,6 +122,14 @@ function get_client() {
|
|
|
103
122
|
const viewer_class: any = customElements.get("perspective-viewer");
|
|
104
123
|
if (viewer_class) {
|
|
105
124
|
GLOBAL_CLIENT_WASM = Promise.resolve(viewer_class.__wasm_module__);
|
|
125
|
+
if (
|
|
126
|
+
GLOBAL_CLIENT_MODULE === undefined &&
|
|
127
|
+
viewer_class.__wasm_client_module__
|
|
128
|
+
) {
|
|
129
|
+
GLOBAL_CLIENT_MODULE = Promise.resolve(
|
|
130
|
+
viewer_class.__wasm_client_module__,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
106
133
|
} else if (GLOBAL_CLIENT_WASM === undefined) {
|
|
107
134
|
throw new Error("Missing perspective-client.wasm");
|
|
108
135
|
}
|
|
@@ -110,6 +137,43 @@ function get_client() {
|
|
|
110
137
|
return GLOBAL_CLIENT_WASM;
|
|
111
138
|
}
|
|
112
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Returns the compiled `WebAssembly.Module` for the perspective-js client
|
|
142
|
+
* runtime. The module is structured-cloneable, so it can be sent via
|
|
143
|
+
* `postMessage` to a Worker which can instantiate its own `Client` without
|
|
144
|
+
* re-fetching or re-compiling the wasm binary.
|
|
145
|
+
*
|
|
146
|
+
* Requires that the client wasm has been initialized — typically by a prior
|
|
147
|
+
* call to `init_client(...)`, or implicitly by mounting a `<perspective-viewer>`
|
|
148
|
+
* element. Throws otherwise.
|
|
149
|
+
*
|
|
150
|
+
* # Examples
|
|
151
|
+
*
|
|
152
|
+
* ```javascript
|
|
153
|
+
* const mod = await perspective.getCompiledClientWasm();
|
|
154
|
+
* worker.postMessage({ kind: "init", clientWasm: mod }, [port]);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export async function getCompiledClientWasm(): Promise<WebAssembly.Module> {
|
|
158
|
+
if (GLOBAL_CLIENT_MODULE !== undefined) {
|
|
159
|
+
return GLOBAL_CLIENT_MODULE;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const viewer_class: any = customElements.get("perspective-viewer");
|
|
163
|
+
if (viewer_class?.__wasm_client_module__) {
|
|
164
|
+
GLOBAL_CLIENT_MODULE = Promise.resolve(
|
|
165
|
+
viewer_class.__wasm_client_module__,
|
|
166
|
+
);
|
|
167
|
+
return GLOBAL_CLIENT_MODULE;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
throw new Error(
|
|
171
|
+
"perspective-js client wasm has not been compiled yet — call " +
|
|
172
|
+
"`init_client(...)` or `perspective.worker()` before " +
|
|
173
|
+
"`getCompiledClientWasm()`.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
113
177
|
function get_server() {
|
|
114
178
|
if (GLOBAL_SERVER_WASM === undefined) {
|
|
115
179
|
throw new Error("Missing perspective-server.wasm");
|
|
@@ -122,13 +186,30 @@ function get_server() {
|
|
|
122
186
|
|
|
123
187
|
let GLOBAL_WORKER: undefined | (() => Promise<Worker>) = undefined;
|
|
124
188
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
189
|
+
// `WorkerPlugin` resolves this import to a stub that exports
|
|
190
|
+
// `getPerspectiveWorkerURL(): Promise<string>`. The URL is either a
|
|
191
|
+
// Blob URL (inline mode — production builds) or a real file path
|
|
192
|
+
// resolved against `import.meta.url` (file mode — debug builds).
|
|
193
|
+
// Constructing the `Worker` lives here in the consumer rather than
|
|
194
|
+
// inside the plugin so the same module text can also be loaded
|
|
195
|
+
// in-process via dynamic `import(url)` when a future caller wants
|
|
196
|
+
// it; the plugin no longer owns Worker lifecycle.
|
|
197
|
+
//
|
|
198
|
+
// `initialize()` constructs `new Worker(blobUrl)` and falls back to
|
|
199
|
+
// running the worker source on the main thread via `new Function(...)`
|
|
200
|
+
// when Worker construction is unavailable (e.g. `file://` origins where
|
|
201
|
+
// module-Worker support is gated). The shim it returns is
|
|
202
|
+
// MessagePort-shaped so downstream code can treat it like a real
|
|
203
|
+
// Worker.
|
|
204
|
+
// @ts-ignore — resolved at build time by `@perspective-dev/esbuild-plugin/worker`
|
|
205
|
+
import { initialize as initializePerspectiveWorker } from "../../src/ts/perspective-server.worker.js";
|
|
206
|
+
|
|
207
|
+
async function get_worker(): Promise<Worker> {
|
|
130
208
|
if (GLOBAL_WORKER === undefined) {
|
|
131
|
-
return
|
|
209
|
+
return await initializePerspectiveWorker({
|
|
210
|
+
type: "module",
|
|
211
|
+
name: "perspective-server",
|
|
212
|
+
});
|
|
132
213
|
}
|
|
133
214
|
|
|
134
215
|
return GLOBAL_WORKER();
|
|
@@ -154,6 +235,7 @@ export default {
|
|
|
154
235
|
init_client,
|
|
155
236
|
init_server,
|
|
156
237
|
createMessageHandler,
|
|
238
|
+
getCompiledClientWasm,
|
|
157
239
|
GenericSQLVirtualServerModel,
|
|
158
240
|
VirtualDataSlice,
|
|
159
241
|
VirtualServer,
|
|
@@ -121,7 +121,7 @@ export async function cwd_static_file_handler(
|
|
|
121
121
|
request: http.IncomingMessage,
|
|
122
122
|
response: http.ServerResponse<http.IncomingMessage>,
|
|
123
123
|
assets = ["./"],
|
|
124
|
-
{ debug =
|
|
124
|
+
{ debug = false } = {},
|
|
125
125
|
) {
|
|
126
126
|
let url =
|
|
127
127
|
request.url
|
|
@@ -143,10 +143,12 @@ export async function cwd_static_file_handler(
|
|
|
143
143
|
if (debug) {
|
|
144
144
|
console.log(`200 ${url}`);
|
|
145
145
|
}
|
|
146
|
+
|
|
146
147
|
response.writeHead(200, {
|
|
147
148
|
"Content-Type": contentType,
|
|
148
149
|
"Access-Control-Allow-Origin": "*",
|
|
149
150
|
});
|
|
151
|
+
|
|
150
152
|
if (extname === ".arrow" || extname === ".feather") {
|
|
151
153
|
response.end(content, "utf8");
|
|
152
154
|
} else {
|
|
@@ -193,6 +195,18 @@ function buffer_to_arraybuffer(
|
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
/**
|
|
199
|
+
* A simple Node `http`-based WebSocket adapter that exposes a
|
|
200
|
+
* `PerspectiveServer` over the wire.
|
|
201
|
+
*
|
|
202
|
+
* @remarks
|
|
203
|
+
*
|
|
204
|
+
* **Security.** `WebSocketServer` is a reference integration with no
|
|
205
|
+
* authentication, authorization, origin enforcement, or rate limiting, and
|
|
206
|
+
* is not safe to expose to untrusted networks — see
|
|
207
|
+
* [`SECURITY.md`](https://github.com/perspective-dev/perspective/blob/master/SECURITY.md)
|
|
208
|
+
* for the full threat model.
|
|
209
|
+
*/
|
|
196
210
|
export class WebSocketServer {
|
|
197
211
|
_server: http.Server | any; // stoppable has no type ...
|
|
198
212
|
_wss: HttpWebSocketServer;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for `with_typed_arrays`, extending `ViewWindow` with
|
|
5
|
+
* typed-array-specific options.
|
|
6
|
+
*/
|
|
7
|
+
export type TypedArrayWindow = {
|
|
8
|
+
/**
|
|
9
|
+
* When `true`, Float64/Date32/Timestamp columns are output as
|
|
10
|
+
* `Float32Array` instead of `Float64Array`.
|
|
11
|
+
*/
|
|
12
|
+
float32: boolean, start_row?: number, start_col?: number, end_row?: number, end_col?: number, id?: boolean, index?: boolean,
|
|
13
|
+
/**
|
|
14
|
+
* Only impacts [`View::to_csv`]
|
|
15
|
+
*/
|
|
16
|
+
formatted?: boolean,
|
|
17
|
+
/**
|
|
18
|
+
* Only impacts [`View::to_arrow`]
|
|
19
|
+
*/
|
|
20
|
+
compression?: string,
|
|
21
|
+
/**
|
|
22
|
+
* When `true`, group-by columns use legacy `"colname (Group by N)"`
|
|
23
|
+
* naming. When `false`, they use `__ROW_PATH_N__` naming consistent
|
|
24
|
+
* with the SQL backend. Defaults to `true` for backwards compatibility.
|
|
25
|
+
*/
|
|
26
|
+
emit_legacy_row_path_names?: boolean, };
|
|
@@ -14,4 +14,10 @@ formatted?: boolean,
|
|
|
14
14
|
/**
|
|
15
15
|
* Only impacts [`View::to_arrow`]
|
|
16
16
|
*/
|
|
17
|
-
compression?: string,
|
|
17
|
+
compression?: string,
|
|
18
|
+
/**
|
|
19
|
+
* When `true`, group-by columns use legacy `"colname (Group by N)"`
|
|
20
|
+
* naming. When `false`, they use `__ROW_PATH_N__` naming consistent
|
|
21
|
+
* with the SQL backend. Defaults to `true` for backwards compatibility.
|
|
22
|
+
*/
|
|
23
|
+
emit_legacy_row_path_names?: boolean, };
|