pub(crate) mod column_defaults_update;
pub(crate) mod drag_drop_update;
mod metadata;
mod op_queue;
mod panel_state;
mod props;
pub(crate) mod replace_expression_update;
mod view_subscription;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::ops::Deref;
use std::rc::Rc;
use perspective_client::config::*;
use perspective_client::proto::ViewDimensionsResp;
use perspective_client::{
Client, ClientError, DescribeError, Description, ExprValidationResult, ReconnectCallback, View,
};
use perspective_js::apierror;
use perspective_js::utils::*;
use wasm_bindgen::prelude::*;
use yew::html::ImplicitClone;
use yew::prelude::*;
use self::metadata::*;
pub use self::metadata::{MetadataRef, SessionMetadata, SessionMetadataRc};
use self::op_queue::OpQueue;
pub use self::op_queue::{EditDelta, OpCtx, OpKind, StepFuture, Ticket};
use self::panel_state::effective;
pub use self::panel_state::{Binding, OverlayClause, PanelState};
pub use self::props::{SessionProps, TableLoadState};
use self::view_subscription::*;
pub use self::view_subscription::{ViewStats, ViewSubscription};
use crate::config::PluginStaticConfig;
use crate::utils::*;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ColumnStats {
pub abs_max: Option<f64>,
}
#[derive(Default)]
pub struct SessionHandle {
session_data: RefCell<SessionData>,
pub table_updated: PubSub<()>,
pub table_loaded: PubSub<()>,
pub table_unloaded: PubSub<bool>,
pub view_created: PubSub<()>,
pub commit_reconciled: PubSub<()>,
pub view_config_changed: PubSub<()>,
pub title_changed: PubSub<Option<String>>,
pub config_runs: InFlight,
pub status_indicator_clicked: PubSub<()>,
column_stats: RefCell<HashMap<String, ColumnStats>>,
cached_config: RefCell<Option<PtrEqRc<ViewConfig>>>,
cached_metadata: RefCell<Option<SessionMetadataRc>>,
pub last_dispatched_config: RefCell<Option<std::rc::Rc<crate::config::ViewerConfig>>>,
queue: Rc<OpQueue>,
config_event_scheduled: Cell<bool>,
dispatches: InFlight,
pub column_stats_changed: PubSub<()>,
pub stats_changed: PubSub<()>,
pub on_stats_changed: RefCell<Option<Callback<()>>>,
pub on_table_errored: RefCell<Option<Callback<()>>>,
}
impl Deref for SessionHandle {
type Target = RefCell<SessionData>;
fn deref(&self) -> &Self::Target {
&self.session_data
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MissingTable {
Error,
Pend,
}
impl MissingTable {
pub fn from_wait(wait_for_table: Option<bool>) -> Self {
if wait_for_table.unwrap_or_default() {
Self::Pend
} else {
Self::Error
}
}
}
pub(crate) async fn probe_table(
client: &Client,
name: &str,
missing: MissingTable,
) -> ApiResult<Option<perspective_client::Table>> {
match client.open_table(name.to_owned()).await {
Ok(table) => Ok(Some(table)),
Err(e) => {
let hosted = client.get_hosted_table_names().await.unwrap_or_default();
if hosted.iter().any(|n| n == name) {
return Err(e.into());
}
match missing {
MissingTable::Pend => Ok(None),
MissingTable::Error => Err(ApiError::new(format!("Unknown table \"{name}\""))),
}
},
}
}
#[derive(Default)]
pub struct SessionData {
state: Lifecycle,
unbound_metadata: Rc<SessionMetadata>,
view_sub: Option<ViewSubscription>,
stats: Option<ViewStats>,
is_paused: bool,
}
enum Lifecycle {
Live(Rc<PanelState>),
Disposed {
last: Rc<PanelState>,
disposal: Disposal,
},
}
impl Default for Lifecycle {
fn default() -> Self {
Self::Live(Rc::default())
}
}
impl Deref for Lifecycle {
type Target = Rc<PanelState>;
fn deref(&self) -> &Self::Target {
match self {
Lifecycle::Live(state) | Lifecycle::Disposed { last: state, .. } => state,
}
}
}
impl Lifecycle {
fn live_mut(&mut self) -> Option<&mut Rc<PanelState>> {
match self {
Lifecycle::Live(state) => Some(state),
Lifecycle::Disposed { .. } => None,
}
}
fn disposal(&self) -> Option<Disposal> {
match self {
Lifecycle::Live(_) => None,
Lifecycle::Disposed { disposal, .. } => Some(*disposal),
}
}
}
pub struct ViewConfigRef(Rc<ViewConfig>);
impl Deref for ViewConfigRef {
type Target = ViewConfig;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl SessionData {
fn error(&self) -> Option<&TableErrorState> {
self.state.lost()
}
fn clear_errors(&mut self) {
if self.state.lost().is_some() {
self.swap(self.state.recovered());
}
}
fn swap(&mut self, next: PanelState) {
if let Some(state) = self.state.live_mut() {
*state = Rc::new(next);
}
}
fn table(&self) -> Option<&perspective_client::Table> {
self.state.bound().map(|bound| &bound.table)
}
fn pending_table(&self) -> Option<&str> {
self.state.awaiting()
}
fn metadata(&self) -> &Rc<SessionMetadata> {
match self.state.bound() {
Some(bound) => &bound.metadata,
None => &self.unbound_metadata,
}
}
fn metadata_mut(&mut self) -> &mut SessionMetadata {
let Some(state) = self.state.live_mut() else {
return Rc::make_mut(&mut self.unbound_metadata);
};
let binding = match &mut Rc::make_mut(state).binding {
Binding::Lost { prior, .. } => Rc::make_mut(prior),
binding => binding,
};
match binding {
Binding::Bound(bound) => Rc::make_mut(&mut Rc::make_mut(bound).metadata),
_ => Rc::make_mut(&mut self.unbound_metadata),
}
}
fn config(&self) -> &ViewConfig {
self.state.config()
}
fn set_config(&mut self, config: ViewConfig) {
self.swap(self.state.with_config(Rc::new(config)));
}
fn description(&self) -> Option<&(Rc<ViewConfig>, Rc<Description>)> {
self.state.bound()?.description.as_ref()
}
fn set_description(&mut self, description: Option<(Rc<ViewConfig>, Rc<Description>)>) {
self.swap(self.state.with_description(description));
}
fn unbind(&mut self) {
self.swap(self.state.unbound());
}
}
#[derive(Clone)]
pub struct TableErrorState(ApiError, Option<ReconnectCallback>);
impl PartialEq for TableErrorState {
fn eq(&self, other: &Self) -> bool {
self.0.to_string() == other.0.to_string()
}
}
impl std::fmt::Debug for TableErrorState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("TableErrorState")
.field(&self.0.to_string())
.finish()
}
}
impl TableErrorState {
pub fn message(&self) -> String {
self.0.message()
}
pub fn stacktrace(&self) -> String {
self.0.stacktrace()
}
pub fn kind(&self) -> &'static str {
self.0.kind()
}
pub fn is_reconnect(&self) -> bool {
self.1.is_some()
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Disposal {
Reject,
Resolve,
}
impl From<Disposal> for ApiResult<()> {
fn from(disposal: Disposal) -> Self {
match disposal {
Disposal::Reject => Err(ApiError::new("Panel disposed")),
Disposal::Resolve => Ok(()),
}
}
}
#[derive(Debug, Default)]
pub enum TableIntermediateState {
#[default]
Ejected,
}
#[derive(Default)]
pub struct ResetOptions {
pub expressions: bool,
pub table: Option<TableIntermediateState>,
pub config: bool,
pub stats: bool,
}
#[derive(Clone)]
pub struct Session(Rc<SessionHandle>);
impl ImplicitClone for Session {}
impl Deref for Session {
type Target = SessionHandle;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}
impl Session {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self(Rc::default())
}
pub(crate) fn metadata(&self) -> MetadataRef {
MetadataRef(self.borrow().metadata().clone())
}
pub(crate) fn metadata_mut(&self) -> MetadataMutRef<'_> {
std::cell::RefMut::map(self.borrow_mut(), |x| x.metadata_mut())
}
pub(crate) fn get_title(&self) -> Option<String> {
let pending = self
.0
.queue
.pending_edits()
.into_iter()
.filter_map(|delta| match delta {
EditDelta::Title(title) => Some(title),
_ => None,
})
.next_back();
pending.unwrap_or_else(|| self.borrow().state.chrome.title.clone())
}
pub fn set_title(&self, title: Option<String>) {
let title = title.filter(|x| !x.is_empty());
let session = self.clone();
let _ticket = self.submit(
OpKind::Edit {
delta: EditDelta::Title(title.clone()),
swaps_plugin: false,
},
move |_ctx| {
Box::pin(async move {
session.commit_title(title);
Ok(None)
})
},
);
self.title_changed.emit(self.get_title());
}
pub(crate) fn commit_title(&self, title: Option<String>) {
let new_title = title.filter(|x| !x.is_empty());
let next = self.borrow().state.with_title(new_title.clone());
self.borrow_mut().swap(next);
self.title_changed.emit(new_title);
}
pub fn reset(&self, options: ResetOptions) -> impl Future<Output = ApiResult<()>> + use<> {
let view = self.0.borrow_mut().view_sub.take();
let had_table = self.borrow().table().is_some();
let err = self.get_error();
self.borrow_mut().clear_errors();
if options.stats {
self.update_stats(ViewStats::default());
}
if options.config {
let mut config = self.borrow().config().clone();
config.reset(options.expressions);
self.borrow_mut().set_config(config);
}
if options.expressions {
self.borrow_mut().set_description(None);
}
match options.table {
Some(TableIntermediateState::Ejected) => self.borrow_mut().unbind(),
None => {},
};
if options.config && options.table.is_none() {
self.commit_table_defaults();
}
let session = self.clone();
async move {
let res = view.delete().await;
if options.table.is_some() && had_table {
session.table_unloaded.emit(true)
}
if let Some(err) = err { Err(err) } else { res }
}
}
pub fn submit(&self, kind: OpKind, step: impl FnOnce(OpCtx) -> StepFuture + 'static) -> Ticket {
if let Some(disposal) = self.disposal() {
return Ticket::settled(disposal.into());
}
let is_edit = matches!(kind, OpKind::Edit {
delta: EditDelta::View(_),
..
});
let guard = self.begin_config_run();
let (ticket, start) = self.0.queue.push(kind, guard, step);
if is_edit {
self.notify_view_config_changed();
}
if start {
let queue = self.0.queue.clone();
let session = self.clone();
ApiFuture::spawn_named("op-queue-drain", async move {
queue
.drain(move |_exit| session.notify_view_config_changed())
.await;
Ok(())
});
}
ticket
}
pub async fn settle_ops(&self) {
self.0.queue.idle().await
}
pub fn has_pending_load(&self) -> bool {
self.0.queue.has_load()
}
pub(crate) fn dispose(&self, disposal: Disposal) {
{
let mut data = self.borrow_mut();
if let Lifecycle::Live(last) = &data.state {
data.state = Lifecycle::Disposed {
last: last.clone(),
disposal,
};
}
}
self.0.queue.reject_all(disposal.into());
}
pub fn take_view_sub(&self) -> Option<ViewSubscription> {
self.borrow_mut().view_sub.take()
}
pub fn put_view_sub(&self, sub: ViewSubscription) {
self.borrow_mut().view_sub = Some(sub);
}
pub(crate) fn is_disposed(&self) -> bool {
self.disposal().is_some()
}
pub(crate) fn disposal(&self) -> Option<Disposal> {
self.borrow().state.disposal()
}
pub(crate) fn has_table(&self) -> Option<TableLoadState> {
let data = self.borrow();
if data.table().is_some() {
Some(TableLoadState::Loaded)
} else if self.0.queue.has_load() {
Some(TableLoadState::Loading)
} else if data.pending_table().is_some() {
Some(TableLoadState::Pending)
} else {
None
}
}
pub fn get_table(&self) -> Option<perspective_client::Table> {
self.borrow().table().cloned()
}
pub fn set_client(&self, client: Client) -> bool {
if Some(&client) != self.get_client().as_ref() {
let next = self.borrow().state.unbound_on(Some(client));
self.borrow_mut().swap(next);
true
} else {
false
}
}
pub fn get_client(&self) -> Option<Client> {
self.borrow().state.client()
}
pub fn pending_table(&self) -> Option<String> {
self.borrow().pending_table().map(str::to_owned)
}
pub fn suspend_table(&self) -> Option<impl Future<Output = ApiResult<()>> + use<>> {
let name = self.borrow().table()?.get_name().to_owned();
let client = self.get_client()?;
let fut = self.reset(ResetOptions {
table: Some(TableIntermediateState::Ejected),
stats: true,
..ResetOptions::default()
});
let next = self.borrow().state.awaiting_table(client, name);
self.borrow_mut().swap(next);
Some(fut)
}
async fn watch_client(&self, client: &Client) -> ApiResult<()> {
let on_error = self.on_table_errored.borrow().clone();
let session = self.clone();
let watched = client.clone();
let poll_loop = LocalPollLoop::new(move |(message, reconnect): (ApiError, _)| {
if session.get_client().as_ref() != Some(&watched) {
return Ok(JsValue::UNDEFINED);
}
let next = session
.borrow()
.state
.with_lost(TableErrorState(message, reconnect));
session.borrow_mut().swap(next);
if let Some(cb) = &on_error {
cb.emit(());
}
if let Some(sub) = session.borrow_mut().view_sub.take() {
sub.dismiss();
}
Ok(JsValue::UNDEFINED)
});
let _callback_id = client
.on_error(Box::new(move |message: ClientError, reconnect| {
let poll_loop = poll_loop.clone();
async move {
poll_loop.poll((message.into(), reconnect)).await;
Ok(())
}
}))
.await?;
Ok(())
}
pub async fn set_error(&self, reset_table: bool, err: ApiError) -> ApiResult<()> {
let session = self.clone();
let poll_loop = LocalPollLoop::new(move |()| {
ApiFuture::spawn(session.reset(ResetOptions {
config: true,
expressions: true,
..ResetOptions::default()
}));
Ok(JsValue::UNDEFINED)
});
let error = TableErrorState(
err.clone(),
Some(ReconnectCallback::new(move || {
clone!(poll_loop);
Box::pin(async move {
poll_loop.poll(()).await;
Ok(())
})
})),
);
let next = self.borrow().state.with_lost(error);
self.borrow_mut().swap(next);
if let Some(cb) = self.on_table_errored.borrow().as_ref() {
cb.emit(());
}
let sub = self.borrow_mut().view_sub.take();
if reset_table {
self.borrow_mut().unbind();
}
sub.delete().await?;
Err(err)
}
pub fn set_pause(&self, pause: bool) -> bool {
if pause == self.borrow().is_paused {
false
} else if pause {
ApiFuture::spawn(self.borrow_mut().view_sub.take().delete());
self.borrow_mut().is_paused = true;
true
} else {
self.borrow_mut().is_paused = false;
true
}
}
pub async fn await_table(&self) -> ApiResult<()> {
if self.js_get_table().is_none() {
self.table_loaded.read_next().await?;
let _ = self.js_get_table().ok_or("No table set")?;
}
Ok(())
}
pub fn js_get_table(&self) -> Option<JsValue> {
Some(perspective_js::Table::from(self.borrow().table().cloned()?).into())
}
pub(crate) fn is_errored(&self) -> bool {
self.borrow().error().is_some()
}
pub(crate) fn get_error(&self) -> Option<ApiError> {
self.borrow().error().map(|x| x.0.clone())
}
pub async fn reconnect(&self) -> ApiResult<()> {
let session = self.clone();
self.submit(OpKind::Restore { update: None }, move |_ctx| {
Box::pin(async move {
session.reconnect_step().await?;
Ok(None)
})
})
.settle()
.await
}
async fn reconnect_step(&self) -> ApiResult<()> {
let err = self.borrow().error().cloned();
if let Some(TableErrorState(_, Some(reconnect))) = err {
reconnect().await?;
self.borrow_mut().clear_errors();
self.borrow_mut().set_description(None);
self.borrow_mut().view_sub = None;
self.table_loaded.emit(());
}
Ok(())
}
pub fn get_view(&self) -> Option<View> {
self.borrow()
.view_sub
.as_ref()
.map(|sub| sub.get_view().clone())
}
pub fn get_view_with_dimensions(&self) -> Option<(View, Option<ViewDimensionsResp>)> {
self.borrow()
.view_sub
.as_ref()
.map(|sub| (sub.get_view().clone(), sub.dimensions()))
}
pub(crate) fn get_table_stats(&self) -> Option<ViewStats> {
self.borrow().stats.clone()
}
pub(crate) fn has_table_cells(&self) -> bool {
self.borrow()
.stats
.as_ref()
.is_some_and(|s| s.num_table_cells.is_some())
}
pub fn get_view_config(&self) -> ViewConfigRef {
ViewConfigRef(self.projected())
}
pub fn committed_view_config(&self) -> ViewConfigRef {
ViewConfigRef(self.borrow().state.config().clone())
}
fn projected(&self) -> Rc<ViewConfig> {
let committed = self.borrow().state.config().clone();
let edits = self
.0
.queue
.pending_edits()
.into_iter()
.filter_map(|delta| match delta {
EditDelta::View(delta) => Some(*delta),
_ => None,
})
.collect::<Vec<_>>();
if edits.is_empty() {
return committed;
}
let mut config = (*committed).clone();
for delta in edits {
let mut candidate = config.clone();
if candidate.apply_update(delta) && self.validate_names(&candidate).is_ok() {
self.normalize_view_config(&mut candidate);
config = candidate;
}
}
Rc::new(config)
}
pub fn check_edit(&self, delta: &ViewConfigUpdate) -> ApiResult<()> {
if let Some(x) = self.borrow().state.lost() {
return Err(ApiError::new(x.0.clone()));
}
let mut candidate = (*self.projected()).clone();
if candidate.apply_update(delta.clone()) {
self.validate_names(&candidate)?;
}
Ok(())
}
fn effective_view_config(&self) -> Rc<ViewConfig> {
let data = self.borrow();
if let Some((effective, _)) = data.description() {
return effective.clone();
}
let metadata = data.metadata().clone();
let (config, _) = effective(data.config(), &data.state.overlay, &|name| {
metadata.get_column_table_type(name)
});
Rc::new(config)
}
pub fn skipped_overlay(&self) -> Vec<usize> {
let data = self.borrow();
let metadata = data.metadata().clone();
effective(data.config(), &data.state.overlay, &|name| {
metadata.get_column_table_type(name)
})
.1
}
pub(crate) fn committed_overlay(&self) -> Rc<Vec<OverlayClause>> {
self.borrow().state.overlay.clone()
}
pub fn get_rendered_view_config(&self) -> Option<Rc<ViewConfig>> {
self.borrow().view_sub.as_ref().map(|s| s.get_view_config())
}
pub fn set_update_column_defaults(
&self,
config_update: &mut ViewConfigUpdate,
config_static: &PluginStaticConfig,
) {
use self::column_defaults_update::*;
let config = self.committed_view_config();
config_update.set_update_column_defaults(
&self.metadata(),
&config,
&config.columns,
config_static,
)
}
pub fn set_update_rollup_defaults(
&self,
config_update: &mut ViewConfigUpdate,
config_static: &PluginStaticConfig,
) {
use self::column_defaults_update::*;
config_update.set_update_rollup_defaults(
&self.metadata(),
&self.committed_view_config(),
config_static,
)
}
fn commit_table_defaults(&self) {
let mut candidate = self.borrow().config().clone();
self.normalize_view_config(&mut candidate);
if candidate != *self.borrow().config() {
self.borrow_mut().set_config(candidate);
self.notify_view_config_changed();
}
}
fn validate_names(&self, config: &ViewConfig) -> ApiResult<()> {
Self::validate_names_with(&self.metadata(), config)
}
fn validate_names_with(metadata: &SessionMetadata, config: &ViewConfig) -> ApiResult<()> {
let table_columns = Self::columns_of(metadata);
if table_columns.is_empty() {
return Ok(());
}
let mut allowed: HashSet<&str> = table_columns.iter().map(|x| x.as_str()).collect();
allowed.extend(config.expressions.0.keys().map(|x| x.as_str()));
allowed.extend(config.windows.keys().map(|x| x.as_str()));
let named = config
.columns
.iter()
.flatten()
.map(|x| ("columns", x))
.chain(config.group_by.iter().map(|x| ("group_by", x)))
.chain(config.split_by.iter().map(|x| ("split_by", x)))
.chain(config.sort.iter().map(|x| ("sort", &x.0)));
for (field, column) in named {
if !allowed.contains(column.as_str()) {
return Err(apierror!(InvalidViewerConfigError(
field,
column.to_owned()
)));
}
}
for filter in config.filter.iter() {
if !allowed.contains(filter.column()) {
return Err(apierror!(InvalidViewerConfigError(
"filter",
filter.column().to_owned()
)));
}
}
Ok(())
}
fn normalize_view_config(&self, config: &mut ViewConfig) {
Self::normalize_with(&self.metadata(), config)
}
fn normalize_with(metadata: &SessionMetadata, config: &mut ViewConfig) {
let table_columns = Self::columns_of(metadata);
if table_columns.is_empty() {
return;
}
if config.columns.is_empty() {
config.columns = table_columns.iter().cloned().map(Some).collect();
}
let view_columns: HashSet<String> = config
.columns
.iter()
.flatten()
.cloned()
.chain(config.group_by.iter().cloned())
.chain(config.split_by.iter().cloned())
.chain(config.sort.iter().map(|x| x.0.clone()))
.chain(config.filter.iter().map(|x| x.column().to_owned()))
.collect();
config
.aggregates
.retain(|column, _| view_columns.contains(column.as_str()));
}
pub fn track_dispatch(&self, fut: impl std::future::Future<Output = ApiResult<()>> + 'static) {
let guard = self.0.dispatches.guard();
ApiFuture::spawn_named("config-update-dispatch", async move {
let _guard = guard;
if let Err(e) = fut.await {
tracing::error!("[config-update dispatch] {}", e);
}
Ok(())
});
}
pub async fn settle_dispatches(&self) -> ApiResult<()> {
self.0.dispatches.settle().await;
Ok(())
}
fn notify_view_config_changed(&self) {
self.clear_column_stats();
if !self.0.config_event_scheduled.replace(true) {
let session = self.clone();
ApiFuture::spawn(async move {
session.0.config_event_scheduled.set(false);
session.view_config_changed.emit(());
Ok(())
});
}
}
pub fn begin_config_run(&self) -> InFlightGuard {
self.0.config_runs.guard()
}
pub fn get_column_stats(&self, column_name: &str) -> Option<ColumnStats> {
self.column_stats.borrow().get(column_name).copied()
}
pub fn set_column_abs_max(&self, column_name: String, abs_max: f64) {
self.column_stats
.borrow_mut()
.entry(column_name)
.or_default()
.abs_max = Some(abs_max);
self.column_stats_changed.emit(());
}
pub fn clear_column_stats(&self) {
if !self.column_stats.borrow().is_empty() {
self.column_stats.borrow_mut().clear();
self.column_stats_changed.emit(());
}
}
pub fn snapshot(&self, _guard: &RenderGuard) -> ConfigSnapshot {
ConfigSnapshot {
config: self.borrow().state.config().clone(),
effective: self.effective_view_config(),
}
}
pub async fn validate_snapshot(
&self,
_guard: &RenderGuard,
snap: ConfigSnapshot,
) -> ApiResult<ValidatedSnapshot> {
let (description, fresh) = self.describe_effective(&snap.effective).await?;
if fresh {
tracing::warn!("Rendering a commit that was not described");
Self::record_description(&mut self.metadata_mut(), &snap.effective, &description)?;
self.borrow_mut()
.set_description(Some((snap.effective.clone(), description.clone())));
}
Ok(ValidatedSnapshot { snap })
}
async fn describe_effective(
&self,
effective: &Rc<ViewConfig>,
) -> ApiResult<(Rc<Description>, bool)> {
let memo = self
.borrow()
.description()
.cloned()
.filter(|(key, _)| **key == **effective)
.map(|(_, description)| description);
if let Some(description) = memo {
return Ok((description, false));
}
let table = self
.borrow()
.table()
.cloned()
.ok_or_else(|| apierror!(NoTableError))?;
Self::describe_with(&table, &self.metadata(), effective)
.await
.map(|description| (description, true))
}
async fn describe_with(
table: &perspective_client::Table,
metadata: &SessionMetadata,
effective: &Rc<ViewConfig>,
) -> ApiResult<Rc<Description>> {
let engine_config = Self::with_default_aggregates_of(metadata, effective);
match table.describe(engine_config.into()).await? {
Ok(description) => Ok(Rc::new(description)),
Err(DescribeError::Expressions {
expression_schema,
errors,
}) => Err(apierror!(InvalidViewerConfigExpressionsError(Rc::new(
ExprValidationResult {
expression_schema,
errors,
expression_alias: effective.expressions.0.clone(),
}
)))),
Err(DescribeError::Config(msg)) => Err(ApiError::new(msg)),
}
}
pub(crate) async fn prepare_view(
&self,
plan: BindPlan,
mut update: ViewConfigUpdate,
defaults: ViewDefaults<'_>,
overlay: Option<Rc<Vec<OverlayClause>>>,
) -> ApiResult<PreparedView> {
if self.is_disposed() {
return Err(ApiError::new("Panel disposed"));
}
if matches!(plan, BindPlan::Keep)
&& let Some(error) = self.borrow().state.lost()
{
return Err(error.0.clone());
}
let committed = self.borrow().state.config().clone();
let base = match &plan {
BindPlan::Bind { reset: true, .. } | BindPlan::Pend { reset: true, .. } => {
Rc::new(ViewConfig::default())
},
_ => committed.clone(),
};
let (binding, checked) = match plan {
BindPlan::Keep => {
let bound = self.borrow().state.bound().cloned();
(
PreparedBinding::Keep,
bound.map(|bound| (bound.table.clone(), bound.metadata.clone())),
)
},
BindPlan::Bind { table, client, .. } => {
let metadata = Rc::new(SessionMetadata::from_table(&table).await?);
let binding = PreparedBinding::Bind {
client: client.clone(),
table: table.clone(),
metadata: metadata.clone(),
};
(binding, Some((*table, metadata)))
},
BindPlan::Pend { client, name, .. } => (PreparedBinding::Pend { client, name }, None),
};
{
use self::column_defaults_update::*;
let metadata = match &checked {
Some((_, metadata)) => metadata.clone(),
None => self.borrow().metadata().clone(),
};
match defaults {
ViewDefaults::Swap(plugin) => {
update.set_update_column_defaults(&metadata, &base, &base.columns, plugin)
},
ViewDefaults::Rollup(plugin) => {
update.set_update_rollup_defaults(&metadata, &base, plugin)
},
ViewDefaults::AsGiven => {},
}
}
let mut candidate = (*base).clone();
candidate.apply_update(update);
if let Some((_, metadata)) = &checked {
Self::validate_names_with(metadata, &candidate)?;
Self::normalize_with(metadata, &mut candidate);
}
let config = if candidate == *committed {
committed.clone()
} else {
Rc::new(candidate)
};
let changed = !Rc::ptr_eq(&config, &committed);
let clauses = overlay
.clone()
.unwrap_or_else(|| self.borrow().state.overlay.clone());
let (effective, description) = match &checked {
None => (config.clone(), None),
Some((table, metadata)) => {
let on_expression = clauses
.iter()
.any(|x| config.expressions.0.contains_key(x.filter.column()));
let bare = if on_expression {
Some(Self::describe_with(table, metadata, &config).await?)
} else {
None
};
let (effective, _) = effective(&config, &clauses, &|name| {
metadata.get_table_schema_type(name).or_else(|| {
bare.as_ref()
.and_then(|x| x.expression_schema.get(name).copied())
})
});
let effective = if effective == *config {
config.clone()
} else {
Rc::new(effective)
};
let description = match (&binding, bare) {
(_, Some(bare)) if Rc::ptr_eq(&effective, &config) => bare,
(PreparedBinding::Keep, _) => self.describe_effective(&effective).await?.0,
_ => Self::describe_with(table, metadata, &effective).await?,
};
(effective, Some(description))
},
};
Ok(PreparedView {
binding,
overlay,
config,
effective,
description,
changed,
announce: true,
})
}
pub(crate) fn commit_view(
&self,
view: PreparedView,
title: Option<Option<String>>,
) -> BindingEffects {
let state = self.borrow().state.clone();
let title_before = state.chrome.title.clone();
let had_table = state.bound().is_some();
let (rebound, watch) = match view.binding {
PreparedBinding::Keep => (None, None),
PreparedBinding::Bind {
client,
table,
metadata,
} => (
Some(state.bound_to(*table, (*metadata).clone())),
Some(client),
),
PreparedBinding::Pend { client, name } => {
(Some(state.awaiting_table(client, name)), None)
},
};
let rebinds = rebound.is_some();
let mut next = rebound.unwrap_or_else(|| (*state).clone());
next = next.with_config(view.config.clone());
if let Some(overlay) = view.overlay {
next = next.with_overlay(overlay);
}
if let Some(description) = view.description {
if let Some(bound) = next.bound() {
let mut metadata = (*bound.metadata).clone();
if Self::record_description(&mut metadata, &view.effective, &description).is_ok() {
next = next.with_metadata(Rc::new(metadata));
}
}
next = next.with_description(Some((view.effective, description)));
}
let next = match title {
Some(title) => next.with_title(title),
None => next,
};
let title_after = next.chrome.title.clone();
let bound = next.bound().is_some();
self.borrow_mut().swap(next);
let outgoing = if rebinds {
self.borrow_mut().view_sub.take()
} else {
None
};
if rebinds {
self.update_stats(ViewStats::default());
}
if (view.changed && view.announce) || rebinds {
self.notify_view_config_changed();
}
if title_before != title_after {
self.title_changed.emit(title_after);
}
BindingEffects {
rebinds,
outgoing,
unloaded: rebinds && had_table,
loaded: rebinds && bound,
watch,
}
}
fn record_description(
metadata: &mut SessionMetadata,
effective: &ViewConfig,
description: &Description,
) -> ApiResult<()> {
metadata.update_expressions(&ExprValidationResult {
expression_schema: description.expression_schema.clone(),
errors: HashMap::new(),
expression_alias: effective.expressions.0.clone(),
})?;
metadata.update_view_schema(&description.view_schema)?;
metadata.update_windows(&effective.windows)?;
Ok(())
}
pub(crate) async fn finish_binding(&self, effects: BindingEffects) -> ApiResult<()> {
let BindingEffects {
outgoing,
unloaded,
loaded,
watch,
..
} = effects;
let deleted = outgoing.delete().await;
if unloaded {
self.table_unloaded.emit(true);
}
if let Some(client) = watch {
self.watch_client(&client).await?;
}
if loaded {
self.table_loaded.emit(());
}
deleted
}
pub async fn bind_view(
&self,
_guard: &RenderGuard,
validated: ValidatedSnapshot,
) -> ApiResult<BindDisposition> {
fn unchanged_or_deferred(session: &Session) -> BindDisposition {
match session.get_view() {
Some(view) => BindDisposition::Unchanged(view),
None => BindDisposition::Deferred,
}
}
let ValidatedSnapshot {
snap: ConfigSnapshot { config, effective },
} = validated;
if self.borrow().is_paused {
self.commit_reconciled.emit(());
return Ok(unchanged_or_deferred(self));
}
{
let bound = self.borrow().view_sub.as_ref().map(|x| x.build_config());
if let Some(bound) = bound {
if *bound == *effective {
self.commit_reconciled.emit(());
return Ok(unchanged_or_deferred(self));
} else if bound.is_equivalent(&effective) {
if let Some(sub) = self.borrow_mut().view_sub.as_mut() {
sub.set_configs(config.clone(), effective.clone());
}
self.commit_reconciled.emit(());
return match self.get_view() {
Some(view) => Ok(BindDisposition::Adopted(view)),
None => Ok(BindDisposition::Deferred),
};
}
}
}
let table = self
.borrow()
.table()
.cloned()
.ok_or("`restore()` called before `load()`")?;
let view_config = self.with_default_aggregates(&effective);
let view = table.view(Some(view_config.into())).await?;
let on_stats = Callback::from({
let this = self.clone();
move |stats| this.update_stats(stats)
});
let sub = {
let on_update = self
.metadata()
.get_features()
.unwrap()
.on_update
.then(|| self.table_updated.callback());
ViewSubscription::new(view, config, effective, on_stats, on_update).await?
};
let old = self.borrow_mut().view_sub.take();
ApiFuture::spawn(old.delete());
self.borrow_mut().view_sub = Some(sub);
self.view_created.emit(());
self.commit_reconciled.emit(());
match self.get_view() {
Some(view) => Ok(BindDisposition::Rebuilt(FreshView::assert_fresh(view))),
None => Ok(BindDisposition::Deferred),
}
}
fn with_default_aggregates(&self, effective: &ViewConfig) -> ViewConfig {
Self::with_default_aggregates_of(&self.metadata(), effective)
}
fn with_default_aggregates_of(
metadata: &SessionMetadata,
effective: &ViewConfig,
) -> ViewConfig {
let mut view_config = effective.clone();
for col in view_config
.columns
.iter()
.flatten()
.chain(view_config.sort.iter().map(|x| &x.0))
{
if !view_config.aggregates.contains_key(col.as_str()) {
let agg = metadata
.get_column_aggregates(col.as_str())
.and_then(|mut aggs| aggs.next())
.into_apierror();
match agg {
Err(_) => {
tracing::debug!("No default aggregate for column '{}' found, skipping", col)
},
Ok(agg) => _ = view_config.aggregates.insert(col.to_string(), agg),
};
}
}
view_config
}
pub async fn create_detached_view(&self) -> ApiResult<View> {
let table = self.borrow().table().cloned().ok_or("No `Table` set")?;
let view_config = self.with_default_aggregates(&self.effective_view_config());
Ok(table.view(Some(view_config.into())).await?)
}
pub async fn reset_failed_config(&self) -> ApiResult<()> {
let session = self.clone();
self.submit(OpKind::Restore { update: None }, move |_ctx| {
Box::pin(async move {
session
.reset(ResetOptions {
config: true,
expressions: true,
..ResetOptions::default()
})
.await?;
Ok(None)
})
})
.settle()
.await
}
fn update_stats(&self, stats: ViewStats) {
self.borrow_mut().stats = Some(stats);
if let Some(cb) = self.on_stats_changed.borrow().as_ref() {
cb.emit(());
}
self.stats_changed.emit(());
}
fn columns_of(metadata: &SessionMetadata) -> Vec<String> {
metadata
.get_table_columns()
.into_iter()
.flatten()
.cloned()
.collect()
}
pub fn to_props(&self) -> SessionProps {
let column_stats = PtrEqRc::new(self.column_stats.borrow().clone());
let projected = self.projected();
let title = self.get_title();
let data = self.borrow();
let config = {
let mut cached = self.cached_config.borrow_mut();
if !matches!(&*cached, Some(c) if **c == *projected) {
*cached = Some(PtrEqRc::new((*projected).clone()));
}
cached.clone().unwrap()
};
let metadata = {
let mut cached = self.cached_metadata.borrow_mut();
if !matches!(&*cached, Some(m) if **m == **data.metadata()) {
*cached = Some(PtrEqRc::new((**data.metadata()).clone()));
}
cached.clone().unwrap()
};
SessionProps {
config,
has_table_cells: data
.stats
.as_ref()
.is_some_and(|s| s.num_table_cells.is_some()),
has_table: if data.table().is_some() {
Some(TableLoadState::Loaded)
} else if self.0.queue.has_load() {
Some(TableLoadState::Loading)
} else if data.pending_table().is_some() {
Some(TableLoadState::Pending)
} else {
None
},
error: data.error().cloned(),
title,
metadata,
column_stats,
}
}
}
pub(crate) struct PreparedView {
binding: PreparedBinding,
overlay: Option<Rc<Vec<OverlayClause>>>,
config: Rc<ViewConfig>,
effective: Rc<ViewConfig>,
description: Option<Rc<Description>>,
changed: bool,
announce: bool,
}
pub(crate) enum ViewDefaults<'a> {
Swap(&'a PluginStaticConfig),
Rollup(&'a PluginStaticConfig),
AsGiven,
}
pub(crate) enum BindPlan {
Keep,
Bind {
client: Client,
table: Box<perspective_client::Table>,
reset: bool,
},
Pend {
client: Client,
name: String,
reset: bool,
},
}
enum PreparedBinding {
Keep,
Bind {
client: Client,
table: Box<perspective_client::Table>,
metadata: Rc<SessionMetadata>,
},
Pend {
client: Client,
name: String,
},
}
#[must_use]
pub(crate) struct BindingEffects {
rebinds: bool,
outgoing: Option<ViewSubscription>,
unloaded: bool,
loaded: bool,
watch: Option<Client>,
}
impl BindingEffects {
pub fn rebound(&self) -> bool {
self.rebinds
}
pub fn forget(self) {}
}
impl PreparedView {
pub fn projected(mut self) -> Self {
self.announce = false;
self
}
pub fn config(&self) -> &ViewConfig {
&self.config
}
pub fn view_schema(&self) -> Option<&HashMap<String, perspective_client::proto::ColumnType>> {
self.description.as_ref().map(|x| &x.view_schema)
}
}
#[derive(Clone)]
pub struct ConfigSnapshot {
pub config: Rc<ViewConfig>,
pub effective: Rc<ViewConfig>,
}
pub struct ValidatedSnapshot {
snap: ConfigSnapshot,
}
pub struct FreshView(View);
impl FreshView {
pub(crate) fn assert_fresh(view: View) -> Self {
Self(view)
}
pub fn view(&self) -> &View {
&self.0
}
}
pub enum BindDisposition {
Rebuilt(FreshView),
Adopted(View),
Unchanged(View),
Deferred,
}
impl BindDisposition {
pub fn view(&self) -> Option<&View> {
match self {
Self::Rebuilt(fresh) => Some(fresh.view()),
Self::Adopted(view) | Self::Unchanged(view) => Some(view),
Self::Deferred => None,
}
}
}