@perspective-dev/client 4.0.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.
Files changed (91) hide show
  1. package/LICENSE.md +193 -0
  2. package/README.md +3 -0
  3. package/dist/cdn/perspective-server.worker.js +2 -0
  4. package/dist/cdn/perspective-server.worker.js.map +7 -0
  5. package/dist/cdn/perspective.js +3 -0
  6. package/dist/cdn/perspective.js.map +7 -0
  7. package/dist/esm/perspective-server.worker.d.ts +1 -0
  8. package/dist/esm/perspective.browser.d.ts +14 -0
  9. package/dist/esm/perspective.inline.js +3 -0
  10. package/dist/esm/perspective.inline.js.map +7 -0
  11. package/dist/esm/perspective.js +3 -0
  12. package/dist/esm/perspective.js.map +7 -0
  13. package/dist/esm/perspective.node.d.ts +60 -0
  14. package/dist/esm/perspective.node.js +2431 -0
  15. package/dist/esm/perspective.node.js.map +7 -0
  16. package/dist/esm/ts-rs/Aggregate.d.ts +1 -0
  17. package/dist/esm/ts-rs/ColumnWindow.d.ts +4 -0
  18. package/dist/esm/ts-rs/DeleteOptions.d.ts +6 -0
  19. package/dist/esm/ts-rs/Expressions.d.ts +3 -0
  20. package/dist/esm/ts-rs/Filter.d.ts +2 -0
  21. package/dist/esm/ts-rs/FilterReducer.d.ts +1 -0
  22. package/dist/esm/ts-rs/FilterTerm.d.ts +2 -0
  23. package/dist/esm/ts-rs/OnUpdateMode.d.ts +9 -0
  24. package/dist/esm/ts-rs/OnUpdateOptions.d.ts +7 -0
  25. package/dist/esm/ts-rs/Scalar.d.ts +5 -0
  26. package/dist/esm/ts-rs/Sort.d.ts +2 -0
  27. package/dist/esm/ts-rs/SortDir.d.ts +1 -0
  28. package/dist/esm/ts-rs/SystemInfo.d.ts +40 -0
  29. package/dist/esm/ts-rs/TableInitOptions.d.ts +22 -0
  30. package/dist/esm/ts-rs/TableReadFormat.d.ts +7 -0
  31. package/dist/esm/ts-rs/UpdateOptions.d.ts +8 -0
  32. package/dist/esm/ts-rs/ViewConfigUpdate.d.ts +90 -0
  33. package/dist/esm/ts-rs/ViewOnUpdateResp.d.ts +4 -0
  34. package/dist/esm/ts-rs/ViewWindow.d.ts +23 -0
  35. package/dist/esm/wasm/browser.d.ts +21 -0
  36. package/dist/esm/wasm/decompress.d.ts +1 -0
  37. package/dist/esm/wasm/emscripten_api.d.ts +5 -0
  38. package/dist/esm/wasm/engine.d.ts +40 -0
  39. package/dist/esm/wasm/perspective-server.poly.d.ts +1 -0
  40. package/dist/esm/websocket.d.ts +4 -0
  41. package/dist/wasm/perspective-js.d.ts +712 -0
  42. package/dist/wasm/perspective-js.js +1934 -0
  43. package/dist/wasm/perspective-js.wasm +0 -0
  44. package/dist/wasm/perspective-js.wasm.d.ts +75 -0
  45. package/package.json +68 -0
  46. package/src/rust/client.rs +483 -0
  47. package/src/rust/lib.rs +70 -0
  48. package/src/rust/table.rs +364 -0
  49. package/src/rust/table_data.rs +159 -0
  50. package/src/rust/utils/browser.rs +39 -0
  51. package/src/rust/utils/console_logger.rs +236 -0
  52. package/src/rust/utils/errors.rs +288 -0
  53. package/src/rust/utils/futures.rs +174 -0
  54. package/src/rust/utils/json.rs +252 -0
  55. package/src/rust/utils/local_poll_loop.rs +63 -0
  56. package/src/rust/utils/mod.rs +32 -0
  57. package/src/rust/utils/serde.rs +46 -0
  58. package/src/rust/utils/trace_allocator.rs +98 -0
  59. package/src/rust/view.rs +355 -0
  60. package/src/ts/perspective-server.worker.ts +54 -0
  61. package/src/ts/perspective.browser.ts +132 -0
  62. package/src/ts/perspective.cdn.ts +22 -0
  63. package/src/ts/perspective.inline.ts +27 -0
  64. package/src/ts/perspective.node.ts +315 -0
  65. package/src/ts/ts-rs/Aggregate.ts +3 -0
  66. package/src/ts/ts-rs/ColumnWindow.ts +3 -0
  67. package/src/ts/ts-rs/DeleteOptions.ts +6 -0
  68. package/src/ts/ts-rs/Expressions.ts +3 -0
  69. package/src/ts/ts-rs/Filter.ts +4 -0
  70. package/src/ts/ts-rs/FilterReducer.ts +3 -0
  71. package/src/ts/ts-rs/FilterTerm.ts +4 -0
  72. package/src/ts/ts-rs/OnUpdateData.ts +8 -0
  73. package/src/ts/ts-rs/OnUpdateMode.ts +11 -0
  74. package/src/ts/ts-rs/OnUpdateOptions.ts +7 -0
  75. package/src/ts/ts-rs/Scalar.ts +7 -0
  76. package/src/ts/ts-rs/Sort.ts +4 -0
  77. package/src/ts/ts-rs/SortDir.ts +3 -0
  78. package/src/ts/ts-rs/SystemInfo.ts +41 -0
  79. package/src/ts/ts-rs/TableInitOptions.ts +21 -0
  80. package/src/ts/ts-rs/TableReadFormat.ts +9 -0
  81. package/src/ts/ts-rs/UpdateOptions.ts +7 -0
  82. package/src/ts/ts-rs/ViewConfigUpdate.ts +87 -0
  83. package/src/ts/ts-rs/ViewOnUpdateResp.ts +3 -0
  84. package/src/ts/ts-rs/ViewWindow.ts +17 -0
  85. package/src/ts/wasm/browser.ts +123 -0
  86. package/src/ts/wasm/decompress.ts +64 -0
  87. package/src/ts/wasm/emscripten_api.ts +63 -0
  88. package/src/ts/wasm/engine.ts +271 -0
  89. package/src/ts/wasm/perspective-server.poly.ts +244 -0
  90. package/src/ts/websocket.ts +95 -0
  91. package/tsconfig.json +20 -0
@@ -0,0 +1,236 @@
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::fmt::{Debug, Write};
14
+ use std::sync::OnceLock;
15
+
16
+ use tracing::Subscriber;
17
+ use tracing::field::{Field, Visit};
18
+ use tracing_subscriber::Layer;
19
+ use tracing_subscriber::layer::Context;
20
+ use tracing_subscriber::registry::LookupSpan;
21
+ use wasm_bindgen::prelude::*;
22
+
23
+ use crate::utils::*;
24
+
25
+ /// A struct to implement the `Visit` visitor pattern trait to process
26
+ /// `tracing::Event`s.
27
+ #[derive(Default)]
28
+ struct LogLineBuffer {
29
+ value: String,
30
+ is_tail: bool,
31
+ }
32
+
33
+ impl Visit for LogLineBuffer {
34
+ fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
35
+ if field.name() == "message" {
36
+ if !self.value.is_empty() {
37
+ self.value = format!("{:?}\n{}", value, self.value)
38
+ } else {
39
+ self.value = format!("{value:?}")
40
+ }
41
+ } else {
42
+ if self.is_tail {
43
+ writeln!(self.value).unwrap();
44
+ } else {
45
+ write!(self.value, " ").unwrap();
46
+ self.is_tail = true;
47
+ }
48
+
49
+ write!(self.value, "{} = {:?};", field.name(), value).unwrap();
50
+ }
51
+ }
52
+ }
53
+
54
+ #[extend::ext]
55
+ impl tracing::Level {
56
+ /// Convert a `tracing::Level` to an equivalent 4-arg call to the
57
+ /// browser console via the `web_sys::console` module.
58
+ fn web_logger_4(&self) -> fn(&JsValue, &JsValue, &JsValue, &JsValue) {
59
+ match *self {
60
+ tracing::Level::TRACE => web_sys::console::trace_4,
61
+ tracing::Level::DEBUG => web_sys::console::debug_4,
62
+ tracing::Level::INFO => web_sys::console::info_4,
63
+ tracing::Level::WARN => web_sys::console::warn_4,
64
+ tracing::Level::ERROR => web_sys::console::error_4,
65
+ }
66
+ }
67
+
68
+ fn web_logger_1(&self) -> fn(&JsValue) {
69
+ match *self {
70
+ tracing::Level::TRACE => web_sys::console::trace_1,
71
+ tracing::Level::DEBUG => web_sys::console::debug_1,
72
+ tracing::Level::INFO => web_sys::console::info_1,
73
+ tracing::Level::WARN => web_sys::console::warn_1,
74
+ tracing::Level::ERROR => web_sys::console::error_1,
75
+ }
76
+ }
77
+
78
+ /// Return a pretty color theme for a `tracing::Level`.
79
+ fn web_log_color(&self) -> &'static str {
80
+ match *self {
81
+ tracing::Level::TRACE => "background: #005F73; color: #000",
82
+ tracing::Level::DEBUG => "background: #0A9396; color: #000",
83
+ tracing::Level::INFO => "background: #E9D8A6; color: #000",
84
+ tracing::Level::WARN => "background: #EE9B00; color: #000",
85
+ tracing::Level::ERROR => "background: #AE2012; color: #000",
86
+ }
87
+ }
88
+ }
89
+
90
+ static IS_CHROME: OnceLock<bool> = OnceLock::new();
91
+
92
+ fn detect_chrome() -> bool {
93
+ if let Some(window) = web_sys::window() {
94
+ window.get("chrome").is_some()
95
+ } else {
96
+ true
97
+ }
98
+ }
99
+
100
+ #[extend::ext]
101
+ impl<'a> tracing::Metadata<'a> {
102
+ /// Log a message in the style of `WasmLogger`.
103
+ fn console_log(&self, msg: &str) {
104
+ let level = self.level();
105
+ let origin = self
106
+ .module_path()
107
+ .and_then(|file| self.line().map(|ln| format!("{}:{}", &file[11..], ln)))
108
+ .unwrap_or_default();
109
+
110
+ if *IS_CHROME.get_or_init(detect_chrome) {
111
+ level.web_logger_4()(
112
+ &format!("%c {level} %c {origin}%c {msg} ").into(),
113
+ &level.web_log_color().into(),
114
+ &"color: gray; font-style: italic".into(),
115
+ &"color: inherit".into(),
116
+ );
117
+ } else {
118
+ level.web_logger_1()(&format!("{origin} {msg}").into());
119
+ }
120
+ }
121
+ }
122
+
123
+ /// A custom logger modelled afer the `tracing_wasm` crate.
124
+ struct WasmLogger {
125
+ max_level: tracing::Level,
126
+ }
127
+
128
+ impl Default for WasmLogger {
129
+ fn default() -> Self {
130
+ Self {
131
+ max_level: tracing::Level::WARN,
132
+ }
133
+ }
134
+ }
135
+
136
+ impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for WasmLogger {
137
+ fn on_event(
138
+ &self,
139
+ event: &tracing::Event<'_>,
140
+ _ctx: tracing_subscriber::layer::Context<'_, S>,
141
+ ) {
142
+ let mut recorder = LogLineBuffer::default();
143
+ event.record(&mut recorder);
144
+ event.metadata().console_log(&recorder.value);
145
+ }
146
+
147
+ fn enabled(&self, metadata: &tracing::Metadata<'_>, _: Context<'_, S>) -> bool {
148
+ let level = metadata.level();
149
+ level <= &self.max_level
150
+ }
151
+
152
+ fn on_new_span(
153
+ &self,
154
+ attrs: &tracing::span::Attributes<'_>,
155
+ id: &tracing::Id,
156
+ ctx: Context<'_, S>,
157
+ ) {
158
+ let mut new_debug_record = LogLineBuffer::default();
159
+ attrs.record(&mut new_debug_record);
160
+ if let Some(span_ref) = ctx.span(id) {
161
+ span_ref
162
+ .extensions_mut()
163
+ .insert::<LogLineBuffer>(new_debug_record);
164
+ }
165
+
166
+ global::performance()
167
+ .mark(&format!("t{:x}", id.into_u64()))
168
+ .unwrap();
169
+ }
170
+
171
+ fn on_record(&self, id: &tracing::Id, values: &tracing::span::Record<'_>, ctx: Context<'_, S>) {
172
+ if let Some(span_ref) = ctx.span(id) {
173
+ if let Some(debug_record) = span_ref.extensions_mut().get_mut::<LogLineBuffer>() {
174
+ values.record(debug_record);
175
+ }
176
+ }
177
+ }
178
+
179
+ fn on_close(&self, id: tracing::Id, ctx: Context<'_, S>) {
180
+ if let Some(span_ref) = ctx.span(&id) {
181
+ let perf = global::performance();
182
+ let meta = span_ref.metadata();
183
+ let mark = format!("t{:x}", id.into_u64());
184
+ let start = perf
185
+ .get_entries_by_name_with_entry_type(&mark, "mark")
186
+ .at(-1)
187
+ .unchecked_into::<web_sys::PerformanceMark>()
188
+ .start_time();
189
+
190
+ meta.console_log(&format!("{:.0}ms", perf.now() - start));
191
+ let msg = format!(
192
+ "\"{}\" {} {}",
193
+ meta.name(),
194
+ meta.module_path().unwrap_or_default(),
195
+ span_ref
196
+ .extensions()
197
+ .get::<LogLineBuffer>()
198
+ .map(|x| &x.value[..])
199
+ .unwrap_or_default(),
200
+ );
201
+
202
+ perf.measure_with_start_mark(&msg, &mark).unwrap();
203
+ }
204
+ }
205
+ }
206
+
207
+ /// Configure `WasmLogger` as a global default for tracing.
208
+ ///
209
+ /// This operation will conflict with any other library which sets a global
210
+ /// default `tracing::Subscriber`, so it should not be called when `perspective`
211
+ /// is used as a library from a larger app; in this case the app itself should
212
+ /// configure `tracing` explicitly.
213
+ pub fn set_global_logging() {
214
+ static INIT_LOGGING: OnceLock<()> = OnceLock::new();
215
+ INIT_LOGGING.get_or_init(|| {
216
+ if !*IS_CHROME.get_or_init(detect_chrome) {
217
+ web_sys::console::warn_1(
218
+ &"Unknown browser detected. Some features may not work, and performance may be \
219
+ degraded."
220
+ .into(),
221
+ );
222
+ }
223
+
224
+ use tracing_subscriber::layer::SubscriberExt;
225
+ let filter = tracing_subscriber::filter::filter_fn(|meta| {
226
+ meta.module_path()
227
+ .as_ref()
228
+ .map(|x| x.starts_with("perspective"))
229
+ .unwrap_or_default()
230
+ });
231
+
232
+ let layer = WasmLogger::default().with_filter(filter);
233
+ let subscriber = tracing_subscriber::Registry::default().with(layer);
234
+ tracing::subscriber::set_global_default(subscriber).unwrap();
235
+ });
236
+ }
@@ -0,0 +1,288 @@
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::fmt::Display;
14
+ use std::rc::Rc;
15
+ use std::string::FromUtf8Error;
16
+
17
+ use perspective_client::{ClientError, ExprValidationResult};
18
+ use thiserror::*;
19
+ use wasm_bindgen::prelude::*;
20
+
21
+ #[macro_export]
22
+ macro_rules! apierror {
23
+ ($msg:expr) => {{
24
+ use $crate::utils::errors::ApiErrorType::*;
25
+ let js_err_type = $msg;
26
+ let err = js_sys::Error::new(js_err_type.to_string().as_str());
27
+ let js_err = $crate::utils::errors::ApiError(
28
+ js_err_type,
29
+ $crate::utils::errors::JsBackTrace(std::rc::Rc::new(err.clone())),
30
+ );
31
+ js_err
32
+ }};
33
+ }
34
+
35
+ fn format_js_error(value: &JsValue) -> String {
36
+ if let Some(err) = value.dyn_ref::<js_sys::Error>() {
37
+ let msg = err.message().as_string().unwrap();
38
+ if let Ok(x) = js_sys::Reflect::get(value, &"stack".into()) {
39
+ format!("{}\n{}", msg, x.as_string().unwrap())
40
+ } else {
41
+ msg
42
+ }
43
+ } else {
44
+ value
45
+ .as_string()
46
+ .unwrap_or_else(|| format!("{value:?}"))
47
+ .to_string()
48
+ }
49
+ }
50
+
51
+ fn format_valid_exprs(recs: &ExprValidationResult) -> String {
52
+ recs.errors
53
+ .iter()
54
+ .map(|x| format!("\"{}\": {}", x.0, x.1.error_message))
55
+ .collect::<Vec<_>>()
56
+ .join(", ")
57
+ }
58
+
59
+ /// A bespoke error class for chaining a litany of error types with the `?`
60
+ /// operator.
61
+ #[derive(Clone, Debug, Error)]
62
+ pub enum ApiErrorType {
63
+ #[error("{}", format_js_error(.0))]
64
+ JsError(JsValue),
65
+
66
+ #[error("{}", format_js_error(.0))]
67
+ JsRawError(js_sys::Error),
68
+
69
+ #[error("Failed to construct table from {0:?}")]
70
+ TableError(JsValue),
71
+
72
+ #[error("{}", format_js_error(.0))]
73
+ ViewerPluginError(JsValue),
74
+
75
+ #[error("{0}")]
76
+ ExternalError(Rc<Box<dyn std::error::Error>>),
77
+
78
+ #[error("{0}")]
79
+ UnknownError(String),
80
+
81
+ #[error("{0}")]
82
+ ClientError(#[from] ClientError),
83
+
84
+ #[error("Cancelled")]
85
+ CancelledError(#[from] futures::channel::oneshot::Canceled),
86
+
87
+ #[error("{0}")]
88
+ SerdeJsonError(Rc<serde_json::Error>),
89
+
90
+ #[error("{0}")]
91
+ ProstError(#[from] prost::DecodeError),
92
+
93
+ #[error("Unknown column \"{1}\" in field `{0}`")]
94
+ InvalidViewerConfigError(&'static str, String),
95
+
96
+ #[error("Invalid `expressions` {}", format_valid_exprs(.0))]
97
+ InvalidViewerConfigExpressionsError(Rc<ExprValidationResult>),
98
+
99
+ #[error("No `Table` attached")]
100
+ NoTableError,
101
+
102
+ #[error(transparent)]
103
+ SerdeWasmBindgenError(Rc<serde_wasm_bindgen::Error>),
104
+
105
+ #[error(transparent)]
106
+ Utf8Error(#[from] FromUtf8Error),
107
+
108
+ #[error(transparent)]
109
+ StdIoError(Rc<std::io::Error>),
110
+
111
+ #[error(transparent)]
112
+ RmpSerdeEncodeError(Rc<rmp_serde::encode::Error>),
113
+
114
+ #[error(transparent)]
115
+ RmpSerdeDecodeError(Rc<rmp_serde::decode::Error>),
116
+
117
+ #[error(transparent)]
118
+ Base64DecodeError(#[from] base64::DecodeError),
119
+
120
+ #[error(transparent)]
121
+ ChronoParseError(#[from] chrono::ParseError),
122
+ }
123
+
124
+ #[derive(Clone, Debug, Error)]
125
+ pub struct ApiError(pub ApiErrorType, pub JsBackTrace);
126
+
127
+ impl ApiError {
128
+ pub fn new<T: Display>(val: T) -> Self {
129
+ apierror!(UnknownError(format!("{val}")))
130
+ }
131
+
132
+ /// The error category
133
+ pub fn kind(&self) -> &'static str {
134
+ match self.0 {
135
+ ApiErrorType::JsError(..) => "[JsError]",
136
+ ApiErrorType::TableError(_) => "[TableError]",
137
+ ApiErrorType::ExternalError(_) => "[ExternalError]",
138
+ ApiErrorType::UnknownError(..) => "[UnknownError]",
139
+ ApiErrorType::ClientError(_) => "[ClientError]",
140
+ ApiErrorType::CancelledError(_) => "[CancelledError]",
141
+ ApiErrorType::SerdeJsonError(_) => "[SerdeJsonError]",
142
+ ApiErrorType::ProstError(_) => "[ProstError]",
143
+ ApiErrorType::InvalidViewerConfigError(..) => "[InvalidViewerConfigError]",
144
+ ApiErrorType::InvalidViewerConfigExpressionsError(_) => "[InvalidViewerConfigError]",
145
+ ApiErrorType::NoTableError => "[NoTableError]",
146
+ ApiErrorType::SerdeWasmBindgenError(_) => "[SerdeWasmBindgenError]",
147
+ ApiErrorType::Utf8Error(_) => "[FromUtf8Error]",
148
+ ApiErrorType::StdIoError(_) => "[StdIoError]",
149
+ ApiErrorType::RmpSerdeEncodeError(_) => "[RmpSerdeEncodeError]",
150
+ ApiErrorType::RmpSerdeDecodeError(_) => "[RmpSerdeDecodeError]",
151
+ ApiErrorType::Base64DecodeError(_) => "[Base64DecodeError]",
152
+ ApiErrorType::ChronoParseError(_) => "[ChronoParseError]",
153
+ ApiErrorType::ViewerPluginError(_) => "[ViewerPluginError]",
154
+ ApiErrorType::JsRawError(_) => "[JsRawError]",
155
+ }
156
+ }
157
+
158
+ /// The raw internal enum
159
+ pub fn inner(&self) -> &'_ ApiErrorType {
160
+ &self.0
161
+ }
162
+
163
+ /// The `Display` for this error
164
+ pub fn message(&self) -> String {
165
+ self.0.to_string()
166
+ }
167
+
168
+ /// This error's stacktrace from when it was constructed.
169
+ pub fn stacktrace(&self) -> String {
170
+ js_sys::Reflect::get(&self.1.0, &"stack".into())
171
+ .unwrap()
172
+ .as_string()
173
+ .unwrap()
174
+ .to_string()
175
+ }
176
+ }
177
+
178
+ unsafe impl Send for ApiError {}
179
+ unsafe impl Sync for ApiError {}
180
+
181
+ impl std::fmt::Display for ApiError {
182
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183
+ self.0.fmt(f)
184
+ }
185
+ }
186
+
187
+ impl<T: Into<ApiErrorType>> From<T> for ApiError {
188
+ fn from(value: T) -> Self {
189
+ let value: ApiErrorType = value.into();
190
+ let err = js_sys::Error::new(value.to_string().as_str());
191
+ ApiError(value, JsBackTrace(Rc::new(err.clone())))
192
+ }
193
+ }
194
+
195
+ impl From<ApiError> for JsValue {
196
+ fn from(err: ApiError) -> Self {
197
+ err.1.0.unchecked_ref::<JsValue>().clone()
198
+ }
199
+ }
200
+
201
+ impl From<serde_wasm_bindgen::Error> for ApiError {
202
+ fn from(value: serde_wasm_bindgen::Error) -> Self {
203
+ ApiErrorType::SerdeWasmBindgenError(Rc::new(value)).into()
204
+ }
205
+ }
206
+
207
+ impl From<std::io::Error> for ApiError {
208
+ fn from(value: std::io::Error) -> Self {
209
+ ApiErrorType::StdIoError(Rc::new(value)).into()
210
+ }
211
+ }
212
+
213
+ impl From<rmp_serde::decode::Error> for ApiError {
214
+ fn from(value: rmp_serde::decode::Error) -> Self {
215
+ ApiErrorType::RmpSerdeDecodeError(Rc::new(value)).into()
216
+ }
217
+ }
218
+
219
+ impl From<rmp_serde::encode::Error> for ApiError {
220
+ fn from(value: rmp_serde::encode::Error) -> Self {
221
+ ApiErrorType::RmpSerdeEncodeError(Rc::new(value)).into()
222
+ }
223
+ }
224
+
225
+ impl From<Box<dyn std::error::Error>> for ApiError {
226
+ fn from(value: Box<dyn std::error::Error>) -> Self {
227
+ ApiErrorType::ExternalError(Rc::new(value)).into()
228
+ }
229
+ }
230
+
231
+ impl From<serde_json::Error> for ApiError {
232
+ fn from(value: serde_json::Error) -> Self {
233
+ ApiErrorType::SerdeJsonError(Rc::new(value)).into()
234
+ }
235
+ }
236
+
237
+ impl From<JsValue> for ApiError {
238
+ fn from(err: JsValue) -> Self {
239
+ if err.is_instance_of::<js_sys::Error>() {
240
+ ApiErrorType::JsRawError(err.clone().unchecked_into()).into()
241
+ } else {
242
+ apierror!(JsError(err))
243
+ }
244
+ }
245
+ }
246
+
247
+ impl From<String> for ApiError {
248
+ fn from(value: String) -> Self {
249
+ apierror!(UnknownError(value.to_owned()))
250
+ }
251
+ }
252
+
253
+ impl From<&str> for ApiError {
254
+ fn from(value: &str) -> Self {
255
+ apierror!(UnknownError(value.to_owned()))
256
+ }
257
+ }
258
+
259
+ /// `ToApiError` handles complex cases that can't be into-d
260
+ pub trait ToApiError<T> {
261
+ fn into_apierror(self) -> ApiResult<T>;
262
+ }
263
+
264
+ impl<T> ToApiError<T> for Option<T> {
265
+ fn into_apierror(self) -> ApiResult<T> {
266
+ self.ok_or_else(|| "Unwrap on None".into())
267
+ }
268
+ }
269
+
270
+ impl ToApiError<JsValue> for Result<(), ApiResult<JsValue>> {
271
+ fn into_apierror(self) -> ApiResult<JsValue> {
272
+ self.map_or_else(|x| x, |()| Ok(JsValue::UNDEFINED))
273
+ }
274
+ }
275
+
276
+ /// A common Rust error handling idiom (see e.g. `anyhow::Result`)
277
+ pub type ApiResult<T> = Result<T, ApiError>;
278
+
279
+ // Backtrace
280
+
281
+ #[derive(Clone, Debug)]
282
+ pub struct JsBackTrace(pub Rc<js_sys::Error>);
283
+
284
+ impl std::fmt::Display for JsBackTrace {
285
+ fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286
+ Ok(())
287
+ }
288
+ }
@@ -0,0 +1,174 @@
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::future::Future;
14
+ use std::pin::Pin;
15
+
16
+ use perspective_client::ClientError;
17
+ // TODO This is risky to rely on, but it is currently impossible to implement
18
+ // this trait locally due to the orphan instance restriction. Using this trait
19
+ // removes alow of boilerplate required by `async` when casting to `Promise`.
20
+ use wasm_bindgen::__rt::IntoJsResult;
21
+ use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi};
22
+ use wasm_bindgen::describe::WasmDescribe;
23
+ use wasm_bindgen::prelude::*;
24
+ use wasm_bindgen_futures::{JsFuture, future_to_promise};
25
+
26
+ use super::errors::*;
27
+
28
+ /// A newtype wrapper for a `Future` trait object which supports being
29
+ /// marshalled to a `JsPromise`.
30
+ ///
31
+ /// This avoids implementing an API which requires type casting to
32
+ /// and from `JsValue` and the associated loss of type safety.
33
+ #[must_use]
34
+ pub struct ApiFuture<T>(Pin<Box<dyn Future<Output = ApiResult<T>>>>)
35
+ where
36
+ Result<T, JsValue>: IntoJsResult + 'static;
37
+
38
+ impl<T> ApiFuture<T>
39
+ where
40
+ Result<T, JsValue>: IntoJsResult + 'static,
41
+ {
42
+ /// Constructor for `ApiFuture`. Note that, like a regular `Future`, the
43
+ /// `ApiFuture` created does _not_ execute without being further cast to a
44
+ /// `Promise`, either explicitly or implcitly (when exposed via
45
+ /// `wasm_bindgen`).
46
+ pub fn new<U: Future<Output = ApiResult<T>> + 'static>(x: U) -> Self {
47
+ Self(Box::pin(x))
48
+ }
49
+ }
50
+
51
+ impl<T> ApiFuture<T>
52
+ where
53
+ Result<T, JsValue>: IntoJsResult + 'static,
54
+ {
55
+ /// Construct an `ApiFuture` and execute it immediately. The `Promise`
56
+ /// handle created internally is dropped, but since JavaScript `Promise`
57
+ /// executes on construction, the async invocation persists.
58
+ pub fn spawn<U: Future<Output = ApiResult<T>> + 'static>(x: U) {
59
+ drop(js_sys::Promise::from(Self::new(x)))
60
+ }
61
+ }
62
+
63
+ impl<T> Default for ApiFuture<T>
64
+ where
65
+ Result<T, JsValue>: IntoJsResult + 'static,
66
+ T: Default,
67
+ {
68
+ fn default() -> Self {
69
+ Self::new(async { Ok(Default::default()) })
70
+ }
71
+ }
72
+
73
+ impl<T> From<ApiFuture<T>> for JsValue
74
+ where
75
+ Result<T, Self>: IntoJsResult + 'static,
76
+ {
77
+ fn from(fut: ApiFuture<T>) -> Self {
78
+ js_sys::Promise::from(fut).unchecked_into()
79
+ }
80
+ }
81
+
82
+ impl<T> From<ApiFuture<T>> for js_sys::Promise
83
+ where
84
+ Result<T, JsValue>: IntoJsResult + 'static,
85
+ {
86
+ fn from(fut: ApiFuture<T>) -> Self {
87
+ future_to_promise(async move {
88
+ match fut.0.await.ignore_view_delete()? {
89
+ Some(x) => Ok(x).into_js_result(),
90
+ None => Ok::<_, JsValue>(()).into_js_result(),
91
+ }
92
+ })
93
+ }
94
+ }
95
+
96
+ impl<T> WasmDescribe for ApiFuture<T>
97
+ where
98
+ Result<T, JsValue>: IntoJsResult + 'static,
99
+ {
100
+ fn describe() {
101
+ <js_sys::Promise as WasmDescribe>::describe()
102
+ }
103
+ }
104
+
105
+ impl<T> IntoWasmAbi for ApiFuture<T>
106
+ where
107
+ Result<T, JsValue>: IntoJsResult + 'static,
108
+ {
109
+ type Abi = <js_sys::Promise as IntoWasmAbi>::Abi;
110
+
111
+ #[inline]
112
+ fn into_abi(self) -> Self::Abi {
113
+ js_sys::Promise::from(self).into_abi()
114
+ }
115
+ }
116
+
117
+ impl<T> FromWasmAbi for ApiFuture<T>
118
+ where
119
+ Result<T, JsValue>: IntoJsResult + 'static,
120
+ T: From<JsValue> + Into<JsValue>,
121
+ {
122
+ type Abi = <js_sys::Promise as IntoWasmAbi>::Abi;
123
+
124
+ #[inline]
125
+ unsafe fn from_abi(js: Self::Abi) -> Self {
126
+ Self::new(async move {
127
+ let promise = unsafe { js_sys::Promise::from_abi(js) };
128
+ Ok(JsFuture::from(promise).await?.into())
129
+ })
130
+ }
131
+ }
132
+
133
+ impl<T> Future for ApiFuture<T>
134
+ where
135
+ Result<T, JsValue>: IntoJsResult + 'static,
136
+ {
137
+ type Output = ApiResult<T>;
138
+
139
+ fn poll(
140
+ self: Pin<&mut Self>,
141
+ cx: &mut std::task::Context<'_>,
142
+ ) -> std::task::Poll<Self::Output> {
143
+ let mut fut = unsafe { self.map_unchecked_mut(|s| &mut s.0) };
144
+ fut.as_mut().poll(cx)
145
+ }
146
+ }
147
+
148
+ #[extend::ext]
149
+ pub impl<T> Result<T, ApiError> {
150
+ /// Wraps an error `JsValue` return from a caught JavaScript exception,
151
+ /// checking for the explicit error type indicating that a
152
+ /// `JsPerspectiveView` call has been cancelled due to it already being
153
+ /// deleted. This is a normal mechanic of the `JsPerspectiveView` to
154
+ /// cancel a `View` call that is no longer need be the viewer, e.g. when
155
+ /// the user updates the UI before the previous update has finished
156
+ /// drawing. Without using exceptions for this, we'd need to wrap every
157
+ /// such `JsPerspectiveView` call individually.
158
+ ///
159
+ /// When `"View method cancelled"` message is received, this call should
160
+ /// silently be replaced with `Ok`. The message itself is returned in this
161
+ /// case (instead of whatever the `async` returns), which is helpful for
162
+ /// detecting this condition when debugging.
163
+ fn ignore_view_delete(self) -> Result<Option<T>, ApiError> {
164
+ self.map(|x| Some(x)).or_else(|x| match x.inner() {
165
+ ApiErrorType::ClientError(ClientError::ViewNotFound) => Ok(None),
166
+ ApiErrorType::JsRawError(..) | ApiErrorType::JsError(..)
167
+ if format!("{x}").contains("View not found") =>
168
+ {
169
+ Ok(None)
170
+ },
171
+ x => Err(x.clone().into()),
172
+ })
173
+ }
174
+ }