@perspective-dev/viewer 5.5.0 → 5.6.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 (201) hide show
  1. package/README.md +82 -3
  2. package/dist/cdn/perspective-viewer.js +9 -9
  3. package/dist/cdn/perspective-viewer.js.map +3 -3
  4. package/dist/css/blueprint.css +1 -1
  5. package/dist/css/botanical.css +1 -1
  6. package/dist/css/dracula.css +1 -1
  7. package/dist/css/eggplant.css +1 -1
  8. package/dist/css/gruvbox-dark.css +1 -1
  9. package/dist/css/gruvbox.css +1 -1
  10. package/dist/css/icons.css +1 -1
  11. package/dist/css/intl/ar.css +1 -1
  12. package/dist/css/intl/de.css +1 -1
  13. package/dist/css/intl/es.css +1 -1
  14. package/dist/css/intl/fr.css +1 -1
  15. package/dist/css/intl/ja.css +1 -1
  16. package/dist/css/intl/pt.css +1 -1
  17. package/dist/css/intl/zh.css +1 -1
  18. package/dist/css/intl.css +1 -1
  19. package/dist/css/ledger.css +1 -1
  20. package/dist/css/monokai.css +1 -1
  21. package/dist/css/nord.css +1 -1
  22. package/dist/css/phosphor.css +1 -1
  23. package/dist/css/pro-dark.css +1 -1
  24. package/dist/css/pro.css +1 -1
  25. package/dist/css/solarized-dark.css +1 -1
  26. package/dist/css/solarized.css +1 -1
  27. package/dist/css/themes.css +1 -1
  28. package/dist/css/vaporwave.css +1 -1
  29. package/dist/css/velvet.css +1 -1
  30. package/dist/docs/perspective-docs.json +1 -1
  31. package/dist/esm/column-format.js +2 -0
  32. package/dist/esm/column-format.js.map +7 -0
  33. package/dist/esm/extensions.d.ts +3 -13
  34. package/dist/esm/perspective-viewer.d.ts +1 -1
  35. package/dist/esm/perspective-viewer.js +9 -9
  36. package/dist/esm/perspective-viewer.js.map +3 -3
  37. package/dist/esm/plugin.d.ts +4 -1
  38. package/dist/esm/select-detail.d.ts +15 -0
  39. package/dist/esm/select-detail.js +2 -0
  40. package/dist/esm/select-detail.js.map +7 -0
  41. package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
  42. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  43. package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
  44. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  45. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  46. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  47. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  48. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  49. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  50. package/dist/wasm/perspective-viewer.d.ts +126 -35
  51. package/dist/wasm/perspective-viewer.js +320 -185
  52. package/dist/wasm/perspective-viewer.wasm +0 -0
  53. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  54. package/package.json +33 -20
  55. package/src/css/column-selector.css +205 -50
  56. package/src/css/ui/containers/context-menu.css +12 -1
  57. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  58. package/src/css/ui/containers/scroll-panel.css +4 -1
  59. package/src/rust/agent/client/protocol.rs +0 -89
  60. package/src/rust/agent/markdown/mod.rs +0 -238
  61. package/src/rust/agent/preamble.md +1 -0
  62. package/src/rust/agent/tools.rs +2 -16
  63. package/src/rust/components/column_selector/active_column.rs +15 -5
  64. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  65. package/src/rust/components/column_selector/config_selector.rs +34 -6
  66. package/src/rust/components/column_selector/derived_control.rs +229 -0
  67. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  68. package/src/rust/components/column_selector/filter_column.rs +0 -2
  69. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  70. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  71. package/src/rust/components/column_selector/sort_column.rs +4 -3
  72. package/src/rust/components/column_selector.rs +21 -1
  73. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  74. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  75. package/src/rust/components/column_settings_sidebar.rs +25 -19
  76. package/src/rust/components/debug_panel.rs +38 -29
  77. package/src/rust/components/dragdrop_list.rs +8 -9
  78. package/src/rust/components/export_dropdown.rs +6 -0
  79. package/src/rust/components/main_panel/msg.rs +6 -4
  80. package/src/rust/components/main_panel/reconcile.rs +6 -1
  81. package/src/rust/components/main_panel/render.rs +70 -51
  82. package/src/rust/components/main_panel/update.rs +39 -1
  83. package/src/rust/components/main_panel.rs +97 -59
  84. package/src/rust/components/new_panel_menu.rs +69 -1
  85. package/src/rust/components/panel_menu.rs +63 -12
  86. package/src/rust/components/plugin_tab.rs +3 -2
  87. package/src/rust/components/render_warning.rs +2 -1
  88. package/src/rust/components/settings_panel.rs +4 -4
  89. package/src/rust/components/status_bar.rs +80 -21
  90. package/src/rust/components/status_indicator.rs +8 -1
  91. package/src/rust/components/viewer/filters.rs +15 -3
  92. package/src/rust/components/viewer/msg.rs +8 -0
  93. package/src/rust/components/viewer/panels.rs +44 -4
  94. package/src/rust/components/viewer/render.rs +1 -0
  95. package/src/rust/components/viewer/settings.rs +0 -22
  96. package/src/rust/components/viewer/snapshots.rs +2 -2
  97. package/src/rust/components/viewer/wiring.rs +18 -1
  98. package/src/rust/components/viewer.rs +47 -27
  99. package/src/rust/components/window_editor.rs +5 -4
  100. package/src/rust/config/column_config_schema.rs +38 -377
  101. package/src/rust/config/export_method.rs +24 -0
  102. package/src/rust/config/number_string_format.rs +0 -94
  103. package/src/rust/config/options.rs +3 -7
  104. package/src/rust/config/plugin_static_config.rs +0 -42
  105. package/src/rust/config/viewer_config.rs +47 -17
  106. package/src/rust/config/workspace_config.rs +158 -128
  107. package/src/rust/custom_elements/viewer.rs +189 -129
  108. package/src/rust/custom_events.rs +33 -18
  109. package/src/rust/derived.rs +266 -0
  110. package/src/rust/exprtk/tokenize.rs +0 -54
  111. package/src/rust/js/plugin.rs +1 -1
  112. package/src/rust/lib.rs +3 -0
  113. package/src/rust/presentation.rs +23 -4
  114. package/src/rust/queries/column_locator.rs +55 -48
  115. package/src/rust/queries/get_viewer_config.rs +1 -1
  116. package/src/rust/queries/plugin_column_styles.rs +22 -23
  117. package/src/rust/renderer/dispatch.rs +2 -0
  118. package/src/rust/renderer/plugin_config.rs +652 -487
  119. package/src/rust/renderer/props.rs +4 -0
  120. package/src/rust/renderer/registry.rs +2 -6
  121. package/src/rust/renderer/state.rs +205 -0
  122. package/src/rust/renderer.rs +201 -59
  123. package/src/rust/session/metadata.rs +35 -19
  124. package/src/rust/session/op_queue.rs +430 -0
  125. package/src/rust/session/panel_state.rs +327 -0
  126. package/src/rust/session/view_subscription.rs +42 -3
  127. package/src/rust/session.rs +931 -419
  128. package/src/rust/tasks/apply_global_filters.rs +49 -35
  129. package/src/rust/tasks/auto_pause.rs +22 -12
  130. package/src/rust/tasks/copy_export.rs +72 -0
  131. package/src/rust/tasks/create_panel.rs +46 -21
  132. package/src/rust/tasks/derive.rs +312 -0
  133. package/src/rust/tasks/mod.rs +6 -2
  134. package/src/rust/tasks/pipeline.rs +152 -89
  135. package/src/rust/tasks/reset_all.rs +31 -20
  136. package/src/rust/tasks/restore_derived.rs +200 -0
  137. package/src/rust/tasks/restore_panel.rs +153 -141
  138. package/src/rust/tasks/send_column_config.rs +56 -29
  139. package/src/rust/tasks/send_plugin_config.rs +52 -23
  140. package/src/rust/tasks/set_edit_mode.rs +33 -25
  141. package/src/rust/tasks/sync_update_panels.rs +40 -13
  142. package/src/rust/tasks/table_lifecycle.rs +70 -34
  143. package/src/rust/tasks/transactional_restore.rs +387 -0
  144. package/src/rust/tasks/update_theme.rs +45 -6
  145. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  146. package/src/rust/ui/containers/split_panel.rs +0 -41
  147. package/src/rust/ui/context_menu.rs +157 -18
  148. package/src/rust/ui/font_loader.rs +23 -35
  149. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  150. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  151. package/src/rust/ui/form/palette_selector.rs +0 -51
  152. package/src/rust/ui/portal.rs +4 -10
  153. package/src/rust/utils/browser/mod.rs +2 -3
  154. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  155. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  156. package/src/rust/utils/css.rs +0 -360
  157. package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
  158. package/src/rust/utils/local_fonts.rs +0 -36
  159. package/src/rust/utils/mod.rs +4 -3
  160. package/src/rust/utils/modal_position.rs +15 -21
  161. package/src/rust/utils/pending_effects.rs +6 -30
  162. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  163. package/src/rust/utils/pubsub.rs +0 -1
  164. package/src/rust/workspace.rs +187 -140
  165. package/src/svg/derived.svg +6 -0
  166. package/src/themes/icons.css +1 -0
  167. package/src/themes/intl/ar.css +10 -0
  168. package/src/themes/intl/de.css +10 -0
  169. package/src/themes/intl/es.css +10 -0
  170. package/src/themes/intl/fr.css +10 -0
  171. package/src/themes/intl/ja.css +10 -0
  172. package/src/themes/intl/pt.css +10 -0
  173. package/src/themes/intl/zh.css +10 -0
  174. package/src/themes/intl.css +10 -0
  175. package/src/themes/monokai.css +1 -1
  176. package/src/themes/vaporwave.css +1 -1
  177. package/src/ts/extensions.ts +3 -36
  178. package/src/ts/perspective-viewer.ts +1 -1
  179. package/src/ts/plugin.ts +4 -1
  180. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  181. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  182. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  183. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  184. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  185. package/src/ts/ts-rs/TableRef.ts +9 -0
  186. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  187. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  188. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  189. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  190. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  191. package/dist/esm/perspective-viewer.inline.js +0 -21
  192. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  193. package/src/rust/tasks/restore_and_render.rs +0 -107
  194. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  195. package/src/rust/utils/tests/pubsub.rs +0 -95
  196. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  197. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  198. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  199. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  200. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  201. /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
@@ -19,7 +19,6 @@ use futures::channel::oneshot::channel;
19
19
  use futures::future::join_all;
20
20
  use js_sys::{Array, JsString};
21
21
  use perspective_client::config::ViewConfigUpdate;
22
- use perspective_client::utils::PerspectiveResultExt;
23
22
  use perspective_js::utils::global;
24
23
  use perspective_js::{JsViewConfig, JsViewWindow, Table, View, apierror};
25
24
  use wasm_bindgen::JsCast;
@@ -38,7 +37,7 @@ use crate::js::*;
38
37
  use crate::presentation::*;
39
38
  use crate::queries::*;
40
39
  use crate::root::Root;
41
- use crate::session::{MissingTable, ResetOptions, TableLoadState};
40
+ use crate::session::{BindPlan, Disposal, MissingTable, OpKind, ResetOptions, TableLoadState};
42
41
  use crate::tasks::*;
43
42
  use crate::utils::*;
44
43
  use crate::workspace::{Panel, PanelId, Workspace};
@@ -212,13 +211,13 @@ impl PerspectiveViewerElement {
212
211
  .panels()
213
212
  .into_iter()
214
213
  .filter(|panel| !fresh.contains(&panel.id))
215
- .filter(|panel| stamp_global_overlay(&self.workspace, &panel.id, &panel.session))
216
214
  .collect::<Vec<_>>();
217
215
 
218
- join_all(retained.iter().map(|panel| async move {
219
- apply_and_render(&panel.session, &panel.renderer, ViewConfigUpdate::default())?.await?;
220
- Ok(())
221
- }))
216
+ join_all(
217
+ retained
218
+ .iter()
219
+ .map(|panel| broadcast_overlay(&self.workspace, panel)),
220
+ )
222
221
  .await
223
222
  .into_iter()
224
223
  .collect::<ApiResult<Vec<_>>>()?;
@@ -237,7 +236,8 @@ impl PerspectiveViewerElement {
237
236
 
238
237
  async fn workspace_config(this: Self, full_palette: bool) -> ApiResult<JsValue> {
239
238
  this.workspace.effects().settle().await;
240
- let mut panels: std::collections::BTreeMap<String, PanelViewerConfig> = Default::default();
239
+ let mut panels: std::collections::BTreeMap<String, PanelViewerConfig<String, String>> =
240
+ Default::default();
241
241
  for id in &this.workspace.panel_ids() {
242
242
  let panel = this.workspace.panel(id).into_apierror()?;
243
243
  let config = panel
@@ -288,6 +288,7 @@ impl PerspectiveViewerElement {
288
288
  active,
289
289
  layout,
290
290
  panels,
291
+ derived_tables: this.workspace.derived_configs(),
291
292
  global_filters: this.workspace.global_filters(),
292
293
  masters: this
293
294
  .workspace
@@ -492,41 +493,17 @@ impl PerspectiveViewerElement {
492
493
  .dyn_into::<js_sys::Promise>()
493
494
  .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
494
495
 
495
- // Resolve the target panel. On an EMPTY element (zero panels):
496
- // - a synchronously-detectable `Client` registers inertly with NO panel (the
497
- // common `load(client)` — no phantom panel is left behind);
498
- // - otherwise (a resolved `Table`, or a `Promise` whose type isn't yet known)
499
- // the first panel is RESERVED synchronously here — a full panel model held
500
- // in the workspace's reservation slot, NOT placed — so its ordering position
501
- // is fixed at the call site: a `restore()` fired right after an unawaited
502
- // `load()` CLAIMS the reservation (placing it) and targets THIS panel, not a
503
- // second one. The reservation is likewise placed when the payload proves to
504
- // be a `Table` (or the load fails, surfacing its error), and discarded —
505
- // only while still unclaimed — for an inert `Client` payload. Placement and
506
- // discard are both atomic slot transfers (`Workspace::claim_reserved` /
507
- // `Workspace::take_reserved`), so an inert payload disposing a panel a
508
- // racing `restore` claimed is unrepresentable.
509
- // A pre-existing active panel is used as-is (a `Client` registers
510
- // inertly against it, never clearing its table).
511
496
  let (panel, notify) = match self.workspace.active_panel() {
512
497
  Some(panel) => (panel, None),
513
498
  None => {
514
- // Empty element — classify the payload synchronously where possible.
515
499
  if let Ok(Some(client)) =
516
500
  try_from_js_option::<perspective_js::Client>(table.clone())
517
501
  {
518
- // A resolved `Client` registers SYNCHRONOUSLY (so an unawaited
519
- // `load(client)` is visible to a `restore()` fired right after,
520
- // which creates the first panel and federates against loaded
521
- // clients) and creates NO panel — inert.
522
502
  self.workspace
523
503
  .set_default_client(client.get_client().clone());
524
504
  return Ok(ApiFuture::new(async { Ok(()) }));
525
505
  }
526
506
 
527
- // A resolved `Table` (or a `Promise` whose type isn't yet known)
528
- // adopts the pending reservation (a second `load()` on a
529
- // still-empty element), else reserves a fresh panel model.
530
507
  let panel = self.workspace.reserved_panel().unwrap_or_else(|| {
531
508
  create_panel_model(
532
509
  &self.elem,
@@ -548,18 +525,28 @@ impl PerspectiveViewerElement {
548
525
 
549
526
  let session = panel.session;
550
527
  let renderer = panel.renderer;
551
- let generation = session.begin_pending_load();
552
528
  clone!(self.workspace, self.presentation);
553
- Ok(ApiFuture::new_throttled(async move {
554
- let _effect = effect;
555
- renderer.set_throttle(None);
556
- let _run_token = session.begin_config_run();
557
- let result = {
558
- clone!(session, renderer, workspace, notify);
559
- renderer
529
+ enum Discard {
530
+ Unclaimed(Panel),
531
+ Evicted(Panel),
532
+ }
533
+
534
+ let table_known = matches!(
535
+ try_from_js_option::<perspective_js::Table>(table.clone()),
536
+ Ok(Some(_))
537
+ );
538
+
539
+ let ticket = session.submit(OpKind::Load { table_known }, {
540
+ clone!(session, renderer);
541
+ move |load| {
542
+ Box::pin(async move {
543
+ let _effect = effect;
544
+ renderer.set_throttle(None);
545
+ let result = {
546
+ clone!(session, renderer, workspace, notify, load);
547
+ renderer
560
548
  .clone()
561
549
  .render_task(|guard| async move {
562
- seed_panel_theme(&presentation, &renderer).await;
563
550
  renderer.stamp_theme(None);
564
551
  let jstable = JsFuture::from(promise)
565
552
  .await
@@ -569,30 +556,17 @@ impl PerspectiveViewerElement {
569
556
  try_from_js_option::<perspective_js::Table>(jstable.clone())
570
557
  {
571
558
  tracing::warn!("{}", DEPRECATED_TABLE_MESSAGE);
572
- let Some(journal) = session.take_pending_load(generation) else {
559
+ if load.is_superseded() {
573
560
  return Ok(None);
574
- };
561
+ }
575
562
 
576
563
  if let Some(notify) = &notify {
577
564
  place_reserved(&workspace, notify, true);
578
565
  }
579
566
 
580
- let _plugin = renderer.ensure_plugin_selected()?;
581
- let _ = renderer.mount_active_plugin();
582
- session
583
- .reset(ResetOptions {
584
- config: true,
585
- expressions: true,
586
- stats: true,
587
- table: Some(session::TableIntermediateState::Reloaded),
588
- })
589
- .await
590
- .unwrap_or_log();
591
-
592
567
  let client = table.get_client().await;
593
568
  let inner_client = client.get_client().clone();
594
- session.set_client(inner_client.clone());
595
- workspace.set_default_client(inner_client);
569
+ workspace.set_default_client(inner_client.clone());
596
570
  let name = table.get_name().await;
597
571
  tracing::debug!(
598
572
  "Loading {:.0} rows from `Table` {}",
@@ -600,21 +574,40 @@ impl PerspectiveViewerElement {
600
574
  name
601
575
  );
602
576
 
603
- session.set_table(name).await?;
604
- for delta in journal {
605
- session.commit_view_config(delta)?;
606
- }
607
-
608
- session.commit_table_defaults();
609
- let (disposition, _pin) =
610
- crate::tasks::bind_snapshot(&guard, &session, &renderer).await?;
577
+ let plan = match session::probe_table(
578
+ &inner_client,
579
+ &name,
580
+ MissingTable::Pend,
581
+ )
582
+ .await?
583
+ {
584
+ Some(table) => BindPlan::Bind {
585
+ client: inner_client,
586
+ table: Box::new(table),
587
+ reset: true,
588
+ },
589
+ None => BindPlan::Pend {
590
+ client: inner_client,
591
+ name,
592
+ reset: true,
593
+ },
594
+ };
611
595
 
612
- crate::tasks::dispatch_bound(
613
- &guard,
596
+ let prepared = crate::tasks::prepare(
597
+ &session,
614
598
  &renderer,
615
- disposition,
599
+ &presentation,
616
600
  false,
617
- crate::tasks::RunOrigin::Public,
601
+ plan,
602
+ renderer
603
+ .slot_name()
604
+ .map(|id| overlay_for(&workspace, &PanelId::from(id))),
605
+ ViewerConfigUpdate::default(),
606
+ )
607
+ .await?;
608
+
609
+ crate::tasks::commit_and_render_locked(
610
+ guard, &session, &renderer, prepared,
618
611
  )
619
612
  .await?;
620
613
 
@@ -626,14 +619,11 @@ impl PerspectiveViewerElement {
626
619
  // INERT: register the client only — never rebind or
627
620
  // reset the active panel (its table is preserved).
628
621
  // Panels bind their client lazily at table-resolution
629
- // time (`Workspace::resolve_client_for_table`). The
630
- // window is discarded (not replayed): a `Client`
631
- // performs no reset, and any racing `restore`'s
632
- // commits already applied live (`commit_view_config`).
633
- let owned_window = session.take_pending_load(generation).is_some();
622
+ // time (`Workspace::resolve_client_for_table`).
623
+ let owned_window = !load.is_superseded();
634
624
  let discard = if owned_window && notify.is_some() {
635
625
  match workspace.take_reserved() {
636
- Some(panel) => Some((panel, None)),
626
+ Some(panel) => Some(Discard::Unclaimed(panel)),
637
627
  // The one-shot claim read: a table-less
638
628
  // `restore` claimed the reservation, and no
639
629
  // table has bound nor is pending — evict the
@@ -655,9 +645,7 @@ impl PerspectiveViewerElement {
655
645
  notify.emit(());
656
646
  }
657
647
 
658
- evicted.map(|panel| {
659
- (panel, Some(ApiError::new(CREATE_REQUIRES_TABLE)))
660
- })
648
+ evicted.map(Discard::Evicted)
661
649
  },
662
650
  None => None,
663
651
  }
@@ -668,33 +656,36 @@ impl PerspectiveViewerElement {
668
656
  workspace.set_default_client(client.get_client().clone());
669
657
  Ok(discard)
670
658
  } else {
671
- session.take_pending_load(generation);
672
659
  Err(ApiError::new("Invalid argument"))
673
660
  }
674
661
  })
675
662
  .await
676
- };
663
+ };
677
664
 
678
- match result {
679
- Err(e) => {
680
- session.take_pending_load(generation);
681
- if let Some(notify) = &notify {
682
- place_reserved(&workspace, notify, true);
683
- }
665
+ match result {
666
+ Err(e) => {
667
+ if let Some(notify) = &notify {
668
+ place_reserved(&workspace, notify, true);
669
+ }
684
670
 
685
- session.set_error(false, e.clone()).await?;
686
- Err(e)
687
- },
688
- Ok(Some((panel, error))) => {
689
- eject_panel(panel).await?;
690
- match error {
691
- Some(e) => Err(e),
692
- None => Ok(()),
671
+ session.set_error(false, e.clone()).await?;
672
+ Err(e)
673
+ },
674
+ Ok(Some(Discard::Unclaimed(panel))) => {
675
+ eject_panel(panel, Disposal::Reject).await?;
676
+ Ok(None)
677
+ },
678
+ Ok(Some(Discard::Evicted(panel))) => {
679
+ eject_panel(panel, Disposal::Resolve).await?;
680
+ Err(ApiError::new(CREATE_REQUIRES_TABLE))
681
+ },
682
+ Ok(None) => Ok(None),
693
683
  }
694
- },
695
- Ok(None) => Ok(()),
684
+ })
696
685
  }
697
- }))
686
+ });
687
+
688
+ Ok(ApiFuture::new_throttled(ticket.settle()))
698
689
  }
699
690
 
700
691
  /// Delete all internal [`View`]s and all associated state, rendering this
@@ -1002,6 +993,7 @@ impl PerspectiveViewerElement {
1002
993
 
1003
994
  let mut fulfilled = false;
1004
995
  for panel in &panels {
996
+ panel.session.settle_ops().await;
1005
997
  panel.renderer.clone().with_lock(async { Ok(()) }).await?;
1006
998
  panel.renderer.clone().with_lock(async { Ok(()) }).await?;
1007
999
  panel.session.settle_dispatches().await?;
@@ -1009,6 +1001,7 @@ impl PerspectiveViewerElement {
1009
1001
  && presentation.is_visible()
1010
1002
  && !panel.renderer.is_plugin_activated()?
1011
1003
  && panel.session.get_error().is_none()
1004
+ && panel.renderer.failure().is_none()
1012
1005
  && matches!(panel.session.has_table(), Some(TableLoadState::Loaded))
1013
1006
  {
1014
1007
  set_panel_paused(&panel.session, &panel.renderer, &presentation, true)
@@ -1060,11 +1053,9 @@ impl PerspectiveViewerElement {
1060
1053
  /// either "json", "string" or "arraybuffer" format.
1061
1054
  /// - `options.panel` - The panel to target, or the active panel when
1062
1055
  /// omitted.
1063
- /// - `options.suppress_errors` - when `true`, a failed restore only rejects
1064
- /// the returned `Promise` and rolls the view config back, leaving the
1065
- /// viewer's visible error state and any element-level state the call
1066
- /// already applied (theme, title, plugin, a successful `table` change)
1067
- /// untouched.
1056
+ /// - `options.suppress_errors` - when `true`, a config that was applied and
1057
+ /// then failed to render only rejects the returned `Promise`, without
1058
+ /// raising the panel's visible error state.
1068
1059
  /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1069
1060
  /// hosts yet leaves the panel empty and pending until the table is
1070
1061
  /// created, instead of the default error.
@@ -1116,6 +1107,7 @@ impl PerspectiveViewerElement {
1116
1107
  Create(Box<ViewerConfigInitial>),
1117
1108
  }
1118
1109
 
1110
+ crate::tasks::resolve_table_ref(&this.workspace, &mut update, missing).await?;
1119
1111
  let notify = this.layout_changed_notify();
1120
1112
  let target = match this.workspace.panel_or_active(id.as_ref()) {
1121
1113
  // An existing (or the active) panel — update it in place.
@@ -1138,14 +1130,24 @@ impl PerspectiveViewerElement {
1138
1130
  },
1139
1131
  };
1140
1132
 
1141
- if !matches!(settings, OptionalUpdate::Missing) {
1142
- let (sender, receiver) = channel::<ApiResult<JsValue>>();
1143
- this.root.borrow().as_ref().into_apierror()?.send_message(
1144
- PerspectiveViewerMsg::ToggleSettingsInit(Some(settings), false, Some(sender)),
1145
- );
1133
+ let settings_toggle: Option<futures::future::LocalBoxFuture<'static, ApiResult<()>>> =
1134
+ (!matches!(settings, OptionalUpdate::Missing)).then(|| {
1135
+ let this = this.clone();
1136
+ Box::pin(async move {
1137
+ let (sender, receiver) = channel::<ApiResult<JsValue>>();
1138
+ this.root.borrow().as_ref().into_apierror()?.send_message(
1139
+ PerspectiveViewerMsg::ToggleSettingsInit(
1140
+ Some(settings),
1141
+ false,
1142
+ Some(sender),
1143
+ ),
1144
+ );
1146
1145
 
1147
- receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
1148
- }
1146
+ receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
1147
+ Ok(())
1148
+ })
1149
+ as futures::future::LocalBoxFuture<'static, ApiResult<()>>
1150
+ });
1149
1151
 
1150
1152
  match target {
1151
1153
  Target::Existing { panel, active } => {
@@ -1158,6 +1160,7 @@ impl PerspectiveViewerElement {
1158
1160
  update,
1159
1161
  errors,
1160
1162
  missing,
1163
+ settings_toggle,
1161
1164
  )
1162
1165
  .await
1163
1166
  },
@@ -1171,10 +1174,15 @@ impl PerspectiveViewerElement {
1171
1174
  update,
1172
1175
  errors,
1173
1176
  missing,
1177
+ settings_toggle,
1174
1178
  )
1175
1179
  .await
1176
1180
  },
1177
1181
  Target::Create(config) => {
1182
+ if let Some(settings_toggle) = settings_toggle {
1183
+ settings_toggle.await?;
1184
+ }
1185
+
1178
1186
  create_panel(
1179
1187
  &this.elem,
1180
1188
  &this.presentation,
@@ -1230,7 +1238,22 @@ impl PerspectiveViewerElement {
1230
1238
  let _effect = effect;
1231
1239
  let RestoreWorkspaceOptions { wait_for_table } = parse_options(options)?;
1232
1240
  let missing = MissingTable::from_wait(wait_for_table);
1233
- let (contents, eject_tasks) = sync_update_panels(&this, update)?;
1241
+ let (contents, eject_tasks, derived_tables) = sync_update_panels(&this, update)?;
1242
+
1243
+ // The outgoing panels' ejects must settle BEFORE the derived
1244
+ // pass: a stale derivation is undeletable (and so cannot be
1245
+ // redefined or pruned) while an ejecting panel still holds a
1246
+ // `View` on it. Their errors are surfaced LAST, after the
1247
+ // replacement set is in place.
1248
+ let ejected = join_all(eject_tasks)
1249
+ .await
1250
+ .into_iter()
1251
+ .collect::<ApiResult<Vec<_>>>();
1252
+
1253
+ if let Some(derived_tables) = derived_tables.clone() {
1254
+ crate::tasks::apply_derived_tables(&this.workspace, derived_tables).await?;
1255
+ }
1256
+
1234
1257
  let fresh = contents
1235
1258
  .iter()
1236
1259
  .map(|(id, ..)| id.clone())
@@ -1238,9 +1261,9 @@ impl PerspectiveViewerElement {
1238
1261
  let results = join_all(contents.into_iter().map(|(id, session, renderer, config)| {
1239
1262
  let presentation = this.presentation.clone();
1240
1263
  let workspace = this.workspace.clone();
1264
+ let notify = this.layout_changed_notify();
1241
1265
  async move {
1242
- stamp_global_overlay(&workspace, &id, &session);
1243
- restore_panel(
1266
+ let result = restore_panel(
1244
1267
  &session,
1245
1268
  &renderer,
1246
1269
  &presentation,
@@ -1249,8 +1272,15 @@ impl PerspectiveViewerElement {
1249
1272
  config,
1250
1273
  crate::tasks::RestoreErrors::Publish,
1251
1274
  missing,
1275
+ None,
1252
1276
  )
1253
- .await?;
1277
+ .await;
1278
+
1279
+ if discard_rejected(&workspace, &id, &session, &result).await? {
1280
+ notify.emit(());
1281
+ }
1282
+
1283
+ result?;
1254
1284
  if workspace.is_master(&id) {
1255
1285
  set_edit_mode(&session, &renderer, "SELECT_ROW_TREE");
1256
1286
  }
@@ -1262,11 +1292,12 @@ impl PerspectiveViewerElement {
1262
1292
 
1263
1293
  results.into_iter().collect::<ApiResult<Vec<_>>>()?;
1264
1294
  this.rerender_retained_overlays(&fresh).await?;
1265
- join_all(eject_tasks)
1266
- .await
1267
- .into_iter()
1268
- .collect::<ApiResult<Vec<_>>>()?;
1295
+ if let Some(derived_tables) = derived_tables {
1296
+ let keep = derived_tables.keys().cloned().collect();
1297
+ crate::tasks::prune_derived(&this.workspace, &keep).await?;
1298
+ }
1269
1299
 
1300
+ ejected?;
1270
1301
  Ok(())
1271
1302
  });
1272
1303
 
@@ -1282,10 +1313,19 @@ impl PerspectiveViewerElement {
1282
1313
  };
1283
1314
 
1284
1315
  let reset_effect = self.workspace.effects().guard();
1285
- let reset_task = panel.session.reset(ResetOptions::default());
1316
+ let reset = panel.session.submit(OpKind::Restore { update: None }, {
1317
+ let session = panel.session.clone();
1318
+ move |_ctx| {
1319
+ Box::pin(async move {
1320
+ session.reset(ResetOptions::default()).await?;
1321
+ Ok(None)
1322
+ })
1323
+ }
1324
+ });
1325
+
1286
1326
  ApiFuture::spawn(async move {
1287
1327
  let _effect = reset_effect;
1288
- reset_task.await
1328
+ reset.settle().await
1289
1329
  });
1290
1330
 
1291
1331
  let effect = self.workspace.effects().guard();
@@ -1338,7 +1378,9 @@ impl PerspectiveViewerElement {
1338
1378
  })
1339
1379
  .await?;
1340
1380
 
1341
- viewer_config.encode()
1381
+ viewer_config
1382
+ .map_table(|name| this.workspace.inline_table_ref(&name))
1383
+ .encode()
1342
1384
  });
1343
1385
 
1344
1386
  js_sys::Promise::from(fut).unchecked_into()
@@ -1402,9 +1444,14 @@ impl PerspectiveViewerElement {
1402
1444
  };
1403
1445
 
1404
1446
  let panel = this.resolve_panel(name)?;
1405
- let blob =
1406
- export_method_to_blob(&panel.session, &panel.renderer, &this.presentation, method)
1407
- .await?;
1447
+ let blob = export_method_to_blob(
1448
+ &panel.session,
1449
+ &panel.renderer,
1450
+ &this.presentation,
1451
+ &this.workspace,
1452
+ method,
1453
+ )
1454
+ .await?;
1408
1455
  let is_chart = panel.renderer.is_chart();
1409
1456
  download(
1410
1457
  format!("untitled{}", method.as_filename(is_chart)).as_ref(),
@@ -1450,8 +1497,14 @@ impl PerspectiveViewerElement {
1450
1497
  };
1451
1498
 
1452
1499
  let panel = this.resolve_panel(name)?;
1453
- export_method_to_jsvalue(&panel.session, &panel.renderer, &this.presentation, method)
1454
- .await
1500
+ export_method_to_jsvalue(
1501
+ &panel.session,
1502
+ &panel.renderer,
1503
+ &this.presentation,
1504
+ &this.workspace,
1505
+ method,
1506
+ )
1507
+ .await
1455
1508
  })
1456
1509
  }
1457
1510
 
@@ -1489,8 +1542,13 @@ impl PerspectiveViewerElement {
1489
1542
  };
1490
1543
 
1491
1544
  let panel = this.resolve_panel(name)?;
1492
- let js_task =
1493
- export_method_to_blob(&panel.session, &panel.renderer, &this.presentation, method);
1545
+ let js_task = export_method_to_blob(
1546
+ &panel.session,
1547
+ &panel.renderer,
1548
+ &this.presentation,
1549
+ &this.workspace,
1550
+ method,
1551
+ );
1494
1552
  copy_to_clipboard(js_task, MimeType::TextPlain).await
1495
1553
  })
1496
1554
  }
@@ -1506,7 +1564,9 @@ impl PerspectiveViewerElement {
1506
1564
  /// # Arguments
1507
1565
  ///
1508
1566
  /// - `reset_all` - If set, will clear expressions and column settings as
1509
- /// well.
1567
+ /// well, and tear down the element's ENTIRE derived-table state: every
1568
+ /// panel bound to a derived `Table` is released to its root table, and
1569
+ /// every derivation (pending ones included) is deleted.
1510
1570
  /// - `options` - An optional `{panel?: string}`; the panel to reset, or
1511
1571
  /// every panel when omitted.
1512
1572
  ///
@@ -1830,7 +1890,7 @@ impl PerspectiveViewerElement {
1830
1890
  // alone must repaint nothing.
1831
1891
  let stale = theme.as_ref().is_none_or(|x| !available.contains(x));
1832
1892
  if (stale || theme == previous) && theme != active {
1833
- panel.renderer.set_theme(active.clone());
1893
+ submit_theme(&panel.session, &panel.renderer, active.clone());
1834
1894
  if panel.renderer.needs_restyle() {
1835
1895
  panel.renderer.restyle_all().await?;
1836
1896
  }