@perspective-dev/viewer 5.5.1 → 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 (194) 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/PanelViewerConfig.d.ts +7 -6
  43. package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
  44. package/dist/esm/ts-rs/TableRef.d.ts +9 -0
  45. package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
  46. package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
  47. package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
  48. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
  49. package/dist/wasm/perspective-viewer.d.ts +115 -35
  50. package/dist/wasm/perspective-viewer.js +314 -190
  51. package/dist/wasm/perspective-viewer.wasm +0 -0
  52. package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
  53. package/package.json +31 -18
  54. package/src/css/column-selector.css +205 -50
  55. package/src/css/ui/containers/context-menu.css +12 -1
  56. package/src/css/ui/containers/dropdown-menu-items.css +29 -0
  57. package/src/css/ui/containers/scroll-panel.css +4 -1
  58. package/src/rust/agent/client/protocol.rs +0 -89
  59. package/src/rust/agent/markdown/mod.rs +0 -238
  60. package/src/rust/agent/preamble.md +1 -0
  61. package/src/rust/agent/tools.rs +2 -16
  62. package/src/rust/components/column_selector/active_column.rs +15 -5
  63. package/src/rust/components/column_selector/add_expression_button.rs +9 -26
  64. package/src/rust/components/column_selector/config_selector.rs +34 -6
  65. package/src/rust/components/column_selector/derived_control.rs +229 -0
  66. package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
  67. package/src/rust/components/column_selector/filter_column.rs +0 -2
  68. package/src/rust/components/column_selector/inactive_column.rs +4 -5
  69. package/src/rust/components/column_selector/pivot_column.rs +21 -1
  70. package/src/rust/components/column_selector/sort_column.rs +4 -3
  71. package/src/rust/components/column_selector.rs +21 -1
  72. package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
  73. package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
  74. package/src/rust/components/column_settings_sidebar.rs +25 -19
  75. package/src/rust/components/debug_panel.rs +38 -29
  76. package/src/rust/components/dragdrop_list.rs +8 -9
  77. package/src/rust/components/main_panel/msg.rs +6 -4
  78. package/src/rust/components/main_panel/reconcile.rs +6 -1
  79. package/src/rust/components/main_panel/render.rs +70 -51
  80. package/src/rust/components/main_panel/update.rs +39 -1
  81. package/src/rust/components/main_panel.rs +97 -59
  82. package/src/rust/components/new_panel_menu.rs +69 -1
  83. package/src/rust/components/panel_menu.rs +63 -12
  84. package/src/rust/components/plugin_tab.rs +3 -2
  85. package/src/rust/components/render_warning.rs +2 -1
  86. package/src/rust/components/settings_panel.rs +4 -4
  87. package/src/rust/components/status_bar.rs +74 -24
  88. package/src/rust/components/status_indicator.rs +8 -1
  89. package/src/rust/components/viewer/filters.rs +13 -1
  90. package/src/rust/components/viewer/msg.rs +4 -0
  91. package/src/rust/components/viewer/panels.rs +41 -1
  92. package/src/rust/components/viewer/render.rs +1 -0
  93. package/src/rust/components/viewer/settings.rs +0 -22
  94. package/src/rust/components/viewer/snapshots.rs +2 -2
  95. package/src/rust/components/viewer/wiring.rs +14 -0
  96. package/src/rust/components/viewer.rs +45 -25
  97. package/src/rust/components/window_editor.rs +5 -4
  98. package/src/rust/config/column_config_schema.rs +38 -377
  99. package/src/rust/config/number_string_format.rs +0 -94
  100. package/src/rust/config/options.rs +3 -7
  101. package/src/rust/config/plugin_static_config.rs +0 -42
  102. package/src/rust/config/viewer_config.rs +47 -17
  103. package/src/rust/config/workspace_config.rs +158 -128
  104. package/src/rust/custom_elements/viewer.rs +187 -131
  105. package/src/rust/custom_events.rs +33 -18
  106. package/src/rust/derived.rs +266 -0
  107. package/src/rust/exprtk/tokenize.rs +0 -54
  108. package/src/rust/js/plugin.rs +1 -1
  109. package/src/rust/lib.rs +3 -0
  110. package/src/rust/presentation.rs +23 -4
  111. package/src/rust/queries/column_locator.rs +55 -48
  112. package/src/rust/queries/get_viewer_config.rs +1 -1
  113. package/src/rust/queries/plugin_column_styles.rs +22 -23
  114. package/src/rust/renderer/dispatch.rs +2 -0
  115. package/src/rust/renderer/plugin_config.rs +652 -487
  116. package/src/rust/renderer/props.rs +4 -0
  117. package/src/rust/renderer/registry.rs +2 -6
  118. package/src/rust/renderer/state.rs +205 -0
  119. package/src/rust/renderer.rs +201 -59
  120. package/src/rust/session/metadata.rs +30 -9
  121. package/src/rust/session/op_queue.rs +430 -0
  122. package/src/rust/session/panel_state.rs +327 -0
  123. package/src/rust/session/view_subscription.rs +42 -3
  124. package/src/rust/session.rs +900 -395
  125. package/src/rust/tasks/apply_global_filters.rs +49 -35
  126. package/src/rust/tasks/auto_pause.rs +22 -12
  127. package/src/rust/tasks/copy_export.rs +5 -0
  128. package/src/rust/tasks/create_panel.rs +34 -9
  129. package/src/rust/tasks/derive.rs +312 -0
  130. package/src/rust/tasks/mod.rs +6 -2
  131. package/src/rust/tasks/pipeline.rs +145 -89
  132. package/src/rust/tasks/reset_all.rs +31 -20
  133. package/src/rust/tasks/restore_derived.rs +200 -0
  134. package/src/rust/tasks/restore_panel.rs +153 -141
  135. package/src/rust/tasks/send_column_config.rs +56 -29
  136. package/src/rust/tasks/send_plugin_config.rs +52 -23
  137. package/src/rust/tasks/set_edit_mode.rs +33 -25
  138. package/src/rust/tasks/sync_update_panels.rs +39 -12
  139. package/src/rust/tasks/table_lifecycle.rs +70 -34
  140. package/src/rust/tasks/transactional_restore.rs +387 -0
  141. package/src/rust/tasks/update_theme.rs +45 -6
  142. package/src/rust/ui/containers/scroll_panel.rs +17 -10
  143. package/src/rust/ui/containers/split_panel.rs +0 -41
  144. package/src/rust/ui/context_menu.rs +157 -18
  145. package/src/rust/ui/font_loader.rs +23 -35
  146. package/src/rust/ui/form/mirrored_textarea.rs +0 -12
  147. package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
  148. package/src/rust/ui/form/palette_selector.rs +0 -51
  149. package/src/rust/ui/portal.rs +4 -10
  150. package/src/rust/utils/browser/mod.rs +2 -3
  151. package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
  152. package/src/rust/utils/browser/request_animation_frame.rs +27 -8
  153. package/src/rust/utils/css.rs +0 -360
  154. package/src/rust/utils/local_fonts.rs +0 -36
  155. package/src/rust/utils/mod.rs +2 -3
  156. package/src/rust/utils/modal_position.rs +15 -21
  157. package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
  158. package/src/rust/utils/pubsub.rs +0 -1
  159. package/src/rust/workspace.rs +187 -140
  160. package/src/svg/derived.svg +6 -0
  161. package/src/themes/icons.css +1 -0
  162. package/src/themes/intl/ar.css +10 -0
  163. package/src/themes/intl/de.css +10 -0
  164. package/src/themes/intl/es.css +10 -0
  165. package/src/themes/intl/fr.css +10 -0
  166. package/src/themes/intl/ja.css +10 -0
  167. package/src/themes/intl/pt.css +10 -0
  168. package/src/themes/intl/zh.css +10 -0
  169. package/src/themes/intl.css +10 -0
  170. package/src/ts/extensions.ts +3 -36
  171. package/src/ts/perspective-viewer.ts +1 -1
  172. package/src/ts/plugin.ts +4 -1
  173. package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
  174. package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
  175. package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
  176. package/src/ts/ts-rs/RestoreOptions.ts +3 -7
  177. package/src/ts/ts-rs/TableRef.ts +9 -0
  178. package/src/ts/ts-rs/ViewerConfig.ts +7 -6
  179. package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
  180. package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
  181. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
  182. package/dist/esm/perspective-viewer.inline.d.ts +0 -3
  183. package/dist/esm/perspective-viewer.inline.js +0 -21
  184. package/dist/esm/perspective-viewer.inline.js.map +0 -7
  185. package/src/rust/tasks/restore_and_render.rs +0 -107
  186. package/src/rust/utils/browser/tests/debounce.rs +0 -163
  187. package/src/rust/utils/tests/mod.rs +0 -14
  188. package/src/rust/utils/tests/pubsub.rs +0 -95
  189. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
  190. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
  191. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
  192. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
  193. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
  194. /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → 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::{Disposal, 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,23 +525,28 @@ impl PerspectiveViewerElement {
548
525
 
549
526
  let session = panel.session;
550
527
  let renderer = panel.renderer;
551
- let load = session.begin_pending_load();
552
528
  clone!(self.workspace, self.presentation);
553
- Ok(ApiFuture::new_throttled(async move {
554
- let _effect = effect;
555
- enum Discard {
556
- Unclaimed(Panel),
557
- Evicted(Panel),
558
- }
529
+ enum Discard {
530
+ Unclaimed(Panel),
531
+ Evicted(Panel),
532
+ }
559
533
 
560
- renderer.set_throttle(None);
561
- let _run_token = session.begin_config_run();
562
- let result = {
563
- clone!(session, renderer, workspace, notify, load);
564
- renderer
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
565
548
  .clone()
566
549
  .render_task(|guard| async move {
567
- seed_panel_theme(&presentation, &renderer).await;
568
550
  renderer.stamp_theme(None);
569
551
  let jstable = JsFuture::from(promise)
570
552
  .await
@@ -574,30 +556,17 @@ impl PerspectiveViewerElement {
574
556
  try_from_js_option::<perspective_js::Table>(jstable.clone())
575
557
  {
576
558
  tracing::warn!("{}", DEPRECATED_TABLE_MESSAGE);
577
- let Some(journal) = load.claim() else {
559
+ if load.is_superseded() {
578
560
  return Ok(None);
579
- };
561
+ }
580
562
 
581
563
  if let Some(notify) = &notify {
582
564
  place_reserved(&workspace, notify, true);
583
565
  }
584
566
 
585
- let _plugin = renderer.ensure_plugin_selected()?;
586
- let _ = renderer.mount_active_plugin();
587
- session
588
- .reset(ResetOptions {
589
- config: true,
590
- expressions: true,
591
- stats: true,
592
- table: Some(session::TableIntermediateState::Reloaded),
593
- })
594
- .await
595
- .unwrap_or_log();
596
-
597
567
  let client = table.get_client().await;
598
568
  let inner_client = client.get_client().clone();
599
- session.set_client(inner_client.clone());
600
- workspace.set_default_client(inner_client);
569
+ workspace.set_default_client(inner_client.clone());
601
570
  let name = table.get_name().await;
602
571
  tracing::debug!(
603
572
  "Loading {:.0} rows from `Table` {}",
@@ -605,21 +574,40 @@ impl PerspectiveViewerElement {
605
574
  name
606
575
  );
607
576
 
608
- session.set_table(name).await?;
609
- for delta in journal {
610
- session.commit_view_config(delta)?;
611
- }
612
-
613
- session.commit_table_defaults();
614
- let (disposition, _pin) =
615
- 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
+ };
616
595
 
617
- crate::tasks::dispatch_bound(
618
- &guard,
596
+ let prepared = crate::tasks::prepare(
597
+ &session,
619
598
  &renderer,
620
- disposition,
599
+ &presentation,
621
600
  false,
622
- 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,
623
611
  )
624
612
  .await?;
625
613
 
@@ -631,11 +619,8 @@ impl PerspectiveViewerElement {
631
619
  // INERT: register the client only — never rebind or
632
620
  // reset the active panel (its table is preserved).
633
621
  // Panels bind their client lazily at table-resolution
634
- // time (`Workspace::resolve_client_for_table`). The
635
- // window is discarded (not replayed): a `Client`
636
- // performs no reset, and any racing `restore`'s
637
- // commits already applied live (`commit_view_config`).
638
- let owned_window = load.claim().is_some();
622
+ // time (`Workspace::resolve_client_for_table`).
623
+ let owned_window = !load.is_superseded();
639
624
  let discard = if owned_window && notify.is_some() {
640
625
  match workspace.take_reserved() {
641
626
  Some(panel) => Some(Discard::Unclaimed(panel)),
@@ -671,34 +656,36 @@ impl PerspectiveViewerElement {
671
656
  workspace.set_default_client(client.get_client().clone());
672
657
  Ok(discard)
673
658
  } else {
674
- load.close();
675
659
  Err(ApiError::new("Invalid argument"))
676
660
  }
677
661
  })
678
662
  .await
679
- };
663
+ };
680
664
 
681
- match result {
682
- Err(e) => {
683
- load.close();
684
- if let Some(notify) = &notify {
685
- place_reserved(&workspace, notify, true);
686
- }
665
+ match result {
666
+ Err(e) => {
667
+ if let Some(notify) = &notify {
668
+ place_reserved(&workspace, notify, true);
669
+ }
687
670
 
688
- session.set_error(false, e.clone()).await?;
689
- Err(e)
690
- },
691
- Ok(Some(Discard::Unclaimed(panel))) => {
692
- eject_panel(panel, Disposal::Reject).await?;
693
- Ok(())
694
- },
695
- Ok(Some(Discard::Evicted(panel))) => {
696
- eject_panel(panel, Disposal::Resolve).await?;
697
- Err(ApiError::new(CREATE_REQUIRES_TABLE))
698
- },
699
- Ok(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),
683
+ }
684
+ })
700
685
  }
701
- }))
686
+ });
687
+
688
+ Ok(ApiFuture::new_throttled(ticket.settle()))
702
689
  }
703
690
 
704
691
  /// Delete all internal [`View`]s and all associated state, rendering this
@@ -1006,6 +993,7 @@ impl PerspectiveViewerElement {
1006
993
 
1007
994
  let mut fulfilled = false;
1008
995
  for panel in &panels {
996
+ panel.session.settle_ops().await;
1009
997
  panel.renderer.clone().with_lock(async { Ok(()) }).await?;
1010
998
  panel.renderer.clone().with_lock(async { Ok(()) }).await?;
1011
999
  panel.session.settle_dispatches().await?;
@@ -1013,6 +1001,7 @@ impl PerspectiveViewerElement {
1013
1001
  && presentation.is_visible()
1014
1002
  && !panel.renderer.is_plugin_activated()?
1015
1003
  && panel.session.get_error().is_none()
1004
+ && panel.renderer.failure().is_none()
1016
1005
  && matches!(panel.session.has_table(), Some(TableLoadState::Loaded))
1017
1006
  {
1018
1007
  set_panel_paused(&panel.session, &panel.renderer, &presentation, true)
@@ -1064,11 +1053,9 @@ impl PerspectiveViewerElement {
1064
1053
  /// either "json", "string" or "arraybuffer" format.
1065
1054
  /// - `options.panel` - The panel to target, or the active panel when
1066
1055
  /// omitted.
1067
- /// - `options.suppress_errors` - when `true`, a failed restore only rejects
1068
- /// the returned `Promise` and rolls the view config back, leaving the
1069
- /// viewer's visible error state and any element-level state the call
1070
- /// already applied (theme, title, plugin, a successful `table` change)
1071
- /// 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.
1072
1059
  /// - `options.wait_for_table` - when `true`, a `table` no loaded client
1073
1060
  /// hosts yet leaves the panel empty and pending until the table is
1074
1061
  /// created, instead of the default error.
@@ -1120,6 +1107,7 @@ impl PerspectiveViewerElement {
1120
1107
  Create(Box<ViewerConfigInitial>),
1121
1108
  }
1122
1109
 
1110
+ crate::tasks::resolve_table_ref(&this.workspace, &mut update, missing).await?;
1123
1111
  let notify = this.layout_changed_notify();
1124
1112
  let target = match this.workspace.panel_or_active(id.as_ref()) {
1125
1113
  // An existing (or the active) panel — update it in place.
@@ -1142,14 +1130,24 @@ impl PerspectiveViewerElement {
1142
1130
  },
1143
1131
  };
1144
1132
 
1145
- if !matches!(settings, OptionalUpdate::Missing) {
1146
- let (sender, receiver) = channel::<ApiResult<JsValue>>();
1147
- this.root.borrow().as_ref().into_apierror()?.send_message(
1148
- PerspectiveViewerMsg::ToggleSettingsInit(Some(settings), false, Some(sender)),
1149
- );
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
+ );
1150
1145
 
1151
- receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
1152
- }
1146
+ receiver.await.map_err(|_| ApiError::new("Cancelled"))??;
1147
+ Ok(())
1148
+ })
1149
+ as futures::future::LocalBoxFuture<'static, ApiResult<()>>
1150
+ });
1153
1151
 
1154
1152
  match target {
1155
1153
  Target::Existing { panel, active } => {
@@ -1162,6 +1160,7 @@ impl PerspectiveViewerElement {
1162
1160
  update,
1163
1161
  errors,
1164
1162
  missing,
1163
+ settings_toggle,
1165
1164
  )
1166
1165
  .await
1167
1166
  },
@@ -1175,10 +1174,15 @@ impl PerspectiveViewerElement {
1175
1174
  update,
1176
1175
  errors,
1177
1176
  missing,
1177
+ settings_toggle,
1178
1178
  )
1179
1179
  .await
1180
1180
  },
1181
1181
  Target::Create(config) => {
1182
+ if let Some(settings_toggle) = settings_toggle {
1183
+ settings_toggle.await?;
1184
+ }
1185
+
1182
1186
  create_panel(
1183
1187
  &this.elem,
1184
1188
  &this.presentation,
@@ -1234,7 +1238,22 @@ impl PerspectiveViewerElement {
1234
1238
  let _effect = effect;
1235
1239
  let RestoreWorkspaceOptions { wait_for_table } = parse_options(options)?;
1236
1240
  let missing = MissingTable::from_wait(wait_for_table);
1237
- 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
+
1238
1257
  let fresh = contents
1239
1258
  .iter()
1240
1259
  .map(|(id, ..)| id.clone())
@@ -1242,9 +1261,9 @@ impl PerspectiveViewerElement {
1242
1261
  let results = join_all(contents.into_iter().map(|(id, session, renderer, config)| {
1243
1262
  let presentation = this.presentation.clone();
1244
1263
  let workspace = this.workspace.clone();
1264
+ let notify = this.layout_changed_notify();
1245
1265
  async move {
1246
- stamp_global_overlay(&workspace, &id, &session);
1247
- restore_panel(
1266
+ let result = restore_panel(
1248
1267
  &session,
1249
1268
  &renderer,
1250
1269
  &presentation,
@@ -1253,8 +1272,15 @@ impl PerspectiveViewerElement {
1253
1272
  config,
1254
1273
  crate::tasks::RestoreErrors::Publish,
1255
1274
  missing,
1275
+ None,
1256
1276
  )
1257
- .await?;
1277
+ .await;
1278
+
1279
+ if discard_rejected(&workspace, &id, &session, &result).await? {
1280
+ notify.emit(());
1281
+ }
1282
+
1283
+ result?;
1258
1284
  if workspace.is_master(&id) {
1259
1285
  set_edit_mode(&session, &renderer, "SELECT_ROW_TREE");
1260
1286
  }
@@ -1266,11 +1292,12 @@ impl PerspectiveViewerElement {
1266
1292
 
1267
1293
  results.into_iter().collect::<ApiResult<Vec<_>>>()?;
1268
1294
  this.rerender_retained_overlays(&fresh).await?;
1269
- join_all(eject_tasks)
1270
- .await
1271
- .into_iter()
1272
- .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
+ }
1273
1299
 
1300
+ ejected?;
1274
1301
  Ok(())
1275
1302
  });
1276
1303
 
@@ -1286,10 +1313,19 @@ impl PerspectiveViewerElement {
1286
1313
  };
1287
1314
 
1288
1315
  let reset_effect = self.workspace.effects().guard();
1289
- 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
+
1290
1326
  ApiFuture::spawn(async move {
1291
1327
  let _effect = reset_effect;
1292
- reset_task.await
1328
+ reset.settle().await
1293
1329
  });
1294
1330
 
1295
1331
  let effect = self.workspace.effects().guard();
@@ -1342,7 +1378,9 @@ impl PerspectiveViewerElement {
1342
1378
  })
1343
1379
  .await?;
1344
1380
 
1345
- viewer_config.encode()
1381
+ viewer_config
1382
+ .map_table(|name| this.workspace.inline_table_ref(&name))
1383
+ .encode()
1346
1384
  });
1347
1385
 
1348
1386
  js_sys::Promise::from(fut).unchecked_into()
@@ -1406,9 +1444,14 @@ impl PerspectiveViewerElement {
1406
1444
  };
1407
1445
 
1408
1446
  let panel = this.resolve_panel(name)?;
1409
- let blob =
1410
- export_method_to_blob(&panel.session, &panel.renderer, &this.presentation, method)
1411
- .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?;
1412
1455
  let is_chart = panel.renderer.is_chart();
1413
1456
  download(
1414
1457
  format!("untitled{}", method.as_filename(is_chart)).as_ref(),
@@ -1454,8 +1497,14 @@ impl PerspectiveViewerElement {
1454
1497
  };
1455
1498
 
1456
1499
  let panel = this.resolve_panel(name)?;
1457
- export_method_to_jsvalue(&panel.session, &panel.renderer, &this.presentation, method)
1458
- .await
1500
+ export_method_to_jsvalue(
1501
+ &panel.session,
1502
+ &panel.renderer,
1503
+ &this.presentation,
1504
+ &this.workspace,
1505
+ method,
1506
+ )
1507
+ .await
1459
1508
  })
1460
1509
  }
1461
1510
 
@@ -1493,8 +1542,13 @@ impl PerspectiveViewerElement {
1493
1542
  };
1494
1543
 
1495
1544
  let panel = this.resolve_panel(name)?;
1496
- let js_task =
1497
- 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
+ );
1498
1552
  copy_to_clipboard(js_task, MimeType::TextPlain).await
1499
1553
  })
1500
1554
  }
@@ -1510,7 +1564,9 @@ impl PerspectiveViewerElement {
1510
1564
  /// # Arguments
1511
1565
  ///
1512
1566
  /// - `reset_all` - If set, will clear expressions and column settings as
1513
- /// 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.
1514
1570
  /// - `options` - An optional `{panel?: string}`; the panel to reset, or
1515
1571
  /// every panel when omitted.
1516
1572
  ///
@@ -1834,7 +1890,7 @@ impl PerspectiveViewerElement {
1834
1890
  // alone must repaint nothing.
1835
1891
  let stale = theme.as_ref().is_none_or(|x| !available.contains(x));
1836
1892
  if (stale || theme == previous) && theme != active {
1837
- panel.renderer.set_theme(active.clone());
1893
+ submit_theme(&panel.session, &panel.renderer, active.clone());
1838
1894
  if panel.renderer.needs_restyle() {
1839
1895
  panel.renderer.restyle_all().await?;
1840
1896
  }