@perspective-dev/viewer 5.5.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 +1 -1
- package/dist/cdn/perspective-viewer.js.map +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/docs/perspective-docs.json +1 -1
- package/dist/esm/perspective-viewer.inline.js +3 -3
- package/dist/esm/perspective-viewer.inline.js.map +2 -2
- package/dist/esm/perspective-viewer.js +1 -1
- package/dist/esm/perspective-viewer.js.map +1 -1
- package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
- package/dist/wasm/perspective-viewer.d.ts +27 -16
- package/dist/wasm/perspective-viewer.js +112 -101
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +16 -16
- package/package.json +7 -7
- package/src/rust/components/export_dropdown.rs +6 -0
- package/src/rust/components/status_bar.rs +12 -3
- 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 +3 -3
- package/src/rust/components/viewer/wiring.rs +4 -1
- package/src/rust/components/viewer.rs +2 -2
- package/src/rust/config/export_method.rs +24 -0
- package/src/rust/custom_elements/viewer.rs +21 -17
- package/src/rust/session/metadata.rs +5 -10
- package/src/rust/session.rs +123 -116
- package/src/rust/tasks/copy_export.rs +67 -0
- package/src/rust/tasks/create_panel.rs +13 -13
- package/src/rust/tasks/pipeline.rs +8 -1
- package/src/rust/tasks/restore_panel.rs +15 -15
- package/src/rust/tasks/sync_update_panels.rs +3 -3
- package/src/rust/utils/in_flight.rs +74 -0
- package/src/rust/utils/mod.rs +2 -0
- package/src/rust/utils/pending_effects.rs +6 -30
- package/src/themes/monokai.css +1 -1
- package/src/themes/vaporwave.css +1 -1
- package/src/ts/ts-rs/ExportMethod.ts +1 -1
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-ea71a3990cad35d7 → 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
|
|
@@ -211,6 +198,11 @@ pub struct SessionData {
|
|
|
211
198
|
is_loading: bool,
|
|
212
199
|
is_paused: bool,
|
|
213
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
|
+
|
|
214
206
|
/// Memo for [`Session::validate_snapshot`]: the expression set validated
|
|
215
207
|
/// by the last successful server round trip; an equal snapshot skips the
|
|
216
208
|
/// round trip. Written only under the draw lock; cleared on table
|
|
@@ -255,6 +247,19 @@ impl TableErrorState {
|
|
|
255
247
|
}
|
|
256
248
|
}
|
|
257
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
|
+
|
|
258
263
|
#[derive(Debug, Default)]
|
|
259
264
|
pub enum TableIntermediateState {
|
|
260
265
|
#[default]
|
|
@@ -262,13 +267,52 @@ pub enum TableIntermediateState {
|
|
|
262
267
|
Reloaded,
|
|
263
268
|
}
|
|
264
269
|
|
|
265
|
-
/// The
|
|
266
|
-
///
|
|
267
|
-
///
|
|
268
|
-
|
|
269
|
-
pub struct
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
316
|
}
|
|
273
317
|
|
|
274
318
|
/// Options for [`Session::reset`]
|
|
@@ -293,19 +337,6 @@ pub struct ResetOptions {
|
|
|
293
337
|
#[derive(Clone)]
|
|
294
338
|
pub struct Session(Rc<SessionHandle>);
|
|
295
339
|
|
|
296
|
-
/// RAII spinner accounting for one config-driven pipeline run.
|
|
297
|
-
pub struct ConfigRunToken(Session);
|
|
298
|
-
|
|
299
|
-
impl Drop for ConfigRunToken {
|
|
300
|
-
fn drop(&mut self) {
|
|
301
|
-
let count = self.0.0.in_flight_config_runs.get();
|
|
302
|
-
debug_assert!(count > 0, "ConfigRunToken underflow");
|
|
303
|
-
let count = count.saturating_sub(1);
|
|
304
|
-
self.0.0.in_flight_config_runs.set(count);
|
|
305
|
-
self.0.run_state_changed.emit(count);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
340
|
impl ImplicitClone for Session {}
|
|
310
341
|
|
|
311
342
|
impl Deref for Session {
|
|
@@ -409,21 +440,19 @@ impl Session {
|
|
|
409
440
|
}
|
|
410
441
|
}
|
|
411
442
|
|
|
412
|
-
/// Open a
|
|
413
|
-
/// `
|
|
414
|
-
///
|
|
415
|
-
///
|
|
416
|
-
/// spinner
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
self.0.load_generation.set(generation);
|
|
421
|
-
*self.0.pending_load.borrow_mut() = Some(PendingLoad {
|
|
422
|
-
generation,
|
|
423
|
-
journal: Vec::new(),
|
|
424
|
-
});
|
|
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);
|
|
425
451
|
self.borrow_mut().is_loading = true;
|
|
426
|
-
|
|
452
|
+
Rc::new(LoadGuard {
|
|
453
|
+
session: self.clone(),
|
|
454
|
+
window,
|
|
455
|
+
})
|
|
427
456
|
}
|
|
428
457
|
|
|
429
458
|
/// Whether a `load()` payload is still awaiting classification. A `true`
|
|
@@ -431,25 +460,23 @@ impl Session {
|
|
|
431
460
|
/// ([`crate::tasks::bind_snapshot`]) — the incoming config must never draw
|
|
432
461
|
/// against the outgoing table.
|
|
433
462
|
pub fn has_pending_load(&self) -> bool {
|
|
434
|
-
self.0.pending_load.borrow().
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/// Close the pending-load window for `generation`, returning its journal
|
|
438
|
-
/// (the raw deltas committed while it was open, in order) for replay.
|
|
439
|
-
/// Returns `None` when a later `load()` already superseded this one (or it
|
|
440
|
-
/// was already closed), which the caller treats as "abandon this
|
|
441
|
-
/// classification, hold the frame". Clears `is_loading` — the
|
|
442
|
-
/// classification that follows sets the real table state.
|
|
443
|
-
pub fn take_pending_load(&self, generation: u32) -> Option<Vec<ViewConfigUpdate>> {
|
|
444
|
-
let mut slot = self.0.pending_load.borrow_mut();
|
|
445
|
-
if slot.as_ref().map(|p| p.generation) != Some(generation) {
|
|
446
|
-
return None;
|
|
447
|
-
}
|
|
463
|
+
self.0.pending_load.borrow().strong_count() > 0
|
|
464
|
+
}
|
|
448
465
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
453
480
|
}
|
|
454
481
|
|
|
455
482
|
pub(crate) fn has_table(&self) -> Option<TableLoadState> {
|
|
@@ -544,6 +571,10 @@ impl Session {
|
|
|
544
571
|
/// Bind an opened `Table`, replacing any previous binding and announcing
|
|
545
572
|
/// `table_loaded`.
|
|
546
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
|
+
|
|
547
578
|
let metadata = SessionMetadata::from_table(&table).await?;
|
|
548
579
|
let client = table.get_client();
|
|
549
580
|
let on_error = self.on_table_errored.borrow().clone();
|
|
@@ -584,6 +615,10 @@ impl Session {
|
|
|
584
615
|
/// Record `name` as pending, dropping any bound `Table` so nothing binds
|
|
585
616
|
/// against the outgoing table while the name waits for a host.
|
|
586
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
|
+
|
|
587
622
|
let sub = self.borrow_mut().view_sub.take();
|
|
588
623
|
self.borrow_mut().table = None;
|
|
589
624
|
self.borrow_mut().metadata = SessionMetadata::default();
|
|
@@ -810,20 +845,20 @@ impl Session {
|
|
|
810
845
|
// absent from the outgoing one — mirroring the "no table bound yet"
|
|
811
846
|
// leniency already in `validate_names`. Authoritative validation is
|
|
812
847
|
// deferred to the replay.
|
|
813
|
-
let
|
|
848
|
+
let window = self.0.pending_load.borrow().upgrade();
|
|
814
849
|
let mut candidate = self.borrow().config.clone();
|
|
815
|
-
let journal_entry =
|
|
850
|
+
let journal_entry = window.is_some().then(|| config_update.clone());
|
|
816
851
|
if !candidate.apply_update(config_update) {
|
|
817
852
|
return Ok(());
|
|
818
853
|
}
|
|
819
854
|
|
|
820
|
-
if let Some(
|
|
821
|
-
&& let Some(
|
|
855
|
+
if let Some(window) = &window
|
|
856
|
+
&& let Some(entry) = journal_entry
|
|
822
857
|
{
|
|
823
|
-
|
|
858
|
+
window.journal.borrow_mut().push(entry);
|
|
824
859
|
}
|
|
825
860
|
|
|
826
|
-
if
|
|
861
|
+
if window.is_none() {
|
|
827
862
|
self.validate_names(&candidate)?;
|
|
828
863
|
}
|
|
829
864
|
|
|
@@ -927,19 +962,10 @@ impl Session {
|
|
|
927
962
|
/// [`Self::settle_dispatches`]. Used by the `perspective-config-update`
|
|
928
963
|
/// dispatcher, whose landing `flush()` must be able to await.
|
|
929
964
|
pub fn track_dispatch(&self, fut: impl std::future::Future<Output = ApiResult<()>> + 'static) {
|
|
930
|
-
self.0
|
|
931
|
-
.pending_dispatches
|
|
932
|
-
.set(self.0.pending_dispatches.get() + 1);
|
|
933
|
-
let session = self.clone();
|
|
965
|
+
let guard = self.0.dispatches.guard();
|
|
934
966
|
ApiFuture::spawn_named("config-update-dispatch", async move {
|
|
935
|
-
let
|
|
936
|
-
let
|
|
937
|
-
session.0.pending_dispatches.set(remaining);
|
|
938
|
-
if remaining == 0 {
|
|
939
|
-
session.0.dispatches_settled.emit(());
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if let Err(e) = result {
|
|
967
|
+
let _guard = guard;
|
|
968
|
+
if let Err(e) = fut.await {
|
|
943
969
|
tracing::error!("[config-update dispatch] {}", e);
|
|
944
970
|
}
|
|
945
971
|
|
|
@@ -950,10 +976,7 @@ impl Session {
|
|
|
950
976
|
/// Resolve once every in-flight tracked dispatch for this panel has
|
|
951
977
|
/// landed. Immediate when none are pending.
|
|
952
978
|
pub async fn settle_dispatches(&self) -> ApiResult<()> {
|
|
953
|
-
|
|
954
|
-
self.0.dispatches_settled.read_next().await?;
|
|
955
|
-
}
|
|
956
|
-
|
|
979
|
+
self.0.dispatches.settle().await;
|
|
957
980
|
Ok(())
|
|
958
981
|
}
|
|
959
982
|
|
|
@@ -973,26 +996,10 @@ impl Session {
|
|
|
973
996
|
}
|
|
974
997
|
|
|
975
998
|
/// Begin spinner accounting for ONE config-driven pipeline run: call
|
|
976
|
-
/// immediately after the run's commit, and move the returned
|
|
977
|
-
/// the run future.
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
/// count cannot strand by construction.
|
|
981
|
-
/// [`SessionHandle::run_state_changed`] broadcasts the ABSOLUTE count
|
|
982
|
-
/// on both edges.
|
|
983
|
-
pub fn begin_config_run(&self) -> ConfigRunToken {
|
|
984
|
-
let count = self.0.in_flight_config_runs.get() + 1;
|
|
985
|
-
self.0.in_flight_config_runs.set(count);
|
|
986
|
-
self.run_state_changed.emit(count);
|
|
987
|
-
ConfigRunToken(self.clone())
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/// The number of config-driven pipeline runs currently in flight (see
|
|
991
|
-
/// [`Self::begin_config_run`]). Read directly when (re)targeting a
|
|
992
|
-
/// panel — level-triggered consumers initialize from this and then
|
|
993
|
-
/// track [`SessionHandle::run_state_changed`].
|
|
994
|
-
pub fn in_flight_config_runs(&self) -> u32 {
|
|
995
|
-
self.0.in_flight_config_runs.get()
|
|
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()
|
|
996
1003
|
}
|
|
997
1004
|
|
|
998
1005
|
/// Read the cached `ColumnStats` for a column. Returns `None` if no
|
|
@@ -107,6 +107,13 @@ pub async fn txt_as_jsvalue(
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/// Generate a result `Blob` for all types of [`ExportMethod`].
|
|
110
|
+
/// The `window` (or a default one) with Arrow IPC body compression `codec`.
|
|
111
|
+
fn compressed(window: Option<ViewWindow>, codec: &str) -> Option<ViewWindow> {
|
|
112
|
+
let mut window = window.unwrap_or_default();
|
|
113
|
+
window.compression = Some(codec.to_owned());
|
|
114
|
+
Some(window)
|
|
115
|
+
}
|
|
116
|
+
|
|
110
117
|
pub async fn export_method_to_blob(
|
|
111
118
|
session: &Session,
|
|
112
119
|
renderer: &Renderer,
|
|
@@ -152,6 +159,36 @@ pub async fn export_method_to_blob(
|
|
|
152
159
|
ExportMethod::ArrowAll => crate::queries::arrow_as_jsvalue(session, true, None)
|
|
153
160
|
.await?
|
|
154
161
|
.as_blob(),
|
|
162
|
+
ExportMethod::ArrowLz4 => {
|
|
163
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(None, "lz4"))
|
|
164
|
+
.await?
|
|
165
|
+
.as_blob()
|
|
166
|
+
},
|
|
167
|
+
ExportMethod::ArrowLz4Selected => {
|
|
168
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(viewport, "lz4"))
|
|
169
|
+
.await?
|
|
170
|
+
.as_blob()
|
|
171
|
+
},
|
|
172
|
+
ExportMethod::ArrowLz4All => {
|
|
173
|
+
crate::queries::arrow_as_jsvalue(session, true, compressed(None, "lz4"))
|
|
174
|
+
.await?
|
|
175
|
+
.as_blob()
|
|
176
|
+
},
|
|
177
|
+
ExportMethod::ArrowZstd => {
|
|
178
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(None, "zstd"))
|
|
179
|
+
.await?
|
|
180
|
+
.as_blob()
|
|
181
|
+
},
|
|
182
|
+
ExportMethod::ArrowZstdSelected => {
|
|
183
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(viewport, "zstd"))
|
|
184
|
+
.await?
|
|
185
|
+
.as_blob()
|
|
186
|
+
},
|
|
187
|
+
ExportMethod::ArrowZstdAll => {
|
|
188
|
+
crate::queries::arrow_as_jsvalue(session, true, compressed(None, "zstd"))
|
|
189
|
+
.await?
|
|
190
|
+
.as_blob()
|
|
191
|
+
},
|
|
155
192
|
ExportMethod::Html => html_as_jsvalue(session, renderer, presentation)
|
|
156
193
|
.await?
|
|
157
194
|
.as_blob(),
|
|
@@ -212,6 +249,36 @@ pub async fn export_method_to_jsvalue(
|
|
|
212
249
|
ExportMethod::ArrowAll => crate::queries::arrow_as_jsvalue(session, true, None)
|
|
213
250
|
.await?
|
|
214
251
|
.into(),
|
|
252
|
+
ExportMethod::ArrowLz4 => {
|
|
253
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(None, "lz4"))
|
|
254
|
+
.await?
|
|
255
|
+
.into()
|
|
256
|
+
},
|
|
257
|
+
ExportMethod::ArrowLz4Selected => {
|
|
258
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(viewport, "lz4"))
|
|
259
|
+
.await?
|
|
260
|
+
.into()
|
|
261
|
+
},
|
|
262
|
+
ExportMethod::ArrowLz4All => {
|
|
263
|
+
crate::queries::arrow_as_jsvalue(session, true, compressed(None, "lz4"))
|
|
264
|
+
.await?
|
|
265
|
+
.into()
|
|
266
|
+
},
|
|
267
|
+
ExportMethod::ArrowZstd => {
|
|
268
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(None, "zstd"))
|
|
269
|
+
.await?
|
|
270
|
+
.into()
|
|
271
|
+
},
|
|
272
|
+
ExportMethod::ArrowZstdSelected => {
|
|
273
|
+
crate::queries::arrow_as_jsvalue(session, false, compressed(viewport, "zstd"))
|
|
274
|
+
.await?
|
|
275
|
+
.into()
|
|
276
|
+
},
|
|
277
|
+
ExportMethod::ArrowZstdAll => {
|
|
278
|
+
crate::queries::arrow_as_jsvalue(session, true, compressed(None, "zstd"))
|
|
279
|
+
.await?
|
|
280
|
+
.into()
|
|
281
|
+
},
|
|
215
282
|
ExportMethod::Html => html_as_jsvalue(session, renderer, presentation).await?,
|
|
216
283
|
ExportMethod::Plugin if renderer.is_chart() => {
|
|
217
284
|
png_as_jsvalue(session, renderer).await?.into()
|
|
@@ -17,7 +17,7 @@ use crate::config::*;
|
|
|
17
17
|
use crate::custom_events::wire_panel_events;
|
|
18
18
|
use crate::presentation::*;
|
|
19
19
|
use crate::renderer::*;
|
|
20
|
-
use crate::session::{MissingTable, ResetOptions, Session};
|
|
20
|
+
use crate::session::{Disposal, MissingTable, ResetOptions, Session};
|
|
21
21
|
use crate::tasks::*;
|
|
22
22
|
use crate::utils::*;
|
|
23
23
|
use crate::workspace::{Panel, PanelId, PanelPhase, Workspace};
|
|
@@ -222,19 +222,19 @@ pub(crate) fn place_reserved(
|
|
|
222
222
|
/// renderer (slot-scoped plugin + light-DOM cleanup) and eject its table.
|
|
223
223
|
/// Shared by the root's `ClosePanel` handler and `restoreWorkspace`'s
|
|
224
224
|
/// batch replacement of the pre-existing panel set.
|
|
225
|
-
pub(crate) fn eject_panel(panel: Panel) -> ApiFuture<()> {
|
|
226
|
-
|
|
227
|
-
let dispose_task = panel.renderer.dispose();
|
|
228
|
-
let reset_task = panel.session.reset(ResetOptions {
|
|
229
|
-
config: true,
|
|
230
|
-
expressions: true,
|
|
231
|
-
table: Some(session::TableIntermediateState::Ejected),
|
|
232
|
-
..ResetOptions::default()
|
|
233
|
-
});
|
|
234
|
-
|
|
225
|
+
pub(crate) fn eject_panel(panel: Panel, disposal: Disposal) -> ApiFuture<()> {
|
|
226
|
+
panel.session.mark_disposed(disposal);
|
|
235
227
|
ApiFuture::new(async move {
|
|
236
|
-
|
|
237
|
-
|
|
228
|
+
panel.renderer.dispose().await?;
|
|
229
|
+
let was_errored = panel.session.is_errored();
|
|
230
|
+
let reset = panel.session.reset(ResetOptions {
|
|
231
|
+
config: true,
|
|
232
|
+
expressions: true,
|
|
233
|
+
table: Some(session::TableIntermediateState::Ejected),
|
|
234
|
+
..ResetOptions::default()
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
match reset.await.ignore_view_delete() {
|
|
238
238
|
Err(_) if was_errored => Ok(()),
|
|
239
239
|
Err(e) => Err(e),
|
|
240
240
|
Ok(_) => Ok(()),
|
|
@@ -22,7 +22,7 @@ use yew::prelude::*;
|
|
|
22
22
|
use crate::config::{ColumnConfigUpdate, PluginConfigUpdate};
|
|
23
23
|
use crate::presentation::Presentation;
|
|
24
24
|
use crate::renderer::{RenderContext, Renderer};
|
|
25
|
-
use crate::session::{BindDisposition, Session};
|
|
25
|
+
use crate::session::{BindDisposition, Disposal, Session};
|
|
26
26
|
use crate::utils::RenderGuard;
|
|
27
27
|
|
|
28
28
|
/// Snapshot → validate → bind → cache + pin the [`RenderContext`]. The core
|
|
@@ -227,6 +227,13 @@ pub(crate) async fn locked_run(
|
|
|
227
227
|
renderer
|
|
228
228
|
.clone()
|
|
229
229
|
.render_task(|guard| async move {
|
|
230
|
+
if let Some(disposal) = session.disposal() {
|
|
231
|
+
return match (disposal, spec.origin) {
|
|
232
|
+
(Disposal::Reject, RunOrigin::Public) => Err(ApiError::new("Panel disposed")),
|
|
233
|
+
_ => Ok(()),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
230
237
|
renderer.mount_active_plugin()?;
|
|
231
238
|
if let Some(task) = spec.task {
|
|
232
239
|
task.await?;
|
|
@@ -16,7 +16,9 @@ use perspective_client::utils::PerspectiveResultExt;
|
|
|
16
16
|
use crate::config::*;
|
|
17
17
|
use crate::presentation::Presentation;
|
|
18
18
|
use crate::renderer::Renderer;
|
|
19
|
-
use crate::session::{
|
|
19
|
+
use crate::session::{
|
|
20
|
+
LoadGuard, MissingTable, ResetOptions, Session, TableIntermediateState, probe_table,
|
|
21
|
+
};
|
|
20
22
|
use crate::tasks::*;
|
|
21
23
|
use crate::workspace::Workspace;
|
|
22
24
|
use crate::*;
|
|
@@ -88,18 +90,18 @@ pub(crate) async fn bind_table_task(
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/// Rebind a session to `name`, probing the incoming table before the
|
|
91
|
-
/// outgoing binding is dropped and replaying
|
|
92
|
-
///
|
|
93
|
+
/// outgoing binding is dropped and replaying `load`'s journal over the
|
|
94
|
+
/// incoming table's defaults.
|
|
93
95
|
async fn rebind_table_task(
|
|
94
96
|
session: &Session,
|
|
95
97
|
renderer: &Renderer,
|
|
96
98
|
workspace: &Workspace,
|
|
97
99
|
name: String,
|
|
98
100
|
missing: MissingTable,
|
|
99
|
-
|
|
101
|
+
load: &LoadGuard,
|
|
100
102
|
) -> ApiResult<()> {
|
|
101
103
|
let probed = probe(session, workspace, &name, missing).await;
|
|
102
|
-
let journal =
|
|
104
|
+
let journal = load.claim();
|
|
103
105
|
let (client, table) = probed?;
|
|
104
106
|
let Some(journal) = journal else {
|
|
105
107
|
return Ok(());
|
|
@@ -186,7 +188,7 @@ pub(crate) async fn restore_panel(
|
|
|
186
188
|
session.pending_table(),
|
|
187
189
|
);
|
|
188
190
|
|
|
189
|
-
let
|
|
191
|
+
let load = match &update.table {
|
|
190
192
|
OptionalUpdate::Update(name)
|
|
191
193
|
if session.is_errored()
|
|
192
194
|
|| (!fresh
|
|
@@ -207,18 +209,16 @@ pub(crate) async fn restore_panel(
|
|
|
207
209
|
RunOrigin::Public,
|
|
208
210
|
update.clone(),
|
|
209
211
|
{
|
|
210
|
-
clone!(session, renderer, update.table, workspace);
|
|
212
|
+
clone!(session, renderer, update.table, workspace, load);
|
|
211
213
|
async move {
|
|
212
214
|
let OptionalUpdate::Update(name) = table else {
|
|
213
215
|
return Ok(());
|
|
214
216
|
};
|
|
215
217
|
|
|
216
|
-
match
|
|
217
|
-
Some(
|
|
218
|
-
rebind_table_task(
|
|
219
|
-
|
|
220
|
-
)
|
|
221
|
-
.await
|
|
218
|
+
match &load {
|
|
219
|
+
Some(load) => {
|
|
220
|
+
rebind_table_task(&session, &renderer, &workspace, name, missing, load)
|
|
221
|
+
.await
|
|
222
222
|
},
|
|
223
223
|
None => bind_table_task(&session, &workspace, name, missing).await,
|
|
224
224
|
}
|
|
@@ -227,8 +227,8 @@ pub(crate) async fn restore_panel(
|
|
|
227
227
|
)
|
|
228
228
|
.await;
|
|
229
229
|
|
|
230
|
-
if let Some(
|
|
231
|
-
|
|
230
|
+
if let Some(load) = &load {
|
|
231
|
+
load.close();
|
|
232
232
|
}
|
|
233
233
|
|
|
234
234
|
if let Err(e) = &result {
|
|
@@ -17,7 +17,7 @@ use wasm_bindgen::prelude::*;
|
|
|
17
17
|
use crate::components::viewer::PerspectiveViewerMsg;
|
|
18
18
|
use crate::config::*;
|
|
19
19
|
use crate::renderer::*;
|
|
20
|
-
use crate::session::Session;
|
|
20
|
+
use crate::session::{Disposal, Session};
|
|
21
21
|
use crate::tasks::*;
|
|
22
22
|
use crate::workspace::PanelId;
|
|
23
23
|
use crate::*;
|
|
@@ -91,12 +91,12 @@ pub fn sync_update_panels(
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if let Some(panel) = this.workspace.take_reserved() {
|
|
94
|
-
eject_tasks.push(eject_panel(panel));
|
|
94
|
+
eject_tasks.push(eject_panel(panel, Disposal::Reject));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
for old in &old_ids {
|
|
98
98
|
if let Some(panel) = this.workspace.remove_panel(old) {
|
|
99
|
-
eject_tasks.push(eject_panel(panel));
|
|
99
|
+
eject_tasks.push(eject_panel(panel, Disposal::Reject));
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
},
|