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