@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
@@ -13,6 +13,7 @@
13
13
  use std::collections::{HashMap, HashSet};
14
14
  use std::iter::IntoIterator;
15
15
  use std::ops::{Deref, DerefMut};
16
+ use std::rc::Rc;
16
17
 
17
18
  use perspective_client::config::*;
18
19
  use perspective_js::apierror;
@@ -56,7 +57,18 @@ impl DerefMut for SessionMetadata {
56
57
  }
57
58
  }
58
59
 
59
- pub type MetadataRef<'a> = std::cell::Ref<'a, SessionMetadata>;
60
+ /// An OWNED read guard over a panel's [`SessionMetadata`]: a snapshot that
61
+ /// borrows nothing, so holding one can never make a later state write panic.
62
+ pub struct MetadataRef(pub(super) Rc<SessionMetadata>);
63
+
64
+ impl Deref for MetadataRef {
65
+ type Target = SessionMetadata;
66
+
67
+ fn deref(&self) -> &Self::Target {
68
+ &self.0
69
+ }
70
+ }
71
+
60
72
  pub type MetadataMutRef<'a> = std::cell::RefMut<'a, SessionMetadata>;
61
73
 
62
74
  /// TODO the multiple `Option` types could probably be merged since they are
@@ -75,15 +87,18 @@ pub struct SessionMetadataState {
75
87
  impl SessionMetadata {
76
88
  /// Creates a new `SessionMetadata` from a `JsPerspectiveTable`.
77
89
  pub(super) async fn from_table(table: &perspective_client::Table) -> ApiResult<Self> {
78
- let features = table.get_features().await?.clone();
79
- let column_names = table.columns().await?;
80
- let table_schema = table.schema().await?;
81
- let edit_port = table.make_port().await? as f64;
90
+ let (features, column_names, table_schema, edit_port) = futures::join!(
91
+ table.get_features(),
92
+ table.columns(),
93
+ table.schema(),
94
+ table.make_port()
95
+ );
96
+
82
97
  Ok(Self(Some(SessionMetadataState {
83
- features,
84
- column_names,
85
- table_schema,
86
- edit_port,
98
+ features: features?,
99
+ column_names: column_names?,
100
+ table_schema: table_schema?,
101
+ edit_port: edit_port? as f64,
87
102
  ..SessionMetadataState::default()
88
103
  })))
89
104
  }
@@ -290,6 +305,12 @@ impl SessionMetadata {
290
305
  self.as_ref().map(|meta| meta.edit_port)
291
306
  }
292
307
 
308
+ /// The type of a column of the `Table` itself — expression and window
309
+ /// columns, which belong to a config, excluded.
310
+ pub fn get_table_schema_type(&self, name: &str) -> Option<ColumnType> {
311
+ self.as_ref()?.table_schema.get(name).copied()
312
+ }
313
+
293
314
  /// Returns the type of a column name relative to the `Table`. Despite the
294
315
  /// name, `get_column_table_type()` also returns the `Table` type for
295
316
  /// Expressions, which despite living on the `View` still have a `table`
@@ -0,0 +1,430 @@
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
+ //! The per-panel op queue: every writer of a panel's committed state is an
14
+ //! [`OpKind`] submitted here, and the queue's single drain runs them one at a
15
+ //! time in SUBMIT order.
16
+
17
+ use std::cell::{Cell, RefCell};
18
+ use std::collections::VecDeque;
19
+ use std::rc::Rc;
20
+
21
+ use futures::channel::oneshot;
22
+ use futures::future::LocalBoxFuture;
23
+ use perspective_client::config::ViewConfigUpdate;
24
+ use perspective_js::utils::*;
25
+
26
+ use crate::config::{OptionalUpdate, ViewerConfigUpdate};
27
+ use crate::utils::InFlightGuard;
28
+
29
+ /// The render an op's ticket waits on after its write is finished.
30
+ pub type RenderFuture = LocalBoxFuture<'static, ApiResult<()>>;
31
+
32
+ /// An op's write, which yields the render its ticket resolves with, if any.
33
+ pub type StepFuture = LocalBoxFuture<'static, ApiResult<Option<RenderFuture>>>;
34
+ type Step = Box<dyn FnOnce(OpCtx) -> StepFuture>;
35
+
36
+ /// What a UI edit changes — enough to PROJECT it onto the committed state
37
+ /// before the drain commits it.
38
+ #[derive(Clone)]
39
+ pub enum EditDelta {
40
+ View(Box<ViewConfigUpdate>),
41
+ Title(Option<String>),
42
+
43
+ /// An edit of the renderer's own state, which the renderer projects.
44
+ Renderer,
45
+ }
46
+
47
+ pub enum OpKind {
48
+ /// A UI edit, which a later op may supersede unless it swaps the plugin.
49
+ Edit {
50
+ delta: EditDelta,
51
+ swaps_plugin: bool,
52
+ },
53
+
54
+ /// A restore-family op, which a later op may supersede when it carries
55
+ /// its `update`.
56
+ Restore {
57
+ update: Option<Rc<ViewerConfigUpdate>>,
58
+ },
59
+
60
+ /// The element's global filter, broadcast to this panel.
61
+ Overlay,
62
+
63
+ /// A `load()`.
64
+ Load { table_known: bool },
65
+ }
66
+
67
+ impl OpKind {
68
+ fn writes(&self) -> Option<Writes<'_>> {
69
+ match self {
70
+ OpKind::Edit {
71
+ delta: EditDelta::View(update),
72
+ swaps_plugin: false,
73
+ } => Some((update, None)),
74
+ OpKind::Restore {
75
+ update: Some(update),
76
+ } => Some((&update.view_config, Some(update))),
77
+ OpKind::Edit { .. }
78
+ | OpKind::Restore { .. }
79
+ | OpKind::Load { .. }
80
+ | OpKind::Overlay => None,
81
+ }
82
+ }
83
+
84
+ /// Whether this LATER op makes the `earlier` one pointless: everything
85
+ /// `earlier` would write, this op overwrites.
86
+ fn covers(&self, earlier: &OpKind) -> bool {
87
+ match (self, earlier) {
88
+ (OpKind::Overlay, OpKind::Overlay) => true,
89
+ (OpKind::Overlay, _) | (_, OpKind::Overlay) => false,
90
+ (OpKind::Load { .. }, OpKind::Load { .. }) => true,
91
+ (OpKind::Load { table_known }, _) => *table_known,
92
+ (_, OpKind::Load { .. }) => false,
93
+ (later, earlier) => match (later.writes(), earlier.writes()) {
94
+ (Some(later), Some(earlier)) => overwrites(later, earlier),
95
+ _ => false,
96
+ },
97
+ }
98
+ }
99
+ }
100
+
101
+ /// A submitted op's handle: resolves exactly once, when the op (or, for a
102
+ /// superseded op, the op that superseded it) has finished AND rendered.
103
+ pub struct Ticket(oneshot::Receiver<ApiResult<()>>);
104
+
105
+ impl Ticket {
106
+ /// A ticket for an op that will never run (the panel is disposed).
107
+ pub fn settled(result: ApiResult<()>) -> Self {
108
+ let (sender, receiver) = oneshot::channel();
109
+ let _ = sender.send(result);
110
+ Ticket(receiver)
111
+ }
112
+
113
+ pub async fn settle(self) -> ApiResult<()> {
114
+ match self.0.await {
115
+ Ok(result) => result,
116
+ Err(_) => Err(ApiError::new("Cancelled")),
117
+ }
118
+ }
119
+ }
120
+
121
+ /// What a running step can ask of the queue.
122
+ #[derive(Clone)]
123
+ pub struct OpCtx {
124
+ queue: std::rc::Rc<OpQueue>,
125
+ kind_is_load: bool,
126
+ }
127
+
128
+ impl OpCtx {
129
+ /// Whether a `load()` submitted since this op began makes its table binding
130
+ /// pointless: a `load()` abandons for ANY later `load()`, a restore's
131
+ /// rebind only for one already known to carry a `Table`.
132
+ pub fn is_superseded(&self) -> bool {
133
+ self.queue
134
+ .entries
135
+ .borrow()
136
+ .iter()
137
+ .any(|later| match later.kind {
138
+ OpKind::Load { table_known } => self.kind_is_load || table_known,
139
+ _ => false,
140
+ })
141
+ }
142
+ }
143
+
144
+ struct Entry {
145
+ kind: OpKind,
146
+ step: Step,
147
+ replies: Vec<oneshot::Sender<ApiResult<()>>>,
148
+ guards: Vec<Pending>,
149
+ }
150
+
151
+ impl Entry {
152
+ fn resolve(self, result: ApiResult<()>) {
153
+ {
154
+ let replies = self.replies;
155
+ for reply in replies {
156
+ let _ = reply.send(result.clone());
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ /// One unsettled op's accounts, released together when its ticket resolves — on
163
+ /// every path (rendered, rejected, superseded, disposed): the busy indicator,
164
+ /// and the queue's own count of ops still in flight.
165
+ struct Pending {
166
+ _busy: InFlightGuard,
167
+ queue: std::rc::Weak<OpQueue>,
168
+ }
169
+
170
+ impl Drop for Pending {
171
+ fn drop(&mut self) {
172
+ if let Some(queue) = self.queue.upgrade() {
173
+ queue.in_flight.set(queue.in_flight.get() - 1);
174
+ if queue.in_flight.get() == 0 {
175
+ for waiter in queue.idle_waiters.take() {
176
+ let _ = waiter.send(());
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ /// What happened to an entry the drain took off the queue without running.
184
+ pub enum Exit {
185
+ Superseded,
186
+ Rejected,
187
+ }
188
+
189
+ #[derive(Default)]
190
+ pub struct OpQueue {
191
+ entries: RefCell<VecDeque<Entry>>,
192
+ draining: Cell<bool>,
193
+ load_running: Cell<bool>,
194
+
195
+ /// Ops whose tickets have not resolved — queued, running, or committed with
196
+ /// a detached render still to land.
197
+ in_flight: Cell<usize>,
198
+ idle_waiters: RefCell<Vec<oneshot::Sender<()>>>,
199
+ }
200
+
201
+ impl OpQueue {
202
+ /// Append an op.
203
+ pub fn push(
204
+ self: &std::rc::Rc<Self>,
205
+ kind: OpKind,
206
+ guard: InFlightGuard,
207
+ step: impl FnOnce(OpCtx) -> StepFuture + 'static,
208
+ ) -> (Ticket, bool) {
209
+ let (sender, receiver) = oneshot::channel();
210
+ self.in_flight.set(self.in_flight.get() + 1);
211
+ self.entries.borrow_mut().push_back(Entry {
212
+ kind,
213
+ step: Box::new(step),
214
+ replies: vec![sender],
215
+ guards: vec![Pending {
216
+ _busy: guard,
217
+ queue: std::rc::Rc::downgrade(self),
218
+ }],
219
+ });
220
+
221
+ (Ticket(receiver), !self.draining.replace(true))
222
+ }
223
+
224
+ /// The deltas of every pending [`OpKind::Edit`], in submit order.
225
+ pub fn pending_edits(&self) -> Vec<EditDelta> {
226
+ self.entries
227
+ .borrow()
228
+ .iter()
229
+ .filter_map(|entry| match &entry.kind {
230
+ OpKind::Edit { delta, .. } => Some(delta.clone()),
231
+ _ => None,
232
+ })
233
+ .collect()
234
+ }
235
+
236
+ /// Resolves once every submitted op's ticket has — nothing queued, nothing
237
+ /// running, no committed render still to land.
238
+ pub async fn idle(&self) {
239
+ while self.in_flight.get() > 0 {
240
+ let (sender, receiver) = oneshot::channel();
241
+ self.idle_waiters.borrow_mut().push(sender);
242
+ let _ = receiver.await;
243
+ }
244
+ }
245
+
246
+ /// Whether a `load()` is queued or running.
247
+ pub fn has_load(&self) -> bool {
248
+ self.load_running.get() || self.has_queued_load()
249
+ }
250
+
251
+ /// Whether a `load()` is queued behind the running op.
252
+ pub fn has_queued_load(&self) -> bool {
253
+ self.entries
254
+ .borrow()
255
+ .iter()
256
+ .any(|entry| matches!(entry.kind, OpKind::Load { .. }))
257
+ }
258
+
259
+ /// Run every queued op, one at a time, in submit order.
260
+ pub async fn drain(self: std::rc::Rc<Self>, on_exit: impl Fn(Exit)) {
261
+ loop {
262
+ let Some(mut entry) = self.entries.borrow_mut().pop_front() else {
263
+ break;
264
+ };
265
+
266
+ let is_edit = matches!(entry.kind, OpKind::Edit {
267
+ delta: EditDelta::View(_),
268
+ ..
269
+ });
270
+ let superseded = {
271
+ let mut entries = self.entries.borrow_mut();
272
+ match entries
273
+ .iter_mut()
274
+ .find(|later| later.kind.covers(&entry.kind))
275
+ {
276
+ Some(later) => {
277
+ later.replies.append(&mut entry.replies);
278
+ later.guards.append(&mut entry.guards);
279
+ true
280
+ },
281
+ None => false,
282
+ }
283
+ };
284
+
285
+ if superseded {
286
+ if is_edit {
287
+ on_exit(Exit::Superseded);
288
+ }
289
+
290
+ continue;
291
+ }
292
+
293
+ let Entry {
294
+ kind,
295
+ step,
296
+ replies,
297
+ guards,
298
+ } = entry;
299
+
300
+ let kind_is_load = matches!(kind, OpKind::Load { .. });
301
+ self.load_running.set(kind_is_load);
302
+ let outcome = step(OpCtx {
303
+ queue: self.clone(),
304
+ kind_is_load,
305
+ })
306
+ .await;
307
+
308
+ self.load_running.set(false);
309
+ match outcome {
310
+ Ok(None) => {
311
+ let result = Ok(());
312
+ for reply in replies {
313
+ let _ = reply.send(result.clone());
314
+ }
315
+
316
+ drop(guards);
317
+ },
318
+ Ok(Some(render)) => ApiFuture::spawn(async move {
319
+ {
320
+ let result = render.await;
321
+ for reply in replies {
322
+ let _ = reply.send(result.clone());
323
+ }
324
+
325
+ drop(guards);
326
+ };
327
+ Ok(())
328
+ }),
329
+ Err(error) => {
330
+ if is_edit {
331
+ tracing::warn!("Edit rejected: {}", error);
332
+ on_exit(Exit::Rejected);
333
+ }
334
+
335
+ {
336
+ let result = Err(error);
337
+ for reply in replies {
338
+ let _ = reply.send(result.clone());
339
+ }
340
+
341
+ drop(guards);
342
+ };
343
+ },
344
+ }
345
+ }
346
+
347
+ self.draining.set(false);
348
+ }
349
+
350
+ /// Reject everything still queued — the panel is gone.
351
+ pub fn reject_all(&self, error: ApiResult<()>) {
352
+ let entries = std::mem::take(&mut *self.entries.borrow_mut());
353
+ for entry in entries {
354
+ entry.resolve(error.clone());
355
+ }
356
+ }
357
+ }
358
+
359
+ type Writes<'a> = (&'a ViewConfigUpdate, Option<&'a ViewerConfigUpdate>);
360
+
361
+ /// Whether `later` overwrites every top-level key `earlier` writes, which a
362
+ /// merging `plugin_config` / `columns_config` update never does.
363
+ fn overwrites((later_view, later): Writes, (earlier_view, earlier): Writes) -> bool {
364
+ let empty = ViewerConfigUpdate::default();
365
+ let later = later.unwrap_or(&empty);
366
+ let ViewerConfigUpdate {
367
+ version,
368
+ plugin,
369
+ plugin_config,
370
+ columns_config,
371
+ settings,
372
+ theme,
373
+ title,
374
+ table,
375
+ view_config,
376
+ } = earlier.unwrap_or(&empty);
377
+
378
+ let _ = (version, view_config);
379
+
380
+ let ViewConfigUpdate {
381
+ group_by,
382
+ split_by,
383
+ columns,
384
+ filter,
385
+ sort,
386
+ expressions,
387
+ windows,
388
+ aggregates,
389
+ group_by_depth,
390
+ filter_op,
391
+ group_rollup_mode,
392
+ split_rollup_mode,
393
+ } = earlier_view;
394
+
395
+ fn merges<T: Clone>(update: &OptionalUpdate<T>) -> bool {
396
+ matches!(update, OptionalUpdate::Update(_))
397
+ }
398
+
399
+ fn kept<T: Clone>(earlier: &OptionalUpdate<T>, later: &OptionalUpdate<T>) -> bool {
400
+ !matches!(earlier, OptionalUpdate::Missing) && matches!(later, OptionalUpdate::Missing)
401
+ }
402
+
403
+ let merging = merges(plugin_config)
404
+ || merges(columns_config)
405
+ || merges(&later.plugin_config)
406
+ || merges(&later.columns_config);
407
+
408
+ let groups = |x: &ViewConfigUpdate| x.group_by.is_some() || x.group_rollup_mode.is_some();
409
+ let kept_view = (group_by.is_some() || group_rollup_mode.is_some()) && !groups(later_view)
410
+ || split_by.is_some() && later_view.split_by.is_none()
411
+ || columns.is_some() && later_view.columns.is_none()
412
+ || filter.is_some() && later_view.filter.is_none()
413
+ || sort.is_some() && later_view.sort.is_none()
414
+ || expressions.is_some() && later_view.expressions.is_none()
415
+ || windows.is_some() && later_view.windows.is_none()
416
+ || aggregates.is_some() && later_view.aggregates.is_none()
417
+ || group_by_depth.is_some() && later_view.group_by_depth.is_none()
418
+ || filter_op.is_some() && later_view.filter_op.is_none()
419
+ || split_rollup_mode.is_some() && later_view.split_rollup_mode.is_none();
420
+
421
+ !merging
422
+ && !kept_view
423
+ && !kept(plugin, &later.plugin)
424
+ && !kept(plugin_config, &later.plugin_config)
425
+ && !kept(columns_config, &later.columns_config)
426
+ && !kept(settings, &later.settings)
427
+ && !kept(theme, &later.theme)
428
+ && !kept(title, &later.title)
429
+ && !kept(table, &later.table)
430
+ }