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