@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.
- package/README.md +82 -3
- package/dist/cdn/perspective-viewer.js +9 -9
- package/dist/cdn/perspective-viewer.js.map +3 -3
- package/dist/css/blueprint.css +1 -1
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/eggplant.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/ar.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/ledger.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/nord.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/css/velvet.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/column-format.js +2 -0
- package/dist/esm/column-format.js.map +7 -0
- package/dist/esm/extensions.d.ts +3 -13
- package/dist/esm/perspective-viewer.d.ts +1 -1
- package/dist/esm/perspective-viewer.js +9 -9
- package/dist/esm/perspective-viewer.js.map +3 -3
- package/dist/esm/plugin.d.ts +4 -1
- package/dist/esm/select-detail.d.ts +15 -0
- package/dist/esm/select-detail.js +2 -0
- package/dist/esm/select-detail.js.map +7 -0
- package/dist/esm/ts-rs/DerivedTableConfig.d.ts +35 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
- package/dist/esm/ts-rs/PanelViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/RestoreOptions.d.ts +3 -7
- package/dist/esm/ts-rs/TableRef.d.ts +9 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +7 -6
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -5
- package/dist/esm/ts-rs/WorkspaceConfig.d.ts +9 -1
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +9 -0
- package/dist/wasm/perspective-viewer.d.ts +126 -35
- package/dist/wasm/perspective-viewer.js +320 -185
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +33 -20
- package/src/css/column-selector.css +205 -50
- package/src/css/ui/containers/context-menu.css +12 -1
- package/src/css/ui/containers/dropdown-menu-items.css +29 -0
- package/src/css/ui/containers/scroll-panel.css +4 -1
- package/src/rust/agent/client/protocol.rs +0 -89
- package/src/rust/agent/markdown/mod.rs +0 -238
- package/src/rust/agent/preamble.md +1 -0
- package/src/rust/agent/tools.rs +2 -16
- package/src/rust/components/column_selector/active_column.rs +15 -5
- package/src/rust/components/column_selector/add_expression_button.rs +9 -26
- package/src/rust/components/column_selector/config_selector.rs +34 -6
- package/src/rust/components/column_selector/derived_control.rs +229 -0
- package/src/rust/components/column_selector/expr_edit_button.rs +3 -2
- package/src/rust/components/column_selector/filter_column.rs +0 -2
- package/src/rust/components/column_selector/inactive_column.rs +4 -5
- package/src/rust/components/column_selector/pivot_column.rs +21 -1
- package/src/rust/components/column_selector/sort_column.rs +4 -3
- package/src/rust/components/column_selector.rs +21 -1
- package/src/rust/components/column_settings_sidebar/save_settings.rs +5 -3
- package/src/rust/components/column_settings_sidebar/style_tab.rs +1 -0
- package/src/rust/components/column_settings_sidebar.rs +25 -19
- package/src/rust/components/debug_panel.rs +38 -29
- package/src/rust/components/dragdrop_list.rs +8 -9
- package/src/rust/components/export_dropdown.rs +6 -0
- package/src/rust/components/main_panel/msg.rs +6 -4
- package/src/rust/components/main_panel/reconcile.rs +6 -1
- package/src/rust/components/main_panel/render.rs +70 -51
- package/src/rust/components/main_panel/update.rs +39 -1
- package/src/rust/components/main_panel.rs +97 -59
- package/src/rust/components/new_panel_menu.rs +69 -1
- package/src/rust/components/panel_menu.rs +63 -12
- package/src/rust/components/plugin_tab.rs +3 -2
- package/src/rust/components/render_warning.rs +2 -1
- package/src/rust/components/settings_panel.rs +4 -4
- package/src/rust/components/status_bar.rs +80 -21
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +15 -3
- package/src/rust/components/viewer/msg.rs +8 -0
- package/src/rust/components/viewer/panels.rs +44 -4
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/settings.rs +0 -22
- package/src/rust/components/viewer/snapshots.rs +2 -2
- package/src/rust/components/viewer/wiring.rs +18 -1
- package/src/rust/components/viewer.rs +47 -27
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- package/src/rust/config/export_method.rs +24 -0
- package/src/rust/config/number_string_format.rs +0 -94
- package/src/rust/config/options.rs +3 -7
- package/src/rust/config/plugin_static_config.rs +0 -42
- package/src/rust/config/viewer_config.rs +47 -17
- package/src/rust/config/workspace_config.rs +158 -128
- package/src/rust/custom_elements/viewer.rs +189 -129
- package/src/rust/custom_events.rs +33 -18
- package/src/rust/derived.rs +266 -0
- package/src/rust/exprtk/tokenize.rs +0 -54
- package/src/rust/js/plugin.rs +1 -1
- package/src/rust/lib.rs +3 -0
- package/src/rust/presentation.rs +23 -4
- package/src/rust/queries/column_locator.rs +55 -48
- package/src/rust/queries/get_viewer_config.rs +1 -1
- package/src/rust/queries/plugin_column_styles.rs +22 -23
- package/src/rust/renderer/dispatch.rs +2 -0
- package/src/rust/renderer/plugin_config.rs +652 -487
- package/src/rust/renderer/props.rs +4 -0
- package/src/rust/renderer/registry.rs +2 -6
- package/src/rust/renderer/state.rs +205 -0
- package/src/rust/renderer.rs +201 -59
- package/src/rust/session/metadata.rs +35 -19
- package/src/rust/session/op_queue.rs +430 -0
- package/src/rust/session/panel_state.rs +327 -0
- package/src/rust/session/view_subscription.rs +42 -3
- package/src/rust/session.rs +931 -419
- package/src/rust/tasks/apply_global_filters.rs +49 -35
- package/src/rust/tasks/auto_pause.rs +22 -12
- package/src/rust/tasks/copy_export.rs +72 -0
- package/src/rust/tasks/create_panel.rs +46 -21
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +152 -89
- package/src/rust/tasks/reset_all.rs +31 -20
- package/src/rust/tasks/restore_derived.rs +200 -0
- package/src/rust/tasks/restore_panel.rs +153 -141
- package/src/rust/tasks/send_column_config.rs +56 -29
- package/src/rust/tasks/send_plugin_config.rs +52 -23
- package/src/rust/tasks/set_edit_mode.rs +33 -25
- package/src/rust/tasks/sync_update_panels.rs +40 -13
- package/src/rust/tasks/table_lifecycle.rs +70 -34
- package/src/rust/tasks/transactional_restore.rs +387 -0
- package/src/rust/tasks/update_theme.rs +45 -6
- package/src/rust/ui/containers/scroll_panel.rs +17 -10
- package/src/rust/ui/containers/split_panel.rs +0 -41
- package/src/rust/ui/context_menu.rs +157 -18
- package/src/rust/ui/font_loader.rs +23 -35
- package/src/rust/ui/form/mirrored_textarea.rs +0 -12
- package/src/rust/ui/form/multi_stop_gradient_selector.rs +0 -89
- package/src/rust/ui/form/palette_selector.rs +0 -51
- package/src/rust/ui/portal.rs +4 -10
- package/src/rust/utils/browser/mod.rs +2 -3
- package/src/{ts/perspective-viewer.inline.ts → rust/utils/browser/mouse.rs} +11 -10
- package/src/rust/utils/browser/request_animation_frame.rs +27 -8
- package/src/rust/utils/css.rs +0 -360
- package/src/rust/utils/{tests/mod.rs → in_flight.rs} +62 -2
- package/src/rust/utils/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +4 -3
- package/src/rust/utils/modal_position.rs +15 -21
- package/src/rust/utils/pending_effects.rs +6 -30
- package/src/rust/utils/{browser/tests/mod.rs → plugin_host_state.rs} +31 -1
- package/src/rust/utils/pubsub.rs +0 -1
- package/src/rust/workspace.rs +187 -140
- package/src/svg/derived.svg +6 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +10 -0
- package/src/themes/intl/de.css +10 -0
- package/src/themes/intl/es.css +10 -0
- package/src/themes/intl/fr.css +10 -0
- package/src/themes/intl/ja.css +10 -0
- package/src/themes/intl/pt.css +10 -0
- package/src/themes/intl/zh.css +10 -0
- package/src/themes/intl.css +10 -0
- package/src/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- package/src/ts/extensions.ts +3 -36
- package/src/ts/perspective-viewer.ts +1 -1
- package/src/ts/plugin.ts +4 -1
- package/src/{rust/utils/tests/request_animation_frame.rs → ts/select-detail.ts} +32 -26
- package/src/ts/ts-rs/DerivedTableConfig.ts +22 -0
- package/src/ts/ts-rs/ExportMethod.ts +1 -1
- package/src/ts/ts-rs/PanelViewerConfig.ts +7 -6
- package/src/ts/ts-rs/RestoreOptions.ts +3 -7
- package/src/ts/ts-rs/TableRef.ts +9 -0
- package/src/ts/ts-rs/ViewerConfig.ts +7 -6
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +7 -5
- package/src/ts/ts-rs/WorkspaceConfig.ts +7 -1
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +7 -0
- package/dist/esm/perspective-viewer.inline.d.ts +0 -3
- package/dist/esm/perspective-viewer.inline.js +0 -21
- package/dist/esm/perspective-viewer.inline.js.map +0 -7
- package/src/rust/tasks/restore_and_render.rs +0 -107
- package/src/rust/utils/browser/tests/debounce.rs +0 -163
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
package/src/rust/session.rs
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
13
|
pub(crate) mod column_defaults_update;
|
|
14
14
|
pub(crate) mod drag_drop_update;
|
|
15
15
|
mod metadata;
|
|
16
|
+
mod op_queue;
|
|
17
|
+
mod panel_state;
|
|
16
18
|
mod props;
|
|
17
19
|
pub(crate) mod replace_expression_update;
|
|
18
20
|
mod view_subscription;
|
|
19
21
|
|
|
20
|
-
use std::cell::{Cell,
|
|
22
|
+
use std::cell::{Cell, RefCell};
|
|
21
23
|
use std::collections::{HashMap, HashSet};
|
|
22
24
|
use std::future::Future;
|
|
23
25
|
use std::ops::Deref;
|
|
@@ -25,7 +27,9 @@ use std::rc::Rc;
|
|
|
25
27
|
|
|
26
28
|
use perspective_client::config::*;
|
|
27
29
|
use perspective_client::proto::ViewDimensionsResp;
|
|
28
|
-
use perspective_client::{
|
|
30
|
+
use perspective_client::{
|
|
31
|
+
Client, ClientError, DescribeError, Description, ExprValidationResult, ReconnectCallback, View,
|
|
32
|
+
};
|
|
29
33
|
use perspective_js::apierror;
|
|
30
34
|
use perspective_js::utils::*;
|
|
31
35
|
use wasm_bindgen::prelude::*;
|
|
@@ -34,9 +38,13 @@ use yew::prelude::*;
|
|
|
34
38
|
|
|
35
39
|
use self::metadata::*;
|
|
36
40
|
pub use self::metadata::{MetadataRef, SessionMetadata, SessionMetadataRc};
|
|
41
|
+
use self::op_queue::OpQueue;
|
|
42
|
+
pub use self::op_queue::{EditDelta, OpCtx, OpKind, StepFuture, Ticket};
|
|
43
|
+
use self::panel_state::effective;
|
|
44
|
+
pub use self::panel_state::{Binding, OverlayClause, PanelState};
|
|
37
45
|
pub use self::props::{SessionProps, TableLoadState};
|
|
38
|
-
pub use self::view_subscription::ViewStats;
|
|
39
46
|
use self::view_subscription::*;
|
|
47
|
+
pub use self::view_subscription::{ViewStats, ViewSubscription};
|
|
40
48
|
use crate::config::PluginStaticConfig;
|
|
41
49
|
use crate::utils::*;
|
|
42
50
|
|
|
@@ -71,12 +79,8 @@ pub struct SessionHandle {
|
|
|
71
79
|
pub view_config_changed: PubSub<()>,
|
|
72
80
|
pub title_changed: PubSub<Option<String>>,
|
|
73
81
|
|
|
74
|
-
///
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
/// Fires with the ABSOLUTE [`Self::in_flight_config_runs`] count after
|
|
78
|
-
/// every change.
|
|
79
|
-
pub run_state_changed: PubSub<u32>,
|
|
82
|
+
/// Account of in-flight CONFIG-DRIVEN pipeline runs.
|
|
83
|
+
pub config_runs: InFlight,
|
|
80
84
|
|
|
81
85
|
/// Fires when the user clicks the status indicator while in
|
|
82
86
|
/// [`StatusIconState::Normal`].
|
|
@@ -100,31 +104,21 @@ pub struct SessionHandle {
|
|
|
100
104
|
/// copying the config.
|
|
101
105
|
pub last_dispatched_config: RefCell<Option<std::rc::Rc<crate::config::ViewerConfig>>>,
|
|
102
106
|
|
|
103
|
-
///
|
|
104
|
-
/// ([`
|
|
105
|
-
|
|
106
|
-
/// repeatedly, unawaited and out of order — no-ops instead of clobbering a
|
|
107
|
-
/// newer load ([`Session::is_current_load`]).
|
|
108
|
-
load_generation: Cell<u32>,
|
|
109
|
-
|
|
110
|
-
/// Open between a `load()` call and its payload's classification as
|
|
111
|
-
/// `Table`/`Client` ([`PendingLoad`]).
|
|
112
|
-
pending_load: RefCell<Option<PendingLoad>>,
|
|
107
|
+
/// Every writer of this panel's committed state, run one at a time in
|
|
108
|
+
/// submit order (see [`op_queue`]).
|
|
109
|
+
queue: Rc<OpQueue>,
|
|
113
110
|
|
|
114
111
|
/// Coalesces `view_config_changed`: multiple synchronous commits in one
|
|
115
112
|
/// task emit ONE event on the next microtask — the cadence the deleted
|
|
116
113
|
/// `is_clean` flag provided by accident, now deliberate.
|
|
117
114
|
config_event_scheduled: Cell<bool>,
|
|
118
115
|
|
|
119
|
-
///
|
|
120
|
-
/// panel (see [`Session::track_dispatch`]). `flush()` joins these via
|
|
116
|
+
/// Account of in-flight `perspective-config-update` dispatch tasks for
|
|
117
|
+
/// this panel (see [`Session::track_dispatch`]). `flush()` joins these via
|
|
121
118
|
/// [`Session::settle_dispatches`], so the "config-update fires before
|
|
122
119
|
/// `flush()` resolves" contract holds by construction instead of by
|
|
123
120
|
/// microtask luck.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/// Fires when [`SessionHandle::pending_dispatches`] returns to zero.
|
|
127
|
-
dispatches_settled: PubSub<()>,
|
|
121
|
+
dispatches: InFlight,
|
|
128
122
|
|
|
129
123
|
/// Fires when [`SessionHandle::column_stats`] is updated (insert or
|
|
130
124
|
/// clear). Subscribers re-render and re-query the schema with the
|
|
@@ -200,24 +194,144 @@ pub(crate) async fn probe_table(
|
|
|
200
194
|
/// Mutable state for `Session`.
|
|
201
195
|
#[derive(Default)]
|
|
202
196
|
pub struct SessionData {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
197
|
+
/// This panel's committed state: ONE immutable value, replaced whole by
|
|
198
|
+
/// [`SessionData::swap`].
|
|
199
|
+
state: Lifecycle,
|
|
200
|
+
|
|
201
|
+
/// What [`Session::metadata`] answers while nothing is bound, and what
|
|
202
|
+
/// [`Session::metadata_mut`] scribbles on (every write is a no-op then).
|
|
203
|
+
unbound_metadata: Rc<SessionMetadata>,
|
|
209
204
|
view_sub: Option<ViewSubscription>,
|
|
210
205
|
stats: Option<ViewStats>,
|
|
211
|
-
is_loading: bool,
|
|
212
206
|
is_paused: bool,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// A panel's state and whether it can still be written.
|
|
210
|
+
enum Lifecycle {
|
|
211
|
+
Live(Rc<PanelState>),
|
|
212
|
+
|
|
213
|
+
/// Terminal: the owning panel was ejected ([`Session::dispose`]).
|
|
214
|
+
Disposed {
|
|
215
|
+
last: Rc<PanelState>,
|
|
216
|
+
disposal: Disposal,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
impl Default for Lifecycle {
|
|
221
|
+
fn default() -> Self {
|
|
222
|
+
Self::Live(Rc::default())
|
|
223
|
+
}
|
|
224
|
+
}
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
226
|
+
impl Deref for Lifecycle {
|
|
227
|
+
type Target = Rc<PanelState>;
|
|
228
|
+
|
|
229
|
+
fn deref(&self) -> &Self::Target {
|
|
230
|
+
match self {
|
|
231
|
+
Lifecycle::Live(state) | Lifecycle::Disposed { last: state, .. } => state,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
impl Lifecycle {
|
|
237
|
+
fn live_mut(&mut self) -> Option<&mut Rc<PanelState>> {
|
|
238
|
+
match self {
|
|
239
|
+
Lifecycle::Live(state) => Some(state),
|
|
240
|
+
Lifecycle::Disposed { .. } => None,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn disposal(&self) -> Option<Disposal> {
|
|
245
|
+
match self {
|
|
246
|
+
Lifecycle::Live(_) => None,
|
|
247
|
+
Lifecycle::Disposed { disposal, .. } => Some(*disposal),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// An OWNED read guard over the committed [`ViewConfig`]: a snapshot that
|
|
253
|
+
/// borrows nothing.
|
|
254
|
+
pub struct ViewConfigRef(Rc<ViewConfig>);
|
|
255
|
+
|
|
256
|
+
impl Deref for ViewConfigRef {
|
|
257
|
+
type Target = ViewConfig;
|
|
258
|
+
|
|
259
|
+
fn deref(&self) -> &Self::Target {
|
|
260
|
+
&self.0
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
impl SessionData {
|
|
265
|
+
fn error(&self) -> Option<&TableErrorState> {
|
|
266
|
+
self.state.lost()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn clear_errors(&mut self) {
|
|
270
|
+
if self.state.lost().is_some() {
|
|
271
|
+
self.swap(self.state.recovered());
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn swap(&mut self, next: PanelState) {
|
|
276
|
+
if let Some(state) = self.state.live_mut() {
|
|
277
|
+
*state = Rc::new(next);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn table(&self) -> Option<&perspective_client::Table> {
|
|
282
|
+
self.state.bound().map(|bound| &bound.table)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn pending_table(&self) -> Option<&str> {
|
|
286
|
+
self.state.awaiting()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn metadata(&self) -> &Rc<SessionMetadata> {
|
|
290
|
+
match self.state.bound() {
|
|
291
|
+
Some(bound) => &bound.metadata,
|
|
292
|
+
None => &self.unbound_metadata,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Copy-on-write: a snapshot held elsewhere never observes the mutation.
|
|
297
|
+
fn metadata_mut(&mut self) -> &mut SessionMetadata {
|
|
298
|
+
let Some(state) = self.state.live_mut() else {
|
|
299
|
+
return Rc::make_mut(&mut self.unbound_metadata);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
let binding = match &mut Rc::make_mut(state).binding {
|
|
303
|
+
Binding::Lost { prior, .. } => Rc::make_mut(prior),
|
|
304
|
+
binding => binding,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
match binding {
|
|
308
|
+
Binding::Bound(bound) => Rc::make_mut(&mut Rc::make_mut(bound).metadata),
|
|
309
|
+
_ => Rc::make_mut(&mut self.unbound_metadata),
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fn config(&self) -> &ViewConfig {
|
|
314
|
+
self.state.config()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn set_config(&mut self, config: ViewConfig) {
|
|
318
|
+
self.swap(self.state.with_config(Rc::new(config)));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/// Memo for [`Session::validate_snapshot`]: the [`Description`] the server
|
|
322
|
+
/// gave for the last validated effective config; an equal snapshot skips
|
|
323
|
+
/// the round trip.
|
|
324
|
+
fn description(&self) -> Option<&(Rc<ViewConfig>, Rc<Description>)> {
|
|
325
|
+
self.state.bound()?.description.as_ref()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fn set_description(&mut self, description: Option<(Rc<ViewConfig>, Rc<Description>)>) {
|
|
329
|
+
self.swap(self.state.with_description(description));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fn unbind(&mut self) {
|
|
333
|
+
self.swap(self.state.unbound());
|
|
334
|
+
}
|
|
221
335
|
}
|
|
222
336
|
|
|
223
337
|
#[derive(Clone)]
|
|
@@ -255,20 +369,32 @@ impl TableErrorState {
|
|
|
255
369
|
}
|
|
256
370
|
}
|
|
257
371
|
|
|
372
|
+
/// How a terminal disposal reports itself to a public run still in flight on
|
|
373
|
+
/// the ejected panel.
|
|
374
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
375
|
+
pub enum Disposal {
|
|
376
|
+
/// A separate call destroyed the panel, so a racing public run rejects
|
|
377
|
+
/// rather than reporting a success it never achieved.
|
|
378
|
+
Reject,
|
|
379
|
+
|
|
380
|
+
/// The ejecting call reports the eviction on its own promise, so a racing
|
|
381
|
+
/// public run resolves rather than surfacing it twice.
|
|
382
|
+
Resolve,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
impl From<Disposal> for ApiResult<()> {
|
|
386
|
+
fn from(disposal: Disposal) -> Self {
|
|
387
|
+
match disposal {
|
|
388
|
+
Disposal::Reject => Err(ApiError::new("Panel disposed")),
|
|
389
|
+
Disposal::Resolve => Ok(()),
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
258
394
|
#[derive(Debug, Default)]
|
|
259
395
|
pub enum TableIntermediateState {
|
|
260
396
|
#[default]
|
|
261
397
|
Ejected,
|
|
262
|
-
Reloaded,
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/// The open-load window state (see [`SessionHandle::pending_load`]): the
|
|
266
|
-
/// generation that opened it (for supersession detection) and the raw config
|
|
267
|
-
/// deltas committed while it was open, in commit order, awaiting replay over a
|
|
268
|
-
/// reset base if the payload classifies as a `Table`.
|
|
269
|
-
pub struct PendingLoad {
|
|
270
|
-
generation: u32,
|
|
271
|
-
journal: Vec<ViewConfigUpdate>,
|
|
272
398
|
}
|
|
273
399
|
|
|
274
400
|
/// Options for [`Session::reset`]
|
|
@@ -293,19 +419,6 @@ pub struct ResetOptions {
|
|
|
293
419
|
#[derive(Clone)]
|
|
294
420
|
pub struct Session(Rc<SessionHandle>);
|
|
295
421
|
|
|
296
|
-
/// RAII spinner accounting for one config-driven pipeline run.
|
|
297
|
-
pub struct ConfigRunToken(Session);
|
|
298
|
-
|
|
299
|
-
impl Drop for ConfigRunToken {
|
|
300
|
-
fn drop(&mut self) {
|
|
301
|
-
let count = self.0.0.in_flight_config_runs.get();
|
|
302
|
-
debug_assert!(count > 0, "ConfigRunToken underflow");
|
|
303
|
-
let count = count.saturating_sub(1);
|
|
304
|
-
self.0.0.in_flight_config_runs.set(count);
|
|
305
|
-
self.0.run_state_changed.emit(count);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
422
|
impl ImplicitClone for Session {}
|
|
310
423
|
|
|
311
424
|
impl Deref for Session {
|
|
@@ -330,21 +443,56 @@ impl Session {
|
|
|
330
443
|
Self(Rc::default())
|
|
331
444
|
}
|
|
332
445
|
|
|
333
|
-
pub(crate) fn metadata(&self) -> MetadataRef
|
|
334
|
-
|
|
446
|
+
pub(crate) fn metadata(&self) -> MetadataRef {
|
|
447
|
+
MetadataRef(self.borrow().metadata().clone())
|
|
335
448
|
}
|
|
336
449
|
|
|
337
450
|
pub(crate) fn metadata_mut(&self) -> MetadataMutRef<'_> {
|
|
338
|
-
std::cell::RefMut::map(self.borrow_mut(), |x|
|
|
451
|
+
std::cell::RefMut::map(self.borrow_mut(), |x| x.metadata_mut())
|
|
339
452
|
}
|
|
340
453
|
|
|
454
|
+
/// The title as the UI and `save()` see it: the committed title, or the
|
|
455
|
+
/// latest pending rename.
|
|
341
456
|
pub(crate) fn get_title(&self) -> Option<String> {
|
|
342
|
-
self
|
|
457
|
+
let pending = self
|
|
458
|
+
.0
|
|
459
|
+
.queue
|
|
460
|
+
.pending_edits()
|
|
461
|
+
.into_iter()
|
|
462
|
+
.filter_map(|delta| match delta {
|
|
463
|
+
EditDelta::Title(title) => Some(title),
|
|
464
|
+
_ => None,
|
|
465
|
+
})
|
|
466
|
+
.next_back();
|
|
467
|
+
|
|
468
|
+
pending.unwrap_or_else(|| self.borrow().state.chrome.title.clone())
|
|
343
469
|
}
|
|
344
470
|
|
|
471
|
+
/// Rename this panel — a UI edit, committed by the drain in its turn.
|
|
345
472
|
pub fn set_title(&self, title: Option<String>) {
|
|
473
|
+
let title = title.filter(|x| !x.is_empty());
|
|
474
|
+
let session = self.clone();
|
|
475
|
+
let _ticket = self.submit(
|
|
476
|
+
OpKind::Edit {
|
|
477
|
+
delta: EditDelta::Title(title.clone()),
|
|
478
|
+
swaps_plugin: false,
|
|
479
|
+
},
|
|
480
|
+
move |_ctx| {
|
|
481
|
+
Box::pin(async move {
|
|
482
|
+
session.commit_title(title);
|
|
483
|
+
Ok(None)
|
|
484
|
+
})
|
|
485
|
+
},
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
self.title_changed.emit(self.get_title());
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/// Write the title NOW.
|
|
492
|
+
pub(crate) fn commit_title(&self, title: Option<String>) {
|
|
346
493
|
let new_title = title.filter(|x| !x.is_empty());
|
|
347
|
-
self.
|
|
494
|
+
let next = self.borrow().state.with_title(new_title.clone());
|
|
495
|
+
self.borrow_mut().swap(next);
|
|
348
496
|
self.title_changed.emit(new_title);
|
|
349
497
|
}
|
|
350
498
|
|
|
@@ -354,37 +502,26 @@ impl Session {
|
|
|
354
502
|
/// `Session`.
|
|
355
503
|
pub fn reset(&self, options: ResetOptions) -> impl Future<Output = ApiResult<()>> + use<> {
|
|
356
504
|
let view = self.0.borrow_mut().view_sub.take();
|
|
357
|
-
let had_table = self.borrow().table.is_some();
|
|
505
|
+
let had_table = self.borrow().table().is_some();
|
|
358
506
|
let err = self.get_error();
|
|
359
|
-
self.borrow_mut().
|
|
507
|
+
self.borrow_mut().clear_errors();
|
|
360
508
|
if options.stats {
|
|
361
509
|
self.update_stats(ViewStats::default());
|
|
362
510
|
}
|
|
363
511
|
|
|
364
512
|
if options.config {
|
|
365
|
-
self.
|
|
513
|
+
let mut config = self.borrow().config().clone();
|
|
514
|
+
config.reset(options.expressions);
|
|
515
|
+
self.borrow_mut().set_config(config);
|
|
366
516
|
}
|
|
367
517
|
|
|
368
518
|
if options.expressions {
|
|
369
|
-
self.borrow_mut().
|
|
519
|
+
self.borrow_mut().set_description(None);
|
|
370
520
|
}
|
|
371
521
|
|
|
372
522
|
match options.table {
|
|
373
|
-
Some(TableIntermediateState::Ejected) =>
|
|
374
|
-
|
|
375
|
-
self.borrow_mut().table = None;
|
|
376
|
-
self.borrow_mut().pending_table = None;
|
|
377
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
378
|
-
},
|
|
379
|
-
Some(TableIntermediateState::Reloaded) => {
|
|
380
|
-
self.borrow_mut().is_loading = true;
|
|
381
|
-
self.borrow_mut().table = None;
|
|
382
|
-
self.borrow_mut().pending_table = None;
|
|
383
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
384
|
-
},
|
|
385
|
-
_ => {
|
|
386
|
-
self.borrow_mut().is_loading = false;
|
|
387
|
-
},
|
|
523
|
+
Some(TableIntermediateState::Ejected) => self.borrow_mut().unbind(),
|
|
524
|
+
None => {},
|
|
388
525
|
};
|
|
389
526
|
|
|
390
527
|
// A config reset that KEEPS its `Table` is itself a commit, and every
|
|
@@ -409,56 +546,94 @@ impl Session {
|
|
|
409
546
|
}
|
|
410
547
|
}
|
|
411
548
|
|
|
412
|
-
///
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
*self.0.pending_load.borrow_mut() = Some(PendingLoad {
|
|
422
|
-
generation,
|
|
423
|
-
journal: Vec::new(),
|
|
549
|
+
/// Submit a writer of this panel's state.
|
|
550
|
+
pub fn submit(&self, kind: OpKind, step: impl FnOnce(OpCtx) -> StepFuture + 'static) -> Ticket {
|
|
551
|
+
if let Some(disposal) = self.disposal() {
|
|
552
|
+
return Ticket::settled(disposal.into());
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let is_edit = matches!(kind, OpKind::Edit {
|
|
556
|
+
delta: EditDelta::View(_),
|
|
557
|
+
..
|
|
424
558
|
});
|
|
425
|
-
|
|
426
|
-
|
|
559
|
+
|
|
560
|
+
let guard = self.begin_config_run();
|
|
561
|
+
let (ticket, start) = self.0.queue.push(kind, guard, step);
|
|
562
|
+
if is_edit {
|
|
563
|
+
self.notify_view_config_changed();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if start {
|
|
567
|
+
let queue = self.0.queue.clone();
|
|
568
|
+
let session = self.clone();
|
|
569
|
+
ApiFuture::spawn_named("op-queue-drain", async move {
|
|
570
|
+
queue
|
|
571
|
+
.drain(move |_exit| session.notify_view_config_changed())
|
|
572
|
+
.await;
|
|
573
|
+
|
|
574
|
+
Ok(())
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
ticket
|
|
427
579
|
}
|
|
428
580
|
|
|
429
|
-
///
|
|
430
|
-
///
|
|
431
|
-
|
|
432
|
-
|
|
581
|
+
/// Resolves once every op submitted to this panel has settled — its write
|
|
582
|
+
/// made (or rejected) AND its render landed.
|
|
583
|
+
pub async fn settle_ops(&self) {
|
|
584
|
+
self.0.queue.idle().await
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// Whether a `load()` is queued or running.
|
|
433
588
|
pub fn has_pending_load(&self) -> bool {
|
|
434
|
-
self.0.
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
///
|
|
438
|
-
///
|
|
439
|
-
///
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
589
|
+
self.0.queue.has_load()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/// The ONE `Live → Disposed` transition (this session's panel was ejected):
|
|
593
|
+
/// the state is frozen as its last value, everything queued settles per
|
|
594
|
+
/// `disposal`, and every later `submit` settles at once.
|
|
595
|
+
pub(crate) fn dispose(&self, disposal: Disposal) {
|
|
596
|
+
{
|
|
597
|
+
let mut data = self.borrow_mut();
|
|
598
|
+
if let Lifecycle::Live(last) = &data.state {
|
|
599
|
+
data.state = Lifecycle::Disposed {
|
|
600
|
+
last: last.clone(),
|
|
601
|
+
disposal,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
447
604
|
}
|
|
448
605
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
606
|
+
self.0.queue.reject_all(disposal.into());
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Take the bound `View` out WITHOUT deleting it, so it can feed a derived
|
|
610
|
+
/// `Table`, leaving the next bind nothing to dispose of.
|
|
611
|
+
pub fn take_view_sub(&self) -> Option<ViewSubscription> {
|
|
612
|
+
self.borrow_mut().view_sub.take()
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/// Put a subscription taken by [`Self::take_view_sub`] back, for a
|
|
616
|
+
/// derivation which could not complete.
|
|
617
|
+
pub fn put_view_sub(&self, sub: ViewSubscription) {
|
|
618
|
+
self.borrow_mut().view_sub = Some(sub);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
pub(crate) fn is_disposed(&self) -> bool {
|
|
622
|
+
self.disposal().is_some()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/// This session's disposal, if its panel was ejected.
|
|
626
|
+
pub(crate) fn disposal(&self) -> Option<Disposal> {
|
|
627
|
+
self.borrow().state.disposal()
|
|
453
628
|
}
|
|
454
629
|
|
|
455
630
|
pub(crate) fn has_table(&self) -> Option<TableLoadState> {
|
|
456
631
|
let data = self.borrow();
|
|
457
|
-
if data.table.is_some() {
|
|
632
|
+
if data.table().is_some() {
|
|
458
633
|
Some(TableLoadState::Loaded)
|
|
459
|
-
} else if
|
|
634
|
+
} else if self.0.queue.has_load() {
|
|
460
635
|
Some(TableLoadState::Loading)
|
|
461
|
-
} else if data.pending_table.is_some() {
|
|
636
|
+
} else if data.pending_table().is_some() {
|
|
462
637
|
Some(TableLoadState::Pending)
|
|
463
638
|
} else {
|
|
464
639
|
None
|
|
@@ -466,13 +641,15 @@ impl Session {
|
|
|
466
641
|
}
|
|
467
642
|
|
|
468
643
|
pub fn get_table(&self) -> Option<perspective_client::Table> {
|
|
469
|
-
self.borrow().table.
|
|
644
|
+
self.borrow().table().cloned()
|
|
470
645
|
}
|
|
471
646
|
|
|
647
|
+
/// Look this panel's tables up on `client` — unbinding it from a table of
|
|
648
|
+
/// any other client's.
|
|
472
649
|
pub fn set_client(&self, client: Client) -> bool {
|
|
473
|
-
if Some(&client) != self.
|
|
474
|
-
self.
|
|
475
|
-
self.borrow_mut().
|
|
650
|
+
if Some(&client) != self.get_client().as_ref() {
|
|
651
|
+
let next = self.borrow().state.unbound_on(Some(client));
|
|
652
|
+
self.borrow_mut().swap(next);
|
|
476
653
|
true
|
|
477
654
|
} else {
|
|
478
655
|
false
|
|
@@ -480,13 +657,13 @@ impl Session {
|
|
|
480
657
|
}
|
|
481
658
|
|
|
482
659
|
pub fn get_client(&self) -> Option<Client> {
|
|
483
|
-
self.borrow().client
|
|
660
|
+
self.borrow().state.client()
|
|
484
661
|
}
|
|
485
662
|
|
|
486
663
|
/// The configured table name awaiting a host, if any (see
|
|
487
664
|
/// [`SessionData::pending_table`]).
|
|
488
665
|
pub fn pending_table(&self) -> Option<String> {
|
|
489
|
-
self.borrow().pending_table.
|
|
666
|
+
self.borrow().pending_table().map(str::to_owned)
|
|
490
667
|
}
|
|
491
668
|
|
|
492
669
|
/// Suspend a BOUND table to PENDING: delete the `View`, drop the `Table`
|
|
@@ -496,60 +673,35 @@ impl Session {
|
|
|
496
673
|
/// as it was when the name is hosted again. `None` when no table is
|
|
497
674
|
/// bound.
|
|
498
675
|
pub fn suspend_table(&self) -> Option<impl Future<Output = ApiResult<()>> + use<>> {
|
|
499
|
-
let name = self.borrow().table
|
|
676
|
+
let name = self.borrow().table()?.get_name().to_owned();
|
|
677
|
+
let client = self.get_client()?;
|
|
500
678
|
let fut = self.reset(ResetOptions {
|
|
501
679
|
table: Some(TableIntermediateState::Ejected),
|
|
502
680
|
stats: true,
|
|
503
681
|
..ResetOptions::default()
|
|
504
682
|
});
|
|
505
683
|
|
|
506
|
-
self.
|
|
684
|
+
let next = self.borrow().state.awaiting_table(client, name);
|
|
685
|
+
self.borrow_mut().swap(next);
|
|
507
686
|
Some(fut)
|
|
508
687
|
}
|
|
509
688
|
|
|
510
|
-
///
|
|
511
|
-
/// `
|
|
512
|
-
|
|
513
|
-
///
|
|
514
|
-
/// # Arguments
|
|
515
|
-
///
|
|
516
|
-
/// - `table_name` The name of the `Table` to load.
|
|
517
|
-
///
|
|
518
|
-
/// # Returns
|
|
519
|
-
///
|
|
520
|
-
/// `table_name` is unique per `Client`, so if this value has not changed,
|
|
521
|
-
/// `Session::set_table` does nothing and returns `Ok(false)`.
|
|
522
|
-
///
|
|
523
|
-
/// A name no loaded client hosts pends ([`MissingTable::Pend`]) and
|
|
524
|
-
/// returns `Ok(false)` until the table lifecycle re-runs the bind.
|
|
525
|
-
pub async fn set_table(&self, table_name: String) -> ApiResult<bool> {
|
|
526
|
-
if Some(table_name.as_str()) == self.0.borrow().table.as_ref().map(|x| x.get_name()) {
|
|
527
|
-
self.0.borrow_mut().pending_table = None;
|
|
528
|
-
return Ok(false);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
let client = self.0.borrow().client.clone().into_apierror()?;
|
|
532
|
-
match probe_table(&client, &table_name, MissingTable::Pend).await? {
|
|
533
|
-
Some(table) => match self.bind_table(table).await {
|
|
534
|
-
Ok(()) => Ok(true),
|
|
535
|
-
Err(err) => self.set_error(false, err).await.map(|_| false),
|
|
536
|
-
},
|
|
537
|
-
None => {
|
|
538
|
-
self.pend_table(table_name).await?;
|
|
539
|
-
Ok(false)
|
|
540
|
-
},
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/// Bind an opened `Table`, replacing any previous binding and announcing
|
|
545
|
-
/// `table_loaded`.
|
|
546
|
-
pub(crate) async fn bind_table(&self, table: perspective_client::Table) -> ApiResult<()> {
|
|
547
|
-
let metadata = SessionMetadata::from_table(&table).await?;
|
|
548
|
-
let client = table.get_client();
|
|
689
|
+
/// Record a connection error on `client` as this panel's lost binding — for
|
|
690
|
+
/// as long as `client` is the one the panel is bound through.
|
|
691
|
+
async fn watch_client(&self, client: &Client) -> ApiResult<()> {
|
|
549
692
|
let on_error = self.on_table_errored.borrow().clone();
|
|
550
693
|
let session = self.clone();
|
|
694
|
+
let watched = client.clone();
|
|
551
695
|
let poll_loop = LocalPollLoop::new(move |(message, reconnect): (ApiError, _)| {
|
|
552
|
-
session.
|
|
696
|
+
if session.get_client().as_ref() != Some(&watched) {
|
|
697
|
+
return Ok(JsValue::UNDEFINED);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let next = session
|
|
701
|
+
.borrow()
|
|
702
|
+
.state
|
|
703
|
+
.with_lost(TableErrorState(message, reconnect));
|
|
704
|
+
session.borrow_mut().swap(next);
|
|
553
705
|
if let Some(cb) = &on_error {
|
|
554
706
|
cb.emit(());
|
|
555
707
|
}
|
|
@@ -570,29 +722,9 @@ impl Session {
|
|
|
570
722
|
}))
|
|
571
723
|
.await?;
|
|
572
724
|
|
|
573
|
-
let sub = self.borrow_mut().view_sub.take();
|
|
574
|
-
self.borrow_mut().metadata = metadata;
|
|
575
|
-
self.borrow_mut().table = Some(table);
|
|
576
|
-
self.borrow_mut().pending_table = None;
|
|
577
|
-
self.borrow_mut().is_loading = false;
|
|
578
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
579
|
-
sub.delete().await?;
|
|
580
|
-
self.table_loaded.emit(());
|
|
581
725
|
Ok(())
|
|
582
726
|
}
|
|
583
727
|
|
|
584
|
-
/// Record `name` as pending, dropping any bound `Table` so nothing binds
|
|
585
|
-
/// against the outgoing table while the name waits for a host.
|
|
586
|
-
pub(crate) async fn pend_table(&self, name: String) -> ApiResult<()> {
|
|
587
|
-
let sub = self.borrow_mut().view_sub.take();
|
|
588
|
-
self.borrow_mut().table = None;
|
|
589
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
590
|
-
self.borrow_mut().pending_table = Some(name);
|
|
591
|
-
self.borrow_mut().is_loading = false;
|
|
592
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
593
|
-
sub.delete().await
|
|
594
|
-
}
|
|
595
|
-
|
|
596
728
|
pub async fn set_error(&self, reset_table: bool, err: ApiError) -> ApiResult<()> {
|
|
597
729
|
let session = self.clone();
|
|
598
730
|
let poll_loop = LocalPollLoop::new(move |()| {
|
|
@@ -604,7 +736,7 @@ impl Session {
|
|
|
604
736
|
Ok(JsValue::UNDEFINED)
|
|
605
737
|
});
|
|
606
738
|
|
|
607
|
-
|
|
739
|
+
let error = TableErrorState(
|
|
608
740
|
err.clone(),
|
|
609
741
|
Some(ReconnectCallback::new(move || {
|
|
610
742
|
clone!(poll_loop);
|
|
@@ -613,7 +745,10 @@ impl Session {
|
|
|
613
745
|
Ok(())
|
|
614
746
|
})
|
|
615
747
|
})),
|
|
616
|
-
)
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
let next = self.borrow().state.with_lost(error);
|
|
751
|
+
self.borrow_mut().swap(next);
|
|
617
752
|
|
|
618
753
|
if let Some(cb) = self.on_table_errored.borrow().as_ref() {
|
|
619
754
|
cb.emit(());
|
|
@@ -621,7 +756,7 @@ impl Session {
|
|
|
621
756
|
|
|
622
757
|
let sub = self.borrow_mut().view_sub.take();
|
|
623
758
|
if reset_table {
|
|
624
|
-
self.borrow_mut().
|
|
759
|
+
self.borrow_mut().unbind();
|
|
625
760
|
}
|
|
626
761
|
|
|
627
762
|
sub.delete().await?;
|
|
@@ -651,24 +786,38 @@ impl Session {
|
|
|
651
786
|
}
|
|
652
787
|
|
|
653
788
|
pub fn js_get_table(&self) -> Option<JsValue> {
|
|
654
|
-
Some(perspective_js::Table::from(self.borrow().table.
|
|
789
|
+
Some(perspective_js::Table::from(self.borrow().table().cloned()?).into())
|
|
655
790
|
}
|
|
656
791
|
|
|
792
|
+
/// Whether the binding is lost.
|
|
657
793
|
pub(crate) fn is_errored(&self) -> bool {
|
|
658
|
-
self.borrow().error.is_some()
|
|
794
|
+
self.borrow().error().is_some()
|
|
659
795
|
}
|
|
660
796
|
|
|
661
797
|
pub(crate) fn get_error(&self) -> Option<ApiError> {
|
|
662
|
-
self.borrow().error
|
|
798
|
+
self.borrow().error().map(|x| x.0.clone())
|
|
663
799
|
}
|
|
664
800
|
|
|
801
|
+
/// Recover from an error state (the overlay's button) — a writer, so an op
|
|
802
|
+
/// on the queue like any other.
|
|
665
803
|
pub async fn reconnect(&self) -> ApiResult<()> {
|
|
666
|
-
let
|
|
804
|
+
let session = self.clone();
|
|
805
|
+
self.submit(OpKind::Restore { update: None }, move |_ctx| {
|
|
806
|
+
Box::pin(async move {
|
|
807
|
+
session.reconnect_step().await?;
|
|
808
|
+
Ok(None)
|
|
809
|
+
})
|
|
810
|
+
})
|
|
811
|
+
.settle()
|
|
812
|
+
.await
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async fn reconnect_step(&self) -> ApiResult<()> {
|
|
816
|
+
let err = self.borrow().error().cloned();
|
|
667
817
|
if let Some(TableErrorState(_, Some(reconnect))) = err {
|
|
668
818
|
reconnect().await?;
|
|
669
|
-
self.borrow_mut().
|
|
670
|
-
self.borrow_mut().
|
|
671
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
819
|
+
self.borrow_mut().clear_errors();
|
|
820
|
+
self.borrow_mut().set_description(None);
|
|
672
821
|
self.borrow_mut().view_sub = None;
|
|
673
822
|
self.table_loaded.emit(());
|
|
674
823
|
}
|
|
@@ -704,36 +853,93 @@ impl Session {
|
|
|
704
853
|
.is_some_and(|s| s.num_table_cells.is_some())
|
|
705
854
|
}
|
|
706
855
|
|
|
707
|
-
|
|
708
|
-
|
|
856
|
+
/// The view config as the UI and `save()` see it: the committed config with
|
|
857
|
+
/// every pending UI edit applied, in submit order.
|
|
858
|
+
pub fn get_view_config(&self) -> ViewConfigRef {
|
|
859
|
+
ViewConfigRef(self.projected())
|
|
709
860
|
}
|
|
710
861
|
|
|
711
|
-
/// The
|
|
712
|
-
///
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
862
|
+
/// The COMMITTED view config — what a running op must read, since the edits
|
|
863
|
+
/// still queued behind it have not happened yet.
|
|
864
|
+
pub fn committed_view_config(&self) -> ViewConfigRef {
|
|
865
|
+
ViewConfigRef(self.borrow().state.config().clone())
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
fn projected(&self) -> Rc<ViewConfig> {
|
|
869
|
+
let committed = self.borrow().state.config().clone();
|
|
870
|
+
let edits = self
|
|
871
|
+
.0
|
|
872
|
+
.queue
|
|
873
|
+
.pending_edits()
|
|
874
|
+
.into_iter()
|
|
875
|
+
.filter_map(|delta| match delta {
|
|
876
|
+
EditDelta::View(delta) => Some(*delta),
|
|
877
|
+
_ => None,
|
|
878
|
+
})
|
|
879
|
+
.collect::<Vec<_>>();
|
|
880
|
+
|
|
881
|
+
if edits.is_empty() {
|
|
882
|
+
return committed;
|
|
719
883
|
}
|
|
720
884
|
|
|
721
|
-
let mut config =
|
|
722
|
-
|
|
723
|
-
|
|
885
|
+
let mut config = (*committed).clone();
|
|
886
|
+
for delta in edits {
|
|
887
|
+
let mut candidate = config.clone();
|
|
888
|
+
if candidate.apply_update(delta) && self.validate_names(&candidate).is_ok() {
|
|
889
|
+
self.normalize_view_config(&mut candidate);
|
|
890
|
+
config = candidate;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
Rc::new(config)
|
|
724
895
|
}
|
|
725
896
|
|
|
726
|
-
///
|
|
727
|
-
///
|
|
728
|
-
///
|
|
729
|
-
pub fn
|
|
730
|
-
if self.borrow().
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
897
|
+
/// Whether `delta` is an edit the UI may submit: SYNCHRONOUS validation
|
|
898
|
+
/// against the projection, so an invalid edit is refused at the control
|
|
899
|
+
/// that made it.
|
|
900
|
+
pub fn check_edit(&self, delta: &ViewConfigUpdate) -> ApiResult<()> {
|
|
901
|
+
if let Some(x) = self.borrow().state.lost() {
|
|
902
|
+
return Err(ApiError::new(x.0.clone()));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
let mut candidate = (*self.projected()).clone();
|
|
906
|
+
if candidate.apply_update(delta.clone()) {
|
|
907
|
+
self.validate_names(&candidate)?;
|
|
736
908
|
}
|
|
909
|
+
|
|
910
|
+
Ok(())
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/// The effective [`ViewConfig`] the `View` is built from — the one the
|
|
914
|
+
/// committed description describes.
|
|
915
|
+
fn effective_view_config(&self) -> Rc<ViewConfig> {
|
|
916
|
+
let data = self.borrow();
|
|
917
|
+
if let Some((effective, _)) = data.description() {
|
|
918
|
+
return effective.clone();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
let metadata = data.metadata().clone();
|
|
922
|
+
let (config, _) = effective(data.config(), &data.state.overlay, &|name| {
|
|
923
|
+
metadata.get_column_table_type(name)
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
Rc::new(config)
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/// The overlay clauses this panel's table cannot honor, by index — a column
|
|
930
|
+
/// it lacks, or has with another type.
|
|
931
|
+
pub fn skipped_overlay(&self) -> Vec<usize> {
|
|
932
|
+
let data = self.borrow();
|
|
933
|
+
let metadata = data.metadata().clone();
|
|
934
|
+
effective(data.config(), &data.state.overlay, &|name| {
|
|
935
|
+
metadata.get_column_table_type(name)
|
|
936
|
+
})
|
|
937
|
+
.1
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/// The element's global filter as last broadcast to this panel.
|
|
941
|
+
pub(crate) fn committed_overlay(&self) -> Rc<Vec<OverlayClause>> {
|
|
942
|
+
self.borrow().state.overlay.clone()
|
|
737
943
|
}
|
|
738
944
|
|
|
739
945
|
/// Snapshot of the [`ViewConfig`] the currently-bound `View` was
|
|
@@ -742,9 +948,8 @@ impl Session {
|
|
|
742
948
|
///
|
|
743
949
|
/// Prefer this over [`Self::get_view_config`] when you need a
|
|
744
950
|
/// value consistent with what the active plugin is rendering.
|
|
745
|
-
/// `get_view_config` returns the
|
|
746
|
-
///
|
|
747
|
-
/// queued run and so may temporarily disagree with the bound `View`.
|
|
951
|
+
/// `get_view_config` returns the PROJECTED config — committed state plus
|
|
952
|
+
/// pending edits — which runs ahead of the bound `View`.
|
|
748
953
|
pub fn get_rendered_view_config(&self) -> Option<Rc<ViewConfig>> {
|
|
749
954
|
self.borrow().view_sub.as_ref().map(|s| s.get_view_config())
|
|
750
955
|
}
|
|
@@ -755,7 +960,7 @@ impl Session {
|
|
|
755
960
|
config_static: &PluginStaticConfig,
|
|
756
961
|
) {
|
|
757
962
|
use self::column_defaults_update::*;
|
|
758
|
-
let config = self.
|
|
963
|
+
let config = self.committed_view_config();
|
|
759
964
|
config_update.set_update_column_defaults(
|
|
760
965
|
&self.metadata(),
|
|
761
966
|
&config,
|
|
@@ -777,72 +982,18 @@ impl Session {
|
|
|
777
982
|
use self::column_defaults_update::*;
|
|
778
983
|
config_update.set_update_rollup_defaults(
|
|
779
984
|
&self.metadata(),
|
|
780
|
-
&self.
|
|
985
|
+
&self.committed_view_config(),
|
|
781
986
|
config_static,
|
|
782
987
|
)
|
|
783
988
|
}
|
|
784
989
|
|
|
785
|
-
///
|
|
786
|
-
///
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
///
|
|
790
|
-
/// Validation is SYNCHRONOUS and happens before anything is applied: an
|
|
791
|
-
/// update naming an unknown column is rejected with `Err` and the config
|
|
792
|
-
/// is untouched (I4 — invalid state is never entered, so no rollback
|
|
793
|
-
/// path exists). Server-side expression compilation is deliberately NOT
|
|
794
|
-
/// checked here; it is a property of a pipeline run
|
|
795
|
-
/// ([`Self::validate_snapshot`]) and fails that run, never the commit.
|
|
796
|
-
pub fn commit_view_config(&self, config_update: ViewConfigUpdate) -> ApiResult<()> {
|
|
797
|
-
if let Some(x) = self.borrow().error.as_ref() {
|
|
798
|
-
tracing::warn!("Errored state");
|
|
799
|
-
|
|
800
|
-
// Load bearing return
|
|
801
|
-
return Err(ApiError::new(x.0.clone()));
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// A `load()` is classifying its payload (see
|
|
805
|
-
// [`SessionHandle::pending_load`]): record the RAW delta so a `Table`
|
|
806
|
-
// classification can replay it over the reset base, validated against
|
|
807
|
-
// the INCOMING table's schema. The live apply below is a best-effort
|
|
808
|
-
// preview (kept for `save()` coherence, I1) that SKIPS name validation
|
|
809
|
-
// — the delta may legitimately name the incoming table's columns,
|
|
810
|
-
// absent from the outgoing one — mirroring the "no table bound yet"
|
|
811
|
-
// leniency already in `validate_names`. Authoritative validation is
|
|
812
|
-
// deferred to the replay.
|
|
813
|
-
let pending = self.has_pending_load();
|
|
814
|
-
let mut candidate = self.borrow().config.clone();
|
|
815
|
-
let journal_entry = pending.then(|| config_update.clone());
|
|
816
|
-
if !candidate.apply_update(config_update) {
|
|
817
|
-
return Ok(());
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if let Some(entry) = journal_entry
|
|
821
|
-
&& let Some(p) = self.0.pending_load.borrow_mut().as_mut()
|
|
822
|
-
{
|
|
823
|
-
p.journal.push(entry);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if !pending {
|
|
827
|
-
self.validate_names(&candidate)?;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
self.normalize_view_config(&mut candidate);
|
|
831
|
-
self.borrow_mut().config = candidate;
|
|
832
|
-
self.notify_view_config_changed();
|
|
833
|
-
Ok(())
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
/// Table-bind commit: normalize the (possibly empty) config against the
|
|
837
|
-
/// newly-bound table's metadata — the default-view materialization that
|
|
838
|
-
/// previously happened inside the async validate write-back. SYNC;
|
|
839
|
-
/// called immediately after `set_table().await` inside the binding run,
|
|
840
|
-
/// so it is ordered like any other commit.
|
|
841
|
-
pub fn commit_table_defaults(&self) {
|
|
842
|
-
let mut candidate = self.borrow().config.clone();
|
|
990
|
+
/// Re-normalize the config against the bound table's metadata — the
|
|
991
|
+
/// default-view materialization after a config reset that keeps its table.
|
|
992
|
+
fn commit_table_defaults(&self) {
|
|
993
|
+
let mut candidate = self.borrow().config().clone();
|
|
843
994
|
self.normalize_view_config(&mut candidate);
|
|
844
|
-
if candidate != self.borrow().config {
|
|
845
|
-
self.borrow_mut().
|
|
995
|
+
if candidate != *self.borrow().config() {
|
|
996
|
+
self.borrow_mut().set_config(candidate);
|
|
846
997
|
self.notify_view_config_changed();
|
|
847
998
|
}
|
|
848
999
|
}
|
|
@@ -854,7 +1005,13 @@ impl Session {
|
|
|
854
1005
|
/// when no table is bound yet: the config rides along until `load()`
|
|
855
1006
|
/// binds one, and the engine surfaces any residual error on that run.
|
|
856
1007
|
fn validate_names(&self, config: &ViewConfig) -> ApiResult<()> {
|
|
857
|
-
|
|
1008
|
+
Self::validate_names_with(&self.metadata(), config)
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/// [`Self::validate_names`] against an explicit table's metadata — the
|
|
1012
|
+
/// INCOMING table's, for a restore that binds one.
|
|
1013
|
+
fn validate_names_with(metadata: &SessionMetadata, config: &ViewConfig) -> ApiResult<()> {
|
|
1014
|
+
let table_columns = Self::columns_of(metadata);
|
|
858
1015
|
if table_columns.is_empty() {
|
|
859
1016
|
return Ok(());
|
|
860
1017
|
}
|
|
@@ -897,7 +1054,11 @@ impl Session {
|
|
|
897
1054
|
/// write-back): fill empty `columns` from the table, prune `aggregates`
|
|
898
1055
|
/// to referenced columns.
|
|
899
1056
|
fn normalize_view_config(&self, config: &mut ViewConfig) {
|
|
900
|
-
|
|
1057
|
+
Self::normalize_with(&self.metadata(), config)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
fn normalize_with(metadata: &SessionMetadata, config: &mut ViewConfig) {
|
|
1061
|
+
let table_columns = Self::columns_of(metadata);
|
|
901
1062
|
if table_columns.is_empty() {
|
|
902
1063
|
return;
|
|
903
1064
|
}
|
|
@@ -927,19 +1088,10 @@ impl Session {
|
|
|
927
1088
|
/// [`Self::settle_dispatches`]. Used by the `perspective-config-update`
|
|
928
1089
|
/// dispatcher, whose landing `flush()` must be able to await.
|
|
929
1090
|
pub fn track_dispatch(&self, fut: impl std::future::Future<Output = ApiResult<()>> + 'static) {
|
|
930
|
-
self.0
|
|
931
|
-
.pending_dispatches
|
|
932
|
-
.set(self.0.pending_dispatches.get() + 1);
|
|
933
|
-
let session = self.clone();
|
|
1091
|
+
let guard = self.0.dispatches.guard();
|
|
934
1092
|
ApiFuture::spawn_named("config-update-dispatch", async move {
|
|
935
|
-
let
|
|
936
|
-
let
|
|
937
|
-
session.0.pending_dispatches.set(remaining);
|
|
938
|
-
if remaining == 0 {
|
|
939
|
-
session.0.dispatches_settled.emit(());
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if let Err(e) = result {
|
|
1093
|
+
let _guard = guard;
|
|
1094
|
+
if let Err(e) = fut.await {
|
|
943
1095
|
tracing::error!("[config-update dispatch] {}", e);
|
|
944
1096
|
}
|
|
945
1097
|
|
|
@@ -950,10 +1102,7 @@ impl Session {
|
|
|
950
1102
|
/// Resolve once every in-flight tracked dispatch for this panel has
|
|
951
1103
|
/// landed. Immediate when none are pending.
|
|
952
1104
|
pub async fn settle_dispatches(&self) -> ApiResult<()> {
|
|
953
|
-
|
|
954
|
-
self.0.dispatches_settled.read_next().await?;
|
|
955
|
-
}
|
|
956
|
-
|
|
1105
|
+
self.0.dispatches.settle().await;
|
|
957
1106
|
Ok(())
|
|
958
1107
|
}
|
|
959
1108
|
|
|
@@ -973,26 +1122,10 @@ impl Session {
|
|
|
973
1122
|
}
|
|
974
1123
|
|
|
975
1124
|
/// Begin spinner accounting for ONE config-driven pipeline run: call
|
|
976
|
-
/// immediately after the run's commit, and move the returned
|
|
977
|
-
/// the run future.
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
/// count cannot strand by construction.
|
|
981
|
-
/// [`SessionHandle::run_state_changed`] broadcasts the ABSOLUTE count
|
|
982
|
-
/// on both edges.
|
|
983
|
-
pub fn begin_config_run(&self) -> ConfigRunToken {
|
|
984
|
-
let count = self.0.in_flight_config_runs.get() + 1;
|
|
985
|
-
self.0.in_flight_config_runs.set(count);
|
|
986
|
-
self.run_state_changed.emit(count);
|
|
987
|
-
ConfigRunToken(self.clone())
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/// The number of config-driven pipeline runs currently in flight (see
|
|
991
|
-
/// [`Self::begin_config_run`]). Read directly when (re)targeting a
|
|
992
|
-
/// panel — level-triggered consumers initialize from this and then
|
|
993
|
-
/// track [`SessionHandle::run_state_changed`].
|
|
994
|
-
pub fn in_flight_config_runs(&self) -> u32 {
|
|
995
|
-
self.0.in_flight_config_runs.get()
|
|
1125
|
+
/// immediately after the run's commit, and move the returned guard INTO
|
|
1126
|
+
/// the run future.
|
|
1127
|
+
pub fn begin_config_run(&self) -> InFlightGuard {
|
|
1128
|
+
self.0.config_runs.guard()
|
|
996
1129
|
}
|
|
997
1130
|
|
|
998
1131
|
/// Read the cached `ColumnStats` for a column. Returns `None` if no
|
|
@@ -1031,53 +1164,336 @@ impl Session {
|
|
|
1031
1164
|
/// that preceded run *N*'s completion (invariant I3).
|
|
1032
1165
|
pub fn snapshot(&self, _guard: &RenderGuard) -> ConfigSnapshot {
|
|
1033
1166
|
ConfigSnapshot {
|
|
1034
|
-
config:
|
|
1035
|
-
effective:
|
|
1167
|
+
config: self.borrow().state.config().clone(),
|
|
1168
|
+
effective: self.effective_view_config(),
|
|
1036
1169
|
}
|
|
1037
1170
|
}
|
|
1038
1171
|
|
|
1039
|
-
/// Validate a snapshot
|
|
1040
|
-
///
|
|
1041
|
-
/// schema. Reads nothing from — and writes nothing to — the live
|
|
1042
|
-
/// config; a failure fails this RUN, never the committed config (I4).
|
|
1043
|
-
///
|
|
1044
|
-
/// Memoized: when the snapshot's expressions equal the last successfully
|
|
1045
|
-
/// validated set, the round trip is skipped entirely — safe because the
|
|
1046
|
-
/// memo key is an immutable snapshot field and the recorded set is
|
|
1047
|
-
/// written only under the draw lock.
|
|
1172
|
+
/// Validate a snapshot against the server with `Table::describe`, failing
|
|
1173
|
+
/// this run and never the committed config.
|
|
1048
1174
|
pub async fn validate_snapshot(
|
|
1049
1175
|
&self,
|
|
1050
1176
|
_guard: &RenderGuard,
|
|
1051
1177
|
snap: ConfigSnapshot,
|
|
1052
1178
|
) -> ApiResult<ValidatedSnapshot> {
|
|
1053
|
-
let
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
.
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1179
|
+
let (description, fresh) = self.describe_effective(&snap.effective).await?;
|
|
1180
|
+
if fresh {
|
|
1181
|
+
tracing::warn!("Rendering a commit that was not described");
|
|
1182
|
+
Self::record_description(&mut self.metadata_mut(), &snap.effective, &description)?;
|
|
1183
|
+
self.borrow_mut()
|
|
1184
|
+
.set_description(Some((snap.effective.clone(), description.clone())));
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
Ok(ValidatedSnapshot { snap })
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/// The server's [`Description`] of `effective`, and whether it took a round
|
|
1191
|
+
/// trip (`false` when the memo already held it).
|
|
1192
|
+
async fn describe_effective(
|
|
1193
|
+
&self,
|
|
1194
|
+
effective: &Rc<ViewConfig>,
|
|
1195
|
+
) -> ApiResult<(Rc<Description>, bool)> {
|
|
1196
|
+
let memo = self
|
|
1197
|
+
.borrow()
|
|
1198
|
+
.description()
|
|
1199
|
+
.cloned()
|
|
1200
|
+
.filter(|(key, _)| **key == **effective)
|
|
1201
|
+
.map(|(_, description)| description);
|
|
1202
|
+
|
|
1203
|
+
if let Some(description) = memo {
|
|
1204
|
+
return Ok((description, false));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
let table = self
|
|
1208
|
+
.borrow()
|
|
1209
|
+
.table()
|
|
1210
|
+
.cloned()
|
|
1211
|
+
.ok_or_else(|| apierror!(NoTableError))?;
|
|
1212
|
+
|
|
1213
|
+
Self::describe_with(&table, &self.metadata(), effective)
|
|
1214
|
+
.await
|
|
1215
|
+
.map(|description| (description, true))
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/// `table`'s [`Description`] of `effective`, with validation failures as
|
|
1219
|
+
/// the errors a `restore()` rejects with.
|
|
1220
|
+
async fn describe_with(
|
|
1221
|
+
table: &perspective_client::Table,
|
|
1222
|
+
metadata: &SessionMetadata,
|
|
1223
|
+
effective: &Rc<ViewConfig>,
|
|
1224
|
+
) -> ApiResult<Rc<Description>> {
|
|
1225
|
+
let engine_config = Self::with_default_aggregates_of(metadata, effective);
|
|
1226
|
+
match table.describe(engine_config.into()).await? {
|
|
1227
|
+
Ok(description) => Ok(Rc::new(description)),
|
|
1228
|
+
Err(DescribeError::Expressions {
|
|
1229
|
+
expression_schema,
|
|
1230
|
+
errors,
|
|
1231
|
+
}) => Err(apierror!(InvalidViewerConfigExpressionsError(Rc::new(
|
|
1232
|
+
ExprValidationResult {
|
|
1233
|
+
expression_schema,
|
|
1234
|
+
errors,
|
|
1235
|
+
expression_alias: effective.expressions.0.clone(),
|
|
1236
|
+
}
|
|
1237
|
+
)))),
|
|
1238
|
+
Err(DescribeError::Config(msg)) => Err(ApiError::new(msg)),
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/// The PREPARE half of a transactional restore's view of the panel: the
|
|
1243
|
+
/// binding `plan` resolved, and the config it leaves — the committed one (a
|
|
1244
|
+
/// default one, for a plan that resets) with `update` applied —
|
|
1245
|
+
/// name-checked, normalized and DESCRIBED by the table it will be bound to.
|
|
1246
|
+
pub(crate) async fn prepare_view(
|
|
1247
|
+
&self,
|
|
1248
|
+
plan: BindPlan,
|
|
1249
|
+
mut update: ViewConfigUpdate,
|
|
1250
|
+
defaults: ViewDefaults<'_>,
|
|
1251
|
+
overlay: Option<Rc<Vec<OverlayClause>>>,
|
|
1252
|
+
) -> ApiResult<PreparedView> {
|
|
1253
|
+
if self.is_disposed() {
|
|
1254
|
+
return Err(ApiError::new("Panel disposed"));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if matches!(plan, BindPlan::Keep)
|
|
1258
|
+
&& let Some(error) = self.borrow().state.lost()
|
|
1259
|
+
{
|
|
1260
|
+
return Err(error.0.clone());
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
let committed = self.borrow().state.config().clone();
|
|
1264
|
+
let base = match &plan {
|
|
1265
|
+
BindPlan::Bind { reset: true, .. } | BindPlan::Pend { reset: true, .. } => {
|
|
1266
|
+
Rc::new(ViewConfig::default())
|
|
1267
|
+
},
|
|
1268
|
+
_ => committed.clone(),
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
let (binding, checked) = match plan {
|
|
1272
|
+
BindPlan::Keep => {
|
|
1273
|
+
let bound = self.borrow().state.bound().cloned();
|
|
1274
|
+
(
|
|
1275
|
+
PreparedBinding::Keep,
|
|
1276
|
+
bound.map(|bound| (bound.table.clone(), bound.metadata.clone())),
|
|
1277
|
+
)
|
|
1278
|
+
},
|
|
1279
|
+
BindPlan::Bind { table, client, .. } => {
|
|
1280
|
+
let metadata = Rc::new(SessionMetadata::from_table(&table).await?);
|
|
1281
|
+
let binding = PreparedBinding::Bind {
|
|
1282
|
+
client: client.clone(),
|
|
1283
|
+
table: table.clone(),
|
|
1284
|
+
metadata: metadata.clone(),
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
(binding, Some((*table, metadata)))
|
|
1288
|
+
},
|
|
1289
|
+
BindPlan::Pend { client, name, .. } => (PreparedBinding::Pend { client, name }, None),
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
{
|
|
1293
|
+
use self::column_defaults_update::*;
|
|
1294
|
+
let metadata = match &checked {
|
|
1295
|
+
Some((_, metadata)) => metadata.clone(),
|
|
1296
|
+
None => self.borrow().metadata().clone(),
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
match defaults {
|
|
1300
|
+
ViewDefaults::Swap(plugin) => {
|
|
1301
|
+
update.set_update_column_defaults(&metadata, &base, &base.columns, plugin)
|
|
1302
|
+
},
|
|
1303
|
+
ViewDefaults::Rollup(plugin) => {
|
|
1304
|
+
update.set_update_rollup_defaults(&metadata, &base, plugin)
|
|
1305
|
+
},
|
|
1306
|
+
ViewDefaults::AsGiven => {},
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
let mut candidate = (*base).clone();
|
|
1311
|
+
candidate.apply_update(update);
|
|
1312
|
+
if let Some((_, metadata)) = &checked {
|
|
1313
|
+
Self::validate_names_with(metadata, &candidate)?;
|
|
1314
|
+
Self::normalize_with(metadata, &mut candidate);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
let config = if candidate == *committed {
|
|
1318
|
+
committed.clone()
|
|
1319
|
+
} else {
|
|
1320
|
+
Rc::new(candidate)
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
let changed = !Rc::ptr_eq(&config, &committed);
|
|
1324
|
+
let clauses = overlay
|
|
1325
|
+
.clone()
|
|
1326
|
+
.unwrap_or_else(|| self.borrow().state.overlay.clone());
|
|
1327
|
+
|
|
1328
|
+
let (effective, description) = match &checked {
|
|
1329
|
+
None => (config.clone(), None),
|
|
1330
|
+
Some((table, metadata)) => {
|
|
1331
|
+
let on_expression = clauses
|
|
1332
|
+
.iter()
|
|
1333
|
+
.any(|x| config.expressions.0.contains_key(x.filter.column()));
|
|
1334
|
+
|
|
1335
|
+
let bare = if on_expression {
|
|
1336
|
+
Some(Self::describe_with(table, metadata, &config).await?)
|
|
1337
|
+
} else {
|
|
1338
|
+
None
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
let (effective, _) = effective(&config, &clauses, &|name| {
|
|
1342
|
+
metadata.get_table_schema_type(name).or_else(|| {
|
|
1343
|
+
bare.as_ref()
|
|
1344
|
+
.and_then(|x| x.expression_schema.get(name).copied())
|
|
1345
|
+
})
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
let effective = if effective == *config {
|
|
1349
|
+
config.clone()
|
|
1350
|
+
} else {
|
|
1351
|
+
Rc::new(effective)
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
let description = match (&binding, bare) {
|
|
1355
|
+
(_, Some(bare)) if Rc::ptr_eq(&effective, &config) => bare,
|
|
1356
|
+
(PreparedBinding::Keep, _) => self.describe_effective(&effective).await?.0,
|
|
1357
|
+
_ => Self::describe_with(table, metadata, &effective).await?,
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
(effective, Some(description))
|
|
1361
|
+
},
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
Ok(PreparedView {
|
|
1365
|
+
binding,
|
|
1366
|
+
overlay,
|
|
1367
|
+
config,
|
|
1368
|
+
effective,
|
|
1369
|
+
description,
|
|
1370
|
+
changed,
|
|
1371
|
+
announce: true,
|
|
1372
|
+
})
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/// The COMMIT half of [`Self::prepare_view`], as one swap of this session's
|
|
1376
|
+
/// state.
|
|
1377
|
+
pub(crate) fn commit_view(
|
|
1378
|
+
&self,
|
|
1379
|
+
view: PreparedView,
|
|
1380
|
+
title: Option<Option<String>>,
|
|
1381
|
+
) -> BindingEffects {
|
|
1382
|
+
let state = self.borrow().state.clone();
|
|
1383
|
+
let title_before = state.chrome.title.clone();
|
|
1384
|
+
let had_table = state.bound().is_some();
|
|
1385
|
+
let (rebound, watch) = match view.binding {
|
|
1386
|
+
PreparedBinding::Keep => (None, None),
|
|
1387
|
+
PreparedBinding::Bind {
|
|
1388
|
+
client,
|
|
1389
|
+
table,
|
|
1390
|
+
metadata,
|
|
1391
|
+
} => (
|
|
1392
|
+
Some(state.bound_to(*table, (*metadata).clone())),
|
|
1393
|
+
Some(client),
|
|
1394
|
+
),
|
|
1395
|
+
PreparedBinding::Pend { client, name } => {
|
|
1396
|
+
(Some(state.awaiting_table(client, name)), None)
|
|
1397
|
+
},
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
let rebinds = rebound.is_some();
|
|
1401
|
+
let mut next = rebound.unwrap_or_else(|| (*state).clone());
|
|
1402
|
+
next = next.with_config(view.config.clone());
|
|
1403
|
+
if let Some(overlay) = view.overlay {
|
|
1404
|
+
next = next.with_overlay(overlay);
|
|
1405
|
+
}
|
|
1073
1406
|
|
|
1074
|
-
|
|
1407
|
+
if let Some(description) = view.description {
|
|
1408
|
+
if let Some(bound) = next.bound() {
|
|
1409
|
+
let mut metadata = (*bound.metadata).clone();
|
|
1410
|
+
if Self::record_description(&mut metadata, &view.effective, &description).is_ok() {
|
|
1411
|
+
next = next.with_metadata(Rc::new(metadata));
|
|
1412
|
+
}
|
|
1075
1413
|
}
|
|
1076
1414
|
|
|
1077
|
-
|
|
1415
|
+
next = next.with_description(Some((view.effective, description)));
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
let next = match title {
|
|
1419
|
+
Some(title) => next.with_title(title),
|
|
1420
|
+
None => next,
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
let title_after = next.chrome.title.clone();
|
|
1424
|
+
let bound = next.bound().is_some();
|
|
1425
|
+
self.borrow_mut().swap(next);
|
|
1426
|
+
let outgoing = if rebinds {
|
|
1427
|
+
self.borrow_mut().view_sub.take()
|
|
1428
|
+
} else {
|
|
1429
|
+
None
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
if rebinds {
|
|
1433
|
+
self.update_stats(ViewStats::default());
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (view.changed && view.announce) || rebinds {
|
|
1437
|
+
self.notify_view_config_changed();
|
|
1078
1438
|
}
|
|
1079
1439
|
|
|
1080
|
-
|
|
1440
|
+
if title_before != title_after {
|
|
1441
|
+
self.title_changed.emit(title_after);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
BindingEffects {
|
|
1445
|
+
rebinds,
|
|
1446
|
+
outgoing,
|
|
1447
|
+
unloaded: rebinds && had_table,
|
|
1448
|
+
loaded: rebinds && bound,
|
|
1449
|
+
watch,
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/// Everything the metadata derives from a [`Description`] of `effective`:
|
|
1454
|
+
/// the expression types, the types the `View` will have, and the window
|
|
1455
|
+
/// columns.
|
|
1456
|
+
fn record_description(
|
|
1457
|
+
metadata: &mut SessionMetadata,
|
|
1458
|
+
effective: &ViewConfig,
|
|
1459
|
+
description: &Description,
|
|
1460
|
+
) -> ApiResult<()> {
|
|
1461
|
+
metadata.update_expressions(&ExprValidationResult {
|
|
1462
|
+
expression_schema: description.expression_schema.clone(),
|
|
1463
|
+
errors: HashMap::new(),
|
|
1464
|
+
expression_alias: effective.expressions.0.clone(),
|
|
1465
|
+
})?;
|
|
1466
|
+
|
|
1467
|
+
metadata.update_view_schema(&description.view_schema)?;
|
|
1468
|
+
metadata.update_windows(&effective.windows)?;
|
|
1469
|
+
Ok(())
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/// Finish a committed rebind: dispose of the outgoing `View`, announce the
|
|
1473
|
+
/// table change, and watch the incoming client for errors.
|
|
1474
|
+
pub(crate) async fn finish_binding(&self, effects: BindingEffects) -> ApiResult<()> {
|
|
1475
|
+
let BindingEffects {
|
|
1476
|
+
outgoing,
|
|
1477
|
+
unloaded,
|
|
1478
|
+
loaded,
|
|
1479
|
+
watch,
|
|
1480
|
+
..
|
|
1481
|
+
} = effects;
|
|
1482
|
+
|
|
1483
|
+
let deleted = outgoing.delete().await;
|
|
1484
|
+
if unloaded {
|
|
1485
|
+
self.table_unloaded.emit(true);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if let Some(client) = watch {
|
|
1489
|
+
self.watch_client(&client).await?;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if loaded {
|
|
1493
|
+
self.table_loaded.emit(());
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
deleted
|
|
1081
1497
|
}
|
|
1082
1498
|
|
|
1083
1499
|
/// Bind the engine `View` for a validated snapshot: SKIP
|
|
@@ -1105,7 +1521,9 @@ impl Session {
|
|
|
1105
1521
|
}
|
|
1106
1522
|
}
|
|
1107
1523
|
|
|
1108
|
-
let
|
|
1524
|
+
let ValidatedSnapshot {
|
|
1525
|
+
snap: ConfigSnapshot { config, effective },
|
|
1526
|
+
} = validated;
|
|
1109
1527
|
if self.borrow().is_paused {
|
|
1110
1528
|
// A paused bind still RECONCILES the committed config (no `View`
|
|
1111
1529
|
// is constructed — `view_created` stays silent). Without this
|
|
@@ -1121,8 +1539,7 @@ impl Session {
|
|
|
1121
1539
|
return Ok(unchanged_or_deferred(self));
|
|
1122
1540
|
}
|
|
1123
1541
|
|
|
1124
|
-
|
|
1125
|
-
if !needs_schema {
|
|
1542
|
+
{
|
|
1126
1543
|
let bound = self.borrow().view_sub.as_ref().map(|x| x.build_config());
|
|
1127
1544
|
if let Some(bound) = bound {
|
|
1128
1545
|
if *bound == *effective {
|
|
@@ -1146,15 +1563,12 @@ impl Session {
|
|
|
1146
1563
|
|
|
1147
1564
|
let table = self
|
|
1148
1565
|
.borrow()
|
|
1149
|
-
.table
|
|
1150
|
-
.
|
|
1566
|
+
.table()
|
|
1567
|
+
.cloned()
|
|
1151
1568
|
.ok_or("`restore()` called before `load()`")?;
|
|
1152
1569
|
|
|
1153
1570
|
let view_config = self.with_default_aggregates(&effective);
|
|
1154
1571
|
let view = table.view(Some(view_config.into())).await?;
|
|
1155
|
-
let view_schema = view.schema().await?;
|
|
1156
|
-
self.metadata_mut().update_view_schema(&view_schema)?;
|
|
1157
|
-
self.metadata_mut().update_windows(&effective.windows)?;
|
|
1158
1572
|
let on_stats = Callback::from({
|
|
1159
1573
|
let this = self.clone();
|
|
1160
1574
|
move |stats| this.update_stats(stats)
|
|
@@ -1185,6 +1599,13 @@ impl Session {
|
|
|
1185
1599
|
/// The engine config for a `View` built from `effective`, per-column
|
|
1186
1600
|
/// default aggregates filled in as a courtesy to the virtual server API.
|
|
1187
1601
|
fn with_default_aggregates(&self, effective: &ViewConfig) -> ViewConfig {
|
|
1602
|
+
Self::with_default_aggregates_of(&self.metadata(), effective)
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
fn with_default_aggregates_of(
|
|
1606
|
+
metadata: &SessionMetadata,
|
|
1607
|
+
effective: &ViewConfig,
|
|
1608
|
+
) -> ViewConfig {
|
|
1188
1609
|
let mut view_config = effective.clone();
|
|
1189
1610
|
for col in view_config
|
|
1190
1611
|
.columns
|
|
@@ -1193,15 +1614,14 @@ impl Session {
|
|
|
1193
1614
|
.chain(view_config.sort.iter().map(|x| &x.0))
|
|
1194
1615
|
{
|
|
1195
1616
|
if !view_config.aggregates.contains_key(col.as_str()) {
|
|
1196
|
-
let agg =
|
|
1197
|
-
.metadata()
|
|
1617
|
+
let agg = metadata
|
|
1198
1618
|
.get_column_aggregates(col.as_str())
|
|
1199
1619
|
.and_then(|mut aggs| aggs.next())
|
|
1200
1620
|
.into_apierror();
|
|
1201
1621
|
|
|
1202
1622
|
match agg {
|
|
1203
1623
|
Err(_) => {
|
|
1204
|
-
tracing::
|
|
1624
|
+
tracing::debug!("No default aggregate for column '{}' found, skipping", col)
|
|
1205
1625
|
},
|
|
1206
1626
|
Ok(agg) => _ = view_config.aggregates.insert(col.to_string(), agg),
|
|
1207
1627
|
};
|
|
@@ -1214,43 +1634,29 @@ impl Session {
|
|
|
1214
1634
|
/// Build a caller-owned `View` from the effective config outside the
|
|
1215
1635
|
/// render pipeline, with no subscription and no interaction with pause.
|
|
1216
1636
|
pub async fn create_detached_view(&self) -> ApiResult<View> {
|
|
1217
|
-
let table = self.borrow().table.
|
|
1637
|
+
let table = self.borrow().table().cloned().ok_or("No `Table` set")?;
|
|
1218
1638
|
let view_config = self.with_default_aggregates(&self.effective_view_config());
|
|
1219
1639
|
Ok(table.view(Some(view_config.into())).await?)
|
|
1220
1640
|
}
|
|
1221
1641
|
|
|
1222
|
-
///
|
|
1223
|
-
|
|
1224
|
-
/// the old `validate()` error path — the committed config is NOT rolled
|
|
1225
|
-
/// back (I4: it holds exactly what the caller committed; the failure
|
|
1226
|
-
/// belongs to the run).
|
|
1227
|
-
pub async fn set_run_error(&self, err: ApiError) -> ApiResult<()> {
|
|
1642
|
+
/// Reset the config a failed draw could not render, as one queued op.
|
|
1643
|
+
pub async fn reset_failed_config(&self) -> ApiResult<()> {
|
|
1228
1644
|
let session = self.clone();
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
self.borrow_mut().error = Some(TableErrorState(
|
|
1239
|
-
err.clone(),
|
|
1240
|
-
Some(ReconnectCallback::new(move || {
|
|
1241
|
-
clone!(poll_loop);
|
|
1242
|
-
Box::pin(async move {
|
|
1243
|
-
poll_loop.poll(()).await;
|
|
1244
|
-
Ok(())
|
|
1245
|
-
})
|
|
1246
|
-
})),
|
|
1247
|
-
));
|
|
1248
|
-
|
|
1249
|
-
if let Some(cb) = self.on_table_errored.borrow().as_ref() {
|
|
1250
|
-
cb.emit(());
|
|
1251
|
-
}
|
|
1645
|
+
self.submit(OpKind::Restore { update: None }, move |_ctx| {
|
|
1646
|
+
Box::pin(async move {
|
|
1647
|
+
session
|
|
1648
|
+
.reset(ResetOptions {
|
|
1649
|
+
config: true,
|
|
1650
|
+
expressions: true,
|
|
1651
|
+
..ResetOptions::default()
|
|
1652
|
+
})
|
|
1653
|
+
.await?;
|
|
1252
1654
|
|
|
1253
|
-
|
|
1655
|
+
Ok(None)
|
|
1656
|
+
})
|
|
1657
|
+
})
|
|
1658
|
+
.settle()
|
|
1659
|
+
.await
|
|
1254
1660
|
}
|
|
1255
1661
|
|
|
1256
1662
|
fn update_stats(&self, stats: ViewStats) {
|
|
@@ -1262,8 +1668,8 @@ impl Session {
|
|
|
1262
1668
|
self.stats_changed.emit(());
|
|
1263
1669
|
}
|
|
1264
1670
|
|
|
1265
|
-
fn
|
|
1266
|
-
|
|
1671
|
+
fn columns_of(metadata: &SessionMetadata) -> Vec<String> {
|
|
1672
|
+
metadata
|
|
1267
1673
|
.get_table_columns()
|
|
1268
1674
|
.into_iter()
|
|
1269
1675
|
.flatten()
|
|
@@ -1276,6 +1682,8 @@ impl Session {
|
|
|
1276
1682
|
/// session-related PubSub event fires.
|
|
1277
1683
|
pub fn to_props(&self) -> SessionProps {
|
|
1278
1684
|
let column_stats = PtrEqRc::new(self.column_stats.borrow().clone());
|
|
1685
|
+
let projected = self.projected();
|
|
1686
|
+
let title = self.get_title();
|
|
1279
1687
|
let data = self.borrow();
|
|
1280
1688
|
|
|
1281
1689
|
// Reuse memoized snapshots when the underlying value hasn't
|
|
@@ -1285,15 +1693,15 @@ impl Session {
|
|
|
1285
1693
|
// these `PtrEqRc`s will spuriously refire.
|
|
1286
1694
|
let config = {
|
|
1287
1695
|
let mut cached = self.cached_config.borrow_mut();
|
|
1288
|
-
if !matches!(&*cached, Some(c) if **c ==
|
|
1289
|
-
*cached = Some(PtrEqRc::new(
|
|
1696
|
+
if !matches!(&*cached, Some(c) if **c == *projected) {
|
|
1697
|
+
*cached = Some(PtrEqRc::new((*projected).clone()));
|
|
1290
1698
|
}
|
|
1291
1699
|
cached.clone().unwrap()
|
|
1292
1700
|
};
|
|
1293
1701
|
let metadata = {
|
|
1294
1702
|
let mut cached = self.cached_metadata.borrow_mut();
|
|
1295
|
-
if !matches!(&*cached, Some(m) if **m == data.metadata) {
|
|
1296
|
-
*cached = Some(PtrEqRc::new(data.metadata.clone()));
|
|
1703
|
+
if !matches!(&*cached, Some(m) if **m == **data.metadata()) {
|
|
1704
|
+
*cached = Some(PtrEqRc::new((**data.metadata()).clone()));
|
|
1297
1705
|
}
|
|
1298
1706
|
cached.clone().unwrap()
|
|
1299
1707
|
};
|
|
@@ -1304,23 +1712,126 @@ impl Session {
|
|
|
1304
1712
|
.stats
|
|
1305
1713
|
.as_ref()
|
|
1306
1714
|
.is_some_and(|s| s.num_table_cells.is_some()),
|
|
1307
|
-
has_table: if data.table.is_some() {
|
|
1715
|
+
has_table: if data.table().is_some() {
|
|
1308
1716
|
Some(TableLoadState::Loaded)
|
|
1309
|
-
} else if
|
|
1717
|
+
} else if self.0.queue.has_load() {
|
|
1310
1718
|
Some(TableLoadState::Loading)
|
|
1311
|
-
} else if data.pending_table.is_some() {
|
|
1719
|
+
} else if data.pending_table().is_some() {
|
|
1312
1720
|
Some(TableLoadState::Pending)
|
|
1313
1721
|
} else {
|
|
1314
1722
|
None
|
|
1315
1723
|
},
|
|
1316
|
-
error: data.error.
|
|
1317
|
-
title
|
|
1724
|
+
error: data.error().cloned(),
|
|
1725
|
+
title,
|
|
1318
1726
|
metadata,
|
|
1319
1727
|
column_stats,
|
|
1320
1728
|
}
|
|
1321
1729
|
}
|
|
1322
1730
|
}
|
|
1323
1731
|
|
|
1732
|
+
/// A view-config change that has passed every check — names, normalization and
|
|
1733
|
+
/// the server's `describe` — and only awaits [`Session::commit_view`].
|
|
1734
|
+
pub(crate) struct PreparedView {
|
|
1735
|
+
binding: PreparedBinding,
|
|
1736
|
+
|
|
1737
|
+
/// A new overlay to commit, when the op broadcasts one.
|
|
1738
|
+
overlay: Option<Rc<Vec<OverlayClause>>>,
|
|
1739
|
+
config: Rc<ViewConfig>,
|
|
1740
|
+
effective: Rc<ViewConfig>,
|
|
1741
|
+
description: Option<Rc<Description>>,
|
|
1742
|
+
changed: bool,
|
|
1743
|
+
|
|
1744
|
+
/// `false` for a projected UI edit, whose SUBMIT already announced it.
|
|
1745
|
+
announce: bool,
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
/// The plugin whose advice fills in what a restore's view config leaves unsaid
|
|
1749
|
+
/// — computed against the table and config the restore LANDS on.
|
|
1750
|
+
pub(crate) enum ViewDefaults<'a> {
|
|
1751
|
+
/// The restore swaps to this plugin: default its columns and rollup mode.
|
|
1752
|
+
Swap(&'a PluginStaticConfig),
|
|
1753
|
+
|
|
1754
|
+
/// The restore stays on this plugin: re-enforce its rollup mode only.
|
|
1755
|
+
Rollup(&'a PluginStaticConfig),
|
|
1756
|
+
|
|
1757
|
+
/// The update is complete as given (a UI edit, whose control applied the
|
|
1758
|
+
/// plugin's advice when it made it).
|
|
1759
|
+
AsGiven,
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/// What a restore does to the panel's table binding — decided by its caller
|
|
1763
|
+
/// from the restore's `table` and the panel it lands on, with the incoming
|
|
1764
|
+
/// table already probed.
|
|
1765
|
+
pub(crate) enum BindPlan {
|
|
1766
|
+
/// The binding stands: bound, awaiting or unbound, as it is.
|
|
1767
|
+
Keep,
|
|
1768
|
+
|
|
1769
|
+
/// Bind `table`.
|
|
1770
|
+
Bind {
|
|
1771
|
+
client: Client,
|
|
1772
|
+
table: Box<perspective_client::Table>,
|
|
1773
|
+
reset: bool,
|
|
1774
|
+
},
|
|
1775
|
+
|
|
1776
|
+
/// Await a host for `name`.
|
|
1777
|
+
Pend {
|
|
1778
|
+
client: Client,
|
|
1779
|
+
name: String,
|
|
1780
|
+
reset: bool,
|
|
1781
|
+
},
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
enum PreparedBinding {
|
|
1785
|
+
Keep,
|
|
1786
|
+
Bind {
|
|
1787
|
+
client: Client,
|
|
1788
|
+
table: Box<perspective_client::Table>,
|
|
1789
|
+
metadata: Rc<SessionMetadata>,
|
|
1790
|
+
},
|
|
1791
|
+
Pend {
|
|
1792
|
+
client: Client,
|
|
1793
|
+
name: String,
|
|
1794
|
+
},
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/// What [`Session::commit_view`] leaves for [`Session::finish_binding`].
|
|
1798
|
+
#[must_use]
|
|
1799
|
+
pub(crate) struct BindingEffects {
|
|
1800
|
+
rebinds: bool,
|
|
1801
|
+
outgoing: Option<ViewSubscription>,
|
|
1802
|
+
unloaded: bool,
|
|
1803
|
+
loaded: bool,
|
|
1804
|
+
watch: Option<Client>,
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
impl BindingEffects {
|
|
1808
|
+
/// Whether the commit replaced the panel's table binding.
|
|
1809
|
+
pub fn rebound(&self) -> bool {
|
|
1810
|
+
self.rebinds
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/// Discard the effects of a commit that kept its binding (there are none).
|
|
1814
|
+
pub fn forget(self) {}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
impl PreparedView {
|
|
1818
|
+
/// Mark this as a projected UI edit: the UI has shown it since it was
|
|
1819
|
+
/// submitted, so committing it announces nothing.
|
|
1820
|
+
pub fn projected(mut self) -> Self {
|
|
1821
|
+
self.announce = false;
|
|
1822
|
+
self
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
pub fn config(&self) -> &ViewConfig {
|
|
1826
|
+
&self.config
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/// The type each column will have in the `View` this config builds.
|
|
1830
|
+
pub fn view_schema(&self) -> Option<&HashMap<String, perspective_client::proto::ColumnType>> {
|
|
1831
|
+
self.description.as_ref().map(|x| &x.view_schema)
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1324
1835
|
/// One pipeline run's frozen input (invariant I2): the persisted
|
|
1325
1836
|
/// [`ViewConfig`] and its EFFECTIVE companion (global-filter overlay
|
|
1326
1837
|
/// appended), captured at the same synchronous instant inside the draw
|
|
@@ -1331,10 +1842,11 @@ pub struct ConfigSnapshot {
|
|
|
1331
1842
|
pub effective: Rc<ViewConfig>,
|
|
1332
1843
|
}
|
|
1333
1844
|
|
|
1334
|
-
/// Type-state token: proof this snapshot
|
|
1335
|
-
/// [`Session::validate_snapshot`]
|
|
1336
|
-
|
|
1337
|
-
|
|
1845
|
+
/// Type-state token: proof this snapshot was validated by
|
|
1846
|
+
/// [`Session::validate_snapshot`], carrying the server's [`Description`] of it.
|
|
1847
|
+
pub struct ValidatedSnapshot {
|
|
1848
|
+
snap: ConfigSnapshot,
|
|
1849
|
+
}
|
|
1338
1850
|
|
|
1339
1851
|
/// Type-state witness that a `View` is NEW for the plugin about to render
|
|
1340
1852
|
/// it.
|