@perspective-dev/viewer 4.0.1 → 4.1.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.
Files changed (183) hide show
  1. package/dist/cdn/perspective-viewer.js +2 -2
  2. package/dist/cdn/perspective-viewer.js.map +4 -4
  3. package/dist/css/dracula.css +1 -1
  4. package/dist/css/gruvbox-dark.css +1 -1
  5. package/dist/css/gruvbox.css +1 -1
  6. package/dist/css/icons.css +1 -1
  7. package/dist/css/monokai.css +1 -1
  8. package/dist/css/pro-dark.css +1 -1
  9. package/dist/css/pro.css +1 -1
  10. package/dist/css/solarized-dark.css +1 -1
  11. package/dist/css/solarized.css +1 -1
  12. package/dist/css/themes.css +1 -1
  13. package/dist/css/vaporwave.css +1 -1
  14. package/dist/esm/extensions.d.ts +23 -2
  15. package/dist/esm/perspective-viewer.d.ts +2 -7
  16. package/dist/esm/perspective-viewer.inline.js +2 -2
  17. package/dist/esm/perspective-viewer.inline.js.map +4 -4
  18. package/dist/esm/perspective-viewer.js +2 -2
  19. package/dist/esm/perspective-viewer.js.map +4 -4
  20. package/dist/esm/plugin.d.ts +1 -1
  21. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +1 -0
  22. package/dist/wasm/perspective-viewer.d.ts +218 -46
  23. package/dist/wasm/perspective-viewer.js +1242 -753
  24. package/dist/wasm/perspective-viewer.wasm +0 -0
  25. package/dist/wasm/perspective-viewer.wasm.d.ts +38 -19
  26. package/package.json +1 -1
  27. package/src/less/containers/scroll-panel.less +0 -1
  28. package/src/less/plugin-selector.less +15 -5
  29. package/src/less/status-bar.less +75 -27
  30. package/src/less/viewer.less +140 -58
  31. package/src/rust/components/column_dropdown.rs +21 -21
  32. package/src/rust/components/column_selector/active_column.rs +131 -120
  33. package/src/rust/components/column_selector/add_expression_button.rs +5 -0
  34. package/src/rust/components/column_selector/aggregate_selector.rs +8 -4
  35. package/src/rust/components/column_selector/config_selector.rs +170 -161
  36. package/src/rust/components/column_selector/empty_column.rs +16 -11
  37. package/src/rust/components/column_selector/{expression_toolbar.rs → expr_edit_button.rs} +7 -0
  38. package/src/rust/components/column_selector/filter_column.rs +195 -194
  39. package/src/rust/components/column_selector/inactive_column.rs +82 -67
  40. package/src/rust/components/column_selector/pivot_column.rs +16 -11
  41. package/src/rust/components/column_selector/sort_column.rs +9 -7
  42. package/src/rust/components/column_selector.rs +42 -37
  43. package/src/rust/components/column_settings_sidebar/save_settings.rs +3 -1
  44. package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +58 -0
  45. package/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +6 -6
  46. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +2 -94
  47. package/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs_item.rs +111 -0
  48. package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +3 -3
  49. package/src/rust/components/column_settings_sidebar/style_tab.rs +23 -83
  50. package/src/rust/components/{column_settings_sidebar/sidebar.rs → column_settings_sidebar.rs} +198 -171
  51. package/src/rust/components/containers/dragdrop_list.rs +20 -20
  52. package/src/rust/components/containers/dropdown_menu.rs +4 -6
  53. package/src/rust/components/containers/mod.rs +1 -4
  54. package/src/rust/components/containers/scroll_panel.rs +80 -80
  55. package/src/rust/components/containers/scroll_panel_item.rs +36 -36
  56. package/src/rust/components/containers/select.rs +46 -44
  57. package/src/rust/components/containers/sidebar.rs +3 -19
  58. package/src/rust/components/{column_settings_sidebar/style_tab/symbol/symbol_config.rs → containers/sidebar_close_button.rs} +15 -9
  59. package/src/rust/components/containers/split_panel.rs +212 -200
  60. package/src/rust/components/containers/tab_list.rs +11 -11
  61. package/src/rust/components/copy_dropdown.rs +22 -25
  62. package/src/rust/components/datetime_column_style/custom.rs +19 -19
  63. package/src/rust/components/datetime_column_style/simple.rs +13 -14
  64. package/src/rust/components/datetime_column_style.rs +75 -76
  65. package/src/rust/components/editable_header.rs +18 -14
  66. package/src/rust/components/empty_row.rs +5 -5
  67. package/src/rust/components/export_dropdown.rs +42 -42
  68. package/src/rust/components/expression_editor.rs +25 -19
  69. package/src/rust/components/filter_dropdown.rs +22 -22
  70. package/src/rust/components/font_loader.rs +11 -9
  71. package/src/rust/components/form/code_editor.rs +106 -105
  72. package/src/rust/components/form/color_range_selector.rs +14 -12
  73. package/src/rust/components/form/color_selector.rs +3 -1
  74. package/src/rust/components/form/debug.rs +95 -94
  75. package/src/rust/components/form/highlight.rs +5 -3
  76. package/src/rust/components/form/mod.rs +3 -2
  77. package/src/rust/components/form/optional_field.rs +2 -2
  78. package/src/rust/components/form/{select_field.rs → select_enum_field.rs} +1 -46
  79. package/src/rust/components/form/select_value_field.rs +64 -0
  80. package/src/rust/components/function_dropdown.rs +21 -21
  81. package/src/rust/components/main_panel.rs +219 -0
  82. package/src/rust/components/mod.rs +6 -6
  83. package/src/rust/components/modal.rs +42 -42
  84. package/src/rust/components/number_column_style.rs +34 -88
  85. package/src/rust/components/plugin_selector.rs +22 -25
  86. package/src/rust/components/render_warning.rs +9 -6
  87. package/src/rust/components/settings_panel.rs +82 -0
  88. package/src/rust/components/status_bar.rs +250 -146
  89. package/src/rust/components/status_bar_counter.rs +26 -119
  90. package/src/rust/components/status_indicator.rs +95 -79
  91. package/src/rust/components/string_column_style.rs +45 -45
  92. package/src/rust/components/style/style_provider.rs +1 -15
  93. package/src/rust/components/style_controls/number_string_format/digits_section.rs +1 -1
  94. package/src/rust/components/style_controls/number_string_format/misc_section.rs +1 -1
  95. package/src/rust/components/style_controls/number_string_format/style_section.rs +1 -1
  96. package/src/rust/components/style_controls/number_string_format.rs +45 -46
  97. package/src/rust/components/type_icon.rs +14 -11
  98. package/src/rust/components/viewer.rs +241 -384
  99. package/src/rust/config/columns_config.rs +2 -2
  100. package/src/rust/config/datetime_column_style.rs +1 -6
  101. package/src/rust/config/mod.rs +1 -0
  102. package/src/rust/config/number_column_style.rs +0 -6
  103. package/src/rust/config/number_string_format.rs +27 -4
  104. package/src/rust/config/viewer_config.rs +27 -167
  105. package/src/rust/custom_elements/copy_dropdown.rs +14 -6
  106. package/src/rust/custom_elements/export_dropdown.rs +15 -7
  107. package/src/rust/custom_elements/filter_dropdown.rs +4 -4
  108. package/src/rust/custom_elements/mod.rs +3 -0
  109. package/src/rust/custom_elements/viewer.rs +367 -169
  110. package/src/rust/custom_events.rs +55 -32
  111. package/src/rust/dragdrop.rs +4 -24
  112. package/src/rust/exprtk/cursor.rs +10 -1
  113. package/src/rust/exprtk/mod.rs +2 -0
  114. package/src/rust/exprtk/tokenize.rs +20 -3
  115. package/src/rust/js/clipboard.rs +2 -2
  116. package/src/rust/js/mimetype.rs +2 -7
  117. package/src/rust/js/mod.rs +0 -1
  118. package/src/rust/js/plugin.rs +7 -0
  119. package/src/rust/lib.rs +18 -5
  120. package/src/rust/model/column_locator.rs +82 -0
  121. package/src/rust/model/columns_iter_set.rs +1 -0
  122. package/src/rust/model/copy_export.rs +50 -14
  123. package/src/rust/model/edit_expression.rs +2 -5
  124. package/src/rust/model/eject.rs +41 -0
  125. package/src/rust/model/get_viewer_config.rs +4 -28
  126. package/src/rust/model/intersection_observer.rs +20 -8
  127. package/src/rust/model/mod.rs +11 -4
  128. package/src/rust/model/plugin_column_styles.rs +0 -31
  129. package/src/rust/model/reset_all.rs +38 -0
  130. package/src/rust/model/resize_observer.rs +34 -7
  131. package/src/rust/model/restore_and_render.rs +12 -7
  132. package/src/rust/{utils/scope.rs → model/send_plugin_config.rs} +32 -35
  133. package/src/rust/model/structural.rs +194 -23
  134. package/src/rust/model/update_and_render.rs +14 -4
  135. package/src/rust/{model/create_col.rs → presentation/column_locator.rs} +73 -42
  136. package/src/rust/{utils/wasm_abi.rs → presentation/sheets.rs} +54 -40
  137. package/src/rust/presentation.rs +60 -119
  138. package/src/rust/renderer/activate.rs +20 -5
  139. package/src/rust/renderer/limits.rs +0 -149
  140. package/src/rust/renderer/render_timer.rs +1 -1
  141. package/src/rust/renderer.rs +34 -18
  142. package/src/rust/root.rs +50 -0
  143. package/src/rust/session/column_defaults_update.rs +4 -4
  144. package/src/rust/session/drag_drop_update.rs +1 -1
  145. package/src/rust/session/metadata.rs +3 -17
  146. package/src/rust/session/replace_expression_update.rs +1 -2
  147. package/src/rust/session.rs +162 -82
  148. package/src/rust/utils/browser/blob.rs +16 -2
  149. package/src/rust/utils/browser/download.rs +1 -0
  150. package/src/rust/{components/column_settings_sidebar/mod.rs → utils/browser/dragdrop.rs} +14 -5
  151. package/src/rust/utils/browser/mod.rs +8 -4
  152. package/src/rust/utils/browser/selection.rs +5 -0
  153. package/src/rust/utils/custom_element.rs +28 -13
  154. package/src/rust/utils/datetime.rs +5 -0
  155. package/src/rust/utils/debounce.rs +7 -1
  156. package/src/rust/utils/hooks/use_async_callback.rs +7 -17
  157. package/src/rust/utils/mod.rs +28 -40
  158. package/src/rust/utils/number_format.rs +6 -5
  159. package/src/rust/utils/pubsub.rs +15 -10
  160. package/src/rust/utils/weak_scope.rs +11 -1
  161. package/src/svg/bookmark-icon.svg +4 -0
  162. package/src/svg/drag-handle copy.svg +10 -0
  163. package/src/svg/drawer-tab-hover.svg +5 -7
  164. package/src/svg/drawer-tab-invert-hover.svg +4 -8
  165. package/src/svg/drawer-tab-invert.svg +4 -7
  166. package/src/svg/drawer-tab.svg +4 -6
  167. package/src/svg/status_ok.svg +24 -24
  168. package/src/ts/extensions.ts +51 -3
  169. package/src/ts/perspective-viewer.ts +2 -14
  170. package/src/ts/plugin.ts +1 -1
  171. package/src/ts/ts-rs/ViewerConfigUpdate.ts +1 -1
  172. package/src/rust/components/column_settings_sidebar/style_tab/column_style.rs +0 -177
  173. package/src/rust/components/containers/tests/mod.rs +0 -11
  174. package/src/rust/components/containers/tests/split_panel.rs +0 -91
  175. package/src/rust/js/testing.rs +0 -149
  176. package/src/rust/utils/tee.rs +0 -88
  177. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline0.js +0 -0
  178. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline1.js +0 -0
  179. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline2.js +0 -0
  180. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline3.js +0 -0
  181. /package/dist/wasm/snippets/{perspective-viewer-9a89352df1552d2b → perspective-viewer-11a3c51b6310ee99}/inline4.js +0 -0
  182. /package/src/rust/components/{style_controls.rs → style_controls/mod.rs} +0 -0
  183. /package/src/rust/{components/containers → config}/kvpair.rs +0 -0
@@ -14,20 +14,19 @@
14
14
 
15
15
  use std::cell::RefCell;
16
16
  use std::rc::Rc;
17
- use std::str::FromStr;
18
17
 
19
- use ::perspective_js::{Table, View};
20
- use futures::future::join;
21
- use js_sys::*;
18
+ use futures::channel::oneshot::channel;
19
+ use js_sys::{Array, JsString};
22
20
  use perspective_client::config::ViewConfigUpdate;
23
- use perspective_js::{JsViewWindow, apierror};
21
+ use perspective_client::utils::PerspectiveResultExt;
22
+ use perspective_js::{JsViewConfig, JsViewWindow, Table, View, apierror};
24
23
  use wasm_bindgen::JsCast;
25
24
  use wasm_bindgen::prelude::*;
25
+ use wasm_bindgen_derive::try_from_js_option;
26
26
  use wasm_bindgen_futures::JsFuture;
27
- use web_sys::*;
28
- use yew::prelude::*;
27
+ use web_sys::HtmlElement;
29
28
 
30
- use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg, PerspectiveViewerProps};
29
+ use crate::components::viewer::{PerspectiveViewerMsg, PerspectiveViewerProps};
31
30
  use crate::config::*;
32
31
  use crate::custom_events::*;
33
32
  use crate::dragdrop::*;
@@ -35,7 +34,8 @@ use crate::js::*;
35
34
  use crate::model::*;
36
35
  use crate::presentation::*;
37
36
  use crate::renderer::*;
38
- use crate::session::Session;
37
+ use crate::root::Root;
38
+ use crate::session::{ResetOptions, Session};
39
39
  use crate::utils::*;
40
40
  use crate::*;
41
41
 
@@ -49,21 +49,36 @@ use crate::*;
49
49
  /// const viewer = document.createElement("perspective-viewer");
50
50
  /// window.body.appendChild(viewer);
51
51
  /// ```
52
- #[derive(Clone)]
52
+ ///
53
+ /// Complete example including loading and restoring the [`Table`]:
54
+ ///
55
+ /// ```javascript
56
+ /// import perspective from "@perspective-dev/viewer";
57
+ /// import perspective from "@perspective-dev/client";
58
+ ///
59
+ /// const viewer = document.createElement("perspective-viewer");
60
+ /// const worker = await perspective.worker();
61
+ ///
62
+ /// await worker.table("x\n1", {name: "table_one"});
63
+ /// await viewer.load(worker);
64
+ /// await viewer.restore({table: "table_one"});
65
+ /// ```
66
+ #[derive(Clone, PerspectiveProperties!)]
53
67
  #[wasm_bindgen]
54
68
  pub struct PerspectiveViewerElement {
55
69
  elem: HtmlElement,
56
- root: Rc<RefCell<Option<AppHandle<PerspectiveViewer>>>>,
70
+ root: Root<components::viewer::PerspectiveViewer>,
57
71
  resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
58
72
  intersection_handle: Rc<RefCell<Option<IntersectionObserverHandle>>>,
59
73
  session: Session,
60
74
  renderer: Renderer,
61
75
  presentation: Presentation,
62
- events: CustomEvents,
63
- _subscriptions: Rc<Subscription>,
76
+ custom_events: CustomEvents,
77
+ _subscriptions: Rc<[Subscription; 2]>,
64
78
  }
65
79
 
66
- derive_model!( Renderer, Session, Presentation for PerspectiveViewerElement);
80
+ // derive_model!( Renderer, Root, Session, Presentation for
81
+ // PerspectiveViewerElement);
67
82
 
68
83
  impl CustomElementMetadata for PerspectiveViewerElement {
69
84
  const CUSTOM_ELEMENT_NAME: &'static str = "perspective-viewer";
@@ -86,10 +101,10 @@ impl PerspectiveViewerElement {
86
101
 
87
102
  fn new_from_shadow(elem: web_sys::HtmlElement, shadow_root: web_sys::Element) -> Self {
88
103
  // Application State
89
- let session = Session::default();
104
+ let session = Session::new();
90
105
  let renderer = Renderer::new(&elem);
91
106
  let presentation = Presentation::new(&elem);
92
- let events = CustomEvents::new(&elem, &session, &renderer, &presentation);
107
+ let custom_events = CustomEvents::new(&elem, &session, &renderer, &presentation);
93
108
 
94
109
  // Create Yew App
95
110
  let props = yew::props!(PerspectiveViewerProps {
@@ -98,50 +113,90 @@ impl PerspectiveViewerElement {
98
113
  renderer: renderer.clone(),
99
114
  presentation: presentation.clone(),
100
115
  dragdrop: DragDrop::default(),
101
- custom_events: events.clone(),
102
- weak_link: WeakScope::default(),
116
+ custom_events: custom_events.clone(),
103
117
  });
104
118
 
105
- let root = yew::Renderer::with_root_and_props(shadow_root, props).render();
119
+ let state = props.clone_state();
120
+ let root = Root::new(shadow_root, props);
106
121
 
107
122
  // Create callbacks
108
123
  let update_sub = session.table_updated.add_listener({
109
124
  clone!(renderer, session);
110
125
  move |_| {
111
126
  clone!(renderer, session);
112
- ApiFuture::spawn(async move { renderer.update(&session).await })
127
+ ApiFuture::spawn(async move {
128
+ renderer
129
+ .update(session.get_view())
130
+ .await
131
+ .ignore_view_delete()
132
+ .map(|_| ())
133
+ })
113
134
  }
114
135
  });
115
136
 
116
- let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &root);
137
+ let eject_sub = presentation.on_eject.add_listener({
138
+ let root = root.clone();
139
+ move |_| ApiFuture::spawn(state.delete_all(&root))
140
+ });
141
+
142
+ let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &session, &root);
143
+ let intersect_handle =
144
+ IntersectionObserverHandle::new(&elem, &presentation, &session, &renderer);
145
+
117
146
  Self {
118
147
  elem,
119
- root: Rc::new(RefCell::new(Some(root))),
148
+ root,
120
149
  session,
121
150
  renderer,
122
151
  presentation,
123
152
  resize_handle: Rc::new(RefCell::new(Some(resize_handle))),
124
- intersection_handle: Rc::new(RefCell::new(None)),
125
- events,
126
- _subscriptions: Rc::new(update_sub),
153
+ intersection_handle: Rc::new(RefCell::new(Some(intersect_handle))),
154
+ custom_events,
155
+ _subscriptions: Rc::new([update_sub, eject_sub]),
127
156
  }
128
157
  }
129
158
 
130
159
  #[doc(hidden)]
131
160
  #[wasm_bindgen(js_name = "connectedCallback")]
132
- pub fn connected_callback(&self) {
161
+ pub fn connected_callback(&self) -> ApiResult<()> {
133
162
  tracing::debug!("Connected <perspective-viewer>");
163
+ Ok(())
134
164
  }
135
165
 
136
- /// Loads a [`Table`] (or rather, a Javascript `Promise` which returns a
137
- /// [`Table`]) in this viewer.
166
+ /// Loads a [`Client`], or optionally [`Table`], or optionally a Javascript
167
+ /// `Promise` which returns a [`Client`] or [`Table`], in this viewer.
168
+ ///
169
+ /// Loading a [`Client`] does not render, but subsequent calls to
170
+ /// [`PerspectiveViewerElement::restore`] will use this [`Client`] to look
171
+ /// up the proviced `table` name field for the provided
172
+ /// [`ViewerConfigUpdate`].
173
+ ///
174
+ /// Loading a [`Table`] is equivalent to subsequently calling
175
+ /// [`Self::restore`] with the `table` field set to [`Table::get_name`], and
176
+ /// will render the UI in its default state when [`Self::load`] resolves.
177
+ /// If you plan to call [`Self::restore`] anyway, prefer passing a
178
+ /// [`Client`] argument to [`Self::load`] as it will conserve one render.
138
179
  ///
139
180
  /// When [`PerspectiveViewerElement::load`] resolves, the first frame of the
140
181
  /// UI + visualization is guaranteed to have been drawn. Awaiting the result
141
182
  /// of this method in a `try`/`catch` block will capture any errors
142
- /// thrown during the loading process, or from the [`Table`] `Promise`
183
+ /// thrown during the loading process, or from the [`Client`] `Promise`
143
184
  /// itself.
144
185
  ///
186
+ /// [`PerspectiveViewerElement::load`] may also be called with a [`Table`],
187
+ /// which is equivalent to:
188
+ ///
189
+ /// ```javascript
190
+ /// await viewer.load(await table.get_client());
191
+ /// await viewer.restore({name: await table.get_name()})
192
+ /// ```
193
+ ///
194
+ /// If you plan to call [`PerspectiveViewerElement::restore`] immediately
195
+ /// after [`PerspectiveViewerElement::load`] yourself, as is commonly
196
+ /// done when loading and configuring a new `<perspective-viewer>`, you
197
+ /// should use a [`Client`] as an argument and set the `table` field in the
198
+ /// restore call as
199
+ ///
145
200
  /// A [`Table`] can be created using the
146
201
  /// [`@perspective-dev/client`](https://www.npmjs.com/package/@perspective-dev/client)
147
202
  /// library from NPM (see [`perspective_js`] documentation for details).
@@ -152,76 +207,104 @@ impl PerspectiveViewerElement {
152
207
  /// import perspective from "@perspective-dev/client";
153
208
  ///
154
209
  /// const worker = await perspective.worker();
155
- /// viewer.load(worker.table("x,y\n1,2"));
210
+ /// viewer.load(worker);
156
211
  /// ```
157
- pub fn load(&self, table: JsValue) -> ApiFuture<()> {
158
- tracing::info!("Loading Table");
159
- self.session.invalidate();
212
+ ///
213
+ /// ... or
214
+ ///
215
+ /// ```javascript
216
+ /// const table = await worker.table(data, {name: "superstore"});
217
+ /// viewer.load(table);
218
+ /// ```
219
+ ///
220
+ /// Complete example:
221
+ ///
222
+ /// ```javascript
223
+ /// const viewer = document.createElement("perspective-viewer");
224
+ /// const worker = await perspective.worker();
225
+ ///
226
+ /// await worker.table("x\n1", {name: "table_one"});
227
+ /// await viewer.load(worker);
228
+ /// await viewer.restore({table: "table_one", columns: ["x"]});
229
+ /// ```
230
+ ///
231
+ /// ... or, if you don't want to pass your own arguments to `restore`:
232
+ ///
233
+ /// ```javascript
234
+ /// const viewer = document.createElement("perspective-viewer");
235
+ /// const worker = await perspective.worker();
236
+ ///
237
+ /// const table = await worker.table("x\n1", {name: "table_one"});
238
+ /// await viewer.load(table);
239
+ /// ```
240
+ pub fn load(&self, table: JsValue) -> ApiResult<ApiFuture<()>> {
160
241
  let promise = table
161
242
  .clone()
162
243
  .dyn_into::<js_sys::Promise>()
163
244
  .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
164
245
 
165
- self.session.reset_stats();
166
- let delete_task = self.session.reset(true);
246
+ let _plugin = self.renderer.get_active_plugin()?;
247
+ let task = self.session.reset(ResetOptions {
248
+ config: true,
249
+ expressions: true,
250
+ stats: true,
251
+ ..ResetOptions::default()
252
+ });
253
+
167
254
  let mut config = ViewConfigUpdate {
168
255
  columns: Some(self.session.get_view_config().columns.clone()),
169
256
  ..ViewConfigUpdate::default()
170
257
  };
171
258
 
259
+ let metadata = self.renderer.metadata();
172
260
  self.session
173
- .set_update_column_defaults(&mut config, &self.renderer.metadata());
261
+ .set_update_column_defaults(&mut config, &metadata);
262
+ self.session.update_view_config(config)?;
174
263
 
175
- let update_task = self.session.update_view_config(config);
176
264
  clone!(self.renderer, self.session);
177
- ApiFuture::new(async move {
265
+ Ok(ApiFuture::new_throttled(async move {
178
266
  let task = async {
179
- update_task?;
267
+ // Ignore this error, which is blown away by the table anyway.
268
+ let _ = task.await;
180
269
  let jstable = JsFuture::from(promise)
181
270
  .await
182
271
  .map_err(|x| apierror!(TableError(x)))?;
183
272
 
184
- if let Some(table) =
185
- wasm_bindgen_derive::try_from_js_option::<perspective_js::Table>(jstable)?
273
+ if let Ok(Some(table)) =
274
+ try_from_js_option::<perspective_js::Table>(jstable.clone())
186
275
  {
187
- if let Some(existing_table) = session.get_table() {
188
- if table.get_table() == &existing_table {
189
- tracing::info!(
190
- "Table `{}` already loaded, skipping",
191
- table.get_name().await
192
- );
193
-
194
- return Ok(&session);
195
- } else {
196
- tracing::debug!(
197
- "New table {} vs {}",
198
- table.get_table().get_name(),
199
- existing_table.get_name()
200
- );
201
- }
202
- }
203
-
276
+ let client = table.get_client().await;
277
+ session.set_client(client.get_client().clone());
278
+ let name = table.get_name().await;
204
279
  tracing::debug!(
205
- "Successfully loaded {:.0} rows from Table",
206
- table.size().await?
280
+ "Loading {:.0} rows from `Table` {}",
281
+ table.size().await?,
282
+ name
207
283
  );
208
284
 
209
- session.set_table(table.get_table().clone()).await?;
210
- session.validate().await?.create_view().await
285
+ if session.set_table(name).await? {
286
+ session.validate().await?.create_view().await?;
287
+ }
288
+
289
+ Ok(session.get_view())
290
+ } else if let Ok(Some(client)) =
291
+ wasm_bindgen_derive::try_from_js_option::<perspective_js::Client>(jstable)
292
+ {
293
+ session.set_client(client.get_client().clone());
294
+ Ok(session.get_view())
211
295
  } else {
212
- Err(ApiError::new("Invalid Table"))
296
+ Err(ApiError::new("Invalid argument"))
213
297
  }
214
298
  };
215
299
 
216
300
  renderer.set_throttle(None);
217
- let (draw, delete) = join(renderer.draw(task), delete_task).await;
218
- let result = draw.and(delete);
301
+ let result = renderer.draw(task).await;
219
302
  if let Err(e) = &result {
220
303
  session.set_error(false, e.clone()).await?;
221
304
  }
222
305
 
223
306
  result
224
- })
307
+ }))
225
308
  }
226
309
 
227
310
  /// Delete the internal [`View`] and all associated state, rendering this
@@ -246,17 +329,7 @@ impl PerspectiveViewerElement {
246
329
  /// await viewer.delete();
247
330
  /// ```
248
331
  pub fn delete(self) -> ApiFuture<()> {
249
- clone!(self.renderer, self.session, self.root);
250
- ApiFuture::new(self.renderer.clone().with_lock(async move {
251
- renderer.delete()?;
252
- root.borrow_mut()
253
- .take()
254
- .ok_or("Already deleted!")?
255
- .destroy();
256
- session.delete().await?;
257
- tracing::info!("Deleted <perspective-viewer>");
258
- Ok(())
259
- }))
332
+ self.delete_all(self.root())
260
333
  }
261
334
 
262
335
  /// Restart this `<perspective-viewer>` to its initial state, before
@@ -273,9 +346,9 @@ impl PerspectiveViewerElement {
273
346
  );
274
347
 
275
348
  std::mem::swap(self, &mut state);
276
- state.delete()
349
+ ApiFuture::new_throttled(state.delete())
277
350
  } else {
278
- ApiFuture::new(async move { Ok(()) })
351
+ ApiFuture::new_throttled(async move { Ok(()) })
279
352
  }
280
353
  }
281
354
 
@@ -304,8 +377,23 @@ impl PerspectiveViewerElement {
304
377
  ApiFuture::new(async move { Ok(session.get_view().ok_or("No table set")?.into()) })
305
378
  }
306
379
 
380
+ /// Get a copy of the [`ViewConfig`] for the current [`View`]. This is
381
+ /// non-blocking as it does not need to access the plugin (unlike
382
+ /// [`PerspectiveViewerElement::save`]), and also makes no API calls to the
383
+ /// server (unlike [`PerspectiveViewerElement::getView`] followed by
384
+ /// [`View::get_config`])
385
+ #[wasm_bindgen]
386
+ pub fn getViewConfig(&self) -> ApiFuture<JsViewConfig> {
387
+ let session = self.session.clone();
388
+ ApiFuture::new(async move {
389
+ let config = session.get_view_config();
390
+ Ok(JsValue::from_serde_ext(&*config)?.unchecked_into())
391
+ })
392
+ }
393
+
307
394
  /// Get the underlying [`Table`] for this viewer (as passed to
308
- /// [`PerspectiveViewerElement::load`]).
395
+ /// [`PerspectiveViewerElement::load`] or as the `table` field to
396
+ /// [`PerspectiveViewerElement::restore`]).
309
397
  ///
310
398
  /// # Arguments
311
399
  ///
@@ -324,10 +412,40 @@ impl PerspectiveViewerElement {
324
412
  ApiFuture::new(async move {
325
413
  match session.get_table() {
326
414
  Some(table) => Ok(table.into()),
327
- None if !wait_for_table.unwrap_or_default() => Err("No table set".into()),
415
+ None if !wait_for_table.unwrap_or_default() => Err("No `Table` set".into()),
328
416
  None => {
329
- session.table_loaded.listen_once().await?;
330
- Ok(session.get_table().ok_or("No table set")?.into())
417
+ session.table_loaded.read_next().await?;
418
+ Ok(session.get_table().ok_or("No `Table` set")?.into())
419
+ },
420
+ }
421
+ })
422
+ }
423
+
424
+ /// Get the underlying [`Client`] for this viewer (as passed to, or
425
+ /// associated with the [`Table`] passed to,
426
+ /// [`PerspectiveViewerElement::load`]).
427
+ ///
428
+ /// # Arguments
429
+ ///
430
+ /// - `wait_for_client` - whether to wait for
431
+ /// [`PerspectiveViewerElement::load`] to be called, or fail immediately
432
+ /// if [`PerspectiveViewerElement::load`] has not yet been called.
433
+ ///
434
+ /// # JavaScript Examples
435
+ ///
436
+ /// ```javascript
437
+ /// const client = await viewer.getClient();
438
+ /// ```
439
+ #[wasm_bindgen]
440
+ pub fn getClient(&self, wait_for_client: Option<bool>) -> ApiFuture<perspective_js::Client> {
441
+ let session = self.session.clone();
442
+ ApiFuture::new(async move {
443
+ match session.get_client() {
444
+ Some(client) => Ok(client.into()),
445
+ None if !wait_for_client.unwrap_or_default() => Err("No `Client` set".into()),
446
+ None => {
447
+ session.table_loaded.read_next().await?;
448
+ Ok(session.get_client().ok_or("No `Client` set")?.into())
331
449
  },
332
450
  }
333
451
  })
@@ -369,14 +487,24 @@ impl PerspectiveViewerElement {
369
487
  /// ```
370
488
  pub fn flush(&self) -> ApiFuture<()> {
371
489
  clone!(self.renderer);
372
- ApiFuture::new(async move {
490
+ ApiFuture::new_throttled(async move {
491
+ // We must let two AFs pass to guarantee listeners to the DOM state
492
+ // have themselves triggered, or else `request_animation_frame`
493
+ // may finish before a `ResizeObserver` triggered before is
494
+ // notifiedd.
495
+ //
496
+ // https://github.com/w3c/csswg-drafts/issues/9560
497
+ // https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering
373
498
  request_animation_frame().await;
499
+ request_animation_frame().await;
500
+ renderer.clone().with_lock(async { Ok(()) }).await?;
374
501
  renderer.with_lock(async { Ok(()) }).await
375
502
  })
376
503
  }
377
504
 
378
505
  /// Restores this element from a full/partial
379
- /// [`perspective_js::JsViewConfig`].
506
+ /// [`perspective_js::JsViewConfig`] (this element's user-configurable
507
+ /// state, including the `Table` name).
380
508
  ///
381
509
  /// One of the best ways to use [`Self::restore`] is by first configuring
382
510
  /// a `<perspective-viewer>` as you wish, then using either the `Debug`
@@ -390,55 +518,70 @@ impl PerspectiveViewerElement {
390
518
  ///
391
519
  /// # JavaScript Examples
392
520
  ///
393
- /// Apply a `group_by` to the current [`View`], without modifying/resetting
394
- /// other fields:
521
+ /// Loads a default plugin for the table named `"superstore"`:
522
+ ///
523
+ /// ```javascript
524
+ /// await viewer.restore({table: "superstore"});
525
+ /// ```
526
+ ///
527
+ /// Apply a `group_by` to the same `viewer` element, without
528
+ /// modifying/resetting other fields - you can omit the `table` field,
529
+ /// this has already been set once and is not modified:
395
530
  ///
396
531
  /// ```javascript
397
532
  /// await viewer.restore({group_by: ["State"]});
398
533
  /// ```
399
534
  pub fn restore(&self, update: JsValue) -> ApiFuture<()> {
400
- tracing::info!("Restoring ViewerConfig");
401
535
  let this = self.clone();
402
- ApiFuture::new(async move {
536
+ ApiFuture::new_throttled(async move {
403
537
  let decoded_update = ViewerConfigUpdate::decode(&update)?;
538
+ tracing::info!("Restoring {}", decoded_update);
404
539
  let root = this.root.clone();
405
540
  let settings = decoded_update.settings.clone();
406
- let result = root
407
- .borrow()
408
- .as_ref()
409
- .into_apierror()?
410
- .send_message_async(move |x| {
411
- PerspectiveViewerMsg::ToggleSettingsComplete(settings, x)
412
- });
541
+ let (sender, receiver) = channel::<()>();
542
+ root.borrow().as_ref().into_apierror()?.send_message(
543
+ PerspectiveViewerMsg::ToggleSettingsComplete(settings, sender),
544
+ );
413
545
 
414
- let task = this
415
- .restore_and_render(decoded_update, async move { Ok(result.await?) })
546
+ let result = this
547
+ .restore_and_render(decoded_update.clone(), {
548
+ clone!(this, decoded_update.table);
549
+ async move {
550
+ if let OptionalUpdate::Update(name) = table {
551
+ this.session.set_table(name).await?;
552
+ this.session
553
+ .update_column_defaults(&this.renderer.metadata());
554
+ };
555
+
556
+ // Something abnormal in the DOM happened, e.g. the
557
+ // element was disconnected while rendering.
558
+ receiver.await.unwrap_or_log();
559
+ Ok(())
560
+ }
561
+ })
416
562
  .await;
417
563
 
418
- if let Err(e) = task {
564
+ if let Err(e) = &result {
419
565
  this.session().set_error(false, e.clone()).await?;
420
- Err(e)
421
- } else {
422
- Ok(())
423
566
  }
567
+ result
424
568
  })
425
569
  }
426
570
 
571
+ /// If this element is in an _errored_ state, this method will clear it and
572
+ /// re-render. Calling this method is equivalent to clicking the error reset
573
+ /// button in the UI.
427
574
  pub fn resetError(&self) -> ApiFuture<()> {
428
- self.session.invalidate();
575
+ ApiFuture::spawn(self.session.reset(ResetOptions::default()));
429
576
  let this = self.clone();
430
- ApiFuture::new(async move {
577
+ ApiFuture::new_throttled(async move {
431
578
  this.update_and_render(ViewConfigUpdate::default())?.await?;
432
579
  Ok(())
433
580
  })
434
581
  }
435
582
 
436
- /// Save this element to serialized state object, one which can be restored
437
- /// via the [`Self::restore`] method.
438
- ///
439
- /// # Arguments
440
- ///
441
- /// - `format` - Supports "json" (default), "arraybuffer" or "string".
583
+ /// Save this element's user-configurable state to a serialized state
584
+ /// object, one which can be restored via the [`Self::restore`] method.
442
585
  ///
443
586
  /// # JavaScript Examples
444
587
  ///
@@ -456,26 +599,25 @@ impl PerspectiveViewerElement {
456
599
  /// await viewer.restore(token);
457
600
  /// });
458
601
  /// ```
459
- pub fn save(&self, format: Option<String>) -> ApiFuture<JsValue> {
602
+ pub fn save(&self) -> ApiFuture<JsValue> {
460
603
  let this = self.clone();
461
604
  ApiFuture::new(async move {
462
- let viewer_config_task = this.get_viewer_config();
463
- let format = format
464
- .as_ref()
465
- .map(|x| ViewerConfigEncoding::from_str(x))
466
- .transpose()?;
605
+ let viewer_config = this
606
+ .renderer
607
+ .clone()
608
+ .with_lock(async { this.get_viewer_config().await })
609
+ .await?;
467
610
 
468
- let viewer_config = viewer_config_task.await?;
469
- viewer_config.encode(&format)
611
+ viewer_config.encode()
470
612
  })
471
613
  }
472
614
 
473
- /// Download this viewer's internal [`View`] data as a `.csv` file.
615
+ /// Download this viewer's internal [`View`] data via a browser download
616
+ /// event.
474
617
  ///
475
618
  /// # Arguments
476
619
  ///
477
- /// - `flat` - Whether to use the current [`perspective_js::JsViewConfig`]
478
- /// to generate this data, or use the default.
620
+ /// - `method` - The `ExportMethod` to use to render the data to download.
479
621
  ///
480
622
  /// # JavaScript Examples
481
623
  ///
@@ -484,16 +626,58 @@ impl PerspectiveViewerElement {
484
626
  /// await viewer.download();
485
627
  /// })
486
628
  /// ```
487
- pub fn download(&self, flat: Option<bool>) -> ApiFuture<()> {
488
- let session = self.session.clone();
629
+ pub fn download(&self, method: Option<JsString>) -> ApiFuture<()> {
630
+ let this = self.clone();
631
+ ApiFuture::new_throttled(async move {
632
+ let method = if let Some(method) = method
633
+ .map(|x| x.unchecked_into())
634
+ .map(serde_wasm_bindgen::from_value)
635
+ {
636
+ method?
637
+ } else {
638
+ ExportMethod::Csv
639
+ };
640
+
641
+ let blob = this.export_method_to_blob(method).await?;
642
+ let is_chart = this.renderer.is_chart();
643
+ download(
644
+ format!("untitled{}", method.as_filename(is_chart)).as_ref(),
645
+ &blob,
646
+ )
647
+ })
648
+ }
649
+
650
+ /// Exports this viewer's internal [`View`] as a JavaSript data, the
651
+ /// exact type of which depends on the `method` but defaults to `String`
652
+ /// in CSV format.
653
+ ///
654
+ /// This method is only really useful for the `"plugin"` method, which
655
+ /// will use the configured plugin's export (e.g. PNG for
656
+ /// `@perspective-dev/viewer-d3fc`). Otherwise, prefer to call the
657
+ /// equivalent method on the underlying [`View`] directly.
658
+ ///
659
+ /// # Arguments
660
+ ///
661
+ /// - `method` - The `ExportMethod` to use to render the data to download.
662
+ ///
663
+ /// # JavaScript Examples
664
+ ///
665
+ /// ```javascript
666
+ /// const data = await viewer.export("plugin");
667
+ /// ```
668
+ pub fn export(&self, method: Option<JsString>) -> ApiFuture<JsValue> {
669
+ let this = self.clone();
489
670
  ApiFuture::new(async move {
490
- let val = session
491
- .csv_as_jsvalue(flat.unwrap_or_default(), None)
492
- .await?
493
- .as_blob()?;
671
+ let method = if let Some(method) = method
672
+ .map(|x| x.unchecked_into())
673
+ .map(serde_wasm_bindgen::from_value)
674
+ {
675
+ method?
676
+ } else {
677
+ ExportMethod::Csv
678
+ };
494
679
 
495
- // TODO name.as_deref().unwrap_or("untitled.csv")
496
- download("untitled.csv", &val)
680
+ this.export_method_to_jsvalue(method).await
497
681
  })
498
682
  }
499
683
 
@@ -514,7 +698,7 @@ impl PerspectiveViewerElement {
514
698
  /// ```
515
699
  pub fn copy(&self, method: Option<JsString>) -> ApiFuture<()> {
516
700
  let this = self.clone();
517
- ApiFuture::new(async move {
701
+ ApiFuture::new_throttled(async move {
518
702
  let method = if let Some(method) = method
519
703
  .map(|x| x.unchecked_into())
520
704
  .map(serde_wasm_bindgen::from_value)
@@ -524,7 +708,7 @@ impl PerspectiveViewerElement {
524
708
  ExportMethod::Csv
525
709
  };
526
710
 
527
- let js_task = this.export_method_to_jsvalue(method);
711
+ let js_task = this.export_method_to_blob(method);
528
712
  copy_to_clipboard(js_task, MimeType::TextPlain).await
529
713
  })
530
714
  }
@@ -542,17 +726,17 @@ impl PerspectiveViewerElement {
542
726
  /// await viewer.reset();
543
727
  /// ```
544
728
  pub fn reset(&self, reset_all: Option<bool>) -> ApiFuture<()> {
545
- tracing::info!("Resetting config");
729
+ tracing::debug!("Resetting config");
546
730
  let root = self.root.clone();
547
731
  let all = reset_all.unwrap_or_default();
548
- ApiFuture::new(async move {
549
- let task = root
550
- .borrow()
732
+ ApiFuture::new_throttled(async move {
733
+ let (sender, receiver) = channel::<()>();
734
+ root.borrow()
551
735
  .as_ref()
552
736
  .ok_or("Already deleted")?
553
- .send_message_async(move |x| PerspectiveViewerMsg::Reset(all, Some(x)));
737
+ .send_message(PerspectiveViewerMsg::Reset(all, Some(sender)));
554
738
 
555
- Ok(task.await?)
739
+ Ok(receiver.await?)
556
740
  })
557
741
  }
558
742
 
@@ -584,8 +768,18 @@ impl PerspectiveViewerElement {
584
768
  *self.resize_handle.borrow_mut() = None;
585
769
  }
586
770
 
587
- let renderer = self.renderer.clone();
588
- ApiFuture::new(async move { renderer.resize().await })
771
+ let state = self.clone_state();
772
+ ApiFuture::new_throttled(async move {
773
+ if !state.renderer().is_plugin_activated()? {
774
+ state
775
+ .update_and_render(ViewConfigUpdate::default())?
776
+ .await?;
777
+ } else {
778
+ state.renderer().resize().await?;
779
+ }
780
+
781
+ Ok(())
782
+ })
589
783
  }
590
784
 
591
785
  /// Sets the auto-size behavior of this component.
@@ -614,7 +808,8 @@ impl PerspectiveViewerElement {
614
808
  let handle = Some(ResizeObserverHandle::new(
615
809
  &self.elem,
616
810
  &self.renderer,
617
- self.root.borrow().as_ref().unwrap(),
811
+ &self.session,
812
+ &self.root,
618
813
  ));
619
814
  *self.resize_handle.borrow_mut() = handle;
620
815
  } else {
@@ -645,9 +840,11 @@ impl PerspectiveViewerElement {
645
840
  if autopause {
646
841
  let handle = Some(IntersectionObserverHandle::new(
647
842
  &self.elem,
843
+ &self.presentation,
648
844
  &self.session,
649
845
  &self.renderer,
650
846
  ));
847
+
651
848
  *self.intersection_handle.borrow_mut() = handle;
652
849
  } else {
653
850
  *self.intersection_handle.borrow_mut() = None;
@@ -666,7 +863,7 @@ impl PerspectiveViewerElement {
666
863
  pub fn setSelection(&self, window: Option<JsViewWindow>) -> ApiResult<()> {
667
864
  let window = window.map(|x| x.into_serde_ext()).transpose()?;
668
865
  if self.renderer.get_selection() != window {
669
- self.events.dispatch_select(window.as_ref())?;
866
+ self.custom_events.dispatch_select(window.as_ref())?;
670
867
  }
671
868
 
672
869
  self.renderer.set_selection(window);
@@ -742,8 +939,10 @@ impl PerspectiveViewerElement {
742
939
  .cloned();
743
940
 
744
941
  changed = presentation.set_theme_name(reset_theme.as_deref()).await? || changed;
745
- if changed && let Some(view) = session.get_view() {
746
- return renderer.restyle_all(&view).await;
942
+ if changed {
943
+ if let Some(view) = session.get_view() {
944
+ return renderer.restyle_all(&view).await;
945
+ }
747
946
  }
748
947
 
749
948
  Ok(JsValue::UNDEFINED)
@@ -789,13 +988,12 @@ impl PerspectiveViewerElement {
789
988
  let root = self.root.clone();
790
989
  ApiFuture::new(async move {
791
990
  let force = force.map(SettingsUpdate::Update);
792
- let task = root
793
- .borrow()
794
- .as_ref()
795
- .into_apierror()?
796
- .send_message_async(|x| PerspectiveViewerMsg::ToggleSettingsInit(force, Some(x)));
991
+ let (sender, receiver) = channel::<ApiResult<wasm_bindgen::JsValue>>();
992
+ root.borrow().as_ref().into_apierror()?.send_message(
993
+ PerspectiveViewerMsg::ToggleSettingsInit(force, Some(sender)),
994
+ );
797
995
 
798
- task.await.map_err(|_| JsValue::from("Cancelled"))?
996
+ receiver.await.map_err(|_| JsValue::from("Cancelled"))?
799
997
  })
800
998
  }
801
999
 
@@ -826,7 +1024,7 @@ impl PerspectiveViewerElement {
826
1024
  #[doc(hidden)]
827
1025
  #[allow(clippy::use_self)]
828
1026
  #[wasm_bindgen]
829
- pub fn get_model(&self) -> PerspectiveViewerElement {
1027
+ pub fn __get_model(&self) -> PerspectiveViewerElement {
830
1028
  self.clone()
831
1029
  }
832
1030
 
@@ -839,18 +1037,18 @@ impl PerspectiveViewerElement {
839
1037
  #[wasm_bindgen]
840
1038
  pub fn toggleColumnSettings(&self, column_name: String) -> ApiFuture<()> {
841
1039
  clone!(self.session, self.root);
842
- ApiFuture::new(async move {
843
- let locator = session.metadata().get_column_locator(Some(column_name));
844
- let task = root
845
- .borrow()
846
- .as_ref()
847
- .into_apierror()?
848
- .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
1040
+ ApiFuture::new_throttled(async move {
1041
+ let locator = session.get_column_locator(Some(column_name));
1042
+ let (sender, receiver) = channel::<()>();
1043
+ root.borrow().as_ref().into_apierror()?.send_message(
1044
+ PerspectiveViewerMsg::OpenColumnSettings {
849
1045
  locator,
850
1046
  sender: Some(sender),
851
1047
  toggle: true,
852
- });
853
- task.await.map_err(|_| ApiError::from("Cancelled"))
1048
+ },
1049
+ );
1050
+
1051
+ receiver.await.map_err(|_| ApiError::from("Cancelled"))
854
1052
  })
855
1053
  }
856
1054
 
@@ -862,19 +1060,19 @@ impl PerspectiveViewerElement {
862
1060
  column_name: Option<String>,
863
1061
  toggle: Option<bool>,
864
1062
  ) -> ApiFuture<()> {
865
- clone!(self.session, self.root);
866
- ApiFuture::new(async move {
867
- let locator = session.metadata().get_column_locator(column_name);
868
- let task = root
869
- .borrow()
870
- .as_ref()
871
- .into_apierror()?
872
- .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
1063
+ let locator = self.get_column_locator(column_name);
1064
+ clone!(self.root);
1065
+ ApiFuture::new_throttled(async move {
1066
+ let (sender, receiver) = channel::<()>();
1067
+ root.borrow().as_ref().into_apierror()?.send_message(
1068
+ PerspectiveViewerMsg::OpenColumnSettings {
873
1069
  locator,
874
1070
  sender: Some(sender),
875
1071
  toggle: toggle.unwrap_or_default(),
876
- });
877
- task.await.map_err(|_| ApiError::from("Cancelled"))
1072
+ },
1073
+ );
1074
+
1075
+ receiver.await.map_err(|_| ApiError::from("Cancelled"))
878
1076
  })
879
1077
  }
880
1078
  }