@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.
- 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/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 +115 -35
- package/dist/wasm/perspective-viewer.js +314 -190
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +21 -17
- package/package.json +31 -18
- 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/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 +74 -24
- package/src/rust/components/status_indicator.rs +8 -1
- package/src/rust/components/viewer/filters.rs +13 -1
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +41 -1
- 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 +14 -0
- package/src/rust/components/viewer.rs +45 -25
- package/src/rust/components/window_editor.rs +5 -4
- package/src/rust/config/column_config_schema.rs +38 -377
- 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 +187 -131
- 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 +30 -9
- 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 +900 -395
- 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 +5 -0
- package/src/rust/tasks/create_panel.rs +34 -9
- package/src/rust/tasks/derive.rs +312 -0
- package/src/rust/tasks/mod.rs +6 -2
- package/src/rust/tasks/pipeline.rs +145 -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 +39 -12
- 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/local_fonts.rs +0 -36
- package/src/rust/utils/mod.rs +2 -3
- package/src/rust/utils/modal_position.rs +15 -21
- 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/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/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/mod.rs +0 -14
- package/src/rust/utils/tests/pubsub.rs +0 -95
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-6813b568219c6763 → perspective-viewer-d0d525977a1acba2}/inline5.js +0 -0
package/src/rust/session.rs
CHANGED
|
@@ -13,19 +13,23 @@
|
|
|
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;
|
|
24
|
-
use std::rc::
|
|
26
|
+
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
|
|
|
@@ -96,10 +104,9 @@ pub struct SessionHandle {
|
|
|
96
104
|
/// copying the config.
|
|
97
105
|
pub last_dispatched_config: RefCell<Option<std::rc::Rc<crate::config::ViewerConfig>>>,
|
|
98
106
|
|
|
99
|
-
///
|
|
100
|
-
///
|
|
101
|
-
|
|
102
|
-
pending_load: RefCell<Weak<LoadWindow>>,
|
|
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>,
|
|
103
110
|
|
|
104
111
|
/// Coalesces `view_config_changed`: multiple synchronous commits in one
|
|
105
112
|
/// task emit ONE event on the next microtask — the cadence the deleted
|
|
@@ -187,29 +194,144 @@ pub(crate) async fn probe_table(
|
|
|
187
194
|
/// Mutable state for `Session`.
|
|
188
195
|
#[derive(Default)]
|
|
189
196
|
pub struct SessionData {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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>,
|
|
196
204
|
view_sub: Option<ViewSubscription>,
|
|
197
205
|
stats: Option<ViewStats>,
|
|
198
|
-
is_loading: bool,
|
|
199
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>),
|
|
200
212
|
|
|
201
|
-
/// Terminal:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
225
|
+
|
|
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
|
+
}
|
|
213
335
|
}
|
|
214
336
|
|
|
215
337
|
#[derive(Clone)]
|
|
@@ -260,59 +382,19 @@ pub enum Disposal {
|
|
|
260
382
|
Resolve,
|
|
261
383
|
}
|
|
262
384
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/// The raw config deltas committed while a `load()` classifies its payload, in
|
|
271
|
-
/// commit order, awaiting replay over a reset base if it classifies as a
|
|
272
|
-
/// `Table` — identified by [`Rc`] identity rather than a generation counter.
|
|
273
|
-
#[derive(Default)]
|
|
274
|
-
pub struct LoadWindow {
|
|
275
|
-
journal: RefCell<Vec<ViewConfigUpdate>>,
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/// A `load()` call site's handle on THE window it opened, closing that window
|
|
279
|
-
/// on `Drop` so no exit can strand `is_loading`.
|
|
280
|
-
pub struct LoadGuard {
|
|
281
|
-
session: Session,
|
|
282
|
-
window: Rc<LoadWindow>,
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
impl LoadGuard {
|
|
286
|
-
/// Close this window and take its journal for replay, or `None` when a
|
|
287
|
-
/// later `load()` superseded it — "abandon this classification, hold the
|
|
288
|
-
/// frame".
|
|
289
|
-
pub fn claim(&self) -> Option<Vec<ViewConfigUpdate>> {
|
|
290
|
-
self.close_current().then(|| self.window.journal.take())
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/// Close this window, discarding its journal; idempotent, and a no-op once
|
|
294
|
-
/// superseded.
|
|
295
|
-
pub fn close(&self) {
|
|
296
|
-
self.close_current();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
fn close_current(&self) -> bool {
|
|
300
|
-
let mut slot = self.session.pending_load.borrow_mut();
|
|
301
|
-
if !slot.upgrade().is_some_and(|x| Rc::ptr_eq(&x, &self.window)) {
|
|
302
|
-
return false;
|
|
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(()),
|
|
303
390
|
}
|
|
304
|
-
|
|
305
|
-
*slot = Weak::new();
|
|
306
|
-
drop(slot);
|
|
307
|
-
self.session.borrow_mut().is_loading = false;
|
|
308
|
-
true
|
|
309
391
|
}
|
|
310
392
|
}
|
|
311
393
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
394
|
+
#[derive(Debug, Default)]
|
|
395
|
+
pub enum TableIntermediateState {
|
|
396
|
+
#[default]
|
|
397
|
+
Ejected,
|
|
316
398
|
}
|
|
317
399
|
|
|
318
400
|
/// Options for [`Session::reset`]
|
|
@@ -361,21 +443,56 @@ impl Session {
|
|
|
361
443
|
Self(Rc::default())
|
|
362
444
|
}
|
|
363
445
|
|
|
364
|
-
pub(crate) fn metadata(&self) -> MetadataRef
|
|
365
|
-
|
|
446
|
+
pub(crate) fn metadata(&self) -> MetadataRef {
|
|
447
|
+
MetadataRef(self.borrow().metadata().clone())
|
|
366
448
|
}
|
|
367
449
|
|
|
368
450
|
pub(crate) fn metadata_mut(&self) -> MetadataMutRef<'_> {
|
|
369
|
-
std::cell::RefMut::map(self.borrow_mut(), |x|
|
|
451
|
+
std::cell::RefMut::map(self.borrow_mut(), |x| x.metadata_mut())
|
|
370
452
|
}
|
|
371
453
|
|
|
454
|
+
/// The title as the UI and `save()` see it: the committed title, or the
|
|
455
|
+
/// latest pending rename.
|
|
372
456
|
pub(crate) fn get_title(&self) -> Option<String> {
|
|
373
|
-
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())
|
|
374
469
|
}
|
|
375
470
|
|
|
471
|
+
/// Rename this panel — a UI edit, committed by the drain in its turn.
|
|
376
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>) {
|
|
377
493
|
let new_title = title.filter(|x| !x.is_empty());
|
|
378
|
-
self.
|
|
494
|
+
let next = self.borrow().state.with_title(new_title.clone());
|
|
495
|
+
self.borrow_mut().swap(next);
|
|
379
496
|
self.title_changed.emit(new_title);
|
|
380
497
|
}
|
|
381
498
|
|
|
@@ -385,37 +502,26 @@ impl Session {
|
|
|
385
502
|
/// `Session`.
|
|
386
503
|
pub fn reset(&self, options: ResetOptions) -> impl Future<Output = ApiResult<()>> + use<> {
|
|
387
504
|
let view = self.0.borrow_mut().view_sub.take();
|
|
388
|
-
let had_table = self.borrow().table.is_some();
|
|
505
|
+
let had_table = self.borrow().table().is_some();
|
|
389
506
|
let err = self.get_error();
|
|
390
|
-
self.borrow_mut().
|
|
507
|
+
self.borrow_mut().clear_errors();
|
|
391
508
|
if options.stats {
|
|
392
509
|
self.update_stats(ViewStats::default());
|
|
393
510
|
}
|
|
394
511
|
|
|
395
512
|
if options.config {
|
|
396
|
-
self.
|
|
513
|
+
let mut config = self.borrow().config().clone();
|
|
514
|
+
config.reset(options.expressions);
|
|
515
|
+
self.borrow_mut().set_config(config);
|
|
397
516
|
}
|
|
398
517
|
|
|
399
518
|
if options.expressions {
|
|
400
|
-
self.borrow_mut().
|
|
519
|
+
self.borrow_mut().set_description(None);
|
|
401
520
|
}
|
|
402
521
|
|
|
403
522
|
match options.table {
|
|
404
|
-
Some(TableIntermediateState::Ejected) =>
|
|
405
|
-
|
|
406
|
-
self.borrow_mut().table = None;
|
|
407
|
-
self.borrow_mut().pending_table = None;
|
|
408
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
409
|
-
},
|
|
410
|
-
Some(TableIntermediateState::Reloaded) => {
|
|
411
|
-
self.borrow_mut().is_loading = true;
|
|
412
|
-
self.borrow_mut().table = None;
|
|
413
|
-
self.borrow_mut().pending_table = None;
|
|
414
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
415
|
-
},
|
|
416
|
-
_ => {
|
|
417
|
-
self.borrow_mut().is_loading = false;
|
|
418
|
-
},
|
|
523
|
+
Some(TableIntermediateState::Ejected) => self.borrow_mut().unbind(),
|
|
524
|
+
None => {},
|
|
419
525
|
};
|
|
420
526
|
|
|
421
527
|
// A config reset that KEEPS its `Table` is itself a commit, and every
|
|
@@ -440,52 +546,94 @@ impl Session {
|
|
|
440
546
|
}
|
|
441
547
|
}
|
|
442
548
|
|
|
443
|
-
///
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
..
|
|
558
|
+
});
|
|
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
|
|
579
|
+
}
|
|
580
|
+
|
|
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
|
|
456
585
|
}
|
|
457
586
|
|
|
458
|
-
/// Whether a `load()`
|
|
459
|
-
/// value makes every config-driven bind DEFER
|
|
460
|
-
/// ([`crate::tasks::bind_snapshot`]) — the incoming config must never draw
|
|
461
|
-
/// against the outgoing table.
|
|
587
|
+
/// Whether a `load()` is queued or running.
|
|
462
588
|
pub fn has_pending_load(&self) -> bool {
|
|
463
|
-
self.0.
|
|
589
|
+
self.0.queue.has_load()
|
|
464
590
|
}
|
|
465
591
|
|
|
466
|
-
///
|
|
467
|
-
///
|
|
468
|
-
///
|
|
469
|
-
pub(crate) fn
|
|
470
|
-
|
|
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
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
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);
|
|
471
619
|
}
|
|
472
620
|
|
|
473
621
|
pub(crate) fn is_disposed(&self) -> bool {
|
|
474
|
-
self.
|
|
622
|
+
self.disposal().is_some()
|
|
475
623
|
}
|
|
476
624
|
|
|
477
625
|
/// This session's disposal, if its panel was ejected.
|
|
478
626
|
pub(crate) fn disposal(&self) -> Option<Disposal> {
|
|
479
|
-
self.borrow().
|
|
627
|
+
self.borrow().state.disposal()
|
|
480
628
|
}
|
|
481
629
|
|
|
482
630
|
pub(crate) fn has_table(&self) -> Option<TableLoadState> {
|
|
483
631
|
let data = self.borrow();
|
|
484
|
-
if data.table.is_some() {
|
|
632
|
+
if data.table().is_some() {
|
|
485
633
|
Some(TableLoadState::Loaded)
|
|
486
|
-
} else if
|
|
634
|
+
} else if self.0.queue.has_load() {
|
|
487
635
|
Some(TableLoadState::Loading)
|
|
488
|
-
} else if data.pending_table.is_some() {
|
|
636
|
+
} else if data.pending_table().is_some() {
|
|
489
637
|
Some(TableLoadState::Pending)
|
|
490
638
|
} else {
|
|
491
639
|
None
|
|
@@ -493,13 +641,15 @@ impl Session {
|
|
|
493
641
|
}
|
|
494
642
|
|
|
495
643
|
pub fn get_table(&self) -> Option<perspective_client::Table> {
|
|
496
|
-
self.borrow().table.
|
|
644
|
+
self.borrow().table().cloned()
|
|
497
645
|
}
|
|
498
646
|
|
|
647
|
+
/// Look this panel's tables up on `client` — unbinding it from a table of
|
|
648
|
+
/// any other client's.
|
|
499
649
|
pub fn set_client(&self, client: Client) -> bool {
|
|
500
|
-
if Some(&client) != self.
|
|
501
|
-
self.
|
|
502
|
-
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);
|
|
503
653
|
true
|
|
504
654
|
} else {
|
|
505
655
|
false
|
|
@@ -507,13 +657,13 @@ impl Session {
|
|
|
507
657
|
}
|
|
508
658
|
|
|
509
659
|
pub fn get_client(&self) -> Option<Client> {
|
|
510
|
-
self.borrow().client
|
|
660
|
+
self.borrow().state.client()
|
|
511
661
|
}
|
|
512
662
|
|
|
513
663
|
/// The configured table name awaiting a host, if any (see
|
|
514
664
|
/// [`SessionData::pending_table`]).
|
|
515
665
|
pub fn pending_table(&self) -> Option<String> {
|
|
516
|
-
self.borrow().pending_table.
|
|
666
|
+
self.borrow().pending_table().map(str::to_owned)
|
|
517
667
|
}
|
|
518
668
|
|
|
519
669
|
/// Suspend a BOUND table to PENDING: delete the `View`, drop the `Table`
|
|
@@ -523,64 +673,35 @@ impl Session {
|
|
|
523
673
|
/// as it was when the name is hosted again. `None` when no table is
|
|
524
674
|
/// bound.
|
|
525
675
|
pub fn suspend_table(&self) -> Option<impl Future<Output = ApiResult<()>> + use<>> {
|
|
526
|
-
let name = self.borrow().table
|
|
676
|
+
let name = self.borrow().table()?.get_name().to_owned();
|
|
677
|
+
let client = self.get_client()?;
|
|
527
678
|
let fut = self.reset(ResetOptions {
|
|
528
679
|
table: Some(TableIntermediateState::Ejected),
|
|
529
680
|
stats: true,
|
|
530
681
|
..ResetOptions::default()
|
|
531
682
|
});
|
|
532
683
|
|
|
533
|
-
self.
|
|
684
|
+
let next = self.borrow().state.awaiting_table(client, name);
|
|
685
|
+
self.borrow_mut().swap(next);
|
|
534
686
|
Some(fut)
|
|
535
687
|
}
|
|
536
688
|
|
|
537
|
-
///
|
|
538
|
-
/// `
|
|
539
|
-
|
|
540
|
-
///
|
|
541
|
-
/// # Arguments
|
|
542
|
-
///
|
|
543
|
-
/// - `table_name` The name of the `Table` to load.
|
|
544
|
-
///
|
|
545
|
-
/// # Returns
|
|
546
|
-
///
|
|
547
|
-
/// `table_name` is unique per `Client`, so if this value has not changed,
|
|
548
|
-
/// `Session::set_table` does nothing and returns `Ok(false)`.
|
|
549
|
-
///
|
|
550
|
-
/// A name no loaded client hosts pends ([`MissingTable::Pend`]) and
|
|
551
|
-
/// returns `Ok(false)` until the table lifecycle re-runs the bind.
|
|
552
|
-
pub async fn set_table(&self, table_name: String) -> ApiResult<bool> {
|
|
553
|
-
if Some(table_name.as_str()) == self.0.borrow().table.as_ref().map(|x| x.get_name()) {
|
|
554
|
-
self.0.borrow_mut().pending_table = None;
|
|
555
|
-
return Ok(false);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
let client = self.0.borrow().client.clone().into_apierror()?;
|
|
559
|
-
match probe_table(&client, &table_name, MissingTable::Pend).await? {
|
|
560
|
-
Some(table) => match self.bind_table(table).await {
|
|
561
|
-
Ok(()) => Ok(true),
|
|
562
|
-
Err(err) => self.set_error(false, err).await.map(|_| false),
|
|
563
|
-
},
|
|
564
|
-
None => {
|
|
565
|
-
self.pend_table(table_name).await?;
|
|
566
|
-
Ok(false)
|
|
567
|
-
},
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/// Bind an opened `Table`, replacing any previous binding and announcing
|
|
572
|
-
/// `table_loaded`.
|
|
573
|
-
pub(crate) async fn bind_table(&self, table: perspective_client::Table) -> ApiResult<()> {
|
|
574
|
-
if self.is_disposed() {
|
|
575
|
-
return Err(ApiError::new("Panel disposed"));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
let metadata = SessionMetadata::from_table(&table).await?;
|
|
579
|
-
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<()> {
|
|
580
692
|
let on_error = self.on_table_errored.borrow().clone();
|
|
581
693
|
let session = self.clone();
|
|
694
|
+
let watched = client.clone();
|
|
582
695
|
let poll_loop = LocalPollLoop::new(move |(message, reconnect): (ApiError, _)| {
|
|
583
|
-
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);
|
|
584
705
|
if let Some(cb) = &on_error {
|
|
585
706
|
cb.emit(());
|
|
586
707
|
}
|
|
@@ -601,33 +722,9 @@ impl Session {
|
|
|
601
722
|
}))
|
|
602
723
|
.await?;
|
|
603
724
|
|
|
604
|
-
let sub = self.borrow_mut().view_sub.take();
|
|
605
|
-
self.borrow_mut().metadata = metadata;
|
|
606
|
-
self.borrow_mut().table = Some(table);
|
|
607
|
-
self.borrow_mut().pending_table = None;
|
|
608
|
-
self.borrow_mut().is_loading = false;
|
|
609
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
610
|
-
sub.delete().await?;
|
|
611
|
-
self.table_loaded.emit(());
|
|
612
725
|
Ok(())
|
|
613
726
|
}
|
|
614
727
|
|
|
615
|
-
/// Record `name` as pending, dropping any bound `Table` so nothing binds
|
|
616
|
-
/// against the outgoing table while the name waits for a host.
|
|
617
|
-
pub(crate) async fn pend_table(&self, name: String) -> ApiResult<()> {
|
|
618
|
-
if self.is_disposed() {
|
|
619
|
-
return Err(ApiError::new("Panel disposed"));
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
let sub = self.borrow_mut().view_sub.take();
|
|
623
|
-
self.borrow_mut().table = None;
|
|
624
|
-
self.borrow_mut().metadata = SessionMetadata::default();
|
|
625
|
-
self.borrow_mut().pending_table = Some(name);
|
|
626
|
-
self.borrow_mut().is_loading = false;
|
|
627
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
628
|
-
sub.delete().await
|
|
629
|
-
}
|
|
630
|
-
|
|
631
728
|
pub async fn set_error(&self, reset_table: bool, err: ApiError) -> ApiResult<()> {
|
|
632
729
|
let session = self.clone();
|
|
633
730
|
let poll_loop = LocalPollLoop::new(move |()| {
|
|
@@ -639,7 +736,7 @@ impl Session {
|
|
|
639
736
|
Ok(JsValue::UNDEFINED)
|
|
640
737
|
});
|
|
641
738
|
|
|
642
|
-
|
|
739
|
+
let error = TableErrorState(
|
|
643
740
|
err.clone(),
|
|
644
741
|
Some(ReconnectCallback::new(move || {
|
|
645
742
|
clone!(poll_loop);
|
|
@@ -648,7 +745,10 @@ impl Session {
|
|
|
648
745
|
Ok(())
|
|
649
746
|
})
|
|
650
747
|
})),
|
|
651
|
-
)
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
let next = self.borrow().state.with_lost(error);
|
|
751
|
+
self.borrow_mut().swap(next);
|
|
652
752
|
|
|
653
753
|
if let Some(cb) = self.on_table_errored.borrow().as_ref() {
|
|
654
754
|
cb.emit(());
|
|
@@ -656,7 +756,7 @@ impl Session {
|
|
|
656
756
|
|
|
657
757
|
let sub = self.borrow_mut().view_sub.take();
|
|
658
758
|
if reset_table {
|
|
659
|
-
self.borrow_mut().
|
|
759
|
+
self.borrow_mut().unbind();
|
|
660
760
|
}
|
|
661
761
|
|
|
662
762
|
sub.delete().await?;
|
|
@@ -686,24 +786,38 @@ impl Session {
|
|
|
686
786
|
}
|
|
687
787
|
|
|
688
788
|
pub fn js_get_table(&self) -> Option<JsValue> {
|
|
689
|
-
Some(perspective_js::Table::from(self.borrow().table.
|
|
789
|
+
Some(perspective_js::Table::from(self.borrow().table().cloned()?).into())
|
|
690
790
|
}
|
|
691
791
|
|
|
792
|
+
/// Whether the binding is lost.
|
|
692
793
|
pub(crate) fn is_errored(&self) -> bool {
|
|
693
|
-
self.borrow().error.is_some()
|
|
794
|
+
self.borrow().error().is_some()
|
|
694
795
|
}
|
|
695
796
|
|
|
696
797
|
pub(crate) fn get_error(&self) -> Option<ApiError> {
|
|
697
|
-
self.borrow().error
|
|
798
|
+
self.borrow().error().map(|x| x.0.clone())
|
|
698
799
|
}
|
|
699
800
|
|
|
801
|
+
/// Recover from an error state (the overlay's button) — a writer, so an op
|
|
802
|
+
/// on the queue like any other.
|
|
700
803
|
pub async fn reconnect(&self) -> ApiResult<()> {
|
|
701
|
-
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();
|
|
702
817
|
if let Some(TableErrorState(_, Some(reconnect))) = err {
|
|
703
818
|
reconnect().await?;
|
|
704
|
-
self.borrow_mut().
|
|
705
|
-
self.borrow_mut().
|
|
706
|
-
self.borrow_mut().last_validated_expressions = None;
|
|
819
|
+
self.borrow_mut().clear_errors();
|
|
820
|
+
self.borrow_mut().set_description(None);
|
|
707
821
|
self.borrow_mut().view_sub = None;
|
|
708
822
|
self.table_loaded.emit(());
|
|
709
823
|
}
|
|
@@ -739,36 +853,93 @@ impl Session {
|
|
|
739
853
|
.is_some_and(|s| s.num_table_cells.is_some())
|
|
740
854
|
}
|
|
741
855
|
|
|
742
|
-
|
|
743
|
-
|
|
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())
|
|
744
860
|
}
|
|
745
861
|
|
|
746
|
-
/// The
|
|
747
|
-
///
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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;
|
|
754
883
|
}
|
|
755
884
|
|
|
756
|
-
let mut config =
|
|
757
|
-
|
|
758
|
-
|
|
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)
|
|
759
895
|
}
|
|
760
896
|
|
|
761
|
-
///
|
|
762
|
-
///
|
|
763
|
-
///
|
|
764
|
-
pub fn
|
|
765
|
-
if self.borrow().
|
|
766
|
-
|
|
767
|
-
self.notify_view_config_changed();
|
|
768
|
-
true
|
|
769
|
-
} else {
|
|
770
|
-
false
|
|
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()));
|
|
771
903
|
}
|
|
904
|
+
|
|
905
|
+
let mut candidate = (*self.projected()).clone();
|
|
906
|
+
if candidate.apply_update(delta.clone()) {
|
|
907
|
+
self.validate_names(&candidate)?;
|
|
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()
|
|
772
943
|
}
|
|
773
944
|
|
|
774
945
|
/// Snapshot of the [`ViewConfig`] the currently-bound `View` was
|
|
@@ -777,9 +948,8 @@ impl Session {
|
|
|
777
948
|
///
|
|
778
949
|
/// Prefer this over [`Self::get_view_config`] when you need a
|
|
779
950
|
/// value consistent with what the active plugin is rendering.
|
|
780
|
-
/// `get_view_config` returns the
|
|
781
|
-
///
|
|
782
|
-
/// 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`.
|
|
783
953
|
pub fn get_rendered_view_config(&self) -> Option<Rc<ViewConfig>> {
|
|
784
954
|
self.borrow().view_sub.as_ref().map(|s| s.get_view_config())
|
|
785
955
|
}
|
|
@@ -790,7 +960,7 @@ impl Session {
|
|
|
790
960
|
config_static: &PluginStaticConfig,
|
|
791
961
|
) {
|
|
792
962
|
use self::column_defaults_update::*;
|
|
793
|
-
let config = self.
|
|
963
|
+
let config = self.committed_view_config();
|
|
794
964
|
config_update.set_update_column_defaults(
|
|
795
965
|
&self.metadata(),
|
|
796
966
|
&config,
|
|
@@ -812,72 +982,18 @@ impl Session {
|
|
|
812
982
|
use self::column_defaults_update::*;
|
|
813
983
|
config_update.set_update_rollup_defaults(
|
|
814
984
|
&self.metadata(),
|
|
815
|
-
&self.
|
|
985
|
+
&self.committed_view_config(),
|
|
816
986
|
config_static,
|
|
817
987
|
)
|
|
818
988
|
}
|
|
819
989
|
|
|
820
|
-
///
|
|
821
|
-
///
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
///
|
|
825
|
-
/// Validation is SYNCHRONOUS and happens before anything is applied: an
|
|
826
|
-
/// update naming an unknown column is rejected with `Err` and the config
|
|
827
|
-
/// is untouched (I4 — invalid state is never entered, so no rollback
|
|
828
|
-
/// path exists). Server-side expression compilation is deliberately NOT
|
|
829
|
-
/// checked here; it is a property of a pipeline run
|
|
830
|
-
/// ([`Self::validate_snapshot`]) and fails that run, never the commit.
|
|
831
|
-
pub fn commit_view_config(&self, config_update: ViewConfigUpdate) -> ApiResult<()> {
|
|
832
|
-
if let Some(x) = self.borrow().error.as_ref() {
|
|
833
|
-
tracing::warn!("Errored state");
|
|
834
|
-
|
|
835
|
-
// Load bearing return
|
|
836
|
-
return Err(ApiError::new(x.0.clone()));
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// A `load()` is classifying its payload (see
|
|
840
|
-
// [`SessionHandle::pending_load`]): record the RAW delta so a `Table`
|
|
841
|
-
// classification can replay it over the reset base, validated against
|
|
842
|
-
// the INCOMING table's schema. The live apply below is a best-effort
|
|
843
|
-
// preview (kept for `save()` coherence, I1) that SKIPS name validation
|
|
844
|
-
// — the delta may legitimately name the incoming table's columns,
|
|
845
|
-
// absent from the outgoing one — mirroring the "no table bound yet"
|
|
846
|
-
// leniency already in `validate_names`. Authoritative validation is
|
|
847
|
-
// deferred to the replay.
|
|
848
|
-
let window = self.0.pending_load.borrow().upgrade();
|
|
849
|
-
let mut candidate = self.borrow().config.clone();
|
|
850
|
-
let journal_entry = window.is_some().then(|| config_update.clone());
|
|
851
|
-
if !candidate.apply_update(config_update) {
|
|
852
|
-
return Ok(());
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if let Some(window) = &window
|
|
856
|
-
&& let Some(entry) = journal_entry
|
|
857
|
-
{
|
|
858
|
-
window.journal.borrow_mut().push(entry);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if window.is_none() {
|
|
862
|
-
self.validate_names(&candidate)?;
|
|
863
|
-
}
|
|
864
|
-
|
|
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();
|
|
865
994
|
self.normalize_view_config(&mut candidate);
|
|
866
|
-
self.
|
|
867
|
-
|
|
868
|
-
Ok(())
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
/// Table-bind commit: normalize the (possibly empty) config against the
|
|
872
|
-
/// newly-bound table's metadata — the default-view materialization that
|
|
873
|
-
/// previously happened inside the async validate write-back. SYNC;
|
|
874
|
-
/// called immediately after `set_table().await` inside the binding run,
|
|
875
|
-
/// so it is ordered like any other commit.
|
|
876
|
-
pub fn commit_table_defaults(&self) {
|
|
877
|
-
let mut candidate = self.borrow().config.clone();
|
|
878
|
-
self.normalize_view_config(&mut candidate);
|
|
879
|
-
if candidate != self.borrow().config {
|
|
880
|
-
self.borrow_mut().config = candidate;
|
|
995
|
+
if candidate != *self.borrow().config() {
|
|
996
|
+
self.borrow_mut().set_config(candidate);
|
|
881
997
|
self.notify_view_config_changed();
|
|
882
998
|
}
|
|
883
999
|
}
|
|
@@ -889,7 +1005,13 @@ impl Session {
|
|
|
889
1005
|
/// when no table is bound yet: the config rides along until `load()`
|
|
890
1006
|
/// binds one, and the engine surfaces any residual error on that run.
|
|
891
1007
|
fn validate_names(&self, config: &ViewConfig) -> ApiResult<()> {
|
|
892
|
-
|
|
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);
|
|
893
1015
|
if table_columns.is_empty() {
|
|
894
1016
|
return Ok(());
|
|
895
1017
|
}
|
|
@@ -932,7 +1054,11 @@ impl Session {
|
|
|
932
1054
|
/// write-back): fill empty `columns` from the table, prune `aggregates`
|
|
933
1055
|
/// to referenced columns.
|
|
934
1056
|
fn normalize_view_config(&self, config: &mut ViewConfig) {
|
|
935
|
-
|
|
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);
|
|
936
1062
|
if table_columns.is_empty() {
|
|
937
1063
|
return;
|
|
938
1064
|
}
|
|
@@ -1038,53 +1164,336 @@ impl Session {
|
|
|
1038
1164
|
/// that preceded run *N*'s completion (invariant I3).
|
|
1039
1165
|
pub fn snapshot(&self, _guard: &RenderGuard) -> ConfigSnapshot {
|
|
1040
1166
|
ConfigSnapshot {
|
|
1041
|
-
config:
|
|
1042
|
-
effective:
|
|
1167
|
+
config: self.borrow().state.config().clone(),
|
|
1168
|
+
effective: self.effective_view_config(),
|
|
1043
1169
|
}
|
|
1044
1170
|
}
|
|
1045
1171
|
|
|
1046
|
-
/// Validate a snapshot
|
|
1047
|
-
///
|
|
1048
|
-
/// schema. Reads nothing from — and writes nothing to — the live
|
|
1049
|
-
/// config; a failure fails this RUN, never the committed config (I4).
|
|
1050
|
-
///
|
|
1051
|
-
/// Memoized: when the snapshot's expressions equal the last successfully
|
|
1052
|
-
/// validated set, the round trip is skipped entirely — safe because the
|
|
1053
|
-
/// memo key is an immutable snapshot field and the recorded set is
|
|
1054
|
-
/// 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.
|
|
1055
1174
|
pub async fn validate_snapshot(
|
|
1056
1175
|
&self,
|
|
1057
1176
|
_guard: &RenderGuard,
|
|
1058
1177
|
snap: ConfigSnapshot,
|
|
1059
1178
|
) -> ApiResult<ValidatedSnapshot> {
|
|
1060
|
-
let
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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);
|
|
1080
1202
|
|
|
1081
|
-
|
|
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 => {},
|
|
1082
1307
|
}
|
|
1308
|
+
}
|
|
1083
1309
|
|
|
1084
|
-
|
|
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);
|
|
1085
1315
|
}
|
|
1086
1316
|
|
|
1087
|
-
|
|
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
|
+
}
|
|
1406
|
+
|
|
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
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
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();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
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
|
|
1088
1497
|
}
|
|
1089
1498
|
|
|
1090
1499
|
/// Bind the engine `View` for a validated snapshot: SKIP
|
|
@@ -1112,7 +1521,9 @@ impl Session {
|
|
|
1112
1521
|
}
|
|
1113
1522
|
}
|
|
1114
1523
|
|
|
1115
|
-
let
|
|
1524
|
+
let ValidatedSnapshot {
|
|
1525
|
+
snap: ConfigSnapshot { config, effective },
|
|
1526
|
+
} = validated;
|
|
1116
1527
|
if self.borrow().is_paused {
|
|
1117
1528
|
// A paused bind still RECONCILES the committed config (no `View`
|
|
1118
1529
|
// is constructed — `view_created` stays silent). Without this
|
|
@@ -1128,8 +1539,7 @@ impl Session {
|
|
|
1128
1539
|
return Ok(unchanged_or_deferred(self));
|
|
1129
1540
|
}
|
|
1130
1541
|
|
|
1131
|
-
|
|
1132
|
-
if !needs_schema {
|
|
1542
|
+
{
|
|
1133
1543
|
let bound = self.borrow().view_sub.as_ref().map(|x| x.build_config());
|
|
1134
1544
|
if let Some(bound) = bound {
|
|
1135
1545
|
if *bound == *effective {
|
|
@@ -1153,15 +1563,12 @@ impl Session {
|
|
|
1153
1563
|
|
|
1154
1564
|
let table = self
|
|
1155
1565
|
.borrow()
|
|
1156
|
-
.table
|
|
1157
|
-
.
|
|
1566
|
+
.table()
|
|
1567
|
+
.cloned()
|
|
1158
1568
|
.ok_or("`restore()` called before `load()`")?;
|
|
1159
1569
|
|
|
1160
1570
|
let view_config = self.with_default_aggregates(&effective);
|
|
1161
1571
|
let view = table.view(Some(view_config.into())).await?;
|
|
1162
|
-
let view_schema = view.schema().await?;
|
|
1163
|
-
self.metadata_mut().update_view_schema(&view_schema)?;
|
|
1164
|
-
self.metadata_mut().update_windows(&effective.windows)?;
|
|
1165
1572
|
let on_stats = Callback::from({
|
|
1166
1573
|
let this = self.clone();
|
|
1167
1574
|
move |stats| this.update_stats(stats)
|
|
@@ -1192,6 +1599,13 @@ impl Session {
|
|
|
1192
1599
|
/// The engine config for a `View` built from `effective`, per-column
|
|
1193
1600
|
/// default aggregates filled in as a courtesy to the virtual server API.
|
|
1194
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 {
|
|
1195
1609
|
let mut view_config = effective.clone();
|
|
1196
1610
|
for col in view_config
|
|
1197
1611
|
.columns
|
|
@@ -1200,15 +1614,14 @@ impl Session {
|
|
|
1200
1614
|
.chain(view_config.sort.iter().map(|x| &x.0))
|
|
1201
1615
|
{
|
|
1202
1616
|
if !view_config.aggregates.contains_key(col.as_str()) {
|
|
1203
|
-
let agg =
|
|
1204
|
-
.metadata()
|
|
1617
|
+
let agg = metadata
|
|
1205
1618
|
.get_column_aggregates(col.as_str())
|
|
1206
1619
|
.and_then(|mut aggs| aggs.next())
|
|
1207
1620
|
.into_apierror();
|
|
1208
1621
|
|
|
1209
1622
|
match agg {
|
|
1210
1623
|
Err(_) => {
|
|
1211
|
-
tracing::
|
|
1624
|
+
tracing::debug!("No default aggregate for column '{}' found, skipping", col)
|
|
1212
1625
|
},
|
|
1213
1626
|
Ok(agg) => _ = view_config.aggregates.insert(col.to_string(), agg),
|
|
1214
1627
|
};
|
|
@@ -1221,43 +1634,29 @@ impl Session {
|
|
|
1221
1634
|
/// Build a caller-owned `View` from the effective config outside the
|
|
1222
1635
|
/// render pipeline, with no subscription and no interaction with pause.
|
|
1223
1636
|
pub async fn create_detached_view(&self) -> ApiResult<View> {
|
|
1224
|
-
let table = self.borrow().table.
|
|
1637
|
+
let table = self.borrow().table().cloned().ok_or("No `Table` set")?;
|
|
1225
1638
|
let view_config = self.with_default_aggregates(&self.effective_view_config());
|
|
1226
1639
|
Ok(table.view(Some(view_config.into())).await?)
|
|
1227
1640
|
}
|
|
1228
1641
|
|
|
1229
|
-
///
|
|
1230
|
-
|
|
1231
|
-
/// the old `validate()` error path — the committed config is NOT rolled
|
|
1232
|
-
/// back (I4: it holds exactly what the caller committed; the failure
|
|
1233
|
-
/// belongs to the run).
|
|
1234
|
-
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<()> {
|
|
1235
1644
|
let session = self.clone();
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
self.borrow_mut().error = Some(TableErrorState(
|
|
1246
|
-
err.clone(),
|
|
1247
|
-
Some(ReconnectCallback::new(move || {
|
|
1248
|
-
clone!(poll_loop);
|
|
1249
|
-
Box::pin(async move {
|
|
1250
|
-
poll_loop.poll(()).await;
|
|
1251
|
-
Ok(())
|
|
1252
|
-
})
|
|
1253
|
-
})),
|
|
1254
|
-
));
|
|
1255
|
-
|
|
1256
|
-
if let Some(cb) = self.on_table_errored.borrow().as_ref() {
|
|
1257
|
-
cb.emit(());
|
|
1258
|
-
}
|
|
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?;
|
|
1259
1654
|
|
|
1260
|
-
|
|
1655
|
+
Ok(None)
|
|
1656
|
+
})
|
|
1657
|
+
})
|
|
1658
|
+
.settle()
|
|
1659
|
+
.await
|
|
1261
1660
|
}
|
|
1262
1661
|
|
|
1263
1662
|
fn update_stats(&self, stats: ViewStats) {
|
|
@@ -1269,8 +1668,8 @@ impl Session {
|
|
|
1269
1668
|
self.stats_changed.emit(());
|
|
1270
1669
|
}
|
|
1271
1670
|
|
|
1272
|
-
fn
|
|
1273
|
-
|
|
1671
|
+
fn columns_of(metadata: &SessionMetadata) -> Vec<String> {
|
|
1672
|
+
metadata
|
|
1274
1673
|
.get_table_columns()
|
|
1275
1674
|
.into_iter()
|
|
1276
1675
|
.flatten()
|
|
@@ -1283,6 +1682,8 @@ impl Session {
|
|
|
1283
1682
|
/// session-related PubSub event fires.
|
|
1284
1683
|
pub fn to_props(&self) -> SessionProps {
|
|
1285
1684
|
let column_stats = PtrEqRc::new(self.column_stats.borrow().clone());
|
|
1685
|
+
let projected = self.projected();
|
|
1686
|
+
let title = self.get_title();
|
|
1286
1687
|
let data = self.borrow();
|
|
1287
1688
|
|
|
1288
1689
|
// Reuse memoized snapshots when the underlying value hasn't
|
|
@@ -1292,15 +1693,15 @@ impl Session {
|
|
|
1292
1693
|
// these `PtrEqRc`s will spuriously refire.
|
|
1293
1694
|
let config = {
|
|
1294
1695
|
let mut cached = self.cached_config.borrow_mut();
|
|
1295
|
-
if !matches!(&*cached, Some(c) if **c ==
|
|
1296
|
-
*cached = Some(PtrEqRc::new(
|
|
1696
|
+
if !matches!(&*cached, Some(c) if **c == *projected) {
|
|
1697
|
+
*cached = Some(PtrEqRc::new((*projected).clone()));
|
|
1297
1698
|
}
|
|
1298
1699
|
cached.clone().unwrap()
|
|
1299
1700
|
};
|
|
1300
1701
|
let metadata = {
|
|
1301
1702
|
let mut cached = self.cached_metadata.borrow_mut();
|
|
1302
|
-
if !matches!(&*cached, Some(m) if **m == data.metadata) {
|
|
1303
|
-
*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()));
|
|
1304
1705
|
}
|
|
1305
1706
|
cached.clone().unwrap()
|
|
1306
1707
|
};
|
|
@@ -1311,23 +1712,126 @@ impl Session {
|
|
|
1311
1712
|
.stats
|
|
1312
1713
|
.as_ref()
|
|
1313
1714
|
.is_some_and(|s| s.num_table_cells.is_some()),
|
|
1314
|
-
has_table: if data.table.is_some() {
|
|
1715
|
+
has_table: if data.table().is_some() {
|
|
1315
1716
|
Some(TableLoadState::Loaded)
|
|
1316
|
-
} else if
|
|
1717
|
+
} else if self.0.queue.has_load() {
|
|
1317
1718
|
Some(TableLoadState::Loading)
|
|
1318
|
-
} else if data.pending_table.is_some() {
|
|
1719
|
+
} else if data.pending_table().is_some() {
|
|
1319
1720
|
Some(TableLoadState::Pending)
|
|
1320
1721
|
} else {
|
|
1321
1722
|
None
|
|
1322
1723
|
},
|
|
1323
|
-
error: data.error.
|
|
1324
|
-
title
|
|
1724
|
+
error: data.error().cloned(),
|
|
1725
|
+
title,
|
|
1325
1726
|
metadata,
|
|
1326
1727
|
column_stats,
|
|
1327
1728
|
}
|
|
1328
1729
|
}
|
|
1329
1730
|
}
|
|
1330
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
|
+
|
|
1331
1835
|
/// One pipeline run's frozen input (invariant I2): the persisted
|
|
1332
1836
|
/// [`ViewConfig`] and its EFFECTIVE companion (global-filter overlay
|
|
1333
1837
|
/// appended), captured at the same synchronous instant inside the draw
|
|
@@ -1338,10 +1842,11 @@ pub struct ConfigSnapshot {
|
|
|
1338
1842
|
pub effective: Rc<ViewConfig>,
|
|
1339
1843
|
}
|
|
1340
1844
|
|
|
1341
|
-
/// Type-state token: proof this snapshot
|
|
1342
|
-
/// [`Session::validate_snapshot`]
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
+
}
|
|
1345
1850
|
|
|
1346
1851
|
/// Type-state witness that a `View` is NEW for the plugin about to render
|
|
1347
1852
|
/// it.
|