Skip to main content

perspective_client/
view.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::collections::HashMap;
14use std::ops::Deref;
15use std::str::FromStr;
16use std::sync::Arc;
17
18use futures::Future;
19use prost::bytes::Bytes;
20use serde::{Deserialize, Serialize};
21use ts_rs::TS;
22
23use self::view_on_update_req::Mode;
24use crate::assert_view_api;
25use crate::client::Client;
26use crate::proto::request::ClientReq;
27use crate::proto::response::ClientResp;
28use crate::proto::*;
29#[cfg(doc)]
30use crate::table::Table;
31pub use crate::utils::*;
32
33/// Options for [`View::on_update`].
34#[derive(Default, Debug, Deserialize, TS)]
35pub struct OnUpdateOptions {
36    pub mode: Option<OnUpdateMode>,
37}
38
39/// The update mode for [`View::on_update`].
40///
41/// `Row` mode calculates and provides the update batch new rows/columns as an
42/// Apache Arrow to the callback provided to [`View::on_update`]. This allows
43/// incremental updates if your callbakc can read this format, but should be
44/// disabled otherwise.
45#[derive(Default, Debug, Deserialize, TS)]
46pub enum OnUpdateMode {
47    #[default]
48    #[serde(rename = "row")]
49    Row,
50}
51
52impl FromStr for OnUpdateMode {
53    type Err = ClientError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        if s == "row" {
57            Ok(OnUpdateMode::Row)
58        } else {
59            Err(ClientError::Option)
60        }
61    }
62}
63
64#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
65pub struct ColumnWindow {
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub start_col: Option<f32>,
68
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub end_col: Option<f32>,
71}
72
73/// Options for serializing a window of data from a [`View`].
74///
75/// Some fields of [`ViewWindow`] are only applicable to specific methods of
76/// [`View`].
77#[derive(Clone, Debug, Default, Deserialize, Serialize, TS, PartialEq)]
78pub struct ViewWindow {
79    #[ts(optional)]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub start_row: Option<f64>,
82
83    #[ts(optional)]
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub start_col: Option<f64>,
86
87    #[ts(optional)]
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub end_row: Option<f64>,
90
91    #[ts(optional)]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub end_col: Option<f64>,
94
95    #[ts(optional)]
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub id: Option<bool>,
98
99    #[ts(optional)]
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub index: Option<bool>,
102
103    /// Only impacts [`View::to_csv`]
104    #[ts(optional)]
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub formatted: Option<bool>,
107
108    /// Only impacts [`View::to_arrow`]
109    #[ts(optional)]
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub compression: Option<String>,
112
113    /// When `true`, group-by columns use legacy `"colname (Group by N)"`
114    /// naming. When `false`, they use `__ROW_PATH_N__` naming consistent
115    /// with the SQL backend. Defaults to `true` for backwards compatibility.
116    #[ts(optional)]
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub emit_legacy_row_path_names: Option<bool>,
119}
120
121impl From<ViewWindow> for ViewPort {
122    fn from(window: ViewWindow) -> Self {
123        ViewPort {
124            start_row: window.start_row.map(|x| x.floor() as u32),
125            start_col: window.start_col.map(|x| x.floor() as u32),
126            end_row: window.end_row.map(|x| x.ceil() as u32),
127            end_col: window.end_col.map(|x| x.ceil() as u32),
128            emit_legacy_row_path_names: window.emit_legacy_row_path_names,
129        }
130    }
131}
132
133impl From<ViewPort> for ViewWindow {
134    fn from(window: ViewPort) -> Self {
135        ViewWindow {
136            start_row: window.start_row.map(|x| x as f64),
137            start_col: window.start_col.map(|x| x as f64),
138            end_row: window.end_row.map(|x| x as f64),
139            end_col: window.end_col.map(|x| x as f64),
140            emit_legacy_row_path_names: window.emit_legacy_row_path_names,
141            ..ViewWindow::default()
142        }
143    }
144}
145
146/// Rows updated and port ID corresponding to an update batch, provided to the
147/// callback argument to [`View::on_update`] with the "rows" mode.
148#[derive(TS)]
149pub struct OnUpdateData(crate::proto::ViewOnUpdateResp);
150
151impl Deref for OnUpdateData {
152    type Target = crate::proto::ViewOnUpdateResp;
153
154    fn deref(&self) -> &Self::Target {
155        &self.0
156    }
157}
158
159/// Removed index values and port ID corresponding to a remove batch, provided
160/// to the callback argument to [`View::on_remove`].
161#[derive(TS)]
162pub struct OnRemoveData(crate::proto::ViewOnRemoveResp);
163
164impl Deref for OnRemoveData {
165    type Target = crate::proto::ViewOnRemoveResp;
166
167    fn deref(&self) -> &Self::Target {
168        &self.0
169    }
170}
171/// The [`View`] struct is Perspective's query and serialization interface. It
172/// represents a query on the `Table`'s dataset and is always created from an
173/// existing `Table` instance via the [`Table::view`] method.
174///
175/// [`View`]s are immutable with respect to the arguments provided to the
176/// [`Table::view`] method; to change these parameters, you must create a new
177/// [`View`] on the same [`Table`]. However, each [`View`] is _live_ with
178/// respect to the [`Table`]'s data, and will (within a conflation window)
179/// update with the latest state as its parent [`Table`] updates, including
180/// incrementally recalculating all aggregates, pivots, filters, etc. [`View`]
181/// query parameters are composable, in that each parameter works independently
182/// _and_ in conjunction with each other, and there is no limit to the number of
183/// pivots, filters, etc. which can be applied.
184///
185/// To construct a [`View`], call the [`Table::view`] factory method. A
186/// [`Table`] can have as many [`View`]s associated with it as you need -
187/// Perspective conserves memory by relying on a single [`Table`] to power
188/// multiple [`View`]s concurrently.
189///
190/// # Examples
191///
192/// ```no_run
193/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, ViewWindow};
194/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
195/// # let client: Client = todo!();
196/// let opts = TableInitOptions::default();
197/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
198/// let table = client.table(data, opts).await?;
199///
200/// let view = table.view(None).await?;
201/// let arrow = view.to_arrow(ViewWindow::default()).await?;
202/// view.delete().await?;
203/// # Ok(()) }
204/// ```
205///
206/// ```no_run
207/// # use std::collections::HashMap;
208/// # use perspective_client::Table;
209/// # use perspective_client::config::*;
210/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
211/// # let table: Table = todo!();
212/// let view = table
213///     .view(Some(ViewConfigUpdate {
214///         columns: Some(vec![Some("Sales".into())]),
215///         aggregates: Some(HashMap::from_iter(vec![("Sales".into(), "sum".into())])),
216///         group_by: Some(vec!["Region".into(), "Country".into()]),
217///         filter: Some(vec![Filter::new("Category", "in", &[
218///             "Furniture",
219///             "Technology",
220///         ])]),
221///         ..ViewConfigUpdate::default()
222///     }))
223///     .await?;
224/// # Ok(()) }
225/// ```
226///
227///  Group By
228///
229/// ```no_run
230/// # use perspective_client::Table;
231/// # use perspective_client::config::*;
232/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
233/// # let table: Table = todo!();
234/// let view = table
235///     .view(Some(ViewConfigUpdate {
236///         group_by: Some(vec!["a".into(), "c".into()]),
237///         ..ViewConfigUpdate::default()
238///     }))
239///     .await?;
240/// # Ok(()) }
241/// ```
242///
243/// Split By
244///
245/// ```no_run
246/// # use perspective_client::Table;
247/// # use perspective_client::config::*;
248/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
249/// # let table: Table = todo!();
250/// let view = table
251///     .view(Some(ViewConfigUpdate {
252///         split_by: Some(vec!["a".into(), "c".into()]),
253///         ..ViewConfigUpdate::default()
254///     }))
255///     .await?;
256/// # Ok(()) }
257/// ```
258///
259/// In Javascript, a [`Table`] can be constructed on a [`Table::view`] instance,
260/// which will return a new [`Table`] based on the [`Table::view`]'s dataset,
261/// and all future updates that affect the [`Table::view`] will be forwarded to
262/// the new [`Table`]. This is particularly useful for implementing a
263/// [Client/Server Replicated](server.md#clientserver-replicated) design, by
264/// serializing the `View` to an arrow and setting up an `on_update` callback.
265///
266/// ```no_run
267/// # use perspective_client::{Client, TableData, TableInitOptions, UpdateData, UpdateOptions};
268/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
269/// # let client: Client = todo!();
270/// let opts = TableInitOptions::default();
271/// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
272/// let table = client.table(data, opts.clone()).await?;
273/// let view = table.view(None).await?;
274/// let table2 = client.table(TableData::View(view), opts).await?;
275/// let more = UpdateData::Csv("x,y\n5,6".into());
276/// table.update(more, UpdateOptions::default()).await?;
277/// # Ok(()) }
278/// ```
279#[derive(Clone, Debug)]
280pub struct View {
281    pub name: String,
282    client: Client,
283    pub(crate) source: Option<ViewSource>,
284}
285
286/// The options of the [`Table`] a [`View`] was created from, as known to the
287/// [`Client`] which created it; a [`View`] opened by name alone has no source.
288#[derive(Clone, Debug)]
289pub(crate) struct ViewSource {
290    pub options: crate::table::TableOptions,
291}
292
293assert_view_api!(View);
294
295impl View {
296    pub fn new(name: String, client: Client) -> Self {
297        View {
298            name,
299            client,
300            source: None,
301        }
302    }
303
304    pub(crate) fn new_with_source(name: String, client: Client, source: ViewSource) -> Self {
305        View {
306            name,
307            client,
308            source: Some(source),
309        }
310    }
311
312    fn client_message(&self, req: ClientReq) -> Request {
313        crate::proto::Request {
314            msg_id: self.client.gen_id(),
315            entity_id: self.name.clone(),
316            client_req: Some(req),
317        }
318    }
319
320    /// Returns an array of strings containing the column paths of the [`View`]
321    /// without any of the source columns.
322    ///
323    /// A column path shows the columns that a given cell belongs to after
324    /// pivots are applied.
325    pub async fn column_paths(&self, window: ColumnWindow) -> ClientResult<Vec<String>> {
326        let msg = self.client_message(ClientReq::ViewColumnPathsReq(ViewColumnPathsReq {
327            start_col: window.start_col.map(|x| x as u32),
328            end_col: window.end_col.map(|x| x as u32),
329        }));
330
331        match self.client.oneshot(&msg).await? {
332            ClientResp::ViewColumnPathsResp(ViewColumnPathsResp { paths }) => {
333                // Ok(paths.into_iter().map(|x| x.path).collect())
334                Ok(paths)
335            },
336            resp => Err(resp.into()),
337        }
338    }
339
340    /// Returns this [`View`]'s _dimensions_, row and column count, as well as
341    /// those of the [`crate::Table`] from which it was derived.
342    ///
343    /// - `num_table_rows` - The number of rows in the underlying
344    ///   [`crate::Table`].
345    /// - `num_table_columns` - The number of columns in the underlying
346    ///   [`crate::Table`] (including the `index` column if this
347    ///   [`crate::Table`] was constructed with one).
348    /// - `num_view_rows` - The number of rows in this [`View`]. If this
349    ///   [`View`] has a `group_by` clause, `num_view_rows` will also include
350    ///   aggregated rows.
351    /// - `num_view_columns` - The number of columns in this [`View`]. If this
352    ///   [`View`] has a `split_by` clause, `num_view_columns` will include all
353    ///   _column paths_, e.g. the number of `columns` clause times the number
354    ///   of `split_by` groups.
355    pub async fn dimensions(&self) -> ClientResult<ViewDimensionsResp> {
356        let msg = self.client_message(ClientReq::ViewDimensionsReq(ViewDimensionsReq {}));
357        match self.client.oneshot(&msg).await? {
358            ClientResp::ViewDimensionsResp(resp) => Ok(resp),
359            resp => Err(resp.into()),
360        }
361    }
362
363    /// The expression schema of this [`View`], which contains only the
364    /// expressions created on this [`View`]. See [`View::schema`] for
365    /// details.
366    pub async fn expression_schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
367        if self.client.get_features().await?.expressions {
368            let msg = self.client_message(ClientReq::ViewExpressionSchemaReq(
369                ViewExpressionSchemaReq {},
370            ));
371            match self.client.oneshot(&msg).await? {
372                ClientResp::ViewExpressionSchemaResp(ViewExpressionSchemaResp { schema }) => {
373                    Ok(schema
374                        .into_iter()
375                        .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
376                        .collect())
377                },
378                resp => Err(resp.into()),
379            }
380        } else {
381            Ok([].into_iter().collect())
382        }
383    }
384
385    /// A copy of the [`ViewConfig`] object passed to the [`Table::view`] method
386    /// which created this [`View`].
387    pub async fn get_config(&self) -> ClientResult<crate::config::ViewConfig> {
388        let msg = self.client_message(ClientReq::ViewGetConfigReq(ViewGetConfigReq {}));
389        match self.client.oneshot(&msg).await? {
390            ClientResp::ViewGetConfigResp(ViewGetConfigResp {
391                config: Some(config),
392            }) => Ok(config.into()),
393            resp => Err(resp.into()),
394        }
395    }
396
397    /// The number of aggregated rows in this [`View`]. This is affected by the
398    /// "group_by" configuration parameter supplied to this view's contructor.
399    ///
400    /// # Returns
401    ///
402    /// The number of aggregated rows.
403    pub async fn num_rows(&self) -> ClientResult<u32> {
404        Ok(self.dimensions().await?.num_view_rows)
405    }
406
407    /// The schema of this [`View`].
408    ///
409    /// The [`View`] schema differs from the `schema` returned by
410    /// [`Table::schema`]; it may have different column names due to
411    /// `expressions` or `columns` configs, or it maye have _different
412    /// column types_ due to the application og `group_by` and `aggregates`
413    /// config. You can think of [`Table::schema`] as the _input_ schema and
414    /// [`View::schema`] as the _output_ schema of a Perspective pipeline.
415    pub async fn schema(&self) -> ClientResult<HashMap<String, ColumnType>> {
416        let msg = self.client_message(ClientReq::ViewSchemaReq(ViewSchemaReq {}));
417        match self.client.oneshot(&msg).await? {
418            ClientResp::ViewSchemaResp(ViewSchemaResp { schema }) => Ok(schema
419                .into_iter()
420                .map(|(x, y)| (x, ColumnType::try_from(y).unwrap()))
421                .collect()),
422            resp => Err(resp.into()),
423        }
424    }
425
426    /// Serializes a [`View`] to the Apache Arrow data format.
427    pub async fn to_arrow(&self, window: ViewWindow) -> ClientResult<Bytes> {
428        let msg = self.client_message(ClientReq::ViewToArrowReq(ViewToArrowReq {
429            viewport: Some(window.clone().into()),
430            compression: window.compression,
431        }));
432
433        match self.client.oneshot(&msg).await? {
434            ClientResp::ViewToArrowResp(ViewToArrowResp { arrow }) => Ok(arrow.into()),
435            resp => Err(resp.into()),
436        }
437    }
438
439    /// Serializes this [`View`] to a string of JSON data. Useful if you want to
440    /// save additional round trip serialize/deserialize cycles.    
441    pub async fn to_columns_string(&self, window: ViewWindow) -> ClientResult<String> {
442        let msg = self.client_message(ClientReq::ViewToColumnsStringReq(ViewToColumnsStringReq {
443            viewport: Some(window.clone().into()),
444            id: window.id,
445            index: window.index,
446            formatted: window.formatted,
447        }));
448
449        match self.client.oneshot(&msg).await? {
450            ClientResp::ViewToColumnsStringResp(ViewToColumnsStringResp { json_string }) => {
451                Ok(json_string)
452            },
453            resp => Err(resp.into()),
454        }
455    }
456
457    /// Render this `View` as a JSON string.
458    pub async fn to_json_string(&self, window: ViewWindow) -> ClientResult<String> {
459        let viewport = ViewPort::from(window.clone());
460        let msg = self.client_message(ClientReq::ViewToRowsStringReq(ViewToRowsStringReq {
461            viewport: Some(viewport),
462            id: window.id,
463            index: window.index,
464            formatted: window.formatted,
465        }));
466
467        match self.client.oneshot(&msg).await? {
468            ClientResp::ViewToRowsStringResp(ViewToRowsStringResp { json_string }) => {
469                Ok(json_string)
470            },
471            resp => Err(resp.into()),
472        }
473    }
474
475    /// Renders this [`View`] as an [NDJSON](https://github.com/ndjson/ndjson-spec)
476    /// formatted [`String`].
477    pub async fn to_ndjson(&self, window: ViewWindow) -> ClientResult<String> {
478        let viewport = ViewPort::from(window.clone());
479        let msg = self.client_message(ClientReq::ViewToNdjsonStringReq(ViewToNdjsonStringReq {
480            viewport: Some(viewport),
481            id: window.id,
482            index: window.index,
483            formatted: window.formatted,
484        }));
485
486        match self.client.oneshot(&msg).await? {
487            ClientResp::ViewToNdjsonStringResp(ViewToNdjsonStringResp { ndjson_string }) => {
488                Ok(ndjson_string)
489            },
490            resp => Err(resp.into()),
491        }
492    }
493
494    /// Serializes this [`View`] to CSV data in a standard format.
495    pub async fn to_csv(&self, window: ViewWindow) -> ClientResult<String> {
496        let msg = self.client_message(ClientReq::ViewToCsvReq(ViewToCsvReq {
497            viewport: Some(window.into()),
498        }));
499
500        match self.client.oneshot(&msg).await? {
501            ClientResp::ViewToCsvResp(ViewToCsvResp { csv }) => Ok(csv),
502            resp => Err(resp.into()),
503        }
504    }
505
506    /// Delete this [`View`] and clean up all resources associated with it.
507    /// [`View`] objects do not stop consuming resources or processing
508    /// updates when they are garbage collected - you must call this method
509    /// to reclaim these.
510    pub async fn delete(&self) -> ClientResult<()> {
511        let msg = self.client_message(ClientReq::ViewDeleteReq(ViewDeleteReq {}));
512        match self.client.oneshot(&msg).await? {
513            ClientResp::ViewDeleteResp(_) => Ok(()),
514            resp => Err(resp.into()),
515        }
516    }
517
518    /// Calculates the [min, max] of the leaf nodes of a column `column_name`.
519    ///
520    /// # Returns
521    ///
522    /// A tuple of [min, max], whose types are column and aggregate dependent.
523    pub async fn get_min_max(
524        &self,
525        column_name: String,
526    ) -> ClientResult<(crate::config::Scalar, crate::config::Scalar)> {
527        let msg = self.client_message(ClientReq::ViewGetMinMaxReq(ViewGetMinMaxReq {
528            column_name,
529        }));
530
531        match self.client.oneshot(&msg).await? {
532            ClientResp::ViewGetMinMaxResp(ViewGetMinMaxResp { min, max }) => {
533                let min = min.map(crate::config::Scalar::from).unwrap_or_default();
534                let max = max.map(crate::config::Scalar::from).unwrap_or_default();
535                Ok((min, max))
536            },
537            resp => Err(resp.into()),
538        }
539    }
540
541    /// Register a callback with this [`View`]. Whenever the view's underlying
542    /// table emits an update, this callback will be invoked with an object
543    /// containing `port_id`, indicating which port the update fired on, and
544    /// optionally `delta`, which is the new data that was updated for each
545    /// cell or each row.
546    ///
547    /// # Arguments
548    ///
549    /// - `on_update` - A callback function invoked on update, which receives an
550    ///   object with two keys: `port_id`, indicating which port the update was
551    ///   triggered on, and `delta`, whose value is dependent on the mode
552    ///   parameter.
553    /// - `options` - If this is provided as `OnUpdateOptions { mode:
554    ///   Some(OnUpdateMode::Row) }`, then `delta` is an Arrow of the updated
555    ///   rows. Otherwise `delta` will be [`Option::None`].
556    pub async fn on_update<T, U>(&self, on_update: T, options: OnUpdateOptions) -> ClientResult<u32>
557    where
558        T: Fn(OnUpdateData) -> U + Send + Sync + 'static,
559        U: Future<Output = ()> + Send + 'static,
560    {
561        let on_update = Arc::new(on_update);
562        let callback = move |resp: Response| {
563            let on_update = on_update.clone();
564            async move {
565                match resp.client_resp {
566                    Some(ClientResp::ViewOnUpdateResp(resp)) => {
567                        on_update(OnUpdateData(resp)).await;
568                        Ok(())
569                    },
570                    resp => Err(resp.into()),
571                }
572            }
573        };
574
575        let msg = self.client_message(ClientReq::ViewOnUpdateReq(ViewOnUpdateReq {
576            mode: options.mode.map(|OnUpdateMode::Row| Mode::Row as i32),
577        }));
578
579        self.client.subscribe(&msg, callback).await?;
580        Ok(msg.msg_id)
581    }
582
583    /// Unregister a previously registered update callback with this [`View`].
584    ///
585    /// # Arguments
586    ///
587    /// - `id` - A callback `id` as returned by a recipricol call to
588    ///   [`View::on_update`].
589    ///
590    /// # Examples
591    ///
592    /// ```no_run
593    /// # use perspective_client::{OnUpdateOptions, View};
594    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
595    /// # let view: View = todo!();
596    /// let callback = |_| async { print!("Updated!") };
597    /// let cid = view.on_update(callback, OnUpdateOptions::default()).await?;
598    /// view.remove_update(cid).await?;
599    /// # Ok(()) }
600    /// ```
601    pub async fn remove_update(&self, update_id: u32) -> ClientResult<()> {
602        let msg = self.client_message(ClientReq::ViewRemoveOnUpdateReq(ViewRemoveOnUpdateReq {
603            id: update_id,
604        }));
605
606        self.client.unsubscribe(update_id).await?;
607        match self.client.oneshot(&msg).await? {
608            ClientResp::ViewRemoveOnUpdateResp(_) => Ok(()),
609            resp => Err(resp.into()),
610        }
611    }
612
613    /// Register a callback which is invoked whenever rows are removed from
614    /// this [`View`]'s [`Table`] by [`Table::remove`], with the removed `index`
615    /// column values as an Apache Arrow of one column named after the index.
616    ///
617    /// [`Table::replace`] reports the keys it does not re-supply and
618    /// [`Table::clear`] reports every key. It never fires for a
619    /// [`Table`] without an `index`.
620    ///
621    /// # Examples
622    ///
623    /// ```no_run
624    /// # use perspective_client::{View, OnRemoveData};
625    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
626    /// # let view: View = todo!();
627    /// let callback = |removed: OnRemoveData| async move { println!("{:?}", removed.port_id) };
628    /// let cid = view.on_remove(callback).await?;
629    /// view.remove_remove(cid).await?;
630    /// # Ok(()) }
631    /// ```
632    pub async fn on_remove<T, U>(&self, on_remove: T) -> ClientResult<u32>
633    where
634        T: Fn(OnRemoveData) -> U + Send + Sync + 'static,
635        U: Future<Output = ()> + Send + 'static,
636    {
637        let on_remove = Arc::new(on_remove);
638        let callback = move |resp: Response| {
639            let on_remove = on_remove.clone();
640            async move {
641                match resp.client_resp {
642                    Some(ClientResp::ViewOnRemoveResp(resp)) => {
643                        on_remove(OnRemoveData(resp)).await;
644                        Ok(())
645                    },
646                    resp => Err(resp.into()),
647                }
648            }
649        };
650
651        let msg = self.client_message(ClientReq::ViewOnRemoveReq(ViewOnRemoveReq {}));
652        self.client.subscribe(&msg, callback).await?;
653        Ok(msg.msg_id)
654    }
655
656    /// Unregister a previously registered [`View::on_remove`] callback.
657    ///
658    /// # Arguments
659    ///
660    /// - `callback_id` - A callback `id` as returned by a reciprocal call to
661    ///   [`View::on_remove`].
662    pub async fn remove_remove(&self, callback_id: u32) -> ClientResult<()> {
663        let msg = self.client_message(ClientReq::ViewRemoveOnRemoveReq(ViewRemoveOnRemoveReq {
664            id: callback_id,
665        }));
666
667        self.client.unsubscribe(callback_id).await?;
668        match self.client.oneshot(&msg).await? {
669            ClientResp::ViewRemoveOnRemoveResp(_) => Ok(()),
670            resp => Err(resp.into()),
671        }
672    }
673
674    /// Register a callback with this [`View`]. Whenever the [`View`] is
675    /// deleted, this callback will be invoked.
676    pub async fn on_delete(
677        &self,
678        on_delete: Box<dyn Fn() + Send + Sync + 'static>,
679    ) -> ClientResult<u32> {
680        let callback = move |resp: Response| match resp.client_resp.unwrap() {
681            ClientResp::ViewOnDeleteResp(_) => {
682                on_delete();
683                Ok(())
684            },
685            resp => Err(resp.into()),
686        };
687
688        let msg = self.client_message(ClientReq::ViewOnDeleteReq(ViewOnDeleteReq {}));
689        self.client.subscribe_once(&msg, Box::new(callback)).await?;
690        Ok(msg.msg_id)
691    }
692
693    /// Unregister a previously registered [`View::on_delete`] callback.
694    pub async fn remove_delete(&self, callback_id: u32) -> ClientResult<()> {
695        let msg = self.client_message(ClientReq::ViewRemoveDeleteReq(ViewRemoveDeleteReq {
696            id: callback_id,
697        }));
698
699        match self.client.oneshot(&msg).await? {
700            ClientResp::ViewRemoveDeleteResp(ViewRemoveDeleteResp {}) => Ok(()),
701            resp => Err(resp.into()),
702        }
703    }
704
705    /// Collapses the `group_by` row at `row_index`.
706    pub async fn collapse(&self, row_index: u32) -> ClientResult<u32> {
707        let msg = self.client_message(ClientReq::ViewCollapseReq(ViewCollapseReq { row_index }));
708        match self.client.oneshot(&msg).await? {
709            ClientResp::ViewCollapseResp(ViewCollapseResp { num_changed }) => Ok(num_changed),
710            resp => Err(resp.into()),
711        }
712    }
713
714    /// Expand the `group_by` row at `row_index`.
715    pub async fn expand(&self, row_index: u32) -> ClientResult<u32> {
716        let msg = self.client_message(ClientReq::ViewExpandReq(ViewExpandReq { row_index }));
717        match self.client.oneshot(&msg).await? {
718            ClientResp::ViewExpandResp(ViewExpandResp { num_changed }) => Ok(num_changed),
719            resp => Err(resp.into()),
720        }
721    }
722
723    /// Set expansion `depth` of the `group_by` tree.
724    pub async fn set_depth(&self, depth: u32) -> ClientResult<()> {
725        let msg = self.client_message(ClientReq::ViewSetDepthReq(ViewSetDepthReq { depth }));
726        match self.client.oneshot(&msg).await? {
727            ClientResp::ViewSetDepthResp(_) => Ok(()),
728            resp => Err(resp.into()),
729        }
730    }
731}