Skip to main content

perspective_client/
client.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::error::Error;
15use std::ops::Deref;
16use std::sync::Arc;
17
18use async_lock::{Mutex, RwLock};
19use futures::Future;
20use futures::future::{BoxFuture, LocalBoxFuture, join_all};
21use prost::Message;
22use serde::{Deserialize, Serialize};
23use ts_rs::TS;
24
25use crate::proto::request::ClientReq;
26use crate::proto::response::ClientResp;
27use crate::proto::{
28    ColumnType, GetFeaturesReq, GetFeaturesResp, GetHostedTablesReq, GetHostedTablesResp,
29    HostedTable, JoinType, MakeJoinTableReq, MakeTableReq, RemoveHostedTablesUpdateReq, Request,
30    Response, ServerError, ServerSystemInfoReq,
31};
32use crate::table::{JoinOptions, Table, TableInitOptions, TableOptions, ViewBinding};
33use crate::table_data::{TableData, UpdateData};
34use crate::table_ref::TableRef;
35use crate::utils::*;
36use crate::view::{OnRemoveData, OnUpdateData, ViewWindow};
37use crate::{OnUpdateMode, OnUpdateOptions, asyncfn, clone};
38
39/// Metadata about the engine runtime (such as total heap utilization).
40#[derive(Clone, Debug, Serialize, Deserialize, TS)]
41pub struct SystemInfo<T = u64> {
42    /// Total available bytes for allocation on the [`Server`].
43    pub heap_size: T,
44
45    /// Bytes allocated for use on the [`Server`].
46    pub used_size: T,
47
48    /// Wall-clock time spent processing requests on the [`Server`], in
49    /// milliseconds (estimated). This does not properly account for the
50    /// internal thread pool (which enables column-parallel processing of
51    /// individual requests).
52    pub cpu_time: u32,
53
54    /// Milliseconds since internal CPU time accumulator was reset.
55    pub cpu_time_epoch: u32,
56
57    /// Timestamp (POSIX) this request was made. This field may be omitted
58    /// for wasm due to `perspective-client` lacking a dependency on
59    /// `wasm_bindgen`.
60    pub timestamp: Option<T>,
61
62    /// Total available bytes for allocation on the [`Client`]. This is only
63    /// available if `trace-allocator` is enabled.
64    pub client_heap: Option<T>,
65
66    /// Bytes allocated for use on the [`Client`].  This is only
67    /// available if `trace-allocator` is enabled.
68    pub client_used: Option<T>,
69}
70
71impl<U: Copy + 'static> SystemInfo<U> {
72    /// Convert the numeric representation for `T` to something else, which is
73    /// useful for JavaScript where there is no `u64` native type.
74    pub fn cast<T: Copy + 'static>(&self) -> SystemInfo<T>
75    where
76        U: num_traits::AsPrimitive<T>,
77    {
78        SystemInfo {
79            heap_size: self.heap_size.as_(),
80            used_size: self.used_size.as_(),
81            cpu_time: self.cpu_time,
82            cpu_time_epoch: self.cpu_time_epoch,
83            timestamp: self.timestamp.map(|x| x.as_()),
84            client_heap: self.client_heap.map(|x| x.as_()),
85            client_used: self.client_used.map(|x| x.as_()),
86        }
87    }
88}
89
90/// Metadata about what features are supported by the `Server` to which this
91/// [`Client`] connects.
92#[derive(Clone, Debug, Default, PartialEq)]
93pub struct Features(Arc<GetFeaturesResp>);
94
95impl Features {
96    pub fn get_group_rollup_modes(&self) -> Vec<crate::config::GroupRollupMode> {
97        self.group_rollup_mode
98            .iter()
99            .map(|x| {
100                crate::config::GroupRollupMode::from(
101                    crate::proto::GroupRollupMode::try_from(*x).unwrap(),
102                )
103            })
104            .collect::<Vec<_>>()
105    }
106
107    /// Unlike [`Features::get_group_rollup_modes`], an empty feature list
108    /// resolves to `[Flat]` rather than "no constraint" - servers predating
109    /// (or not implementing) split rollup can only produce leaf columns, so
110    /// absence must not offer the `Rollup` option.
111    pub fn get_split_rollup_modes(&self) -> Vec<crate::config::SplitRollupMode> {
112        if self.split_rollup_mode.is_empty() {
113            return vec![crate::config::SplitRollupMode::Flat];
114        }
115
116        self.split_rollup_mode
117            .iter()
118            .map(|x| {
119                crate::config::SplitRollupMode::from(
120                    crate::proto::SplitRollupMode::try_from(*x).unwrap(),
121                )
122            })
123            .collect::<Vec<_>>()
124    }
125}
126
127impl Deref for Features {
128    type Target = GetFeaturesResp;
129
130    fn deref(&self) -> &Self::Target {
131        &self.0
132    }
133}
134
135impl GetFeaturesResp {
136    pub fn default_op(&self, col_type: ColumnType) -> Option<&str> {
137        self.filter_ops
138            .get(&(col_type as u32))?
139            .options
140            .first()
141            .map(|x| x.as_str())
142    }
143
144    /// The window aggregates this server supports for a `col_type` SOURCE
145    /// column, in the server's declared (menu) order.
146    pub fn get_window_aggregates(
147        &self,
148        col_type: ColumnType,
149    ) -> Vec<crate::proto::WindowAggregateArgs> {
150        self.window_aggregates
151            .get(&(col_type as u32))
152            .map(|x| x.options.clone())
153            .unwrap_or_default()
154    }
155
156    /// Whether this server supports window columns at all - the
157    /// `window_aggregates` declaration is the single source of truth.
158    pub fn has_window_aggregates(&self) -> bool {
159        self.window_aggregates
160            .values()
161            .any(|x| !x.options.is_empty())
162    }
163}
164
165type Box2Fn<I, J, O> = Box<dyn Fn(I, J) -> O + Send + Sync + 'static>;
166
167type Subscriptions<C> = Arc<RwLock<HashMap<u32, C>>>;
168type UpdateCallback =
169    Arc<dyn Fn(Response) -> BoxFuture<'static, Result<(), ClientError>> + Send + Sync + 'static>;
170type OnErrorCallback =
171    Box2Fn<ClientError, Option<ReconnectCallback>, BoxFuture<'static, Result<(), ClientError>>>;
172
173type OnceCallback = Box<dyn FnOnce(Response) -> ClientResult<()> + Send + Sync + 'static>;
174type SendCallback = Arc<
175    dyn for<'a> Fn(&'a Request) -> BoxFuture<'a, Result<(), Box<dyn Error + Send + Sync>>>
176        + Send
177        + Sync
178        + 'static,
179>;
180
181/// The client-side representation of a connection to a `Server`.
182pub trait ClientHandler: Clone + Send + Sync + 'static {
183    fn send_request(
184        &self,
185        msg: Vec<u8>,
186    ) -> impl Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + Send;
187}
188
189mod name_registry {
190    use std::collections::HashSet;
191    use std::sync::{Arc, LazyLock, Mutex};
192
193    use crate::ClientError;
194    use crate::view::ClientResult;
195
196    static CLIENT_ID_GEN: LazyLock<Arc<Mutex<u32>>> = LazyLock::new(Arc::default);
197    static REGISTERED_CLIENTS: LazyLock<Arc<Mutex<HashSet<String>>>> = LazyLock::new(Arc::default);
198
199    pub(crate) fn generate_name(name: Option<&str>) -> ClientResult<String> {
200        if let Some(name) = name {
201            if let Some(name) = REGISTERED_CLIENTS
202                .lock()
203                .map_err(ClientError::from)?
204                .get(name)
205            {
206                Err(ClientError::DuplicateNameError(name.to_owned()))
207            } else {
208                Ok(name.to_owned())
209            }
210        } else {
211            let mut guard = CLIENT_ID_GEN.lock()?;
212            *guard += 1;
213            Ok(format!("client-{guard}"))
214        }
215    }
216}
217
218/// The type of the `reconnect` parameter passed to [`Client::handle_error`},
219/// and to the callback closure of [`Client::on_error`].
220///
221/// Calling this function from a [`Client::on_error`] closure should run the
222/// (implementation specific) client reconnect logic, e.g. rebindign a
223/// websocket.
224#[derive(Clone)]
225#[allow(clippy::type_complexity)]
226pub struct ReconnectCallback(
227    Arc<dyn Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync>,
228);
229
230impl Deref for ReconnectCallback {
231    type Target = dyn Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync;
232
233    fn deref(&self) -> &Self::Target {
234        &*self.0
235    }
236}
237
238impl ReconnectCallback {
239    pub fn new(
240        f: impl Fn() -> LocalBoxFuture<'static, Result<(), Box<dyn Error>>> + Send + Sync + 'static,
241    ) -> Self {
242        ReconnectCallback(Arc::new(f))
243    }
244}
245
246/// An instance of a [`Client`] is a connection to a single
247/// `perspective_server::Server`, whether locally in-memory or remote over some
248/// transport like a WebSocket.
249#[derive(Clone)]
250pub struct Client {
251    name: Arc<String>,
252    features: Arc<Mutex<Option<Features>>>,
253    send: SendCallback,
254    id_gen: IDGen,
255    subscriptions_errors: Subscriptions<OnErrorCallback>,
256    subscriptions_once: Subscriptions<OnceCallback>,
257    subscriptions: Subscriptions<UpdateCallback>,
258}
259
260impl PartialEq for Client {
261    fn eq(&self, other: &Self) -> bool {
262        self.name == other.name
263    }
264}
265
266impl std::fmt::Debug for Client {
267    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
268        f.debug_struct("Client").finish()
269    }
270}
271
272impl Client {
273    /// Create a new client instance with a closure that handles message
274    /// dispatch. See [`Client::new`] for details.
275    pub fn new_with_callback<T, U>(name: Option<&str>, send_request: T) -> ClientResult<Self>
276    where
277        T: Fn(Vec<u8>) -> U + 'static + Sync + Send,
278        U: Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + Send + 'static,
279    {
280        let name = name_registry::generate_name(name)?;
281        let send_request = Arc::new(send_request);
282        let send: SendCallback = Arc::new(move |req| {
283            let mut bytes: Vec<u8> = Vec::new();
284            req.encode(&mut bytes).unwrap();
285            let send_request = send_request.clone();
286            Box::pin(async move { send_request(bytes).await })
287        });
288
289        Ok(Client {
290            name: Arc::new(name),
291            features: Arc::default(),
292            id_gen: IDGen::default(),
293            send,
294            subscriptions: Subscriptions::default(),
295            subscriptions_errors: Arc::default(),
296            subscriptions_once: Arc::default(),
297        })
298    }
299
300    /// Create a new [`Client`] instance with [`ClientHandler`].
301    pub fn new<T>(name: Option<&str>, client_handler: T) -> ClientResult<Self>
302    where
303        T: ClientHandler + 'static + Sync + Send,
304    {
305        Self::new_with_callback(
306            name,
307            asyncfn!(client_handler, async move |req| {
308                client_handler.send_request(req).await
309            }),
310        )
311    }
312
313    pub fn get_name(&self) -> &'_ str {
314        self.name.as_str()
315    }
316
317    /// Handle a message from the external message queue.
318    /// [`Client::handle_response`] is part of the low-level message-handling
319    /// API necessary to implement new transports for a [`Client`]
320    /// connection to a local-or-remote `perspective_server::Server`, and
321    /// doesn't generally need to be called directly by "users" of a
322    /// [`Client`] once connected.
323    pub async fn handle_response<'a>(&'a self, msg: &'a [u8]) -> ClientResult<bool> {
324        let msg = Response::decode(msg)?;
325        tracing::debug!("RECV {}", msg);
326        let mut wr = self.subscriptions_once.write().await;
327        if let Some(handler) = (*wr).remove(&msg.msg_id) {
328            drop(wr);
329            handler(msg)?;
330            return Ok(true);
331        }
332
333        let handler = self.subscriptions.read().await.get(&msg.msg_id).cloned();
334        drop(wr);
335        if let Some(handler) = handler {
336            handler(msg).await?;
337            return Ok(true);
338        }
339
340        if let Response {
341            client_resp: Some(ClientResp::ServerError(ServerError { message, .. })),
342            ..
343        } = &msg
344        {
345            tracing::error!("{}", message);
346        } else {
347            tracing::debug!("Received unsolicited server response: {}", msg);
348        }
349
350        Ok(false)
351    }
352
353    /// Handle an exception from the underlying transport.
354    pub async fn handle_error<T, U>(
355        &self,
356        message: ClientError,
357        reconnect: Option<T>,
358    ) -> ClientResult<()>
359    where
360        T: Fn() -> U + Clone + Send + Sync + 'static,
361        U: Future<Output = ClientResult<()>>,
362    {
363        let subs = self.subscriptions_errors.read().await;
364        let tasks = join_all(subs.values().map(|callback| {
365            callback(
366                message.clone(),
367                reconnect.clone().map(move |f| {
368                    ReconnectCallback(Arc::new(move || {
369                        clone!(f);
370                        Box::pin(async move { Ok(f().await?) }) as LocalBoxFuture<'static, _>
371                    }))
372                }),
373            )
374        }));
375
376        tasks.await.into_iter().collect::<Result<(), _>>()?;
377        self.close_and_error_subscriptions(&message).await
378    }
379
380    /// TODO Synthesize an error to provide to the caller, since the
381    /// server did not respond and the other option is to just drop the call
382    /// which results in a non-descript error message. It would be nice to
383    /// have client-side failures be a native part of the Client API.
384    async fn close_and_error_subscriptions(&self, message: &ClientError) -> ClientResult<()> {
385        let synthetic_error = |msg_id| Response {
386            msg_id,
387            entity_id: "".to_string(),
388            client_resp: Some(ClientResp::ServerError(ServerError {
389                message: format!("{message}"),
390                status_code: 2,
391            })),
392        };
393
394        self.subscriptions.write().await.clear();
395        let callbacks_once = self
396            .subscriptions_once
397            .write()
398            .await
399            .drain()
400            .collect::<Vec<_>>();
401
402        callbacks_once
403            .into_iter()
404            .try_for_each(|(msg_id, f)| f(synthetic_error(msg_id)))
405    }
406
407    pub async fn on_error<T, U, V>(&self, on_error: T) -> ClientResult<u32>
408    where
409        T: Fn(ClientError, Option<ReconnectCallback>) -> U + Clone + Send + Sync + 'static,
410        U: Future<Output = V> + Send + 'static,
411        V: Into<Result<(), ClientError>> + Sync + 'static,
412    {
413        let id = self.gen_id();
414        let callback = asyncfn!(on_error, async move |x, y| on_error(x, y).await.into());
415        self.subscriptions_errors
416            .write()
417            .await
418            .insert(id, Box::new(move |x, y| Box::pin(callback(x, y))));
419
420        Ok(id)
421    }
422
423    /// Generate a message ID unique to this client.
424    pub(crate) fn gen_id(&self) -> u32 {
425        self.id_gen.next()
426    }
427
428    pub(crate) async fn unsubscribe(&self, update_id: u32) -> ClientResult<()> {
429        let callback = self
430            .subscriptions
431            .write()
432            .await
433            .remove(&update_id)
434            .ok_or(ClientError::Unknown("remove_update".to_string()))?;
435
436        drop(callback);
437        Ok(())
438    }
439
440    /// Register a callback which is expected to respond exactly once.
441    pub(crate) async fn subscribe_once(
442        &self,
443        msg: &Request,
444        on_update: Box<dyn FnOnce(Response) -> ClientResult<()> + Send + Sync + 'static>,
445    ) -> ClientResult<()> {
446        self.subscriptions_once
447            .write()
448            .await
449            .insert(msg.msg_id, on_update);
450
451        tracing::debug!("SEND {}", msg);
452        if let Err(e) = (self.send)(msg).await {
453            self.subscriptions_once.write().await.remove(&msg.msg_id);
454            Err(ClientError::Unknown(e.to_string()))
455        } else {
456            Ok(())
457        }
458    }
459
460    pub(crate) async fn subscribe<T, U>(&self, msg: &Request, on_update: T) -> ClientResult<()>
461    where
462        T: Fn(Response) -> U + Send + Sync + 'static,
463        U: Future<Output = Result<(), ClientError>> + Send + 'static,
464    {
465        self.subscriptions
466            .write()
467            .await
468            .insert(msg.msg_id, Arc::new(move |x| Box::pin(on_update(x))));
469
470        tracing::debug!("SEND {}", msg);
471        if let Err(e) = (self.send)(msg).await {
472            self.subscriptions.write().await.remove(&msg.msg_id);
473            Err(ClientError::Unknown(e.to_string()))
474        } else {
475            Ok(())
476        }
477    }
478
479    /// Send a `ClientReq` and await both the successful completion of the
480    /// `send`, _and_ the `ClientResp` which is returned.
481    pub(crate) async fn oneshot(&self, req: &Request) -> ClientResult<ClientResp> {
482        let (sender, receiver) = futures::channel::oneshot::channel::<ClientResp>();
483        let on_update = Box::new(move |res: Response| {
484            sender.send(res.client_resp.unwrap()).map_err(|x| x.into())
485        });
486
487        self.subscribe_once(req, on_update).await?;
488        receiver
489            .await
490            .map_err(|_| ClientError::Unknown(format!("Internal error for req {req}")))
491    }
492
493    pub(crate) async fn get_features(&self) -> ClientResult<Features> {
494        let mut guard = self.features.lock().await;
495        let features = if let Some(features) = &*guard {
496            features.clone()
497        } else {
498            let msg = Request {
499                msg_id: self.gen_id(),
500                entity_id: "".to_owned(),
501                client_req: Some(ClientReq::GetFeaturesReq(GetFeaturesReq {})),
502            };
503
504            let features = Features(Arc::new(match self.oneshot(&msg).await? {
505                ClientResp::GetFeaturesResp(features) => Ok(features),
506                resp => Err(resp),
507            }?));
508
509            *guard = Some(features.clone());
510            features
511        };
512
513        Ok(features)
514    }
515
516    /// Creates a new [`Table`] from either a _schema_ or _data_.
517    ///
518    /// The [`Client::table`] factory function can be initialized with either a
519    /// _schema_ (see [`Table::schema`]), or data in one of these formats:
520    ///
521    /// - Apache Arrow
522    /// - CSV
523    /// - JSON row-oriented
524    /// - JSON column-oriented
525    /// - NDJSON
526    ///
527    /// When instantiated with _data_, the schema is inferred from this data.
528    /// While this is convenient, inferrence is sometimes imperfect e.g.
529    /// when the input is empty, null or ambiguous. For these cases,
530    /// [`Client::table`] can first be instantiated with a explicit schema.
531    ///
532    /// When instantiated with a _schema_, the resulting [`Table`] is empty but
533    /// with known column names and column types. When subsqeuently
534    /// populated with [`Table::update`], these columns will be _coerced_ to
535    /// the schema's type. This behavior can be useful when
536    /// [`Client::table`]'s column type inferences doesn't work.
537    ///
538    /// The resulting [`Table`] is _virtual_, and invoking its methods
539    /// dispatches events to the `perspective_server::Server` this
540    /// [`Client`] connects to, where the data is stored and all calculation
541    /// occurs.
542    ///
543    /// When instantiated with a [`View`] of this [`Client`], the resulting
544    /// [`Table`] is _derived_: it is read-only, its rows are the [`View`]'s
545    /// output, and the engine keeps it current as the [`View`]'s source is
546    /// updated.
547    ///
548    /// # Arguments
549    ///
550    /// - `arg` - Either _schema_, initialization _data_, or a [`View`].
551    /// - `options` - Optional configuration which provides one of:
552    ///     - `limit` - The max number of rows the resulting [`Table`] can
553    ///       store.
554    ///     - `index` - The column name to use as an _index_ column. If this
555    ///       `Table` is being instantiated by _data_, this column name must be
556    ///       present in the data.
557    ///     - `name` - The name of the table. This will be generated if it is
558    ///       not provided.
559    ///     - `format` - The explicit format of the input data, can be one of
560    ///       `"json"`, `"columns"`, `"csv"` or `"arrow"`. This overrides
561    ///       language-specific type dispatch behavior, which allows stringified
562    ///       and byte array alternative inputs.
563    ///     - `schema` - The columns of a [`Table`] derived from a [`View`], in
564    ///       place of the ones inferred from it.
565    ///
566    /// # Examples
567    ///
568    /// Load a CSV from a `String`:
569    ///
570    /// ```no_run
571    /// # use perspective_client::*;
572    /// # async fn run(client: Client) -> Result<(), Box<dyn std::error::Error>> {
573    /// let opts = TableInitOptions::default();
574    /// let data = TableData::Update(UpdateData::Csv("x,y\n1,2\n3,4".into()));
575    /// let table = client.table(data, opts).await?;
576    /// # Ok(()) }
577    /// ```
578    pub async fn table(&self, input: TableData, options: TableInitOptions) -> ClientResult<Table> {
579        let entity_id = match options.name.clone() {
580            Some(x) => x.to_owned(),
581            None => randid(),
582        };
583
584        if let TableData::View(view) = &input {
585            let request_options: TableOptions = options.clone().into();
586            let mut options = options;
587            let source_index = view.source.as_ref().and_then(|x| x.options.index.clone());
588            if let (None, Some(index)) = (&options.index, &source_index) {
589                let config = view.get_config().await?;
590                let is_flat = config.group_by.is_empty() && config.split_by.is_empty();
591                let has_index = config.columns.iter().flatten().any(|x| x == index);
592                if is_flat && has_index {
593                    options.index = Some(index.clone());
594                }
595            }
596
597            if options.index.is_none() && options.limit.is_none() {
598                options.limit = view.source.as_ref().and_then(|x| x.options.limit);
599            }
600
601            if view.client() == self {
602                let data = TableData::View(view.clone());
603                self.crate_table_inner(data, request_options, entity_id.clone())
604                    .await?;
605
606                return Ok(Table::new(entity_id, self.clone(), options.into()));
607            }
608
609            if options.schema.is_some() {
610                return Err(ClientError::BadTableOptions);
611            }
612
613            let window = ViewWindow::default();
614            let arrow = view.to_arrow(window).await?;
615            let mut table = self
616                .crate_table_inner(UpdateData::Arrow(arrow).into(), options.into(), entity_id)
617                .await?;
618
619            let table_ = table.clone();
620            let callback = asyncfn!(table_, update, async move |update: OnUpdateData| {
621                let update = UpdateData::Arrow(update.delta.expect("Malformed message").into());
622                let options = crate::UpdateOptions::default();
623                table_.update(update, options).await.unwrap_or_log();
624            });
625
626            let options = OnUpdateOptions {
627                mode: Some(OnUpdateMode::Row),
628            };
629
630            let update_token = view.on_update(callback, options).await?;
631            let remove_token = if source_index.is_some() && source_index == table.get_index() {
632                let table_ = table.clone();
633                let callback = asyncfn!(table_, async move |removed: OnRemoveData| {
634                    if let Some(indices) = removed.indices.as_ref().filter(|x| !x.is_empty()) {
635                        let indices = UpdateData::Arrow(indices.clone().into());
636                        table_.remove(indices).await.unwrap_or_log();
637                    }
638                });
639
640                Some(view.on_remove(callback).await?)
641            } else {
642                None
643            };
644
645            table.view_binding = Some(ViewBinding {
646                view: view.clone(),
647                update_token,
648                remove_token,
649            });
650
651            Ok(table)
652        } else {
653            self.crate_table_inner(input, options.into(), entity_id)
654                .await
655        }
656    }
657
658    async fn crate_table_inner(
659        &self,
660        input: TableData,
661        options: TableOptions,
662        entity_id: String,
663    ) -> ClientResult<Table> {
664        let msg = Request {
665            msg_id: self.gen_id(),
666            entity_id: entity_id.clone(),
667            client_req: Some(ClientReq::MakeTableReq(MakeTableReq {
668                data: Some(input.into()),
669                options: Some(options.clone().try_into()?),
670            })),
671        };
672
673        let client = self.clone();
674        match self.oneshot(&msg).await? {
675            ClientResp::MakeTableResp(_) => Ok(Table::new(entity_id, client, options)),
676            resp => Err(resp.into()),
677        }
678    }
679
680    /// Create a new read-only [`Table`] by performing a JOIN on two source
681    /// tables. The resulting table is reactive: when either source table is
682    /// updated, the join is automatically recomputed.
683    ///
684    /// # Arguments
685    ///
686    /// * `left` - The left source table (as a [`Table`] or name string).
687    /// * `right` - The right source table (as a [`Table`] or name string).
688    /// * `on` - The column name to join on. Must exist in both tables with the
689    ///   same type.
690    /// * `options` - Join configuration (join type, table name).
691    pub async fn join(
692        &self,
693        left: TableRef,
694        right: TableRef,
695        on: &str,
696        options: JoinOptions,
697    ) -> ClientResult<Table> {
698        let entity_id = options.name.unwrap_or_else(randid);
699        let join_type: JoinType = options.join_type.unwrap_or_default();
700        let right_on_column = options.right_on.unwrap_or_default();
701        let msg = Request {
702            msg_id: self.gen_id(),
703            entity_id: entity_id.clone(),
704            client_req: Some(ClientReq::MakeJoinTableReq(MakeJoinTableReq {
705                left_table_id: left.table_name().to_owned(),
706                right_table_id: right.table_name().to_owned(),
707                on_column: on.to_owned(),
708                join_type: join_type.into(),
709                right_on_column,
710            })),
711        };
712
713        let client = self.clone();
714        match self.oneshot(&msg).await? {
715            ClientResp::MakeJoinTableResp(_) => Ok(Table::new(entity_id, client, TableOptions {
716                index: Some(on.to_owned()),
717                limit: None,
718                page_to_disk: None,
719                list_flatten: None,
720                schema: None,
721            })),
722            resp => Err(resp.into()),
723        }
724    }
725
726    async fn get_table_infos(&self) -> ClientResult<Vec<HostedTable>> {
727        let msg = Request {
728            msg_id: self.gen_id(),
729            entity_id: "".to_owned(),
730            client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
731                subscribe: false,
732            })),
733        };
734
735        match self.oneshot(&msg).await? {
736            ClientResp::GetHostedTablesResp(GetHostedTablesResp { table_infos }) => Ok(table_infos),
737            resp => Err(resp.into()),
738        }
739    }
740
741    /// Opens a [`Table`] that is hosted on the `perspective_server::Server`
742    /// that is connected to this [`Client`].
743    ///
744    /// The `name` property of [`TableInitOptions`] is used to identify each
745    /// [`Table`]. [`Table`] `name`s can be looked up for each [`Client`]
746    /// via [`Client::get_hosted_table_names`].
747    ///
748    /// # Examples
749    ///
750    /// ```no_run
751    /// # use perspective_client::Client;
752    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
753    /// # let client: Client = todo!();
754    /// let table = client.open_table("table_one".to_owned()).await?;
755    /// # Ok(()) }
756    /// ```
757    pub async fn open_table(&self, entity_id: String) -> ClientResult<Table> {
758        let infos = self.get_table_infos().await?;
759
760        // TODO fix this - name is repeated 2x
761        if let Some(info) = infos.into_iter().find(|i| i.entity_id == entity_id) {
762            let options = TableOptions {
763                index: info.index,
764                limit: info.limit,
765                page_to_disk: None,
766                list_flatten: None,
767                schema: None,
768            };
769
770            let client = self.clone();
771            Ok(Table::new(entity_id, client, options))
772        } else {
773            Err(ClientError::Unknown(format!(
774                "Unknown table \"{}\"",
775                entity_id
776            )))
777        }
778    }
779
780    /// Retrieves the names of all tables that this client has access to.
781    ///
782    /// `name` is a string identifier unique to the [`Table`] (per [`Client`]),
783    /// which can be used in conjunction with [`Client::open_table`] to get
784    /// a [`Table`] instance without the use of [`Client::table`]
785    /// constructor directly (e.g., one created by another [`Client`]).
786    ///
787    /// # Examples
788    ///
789    /// ```no_run
790    /// # use perspective_client::Client;
791    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
792    /// # let client: Client = todo!();
793    /// let tables = client.get_hosted_table_names().await?;
794    /// # Ok(()) }
795    /// ```
796    pub async fn get_hosted_table_names(&self) -> ClientResult<Vec<String>> {
797        let msg = Request {
798            msg_id: self.gen_id(),
799            entity_id: "".to_owned(),
800            client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
801                subscribe: false,
802            })),
803        };
804
805        match self.oneshot(&msg).await? {
806            ClientResp::GetHostedTablesResp(GetHostedTablesResp { table_infos }) => {
807                Ok(table_infos.into_iter().map(|i| i.entity_id).collect())
808            },
809            resp => Err(resp.into()),
810        }
811    }
812
813    /// Register a callback which is invoked whenever [`Client::table`] (on this
814    /// [`Client`]) or [`Table::delete`] (on a [`Table`] belinging to this
815    /// [`Client`]) are called.
816    pub async fn on_hosted_tables_update<T, U>(&self, on_update: T) -> ClientResult<u32>
817    where
818        T: Fn() -> U + Send + Sync + 'static,
819        U: Future<Output = ()> + Send + 'static,
820    {
821        let on_update = Arc::new(on_update);
822        let callback = asyncfn!(on_update, async move |resp: Response| {
823            match resp.client_resp {
824                Some(ClientResp::GetHostedTablesResp(_)) | None => {
825                    on_update().await;
826                    Ok(())
827                },
828                resp => Err(resp.into()),
829            }
830        });
831
832        let msg = Request {
833            msg_id: self.gen_id(),
834            entity_id: "".to_owned(),
835            client_req: Some(ClientReq::GetHostedTablesReq(GetHostedTablesReq {
836                subscribe: true,
837            })),
838        };
839
840        self.subscribe(&msg, callback).await?;
841        Ok(msg.msg_id)
842    }
843
844    /// Remove a callback previously registered via
845    /// `Client::on_hosted_tables_update`.
846    pub async fn remove_hosted_tables_update(&self, update_id: u32) -> ClientResult<()> {
847        let msg = Request {
848            msg_id: self.gen_id(),
849            entity_id: "".to_owned(),
850            client_req: Some(ClientReq::RemoveHostedTablesUpdateReq(
851                RemoveHostedTablesUpdateReq { id: update_id },
852            )),
853        };
854
855        self.unsubscribe(update_id).await?;
856        match self.oneshot(&msg).await? {
857            ClientResp::RemoveHostedTablesUpdateResp(_) => Ok(()),
858            resp => Err(resp.into()),
859        }
860    }
861
862    /// Provides the [`SystemInfo`] struct, implementation-specific metadata
863    /// about the [`perspective_server::Server`] runtime such as Memory and
864    /// CPU usage.
865    pub async fn system_info(&self) -> ClientResult<SystemInfo> {
866        let msg = Request {
867            msg_id: self.gen_id(),
868            entity_id: "".to_string(),
869            client_req: Some(ClientReq::ServerSystemInfoReq(ServerSystemInfoReq {})),
870        };
871
872        match self.oneshot(&msg).await? {
873            ClientResp::ServerSystemInfoResp(resp) => {
874                #[cfg(not(target_family = "wasm"))]
875                let timestamp = Some(
876                    std::time::SystemTime::now()
877                        .duration_since(std::time::UNIX_EPOCH)?
878                        .as_millis() as u64,
879                );
880
881                #[cfg(target_family = "wasm")]
882                let timestamp = None;
883
884                #[cfg(feature = "talc-allocator")]
885                let (client_used, client_heap) = {
886                    let (client_used, client_heap) = crate::utils::get_used();
887                    (Some(client_used as u64), Some(client_heap as u64))
888                };
889
890                #[cfg(not(feature = "talc-allocator"))]
891                let (client_used, client_heap) = (None, None);
892
893                let info = SystemInfo {
894                    heap_size: resp.heap_size,
895                    used_size: resp.used_size,
896                    cpu_time: resp.cpu_time,
897                    cpu_time_epoch: resp.cpu_time_epoch,
898                    timestamp,
899                    client_heap,
900                    client_used,
901                };
902
903                Ok(info)
904            },
905            resp => Err(resp.into()),
906        }
907    }
908}