@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
@@ -12,17 +12,17 @@
12
12
 
13
13
  use std::rc::Rc;
14
14
 
15
- use futures::future::LocalBoxFuture;
15
+ use futures::FutureExt;
16
16
  use perspective_client::config::ViewConfigUpdate;
17
17
  use perspective_client::{View, clone};
18
18
  use perspective_js::utils::*;
19
19
  use wasm_bindgen::JsValue;
20
20
  use yew::prelude::*;
21
21
 
22
- use crate::config::{ColumnConfigUpdate, PluginConfigUpdate};
22
+ use super::transactional_restore::{commit_edit, prepare_edit};
23
23
  use crate::presentation::Presentation;
24
24
  use crate::renderer::{RenderContext, Renderer};
25
- use crate::session::{BindDisposition, Session};
25
+ use crate::session::{BindDisposition, BindingEffects, Disposal, EditDelta, OpKind, Session};
26
26
  use crate::utils::RenderGuard;
27
27
 
28
28
  /// Snapshot → validate → bind → cache + pin the [`RenderContext`]. The core
@@ -35,15 +35,6 @@ pub async fn bind_snapshot(
35
35
  session: &Session,
36
36
  renderer: &Renderer,
37
37
  ) -> ApiResult<(BindDisposition, Option<crate::renderer::ContextPin>)> {
38
- // A `load()` is mid-classification (see [`Session::pending_load`]): DEFER
39
- // rather than bind the incoming config against the still-bound outgoing
40
- // table. Holds the last painted frame; the `load()` run performs the one
41
- // reconciling bind once its payload resolves — the single visible
42
- // transition, correct in whichever future (Table/Client) materializes.
43
- if session.has_pending_load() {
44
- return Ok((BindDisposition::Deferred, None));
45
- }
46
-
47
38
  let snap = session.snapshot(guard);
48
39
  let validated = session.validate_snapshot(guard, snap).await?;
49
40
  let disposition = session.bind_view(guard, validated).await?;
@@ -132,34 +123,73 @@ pub fn apply_and_render(
132
123
  }
133
124
 
134
125
  /// [`apply_and_render`] plus a plugin selection (resolved via
135
- /// [`Renderer::resolve_plugin_update`]). The plugin swap is committed inside
136
- /// the locked run, atomically with the view rebind — never staged on the
137
- /// `Renderer` where a concurrent draw could observe it.
126
+ /// [`Renderer::resolve_plugin_update`]), committed in the SAME swap as the
127
+ /// view config — in the op's step, never by the detached render.
138
128
  pub fn update_plugin_and_render(
139
129
  session: &Session,
140
130
  renderer: &Renderer,
141
131
  update: ViewConfigUpdate,
142
132
  plugin_idx: Option<usize>,
143
133
  ) -> ApiResult<ApiFuture<()>> {
144
- session.commit_view_config(update)?;
145
- // Spinner accounting (RAII): created only when the commit succeeded,
146
- // moved into the run future, settled on every exit path by `Drop`.
147
- let run_token = session.begin_config_run();
148
- clone!(session, renderer);
149
- Ok(ApiFuture::new(async move {
150
- let _run_token = run_token;
151
- render_run(session, renderer, plugin_idx).await
152
- }))
134
+ session.check_edit(&update)?;
135
+ let kind = OpKind::Edit {
136
+ delta: EditDelta::View(Box::new(update.clone())),
137
+ swaps_plugin: plugin_idx.is_some(),
138
+ };
139
+
140
+ let ticket = session.submit(kind, {
141
+ clone!(session, renderer);
142
+ move |_ctx| {
143
+ Box::pin(async move {
144
+ let prepared = prepare_edit(&session, &renderer, update, plugin_idx).await?;
145
+ let committed = commit_edit(&session, &renderer, prepared);
146
+ Ok(Some(
147
+ render_run(session, renderer, RunCommit::Done(committed)).boxed_local(),
148
+ ))
149
+ })
150
+ }
151
+ });
152
+
153
+ Ok(ApiFuture::new(ticket.settle()))
153
154
  }
154
155
 
155
156
  /// Re-render from the current commit without applying an update.
156
157
  pub fn just_render(session: &Session, renderer: &Renderer) -> ApiResult<ApiFuture<()>> {
157
158
  clone!(session, renderer);
158
159
  Ok(ApiFuture::new(async move {
159
- render_run(session, renderer, None).await
160
+ render_run(session, renderer, RunCommit::None).await
160
161
  }))
161
162
  }
162
163
 
164
+ /// What a transactional restore committed (see
165
+ /// [`super::transactional_restore`]): the run has to bring the plugin element
166
+ /// and the paint in line with it.
167
+ pub(crate) struct Committed {
168
+ /// A plugin selection was committed; its element must be activated.
169
+ pub activate: bool,
170
+
171
+ /// ...and it REPLACED an earlier selection (a first selection is not a
172
+ /// swap, and does not force a `plugin.restore`).
173
+ pub plugin_swapped: bool,
174
+ pub plugin_config_changed: bool,
175
+ pub columns_config_changed: bool,
176
+ }
177
+
178
+ /// The commit a locked run renders.
179
+ pub(crate) enum RunCommit {
180
+ /// Whatever is committed (a repaint, an activation, a resize).
181
+ None,
182
+
183
+ /// A UI edit its op's step ALREADY committed; the plugin receives the final
184
+ /// buckets in ONE `plugin.restore`.
185
+ Done(Committed),
186
+
187
+ /// A restore's commit, run first thing under the lock — a restore may
188
+ /// REBIND, and under the lock no render of the outgoing state can bind a
189
+ /// `View` over the incoming table.
190
+ Deferred(Box<dyn FnOnce() -> (Committed, BindingEffects)>),
191
+ }
192
+
163
193
  /// Everything that varies between locked render runs, consumed by
164
194
  /// [`locked_run`] — the ONE lock-body composition. `Default` is the
165
195
  /// host-internal UI-commit run: `Internal` origin, no plugin swap, no
@@ -168,21 +198,8 @@ pub(crate) struct RunSpec {
168
198
  /// Who initiated this run (see [`RunOrigin`]).
169
199
  pub origin: RunOrigin,
170
200
 
171
- /// A plugin swap resolved by [`Renderer::resolve_plugin_update`],
172
- /// committed inside the run atomically with the rebind.
173
- pub plugin_idx: Option<usize>,
174
-
175
- /// Plugin-level bucket update, applied inside the run after the bind
176
- /// (so strip-on-write sees fresh schemas).
177
- pub plugin_config: PluginConfigUpdate,
178
-
179
- /// Per-column bucket update; same timing as `plugin_config`.
180
- pub columns_config: ColumnConfigUpdate,
181
-
182
- /// Pre-bind hook awaited inside the lock (table binding, resets) —
183
- /// BEFORE the error guard, so a task that recovers the session (e.g.
184
- /// `restorePanel`'s errored-recovery reset) unblocks its own run.
185
- pub task: Option<LocalBoxFuture<'static, ApiResult<()>>>,
201
+ /// What this run renders — see [`RunCommit`].
202
+ pub commit: RunCommit,
186
203
 
187
204
  /// When set, plugin dispatch is skipped while the host element is not
188
205
  /// visible (`Presentation::is_visible`) — the `restore` family's gate
@@ -196,10 +213,7 @@ impl Default for RunSpec {
196
213
  fn default() -> Self {
197
214
  Self {
198
215
  origin: RunOrigin::Internal,
199
- plugin_idx: None,
200
- plugin_config: PluginConfigUpdate::Missing,
201
- columns_config: ColumnConfigUpdate::Missing,
202
- task: None,
216
+ commit: RunCommit::None,
203
217
  presentation: None,
204
218
  }
205
219
  }
@@ -208,7 +222,7 @@ impl Default for RunSpec {
208
222
  /// One locked, witnessed, snapshot-consuming render run — the SINGLE lock
209
223
  /// body shared by every config-driven run (`apply_and_render` &co. via
210
224
  /// [`render_run`]'s tail, the `restore` family via
211
- /// [`super::restore_and_render`]): eager mount → pre-bind `task` →
225
+ /// [`super::transactional_restore`]): the restore's commit → eager mount →
212
226
  /// plugin-swap commit → theme stamp → error guard → [`bind_snapshot`]
213
227
  /// (gated on a bound `Table`, else `Deferred` — the config is already
214
228
  /// committed, so the eventual `load()` run binds from it) → bucket updates
@@ -216,7 +230,7 @@ impl Default for RunSpec {
216
230
  ///
217
231
  /// A pre-existing session error skips the run for `Internal` origins and
218
232
  /// fails it for `Public` ones (the caller asked and must hear the failure —
219
- /// I6). Returns the RAW run result; error-reporting policy (`set_run_error`
233
+ /// I6). Returns the RAW run result; error-reporting policy (`Renderer::fail`
220
234
  /// vs. propagate) belongs to the caller.
221
235
  pub(crate) async fn locked_run(
222
236
  session: &Session,
@@ -226,16 +240,68 @@ pub(crate) async fn locked_run(
226
240
  clone!(session, renderer);
227
241
  renderer
228
242
  .clone()
229
- .render_task(|guard| async move {
230
- renderer.mount_active_plugin()?;
231
- if let Some(task) = spec.task {
232
- task.await?;
243
+ .render_task(|guard| run_locked(guard, session, renderer, spec))
244
+ .await
245
+ }
246
+
247
+ /// [`locked_run`]'s body, for a caller that already holds the draw lock.
248
+ pub(crate) async fn run_locked(
249
+ guard: RenderGuard,
250
+ session: Session,
251
+ renderer: Renderer,
252
+ spec: RunSpec,
253
+ ) -> ApiResult<()> {
254
+ {
255
+ {
256
+ if let Some(disposal) = session.disposal() {
257
+ return match (disposal, spec.origin) {
258
+ (Disposal::Reject, RunOrigin::Public) => Err(ApiError::new("Panel disposed")),
259
+ _ => Ok(()),
260
+ };
261
+ }
262
+
263
+ let committed = match spec.commit {
264
+ RunCommit::Done(committed) => Some(committed),
265
+ RunCommit::Deferred(commit) => {
266
+ let (committed, binding) = commit();
267
+ let rebound = binding.rebound();
268
+ session.finish_binding(binding).await?;
269
+ if rebound
270
+ && session.get_table().is_none()
271
+ && let Some(plugin) = renderer.active_plugin()
272
+ {
273
+ plugin.clear().await?;
274
+ }
275
+
276
+ Some(committed)
277
+ },
278
+ RunCommit::None => None,
279
+ };
280
+
281
+ if !committed.as_ref().is_some_and(|c| c.activate) {
282
+ renderer.mount_active_plugin()?;
233
283
  }
234
284
 
235
- let plugin_swapped = renderer.commit_plugin(spec.plugin_idx)?;
285
+ let plugin_swapped = match &committed {
286
+ Some(committed) => {
287
+ if committed.activate {
288
+ renderer.activate_committed_plugin()?;
289
+ }
290
+
291
+ committed.plugin_swapped
292
+ },
293
+ None => {
294
+ renderer.ensure_plugin_selected()?;
295
+ false
296
+ },
297
+ };
236
298
  let plugin = renderer.active_plugin().ok_or("No Plugin")?;
237
299
  renderer.stamp_theme(Some(&plugin));
238
- if let Some(error) = session.get_error() {
300
+ let blocking = session
301
+ .get_error()
302
+ .or_else(|| renderer.failure().map(|x| x.0));
303
+
304
+ if let Some(error) = blocking {
239
305
  return match spec.origin {
240
306
  RunOrigin::Public => Err(error),
241
307
  RunOrigin::Internal => Ok(()),
@@ -248,39 +314,35 @@ pub(crate) async fn locked_run(
248
314
  (BindDisposition::Deferred, None)
249
315
  };
250
316
 
251
- let view_config_snapshot = session.get_view_config().clone();
252
- let plugin_config_changed =
253
- renderer.update_plugin_config(&view_config_snapshot, spec.plugin_config)?;
254
- let plugin_config_snapshot = renderer.get_plugin_config();
255
- let plugin_update =
256
- JsValue::from_serde_ext(&plugin_config_snapshot).unwrap_or(JsValue::NULL);
257
-
258
- let plugin_restored = plugin_config_changed || plugin_swapped;
259
- if plugin_restored {
260
- let columns_config = renderer
261
- .all_columns_configs_materialized(&view_config_snapshot, &session)
262
- .await;
263
- plugin.restore(&plugin_update, Some(&columns_config))?;
264
- }
317
+ let view_config_snapshot = session.committed_view_config().clone();
318
+ let changed = match committed {
319
+ Some(committed) => {
320
+ let changed =
321
+ committed.plugin_config_changed || committed.columns_config_changed;
265
322
 
266
- let columns_config_changed = renderer.update_columns_configs(
267
- &view_config_snapshot,
268
- &session,
269
- spec.columns_config,
270
- )?;
271
-
272
- let changed = plugin_config_changed || columns_config_changed;
273
- if columns_config_changed {
274
- let columns_config = renderer
275
- .all_columns_configs_materialized(&view_config_snapshot, &session)
276
- .await;
277
- plugin.restore(&plugin_update, Some(&columns_config))?;
278
- renderer.columns_config_changed.emit(columns_config);
279
- }
323
+ if changed || plugin_swapped {
324
+ let plugin_config = renderer.committed_plugin_config();
325
+ let plugin_update =
326
+ JsValue::from_serde_ext(&plugin_config).unwrap_or(JsValue::NULL);
280
327
 
281
- if plugin_config_changed {
282
- renderer.plugin_config_changed.emit(plugin_config_snapshot);
283
- }
328
+ let columns_config = renderer
329
+ .all_columns_configs_materialized(&view_config_snapshot, &session)
330
+ .await;
331
+
332
+ plugin.restore(&plugin_update, Some(&columns_config))?;
333
+ if committed.columns_config_changed {
334
+ renderer.columns_config_changed.emit(columns_config);
335
+ }
336
+
337
+ if committed.plugin_config_changed {
338
+ renderer.plugin_config_changed.emit(plugin_config);
339
+ }
340
+ }
341
+
342
+ changed
343
+ },
344
+ None => false,
345
+ };
284
346
 
285
347
  if spec
286
348
  .presentation
@@ -291,22 +353,23 @@ pub(crate) async fn locked_run(
291
353
  dispatch_bound(&guard, &renderer, disposition, changed, spec.origin).await?;
292
354
  }
293
355
 
356
+ renderer.landed();
294
357
  Ok(())
295
- })
296
- .await
358
+ }
359
+ }
297
360
  }
298
361
 
299
362
  /// [`locked_run`]'s host-internal tail: a failed RUN sets error state (with
300
363
  /// the reset-reconnect affordance); the committed config is NOT rolled back
301
364
  /// (I4). Cancellation by a superseding run ("View already deleted") is not
302
365
  /// a failure.
303
- async fn render_run(
366
+ pub(super) async fn render_run(
304
367
  session: Session,
305
368
  renderer: Renderer,
306
- plugin_idx: Option<usize>,
369
+ commit: RunCommit,
307
370
  ) -> ApiResult<()> {
308
371
  let spec = RunSpec {
309
- plugin_idx,
372
+ commit,
310
373
  ..RunSpec::default()
311
374
  };
312
375
 
@@ -314,7 +377,7 @@ async fn render_run(
314
377
  .await
315
378
  .ignore_view_delete()
316
379
  {
317
- Err(e) => session.set_run_error(e).await,
380
+ Err(e) => renderer.fail(e),
318
381
  Ok(_) => Ok(()),
319
382
  }
320
383
  }
@@ -11,20 +11,20 @@
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
13
  //! Cross-engine reset orchestration: reset session config, optionally clear
14
- //! presentation columns config / theme, then delegate to `restore_and_render`
15
- //! to switch back to the default plugin and redraw.
14
+ //! presentation columns config / theme, then delegate to a transactional
15
+ //! restore to switch back to the default plugin and redraw.
16
16
 
17
17
  use perspective_client::clone;
18
- use perspective_js::utils::ApiFuture;
18
+ use perspective_js::utils::{ApiFuture, ApiResult};
19
19
 
20
20
  use super::pipeline::RunOrigin;
21
- use super::restore_and_render;
21
+ use super::transactional_restore::restore_in_place;
22
22
  use crate::config::{
23
23
  ColumnConfigUpdate, OptionalUpdate, PluginConfigUpdate, PluginUpdate, ViewerConfigUpdate,
24
24
  };
25
25
  use crate::presentation::Presentation;
26
26
  use crate::renderer::Renderer;
27
- use crate::session::{ResetOptions, Session};
27
+ use crate::session::{OpKind, ResetOptions, Session};
28
28
 
29
29
  /// Reset the viewer's `ViewerConfig` to the default.
30
30
  ///
@@ -37,7 +37,7 @@ use crate::session::{ResetOptions, Session};
37
37
  /// futures like this one) and error, rather than this task spawning unowned
38
38
  /// work.
39
39
  ///
40
- /// Delegates plugin selection + draw to [`restore_and_render`], whose
40
+ /// Delegates plugin selection + draw to [`restore_in_place`], whose
41
41
  /// two-pass restore guarantees the default plugin sees materialized
42
42
  /// `columns_config` / `plugin_config` on its first draw — fixing a race
43
43
  /// where the raw post-reset bucket would reach the plugin before
@@ -49,8 +49,27 @@ pub fn reset_all(
49
49
  all: bool,
50
50
  ) -> ApiFuture<()> {
51
51
  presentation.set_open_column_settings(None);
52
- clone!(session, renderer, presentation);
53
- ApiFuture::new(async move {
52
+ let ticket = session.submit(OpKind::Restore { update: None }, {
53
+ clone!(session, renderer, presentation);
54
+ move |_ctx| {
55
+ Box::pin(async move {
56
+ reset_all_step(&session, &renderer, &presentation, all).await?;
57
+ Ok(None)
58
+ })
59
+ }
60
+ });
61
+
62
+ ApiFuture::new(ticket.settle())
63
+ }
64
+
65
+ /// The body of [`reset_all`], run as one op on the panel's queue.
66
+ async fn reset_all_step(
67
+ session: &Session,
68
+ renderer: &Renderer,
69
+ presentation: &Presentation,
70
+ all: bool,
71
+ ) -> ApiResult<()> {
72
+ {
54
73
  session
55
74
  .reset(ResetOptions {
56
75
  config: true,
@@ -65,10 +84,10 @@ pub fn reset_all(
65
84
  // `reset_theme` only resets the host, which an explicitly-themed
66
85
  // panel would otherwise keep overriding.
67
86
  presentation.reset_theme().await?;
68
- renderer.set_theme(presentation.get_default_theme_name().await);
87
+ renderer.commit_theme(presentation.get_default_theme_name().await);
69
88
  }
70
89
 
71
- // For `all = true`, route the bucket clears through `restore_and_render`'s
90
+ // For `all = true`, route the bucket clears through the restore's
72
91
  // `update_*` paths as `SetDefault`. This guarantees the materialized
73
92
  // restore fires even when the user is already on the default plugin
74
93
  // (no plugin_swap signal), since `SetDefault` reports the bucket as
@@ -93,16 +112,8 @@ pub fn reset_all(
93
112
 
94
113
  // `reset()` is a public element API — `Public` keeps its repaint
95
114
  // affordance even on an already-default config.
96
- restore_and_render(
97
- &session,
98
- &renderer,
99
- &presentation,
100
- RunOrigin::Public,
101
- update,
102
- async { Ok(()) },
103
- )
104
- .await?;
115
+ restore_in_place(session, renderer, presentation, RunOrigin::Public, update).await?;
105
116
  renderer.reset_changed.emit(());
106
117
  Ok(())
107
- })
118
+ }
108
119
  }
@@ -0,0 +1,200 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! Reconcile the workspace's derived `Table`s with a restored
14
+ //! `derived_tables` map.
15
+
16
+ use std::collections::{BTreeMap, BTreeSet};
17
+
18
+ use perspective_js::utils::*;
19
+
20
+ use crate::config::{DerivedTableConfig, OptionalUpdate, ViewerConfigUpdate};
21
+ use crate::derived::topo_order;
22
+ use crate::presentation::Presentation;
23
+ use crate::session::MissingTable;
24
+ use crate::tasks::{RestoreErrors, RestoreMode, build_derived, restore_panel};
25
+ use crate::workspace::Workspace;
26
+
27
+ /// Build every derivation in `configs` that is not already reusable,
28
+ /// parents-first, deferring any whose parent is not yet hosted.
29
+ pub async fn apply_derived_tables(
30
+ workspace: &Workspace,
31
+ configs: BTreeMap<String, DerivedTableConfig>,
32
+ ) -> ApiResult<()> {
33
+ let order = topo_order(&configs).map_err(ApiError::new)?;
34
+ let reuse = workspace.derived_reusable(&order, &configs);
35
+
36
+ // Delete the stale generations children-first BEFORE building: a
37
+ // non-reusable name's old registration otherwise blocks its parent's
38
+ // redefinition (the parent's consumer count includes the stale child,
39
+ // and the engine refuses to delete a table the child's view reads).
40
+ for name in order.iter().rev() {
41
+ if reuse.contains(name) || !workspace.is_derived(name) {
42
+ continue;
43
+ }
44
+
45
+ let consumers = workspace.derived_consumers(name);
46
+ if consumers > 0 {
47
+ return Err(ApiError::new(format!(
48
+ "Cannot redefine `{name}` while {consumers} panels or tables read it"
49
+ )));
50
+ }
51
+
52
+ if let Some(stale) = workspace.take_derived(name) {
53
+ stale.delete().await?;
54
+ }
55
+ }
56
+
57
+ let mut deferred = BTreeMap::new();
58
+ for name in &order {
59
+ let config = &configs[name];
60
+ if reuse.contains(name) {
61
+ continue;
62
+ }
63
+
64
+ if deferred.contains_key(&config.table) {
65
+ deferred.insert(name.clone(), config.clone());
66
+ continue;
67
+ }
68
+
69
+ let Some(client) = workspace
70
+ .resolve_client_for_table(&config.table, None)
71
+ .await
72
+ else {
73
+ deferred.insert(name.clone(), config.clone());
74
+ continue;
75
+ };
76
+
77
+ if !build_derived(&client, workspace, name, &config.table, &config.view_config).await? {
78
+ deferred.insert(name.clone(), config.clone());
79
+ }
80
+ }
81
+
82
+ workspace.set_derived_pending(deferred);
83
+ Ok(())
84
+ }
85
+
86
+ /// Delete every registered derivation absent from `keep`, children first.
87
+ pub async fn prune_derived(workspace: &Workspace, keep: &BTreeSet<String>) -> ApiResult<()> {
88
+ let mut doomed = workspace
89
+ .derived_names()
90
+ .into_iter()
91
+ .filter(|name| !keep.contains(name))
92
+ .collect::<Vec<_>>();
93
+
94
+ doomed.sort_by_key(|name| std::cmp::Reverse(workspace.derived_ancestors(name).len()));
95
+ for name in doomed {
96
+ if workspace.derived_consumers(&name) > 0 {
97
+ tracing::warn!("Keeping derived table \"{name}\", which is still read");
98
+ continue;
99
+ }
100
+
101
+ if let Some(derived) = workspace.take_derived(&name)
102
+ && let Err(err) = derived.delete().await
103
+ {
104
+ tracing::warn!("Failed to delete derived table \"{name}\": {err}");
105
+ }
106
+ }
107
+
108
+ Ok(())
109
+ }
110
+
111
+ /// Retry the derivations whose parent was not hosted when they were restored.
112
+ pub async fn resume_derived(workspace: &Workspace) -> ApiResult<()> {
113
+ let pending = workspace.derived_pending();
114
+ if pending.is_empty() {
115
+ return Ok(());
116
+ }
117
+
118
+ let mut configs = workspace.derived_configs();
119
+ configs.extend(pending);
120
+ apply_derived_tables(workspace, configs).await
121
+ }
122
+
123
+ /// Tear down the element's ENTIRE derived state (the "all" reset): release
124
+ /// every panel bound or pending to a derived (or pending-derived) table to
125
+ /// its root ancestor, clear the pending registry, and delete every
126
+ /// registered derivation children-first.
127
+ pub async fn purge_derived(workspace: &Workspace, presentation: &Presentation) -> ApiResult<()> {
128
+ let pending = workspace.derived_pending();
129
+ if workspace.derived_names().is_empty() && pending.is_empty() {
130
+ return Ok(());
131
+ }
132
+
133
+ let parent_of = |name: &str| {
134
+ workspace
135
+ .derived_config(name)
136
+ .map(|x| x.table)
137
+ .or_else(|| pending.get(name).map(|x| x.table.clone()))
138
+ };
139
+
140
+ let root_of = |name: &str| {
141
+ let mut seen = BTreeSet::new();
142
+ let mut cursor = name.to_owned();
143
+ while seen.insert(cursor.clone()) {
144
+ match parent_of(&cursor) {
145
+ Some(parent) => cursor = parent,
146
+ None => break,
147
+ }
148
+ }
149
+
150
+ cursor
151
+ };
152
+
153
+ let active = workspace.active_id();
154
+ let mut rebinds = Vec::new();
155
+ for panel in workspace.panels() {
156
+ let Some(bound) = panel
157
+ .session
158
+ .get_table()
159
+ .map(|x| x.get_name().to_owned())
160
+ .or_else(|| panel.session.pending_table())
161
+ else {
162
+ continue;
163
+ };
164
+
165
+ if parent_of(&bound).is_none() {
166
+ continue;
167
+ }
168
+
169
+ let update = ViewerConfigUpdate {
170
+ table: OptionalUpdate::Update(root_of(&bound).into()),
171
+ ..ViewerConfigUpdate::default()
172
+ };
173
+
174
+ let is_active = active.as_ref() == Some(&panel.id);
175
+ let presentation = presentation.clone();
176
+ let workspace = workspace.clone();
177
+ rebinds.push(async move {
178
+ restore_panel(
179
+ &panel.session,
180
+ &panel.renderer,
181
+ &presentation,
182
+ &workspace,
183
+ RestoreMode::Existing { active: is_active },
184
+ update,
185
+ RestoreErrors::Publish,
186
+ MissingTable::Pend,
187
+ None,
188
+ )
189
+ .await
190
+ });
191
+ }
192
+
193
+ futures::future::join_all(rebinds)
194
+ .await
195
+ .into_iter()
196
+ .collect::<ApiResult<Vec<_>>>()?;
197
+
198
+ workspace.set_derived_pending(BTreeMap::new());
199
+ prune_derived(workspace, &BTreeSet::new()).await
200
+ }