@perspective-dev/viewer 5.4.0 → 5.5.1
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/dist/cdn/perspective-viewer.js +4 -4
- package/dist/cdn/perspective-viewer.js.map +2 -2
- 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/perspective-viewer.d.ts +2 -0
- package/dist/esm/perspective-viewer.inline.js +9 -9
- package/dist/esm/perspective-viewer.inline.js.map +3 -3
- package/dist/esm/perspective-viewer.js +4 -4
- package/dist/esm/perspective-viewer.js.map +2 -2
- package/dist/esm/plugin.d.ts +9 -0
- package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
- package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
- package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
- package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
- package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
- package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
- package/dist/wasm/perspective-viewer.d.ts +102 -47
- package/dist/wasm/perspective-viewer.js +233 -142
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
- package/package.json +7 -7
- package/src/css/column-selector.css +11 -3
- package/src/css/column-style.css +162 -0
- package/src/css/config-selector.css +1 -1
- package/src/css/form/debug.css +9 -0
- package/src/css/status-bar.css +11 -0
- package/src/css/ui/containers/tabs.css +2 -1
- package/src/rust/agent/tools.rs +80 -7
- package/src/rust/components/column_selector/active_column.rs +9 -0
- package/src/rust/components/column_selector.rs +11 -0
- package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
- package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
- package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
- package/src/rust/components/debug_panel.rs +107 -50
- package/src/rust/components/export_dropdown.rs +6 -0
- package/src/rust/components/main_panel/msg.rs +4 -0
- package/src/rust/components/main_panel.rs +14 -0
- package/src/rust/components/mod.rs +0 -1
- package/src/rust/components/plugin_tab.rs +57 -3
- package/src/rust/components/settings_panel.rs +6 -0
- package/src/rust/components/status_bar.rs +12 -3
- package/src/rust/components/status_indicator.rs +4 -0
- package/src/rust/components/viewer/filters.rs +2 -2
- package/src/rust/components/viewer/msg.rs +4 -0
- package/src/rust/components/viewer/panels.rs +6 -3
- package/src/rust/components/viewer/render.rs +1 -0
- package/src/rust/components/viewer/wiring.rs +14 -2
- package/src/rust/components/viewer.rs +2 -2
- package/src/rust/config/column_config_schema.rs +312 -8
- package/src/rust/config/datetime_column_style.rs +0 -11
- package/src/rust/config/export_method.rs +24 -0
- package/src/rust/config/mod.rs +0 -2
- package/src/rust/config/options.rs +50 -0
- package/src/rust/config/workspace_config.rs +69 -24
- package/src/rust/custom_elements/viewer.rs +214 -113
- package/src/rust/js/plugin.rs +3 -1
- package/src/rust/lib.rs +7 -3
- package/src/rust/queries/plugin_column_styles.rs +6 -5
- package/src/rust/renderer/plugin_config.rs +200 -41
- package/src/rust/renderer/plugin_store.rs +2 -1
- package/src/rust/renderer/props.rs +5 -0
- package/src/rust/renderer.rs +9 -0
- package/src/rust/session/metadata.rs +5 -10
- package/src/rust/session/props.rs +4 -0
- package/src/rust/session.rs +273 -195
- package/src/rust/tasks/copy_export.rs +67 -0
- package/src/rust/tasks/create_panel.rs +16 -13
- package/src/rust/tasks/pipeline.rs +26 -8
- package/src/rust/tasks/restore_panel.rs +129 -30
- package/src/rust/tasks/send_column_config.rs +3 -0
- package/src/rust/tasks/sync_update_panels.rs +148 -68
- package/src/rust/tasks/table_lifecycle.rs +7 -1
- package/src/rust/ui/containers/tab_list.rs +1 -1
- package/src/rust/ui/form/intl_label.rs +4 -23
- package/src/rust/ui/portal.rs +1 -0
- package/src/rust/utils/css_vars.rs +13 -0
- package/src/rust/{config/datetime_column_style/color_mode.rs → utils/in_flight.rs} +47 -35
- package/src/rust/utils/local_fonts.rs +395 -0
- package/src/rust/utils/mod.rs +4 -0
- package/src/rust/utils/pending_effects.rs +6 -30
- package/src/rust/workspace.rs +12 -0
- package/src/svg/status_pending.svg +3 -0
- package/src/themes/icons.css +1 -0
- package/src/themes/intl/ar.css +20 -7
- package/src/themes/intl/de.css +20 -7
- package/src/themes/intl/es.css +20 -7
- package/src/themes/intl/fr.css +20 -7
- package/src/themes/intl/ja.css +20 -7
- package/src/themes/intl/pt.css +20 -7
- package/src/themes/intl/zh.css +20 -7
- package/src/themes/intl.css +20 -7
- package/src/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- package/src/ts/perspective-viewer.ts +2 -0
- package/src/ts/plugin.ts +9 -0
- package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
- package/src/ts/ts-rs/ExportMethod.ts +1 -1
- package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
- package/src/ts/ts-rs/GetViewOptions.ts +7 -0
- package/src/ts/ts-rs/RestoreOptions.ts +7 -1
- package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
- package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
- package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
- package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
- package/src/rust/components/string_column_style.rs +0 -120
- package/src/rust/config/string_column_style.rs +0 -135
- package/src/ts/ts-rs/FormatMode.ts +0 -3
- package/src/ts/ts-rs/StringColorMode.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline5.js +0 -0
package/src/rust/session.rs
CHANGED
|
@@ -21,7 +21,7 @@ use std::cell::{Cell, Ref, RefCell};
|
|
|
21
21
|
use std::collections::{HashMap, HashSet};
|
|
22
22
|
use std::future::Future;
|
|
23
23
|
use std::ops::Deref;
|
|
24
|
-
use std::rc::Rc;
|
|
24
|
+
use std::rc::{Rc, Weak};
|
|
25
25
|
|
|
26
26
|
use perspective_client::config::*;
|
|
27
27
|
use perspective_client::proto::ViewDimensionsResp;
|
|
@@ -71,12 +71,8 @@ pub struct SessionHandle {
|
|
|
71
71
|
pub view_config_changed: PubSub<()>,
|
|
72
72
|
pub title_changed: PubSub<Option<String>>,
|
|
73
73
|
|
|
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>,
|
|
74
|
+
/// Account of in-flight CONFIG-DRIVEN pipeline runs.
|
|
75
|
+
pub config_runs: InFlight,
|
|
80
76
|
|
|
81
77
|
/// Fires when the user clicks the status indicator while in
|
|
82
78
|
/// [`StatusIconState::Normal`].
|
|
@@ -100,31 +96,22 @@ pub struct SessionHandle {
|
|
|
100
96
|
/// copying the config.
|
|
101
97
|
pub last_dispatched_config: RefCell<Option<std::rc::Rc<crate::config::ViewerConfig>>>,
|
|
102
98
|
|
|
103
|
-
/// Monotone load counter, bumped once per `load()` call
|
|
104
|
-
/// ([`Session::begin_pending_load`]). Identifies the LATEST load so a
|
|
105
|
-
/// stale async classification — React binds `load` to a prop and fires it
|
|
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
99
|
/// Open between a `load()` call and its payload's classification as
|
|
111
|
-
/// `Table`/`Client` ([`
|
|
112
|
-
|
|
100
|
+
/// `Table`/`Client` ([`LoadWindow`]), held weakly so its [`LoadGuard`] is
|
|
101
|
+
/// the sole owner.
|
|
102
|
+
pending_load: RefCell<Weak<LoadWindow>>,
|
|
113
103
|
|
|
114
104
|
/// Coalesces `view_config_changed`: multiple synchronous commits in one
|
|
115
105
|
/// task emit ONE event on the next microtask — the cadence the deleted
|
|
116
106
|
/// `is_clean` flag provided by accident, now deliberate.
|
|
117
107
|
config_event_scheduled: Cell<bool>,
|
|
118
108
|
|
|
119
|
-
///
|
|
120
|
-
/// panel (see [`Session::track_dispatch`]). `flush()` joins these via
|
|
109
|
+
/// Account of in-flight `perspective-config-update` dispatch tasks for
|
|
110
|
+
/// this panel (see [`Session::track_dispatch`]). `flush()` joins these via
|
|
121
111
|
/// [`Session::settle_dispatches`], so the "config-update fires before
|
|
122
112
|
/// `flush()` resolves" contract holds by construction instead of by
|
|
123
113
|
/// microtask luck.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/// Fires when [`SessionHandle::pending_dispatches`] returns to zero.
|
|
127
|
-
dispatches_settled: PubSub<()>,
|
|
114
|
+
dispatches: InFlight,
|
|
128
115
|
|
|
129
116
|
/// Fires when [`SessionHandle::column_stats`] is updated (insert or
|
|
130
117
|
/// clear). Subscribers re-render and re-query the schema with the
|
|
@@ -152,6 +139,51 @@ impl Deref for SessionHandle {
|
|
|
152
139
|
}
|
|
153
140
|
}
|
|
154
141
|
|
|
142
|
+
/// What an un-hosted `table` name means to a bind, per the `wait_for_table`
|
|
143
|
+
/// option.
|
|
144
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
145
|
+
pub enum MissingTable {
|
|
146
|
+
/// A hard error that leaves the panel as it was.
|
|
147
|
+
Error,
|
|
148
|
+
|
|
149
|
+
/// The panel waits unbound and binds when the name is hosted.
|
|
150
|
+
Pend,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl MissingTable {
|
|
154
|
+
/// Decode the `wait_for_table` option (absent = `false` = `Error`).
|
|
155
|
+
pub fn from_wait(wait_for_table: Option<bool>) -> Self {
|
|
156
|
+
if wait_for_table.unwrap_or_default() {
|
|
157
|
+
Self::Pend
|
|
158
|
+
} else {
|
|
159
|
+
Self::Error
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Open `name` on `client`, returning `Ok(None)` when the name is not hosted
|
|
165
|
+
/// and `missing` is [`MissingTable::Pend`].
|
|
166
|
+
pub(crate) async fn probe_table(
|
|
167
|
+
client: &Client,
|
|
168
|
+
name: &str,
|
|
169
|
+
missing: MissingTable,
|
|
170
|
+
) -> ApiResult<Option<perspective_client::Table>> {
|
|
171
|
+
match client.open_table(name.to_owned()).await {
|
|
172
|
+
Ok(table) => Ok(Some(table)),
|
|
173
|
+
Err(e) => {
|
|
174
|
+
let hosted = client.get_hosted_table_names().await.unwrap_or_default();
|
|
175
|
+
if hosted.iter().any(|n| n == name) {
|
|
176
|
+
return Err(e.into());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
match missing {
|
|
180
|
+
MissingTable::Pend => Ok(None),
|
|
181
|
+
MissingTable::Error => Err(ApiError::new(format!("Unknown table \"{name}\""))),
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
155
187
|
/// Mutable state for `Session`.
|
|
156
188
|
#[derive(Default)]
|
|
157
189
|
pub struct SessionData {
|
|
@@ -166,6 +198,11 @@ pub struct SessionData {
|
|
|
166
198
|
is_loading: bool,
|
|
167
199
|
is_paused: bool,
|
|
168
200
|
|
|
201
|
+
/// Terminal: set by [`Session::mark_disposed`] when the owning panel is
|
|
202
|
+
/// ejected, never cleared. Distinct from an `Ejected` reset, which a
|
|
203
|
+
/// suspended or element-ejected session may legitimately rebind after.
|
|
204
|
+
disposed: Option<Disposal>,
|
|
205
|
+
|
|
169
206
|
/// Memo for [`Session::validate_snapshot`]: the expression set validated
|
|
170
207
|
/// by the last successful server round trip; an equal snapshot skips the
|
|
171
208
|
/// round trip. Written only under the draw lock; cleared on table
|
|
@@ -210,6 +247,19 @@ impl TableErrorState {
|
|
|
210
247
|
}
|
|
211
248
|
}
|
|
212
249
|
|
|
250
|
+
/// How a terminal disposal reports itself to a public run still in flight on
|
|
251
|
+
/// the ejected panel.
|
|
252
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
253
|
+
pub enum Disposal {
|
|
254
|
+
/// A separate call destroyed the panel, so a racing public run rejects
|
|
255
|
+
/// rather than reporting a success it never achieved.
|
|
256
|
+
Reject,
|
|
257
|
+
|
|
258
|
+
/// The ejecting call reports the eviction on its own promise, so a racing
|
|
259
|
+
/// public run resolves rather than surfacing it twice.
|
|
260
|
+
Resolve,
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
#[derive(Debug, Default)]
|
|
214
264
|
pub enum TableIntermediateState {
|
|
215
265
|
#[default]
|
|
@@ -217,13 +267,52 @@ pub enum TableIntermediateState {
|
|
|
217
267
|
Reloaded,
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
/// The
|
|
221
|
-
///
|
|
222
|
-
///
|
|
223
|
-
|
|
224
|
-
pub struct
|
|
225
|
-
|
|
226
|
-
|
|
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;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
*slot = Weak::new();
|
|
306
|
+
drop(slot);
|
|
307
|
+
self.session.borrow_mut().is_loading = false;
|
|
308
|
+
true
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
impl Drop for LoadGuard {
|
|
313
|
+
fn drop(&mut self) {
|
|
314
|
+
self.close();
|
|
315
|
+
}
|
|
227
316
|
}
|
|
228
317
|
|
|
229
318
|
/// Options for [`Session::reset`]
|
|
@@ -248,19 +337,6 @@ pub struct ResetOptions {
|
|
|
248
337
|
#[derive(Clone)]
|
|
249
338
|
pub struct Session(Rc<SessionHandle>);
|
|
250
339
|
|
|
251
|
-
/// RAII spinner accounting for one config-driven pipeline run.
|
|
252
|
-
pub struct ConfigRunToken(Session);
|
|
253
|
-
|
|
254
|
-
impl Drop for ConfigRunToken {
|
|
255
|
-
fn drop(&mut self) {
|
|
256
|
-
let count = self.0.0.in_flight_config_runs.get();
|
|
257
|
-
debug_assert!(count > 0, "ConfigRunToken underflow");
|
|
258
|
-
let count = count.saturating_sub(1);
|
|
259
|
-
self.0.0.in_flight_config_runs.set(count);
|
|
260
|
-
self.0.run_state_changed.emit(count);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
340
|
impl ImplicitClone for Session {}
|
|
265
341
|
|
|
266
342
|
impl Deref for Session {
|
|
@@ -309,6 +385,7 @@ impl Session {
|
|
|
309
385
|
/// `Session`.
|
|
310
386
|
pub fn reset(&self, options: ResetOptions) -> impl Future<Output = ApiResult<()>> + use<> {
|
|
311
387
|
let view = self.0.borrow_mut().view_sub.take();
|
|
388
|
+
let had_table = self.borrow().table.is_some();
|
|
312
389
|
let err = self.get_error();
|
|
313
390
|
self.borrow_mut().error = None;
|
|
314
391
|
if options.stats {
|
|
@@ -328,11 +405,13 @@ impl Session {
|
|
|
328
405
|
self.borrow_mut().is_loading = false;
|
|
329
406
|
self.borrow_mut().table = None;
|
|
330
407
|
self.borrow_mut().pending_table = None;
|
|
408
|
+
self.borrow_mut().metadata = SessionMetadata::default();
|
|
331
409
|
},
|
|
332
410
|
Some(TableIntermediateState::Reloaded) => {
|
|
333
411
|
self.borrow_mut().is_loading = true;
|
|
334
412
|
self.borrow_mut().table = None;
|
|
335
413
|
self.borrow_mut().pending_table = None;
|
|
414
|
+
self.borrow_mut().metadata = SessionMetadata::default();
|
|
336
415
|
},
|
|
337
416
|
_ => {
|
|
338
417
|
self.borrow_mut().is_loading = false;
|
|
@@ -353,7 +432,7 @@ impl Session {
|
|
|
353
432
|
let session = self.clone();
|
|
354
433
|
async move {
|
|
355
434
|
let res = view.delete().await;
|
|
356
|
-
if options.table.is_some() {
|
|
435
|
+
if options.table.is_some() && had_table {
|
|
357
436
|
session.table_unloaded.emit(true)
|
|
358
437
|
}
|
|
359
438
|
|
|
@@ -361,21 +440,19 @@ impl Session {
|
|
|
361
440
|
}
|
|
362
441
|
}
|
|
363
442
|
|
|
364
|
-
/// Open a
|
|
365
|
-
/// `
|
|
366
|
-
///
|
|
367
|
-
///
|
|
368
|
-
/// spinner
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
self.0.load_generation.set(generation);
|
|
373
|
-
*self.0.pending_load.borrow_mut() = Some(PendingLoad {
|
|
374
|
-
generation,
|
|
375
|
-
journal: Vec::new(),
|
|
376
|
-
});
|
|
443
|
+
/// Open a load window (see [`LoadWindow`]) at the `load()` call site,
|
|
444
|
+
/// returning the [`LoadGuard`] that owns it. Sets `is_loading` so a FRESH
|
|
445
|
+
/// panel shows the spinner while a slow payload resolves (a RELOAD keeps
|
|
446
|
+
/// its bound table, hence [`TableLoadState::Loaded`], throughout — no
|
|
447
|
+
/// spinner flicker).
|
|
448
|
+
pub fn begin_pending_load(&self) -> Rc<LoadGuard> {
|
|
449
|
+
let window = Rc::new(LoadWindow::default());
|
|
450
|
+
*self.0.pending_load.borrow_mut() = Rc::downgrade(&window);
|
|
377
451
|
self.borrow_mut().is_loading = true;
|
|
378
|
-
|
|
452
|
+
Rc::new(LoadGuard {
|
|
453
|
+
session: self.clone(),
|
|
454
|
+
window,
|
|
455
|
+
})
|
|
379
456
|
}
|
|
380
457
|
|
|
381
458
|
/// Whether a `load()` payload is still awaiting classification. A `true`
|
|
@@ -383,25 +460,23 @@ impl Session {
|
|
|
383
460
|
/// ([`crate::tasks::bind_snapshot`]) — the incoming config must never draw
|
|
384
461
|
/// against the outgoing table.
|
|
385
462
|
pub fn has_pending_load(&self) -> bool {
|
|
386
|
-
self.0.pending_load.borrow().
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/// Close the pending-load window for `generation`, returning its journal
|
|
390
|
-
/// (the raw deltas committed while it was open, in order) for replay.
|
|
391
|
-
/// Returns `None` when a later `load()` already superseded this one (or it
|
|
392
|
-
/// was already closed), which the caller treats as "abandon this
|
|
393
|
-
/// classification, hold the frame". Clears `is_loading` — the
|
|
394
|
-
/// classification that follows sets the real table state.
|
|
395
|
-
pub fn take_pending_load(&self, generation: u32) -> Option<Vec<ViewConfigUpdate>> {
|
|
396
|
-
let mut slot = self.0.pending_load.borrow_mut();
|
|
397
|
-
if slot.as_ref().map(|p| p.generation) != Some(generation) {
|
|
398
|
-
return None;
|
|
399
|
-
}
|
|
463
|
+
self.0.pending_load.borrow().strong_count() > 0
|
|
464
|
+
}
|
|
400
465
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
466
|
+
/// Mark this session disposed (its panel was ejected): every later
|
|
467
|
+
/// table bind and locked run refuses, so nothing can rebind or draw a
|
|
468
|
+
/// panel that no longer exists.
|
|
469
|
+
pub(crate) fn mark_disposed(&self, disposal: Disposal) {
|
|
470
|
+
self.borrow_mut().disposed = Some(disposal);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
pub(crate) fn is_disposed(&self) -> bool {
|
|
474
|
+
self.borrow().disposed.is_some()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/// This session's disposal, if its panel was ejected.
|
|
478
|
+
pub(crate) fn disposal(&self) -> Option<Disposal> {
|
|
479
|
+
self.borrow().disposed
|
|
405
480
|
}
|
|
406
481
|
|
|
407
482
|
pub(crate) fn has_table(&self) -> Option<TableLoadState> {
|
|
@@ -410,6 +485,8 @@ impl Session {
|
|
|
410
485
|
Some(TableLoadState::Loaded)
|
|
411
486
|
} else if data.is_loading {
|
|
412
487
|
Some(TableLoadState::Loading)
|
|
488
|
+
} else if data.pending_table.is_some() {
|
|
489
|
+
Some(TableLoadState::Pending)
|
|
413
490
|
} else {
|
|
414
491
|
None
|
|
415
492
|
}
|
|
@@ -470,10 +547,8 @@ impl Session {
|
|
|
470
547
|
/// `table_name` is unique per `Client`, so if this value has not changed,
|
|
471
548
|
/// `Session::set_table` does nothing and returns `Ok(false)`.
|
|
472
549
|
///
|
|
473
|
-
/// A name
|
|
474
|
-
///
|
|
475
|
-
/// `Ok(false)`; the element's hosted-tables subscription
|
|
476
|
-
/// (`tasks::table_lifecycle`) re-runs the bind when the table is created.
|
|
550
|
+
/// A name no loaded client hosts pends ([`MissingTable::Pend`]) and
|
|
551
|
+
/// returns `Ok(false)` until the table lifecycle re-runs the bind.
|
|
477
552
|
pub async fn set_table(&self, table_name: String) -> ApiResult<bool> {
|
|
478
553
|
if Some(table_name.as_str()) == self.0.borrow().table.as_ref().map(|x| x.get_name()) {
|
|
479
554
|
self.0.borrow_mut().pending_table = None;
|
|
@@ -481,60 +556,76 @@ impl Session {
|
|
|
481
556
|
}
|
|
482
557
|
|
|
483
558
|
let client = self.0.borrow().client.clone().into_apierror()?;
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// open failure by the hosted set, not the error's text.
|
|
489
|
-
let hosted = client.get_hosted_table_names().await.unwrap_or_default();
|
|
490
|
-
if hosted.iter().any(|n| n == &table_name) {
|
|
491
|
-
return Err(e.into());
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
self.0.borrow_mut().pending_table = Some(table_name);
|
|
495
|
-
return Ok(false);
|
|
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),
|
|
496
563
|
},
|
|
497
|
-
|
|
564
|
+
None => {
|
|
565
|
+
self.pend_table(table_name).await?;
|
|
566
|
+
Ok(false)
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
}
|
|
498
570
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
session.borrow_mut().error = Some(TableErrorState(message, reconnect));
|
|
506
|
-
if let Some(cb) = &on_error {
|
|
507
|
-
cb.emit(());
|
|
508
|
-
}
|
|
509
|
-
if let Some(sub) = session.borrow_mut().view_sub.take() {
|
|
510
|
-
sub.dismiss();
|
|
511
|
-
}
|
|
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
|
+
}
|
|
512
577
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
578
|
+
let metadata = SessionMetadata::from_table(&table).await?;
|
|
579
|
+
let client = table.get_client();
|
|
580
|
+
let on_error = self.on_table_errored.borrow().clone();
|
|
581
|
+
let session = self.clone();
|
|
582
|
+
let poll_loop = LocalPollLoop::new(move |(message, reconnect): (ApiError, _)| {
|
|
583
|
+
session.borrow_mut().error = Some(TableErrorState(message, reconnect));
|
|
584
|
+
if let Some(cb) = &on_error {
|
|
585
|
+
cb.emit(());
|
|
586
|
+
}
|
|
587
|
+
if let Some(sub) = session.borrow_mut().view_sub.take() {
|
|
588
|
+
sub.dismiss();
|
|
589
|
+
}
|
|
525
590
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
591
|
+
Ok(JsValue::UNDEFINED)
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
let _callback_id = client
|
|
595
|
+
.on_error(Box::new(move |message: ClientError, reconnect| {
|
|
596
|
+
let poll_loop = poll_loop.clone();
|
|
597
|
+
async move {
|
|
598
|
+
poll_loop.poll((message.into(), reconnect)).await;
|
|
599
|
+
Ok(())
|
|
600
|
+
}
|
|
601
|
+
}))
|
|
602
|
+
.await?;
|
|
603
|
+
|
|
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
|
+
Ok(())
|
|
613
|
+
}
|
|
614
|
+
|
|
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"));
|
|
537
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
|
|
538
629
|
}
|
|
539
630
|
|
|
540
631
|
pub async fn set_error(&self, reset_table: bool, err: ApiError) -> ApiResult<()> {
|
|
@@ -754,20 +845,20 @@ impl Session {
|
|
|
754
845
|
// absent from the outgoing one — mirroring the "no table bound yet"
|
|
755
846
|
// leniency already in `validate_names`. Authoritative validation is
|
|
756
847
|
// deferred to the replay.
|
|
757
|
-
let
|
|
848
|
+
let window = self.0.pending_load.borrow().upgrade();
|
|
758
849
|
let mut candidate = self.borrow().config.clone();
|
|
759
|
-
let journal_entry =
|
|
850
|
+
let journal_entry = window.is_some().then(|| config_update.clone());
|
|
760
851
|
if !candidate.apply_update(config_update) {
|
|
761
852
|
return Ok(());
|
|
762
853
|
}
|
|
763
854
|
|
|
764
|
-
if let Some(
|
|
765
|
-
&& let Some(
|
|
855
|
+
if let Some(window) = &window
|
|
856
|
+
&& let Some(entry) = journal_entry
|
|
766
857
|
{
|
|
767
|
-
|
|
858
|
+
window.journal.borrow_mut().push(entry);
|
|
768
859
|
}
|
|
769
860
|
|
|
770
|
-
if
|
|
861
|
+
if window.is_none() {
|
|
771
862
|
self.validate_names(&candidate)?;
|
|
772
863
|
}
|
|
773
864
|
|
|
@@ -871,19 +962,10 @@ impl Session {
|
|
|
871
962
|
/// [`Self::settle_dispatches`]. Used by the `perspective-config-update`
|
|
872
963
|
/// dispatcher, whose landing `flush()` must be able to await.
|
|
873
964
|
pub fn track_dispatch(&self, fut: impl std::future::Future<Output = ApiResult<()>> + 'static) {
|
|
874
|
-
self.0
|
|
875
|
-
.pending_dispatches
|
|
876
|
-
.set(self.0.pending_dispatches.get() + 1);
|
|
877
|
-
let session = self.clone();
|
|
965
|
+
let guard = self.0.dispatches.guard();
|
|
878
966
|
ApiFuture::spawn_named("config-update-dispatch", async move {
|
|
879
|
-
let
|
|
880
|
-
let
|
|
881
|
-
session.0.pending_dispatches.set(remaining);
|
|
882
|
-
if remaining == 0 {
|
|
883
|
-
session.0.dispatches_settled.emit(());
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if let Err(e) = result {
|
|
967
|
+
let _guard = guard;
|
|
968
|
+
if let Err(e) = fut.await {
|
|
887
969
|
tracing::error!("[config-update dispatch] {}", e);
|
|
888
970
|
}
|
|
889
971
|
|
|
@@ -894,10 +976,7 @@ impl Session {
|
|
|
894
976
|
/// Resolve once every in-flight tracked dispatch for this panel has
|
|
895
977
|
/// landed. Immediate when none are pending.
|
|
896
978
|
pub async fn settle_dispatches(&self) -> ApiResult<()> {
|
|
897
|
-
|
|
898
|
-
self.0.dispatches_settled.read_next().await?;
|
|
899
|
-
}
|
|
900
|
-
|
|
979
|
+
self.0.dispatches.settle().await;
|
|
901
980
|
Ok(())
|
|
902
981
|
}
|
|
903
982
|
|
|
@@ -917,26 +996,10 @@ impl Session {
|
|
|
917
996
|
}
|
|
918
997
|
|
|
919
998
|
/// Begin spinner accounting for ONE config-driven pipeline run: call
|
|
920
|
-
/// immediately after the run's commit, and move the returned
|
|
921
|
-
/// the run future.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
/// count cannot strand by construction.
|
|
925
|
-
/// [`SessionHandle::run_state_changed`] broadcasts the ABSOLUTE count
|
|
926
|
-
/// on both edges.
|
|
927
|
-
pub fn begin_config_run(&self) -> ConfigRunToken {
|
|
928
|
-
let count = self.0.in_flight_config_runs.get() + 1;
|
|
929
|
-
self.0.in_flight_config_runs.set(count);
|
|
930
|
-
self.run_state_changed.emit(count);
|
|
931
|
-
ConfigRunToken(self.clone())
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/// The number of config-driven pipeline runs currently in flight (see
|
|
935
|
-
/// [`Self::begin_config_run`]). Read directly when (re)targeting a
|
|
936
|
-
/// panel — level-triggered consumers initialize from this and then
|
|
937
|
-
/// track [`SessionHandle::run_state_changed`].
|
|
938
|
-
pub fn in_flight_config_runs(&self) -> u32 {
|
|
939
|
-
self.0.in_flight_config_runs.get()
|
|
999
|
+
/// immediately after the run's commit, and move the returned guard INTO
|
|
1000
|
+
/// the run future.
|
|
1001
|
+
pub fn begin_config_run(&self) -> InFlightGuard {
|
|
1002
|
+
self.0.config_runs.guard()
|
|
940
1003
|
}
|
|
941
1004
|
|
|
942
1005
|
/// Read the cached `ColumnStats` for a column. Returns `None` if no
|
|
@@ -1094,31 +1157,7 @@ impl Session {
|
|
|
1094
1157
|
.clone()
|
|
1095
1158
|
.ok_or("`restore()` called before `load()`")?;
|
|
1096
1159
|
|
|
1097
|
-
|
|
1098
|
-
// virtual server api. Snapshot-local — never written back.
|
|
1099
|
-
let mut view_config = (*effective).clone();
|
|
1100
|
-
for col in view_config
|
|
1101
|
-
.columns
|
|
1102
|
-
.iter()
|
|
1103
|
-
.flatten()
|
|
1104
|
-
.chain(view_config.sort.iter().map(|x| &x.0))
|
|
1105
|
-
{
|
|
1106
|
-
if !view_config.aggregates.contains_key(col.as_str()) {
|
|
1107
|
-
let agg = self
|
|
1108
|
-
.metadata()
|
|
1109
|
-
.get_column_aggregates(col.as_str())
|
|
1110
|
-
.and_then(|mut aggs| aggs.next())
|
|
1111
|
-
.into_apierror();
|
|
1112
|
-
|
|
1113
|
-
match agg {
|
|
1114
|
-
Err(_) => {
|
|
1115
|
-
tracing::warn!("No default aggregate for column '{}' found, skipping", col)
|
|
1116
|
-
},
|
|
1117
|
-
Ok(agg) => _ = view_config.aggregates.insert(col.to_string(), agg),
|
|
1118
|
-
};
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1160
|
+
let view_config = self.with_default_aggregates(&effective);
|
|
1122
1161
|
let view = table.view(Some(view_config.into())).await?;
|
|
1123
1162
|
let view_schema = view.schema().await?;
|
|
1124
1163
|
self.metadata_mut().update_view_schema(&view_schema)?;
|
|
@@ -1150,6 +1189,43 @@ impl Session {
|
|
|
1150
1189
|
}
|
|
1151
1190
|
}
|
|
1152
1191
|
|
|
1192
|
+
/// The engine config for a `View` built from `effective`, per-column
|
|
1193
|
+
/// default aggregates filled in as a courtesy to the virtual server API.
|
|
1194
|
+
fn with_default_aggregates(&self, effective: &ViewConfig) -> ViewConfig {
|
|
1195
|
+
let mut view_config = effective.clone();
|
|
1196
|
+
for col in view_config
|
|
1197
|
+
.columns
|
|
1198
|
+
.iter()
|
|
1199
|
+
.flatten()
|
|
1200
|
+
.chain(view_config.sort.iter().map(|x| &x.0))
|
|
1201
|
+
{
|
|
1202
|
+
if !view_config.aggregates.contains_key(col.as_str()) {
|
|
1203
|
+
let agg = self
|
|
1204
|
+
.metadata()
|
|
1205
|
+
.get_column_aggregates(col.as_str())
|
|
1206
|
+
.and_then(|mut aggs| aggs.next())
|
|
1207
|
+
.into_apierror();
|
|
1208
|
+
|
|
1209
|
+
match agg {
|
|
1210
|
+
Err(_) => {
|
|
1211
|
+
tracing::warn!("No default aggregate for column '{}' found, skipping", col)
|
|
1212
|
+
},
|
|
1213
|
+
Ok(agg) => _ = view_config.aggregates.insert(col.to_string(), agg),
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
view_config
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/// Build a caller-owned `View` from the effective config outside the
|
|
1222
|
+
/// render pipeline, with no subscription and no interaction with pause.
|
|
1223
|
+
pub async fn create_detached_view(&self) -> ApiResult<View> {
|
|
1224
|
+
let table = self.borrow().table.clone().ok_or("No `Table` set")?;
|
|
1225
|
+
let view_config = self.with_default_aggregates(&self.effective_view_config());
|
|
1226
|
+
Ok(table.view(Some(view_config.into())).await?)
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1153
1229
|
/// Record a failed pipeline run: error state plus a reconnect affordance
|
|
1154
1230
|
/// that resets the config (the error screen's reset button). Replaces
|
|
1155
1231
|
/// the old `validate()` error path — the committed config is NOT rolled
|
|
@@ -1239,6 +1315,8 @@ impl Session {
|
|
|
1239
1315
|
Some(TableLoadState::Loaded)
|
|
1240
1316
|
} else if data.is_loading {
|
|
1241
1317
|
Some(TableLoadState::Loading)
|
|
1318
|
+
} else if data.pending_table.is_some() {
|
|
1319
|
+
Some(TableLoadState::Pending)
|
|
1242
1320
|
} else {
|
|
1243
1321
|
None
|
|
1244
1322
|
},
|