From ebed4c368828baa1b8b7ccedf5ed5e1246badf38 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Tue, 13 Jan 2026 12:58:17 -0500 Subject: [PATCH 1/4] Split WS into v1 and v2; make various changes in v2. --- crates/client-api-messages/src/websocket.rs | 959 +----------------- .../src/websocket/common.rs | 53 + .../client-api-messages/src/websocket/v1.rs | 900 ++++++++++++++++ .../client-api-messages/src/websocket/v2.rs | 294 ++++++ 4 files changed, 1250 insertions(+), 956 deletions(-) create mode 100644 crates/client-api-messages/src/websocket/common.rs create mode 100644 crates/client-api-messages/src/websocket/v1.rs create mode 100644 crates/client-api-messages/src/websocket/v2.rs diff --git a/crates/client-api-messages/src/websocket.rs b/crates/client-api-messages/src/websocket.rs index f0ccbe2bb7d..0935d2e3c55 100644 --- a/crates/client-api-messages/src/websocket.rs +++ b/crates/client-api-messages/src/websocket.rs @@ -14,959 +14,6 @@ //! Changes to the Rust SDK are not necessarily required, as it depends on this crate //! rather than using an external mirror of this schema. -use crate::energy::EnergyQuanta; -use bytes::Bytes; -use bytestring::ByteString; -use core::{ - fmt::Debug, - ops::{Deref, Range}, -}; -use enum_as_inner::EnumAsInner; -use smallvec::SmallVec; -use spacetimedb_lib::{ConnectionId, Identity, TimeDuration, Timestamp}; -use spacetimedb_primitives::TableId; -use spacetimedb_sats::{ - de::{Deserialize, Error}, - impl_deserialize, impl_serialize, impl_st, - ser::Serialize, - AlgebraicType, SpacetimeType, -}; -use std::sync::Arc; - -pub const TEXT_PROTOCOL: &str = "v1.json.spacetimedb"; -pub const BIN_PROTOCOL: &str = "v1.bsatn.spacetimedb"; - -pub trait RowListLen { - /// Returns the length, in number of rows, not bytes, of the row list. - fn len(&self) -> usize; - /// Returns whether the list is empty or not. - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl> RowListLen for L { - fn len(&self) -> usize { - self.deref().len() - } - fn is_empty(&self) -> bool { - self.deref().is_empty() - } -} - -pub trait ByteListLen { - /// Returns the uncompressed size of the list in bytes - fn num_bytes(&self) -> usize; -} - -impl ByteListLen for Vec { - fn num_bytes(&self) -> usize { - self.iter().map(|str| str.len()).sum() - } -} - -/// A format / codec used by the websocket API. -/// -/// This can be e.g., BSATN, JSON. -pub trait WebsocketFormat: Sized { - /// The type used for the encoding of a single item. - type Single: SpacetimeType + for<'de> Deserialize<'de> + Serialize + Debug + Clone; - - /// The type used for the encoding of a list of items. - type List: SpacetimeType - + for<'de> Deserialize<'de> - + Serialize - + RowListLen - + ByteListLen - + Debug - + Clone - + Default; - - /// The type used to encode query updates. - /// This type exists so that some formats, e.g., BSATN, can compress an update. - type QueryUpdate: SpacetimeType + for<'de> Deserialize<'de> + Serialize + Debug + Clone + Send; -} - -/// Messages sent from the client to the server. -/// -/// Parametric over the reducer argument type to enable [`ClientMessage::map_args`]. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub enum ClientMessage { - /// Request a reducer run. - CallReducer(CallReducer), - /// Register SQL queries on which to receive updates. - Subscribe(Subscribe), - /// Send a one-off SQL query without establishing a subscription. - OneOffQuery(OneOffQuery), - /// Register a SQL query to to subscribe to updates. This does not affect other subscriptions. - SubscribeSingle(SubscribeSingle), - SubscribeMulti(SubscribeMulti), - /// Remove a subscription to a SQL query that was added with SubscribeSingle. - Unsubscribe(Unsubscribe), - UnsubscribeMulti(UnsubscribeMulti), - /// Request a procedure run. - CallProcedure(CallProcedure), -} - -impl ClientMessage { - pub fn map_args(self, f: impl FnOnce(Args) -> Args2) -> ClientMessage { - match self { - ClientMessage::CallReducer(CallReducer { - reducer, - args, - request_id, - flags, - }) => ClientMessage::CallReducer(CallReducer { - reducer, - args: f(args), - request_id, - flags, - }), - ClientMessage::OneOffQuery(x) => ClientMessage::OneOffQuery(x), - ClientMessage::SubscribeSingle(x) => ClientMessage::SubscribeSingle(x), - ClientMessage::Unsubscribe(x) => ClientMessage::Unsubscribe(x), - ClientMessage::Subscribe(x) => ClientMessage::Subscribe(x), - ClientMessage::SubscribeMulti(x) => ClientMessage::SubscribeMulti(x), - ClientMessage::UnsubscribeMulti(x) => ClientMessage::UnsubscribeMulti(x), - ClientMessage::CallProcedure(CallProcedure { - procedure, - args, - request_id, - flags, - }) => ClientMessage::CallProcedure(CallProcedure { - procedure, - args: f(args), - request_id, - flags, - }), - } - } -} - -/// Request a reducer run. -/// -/// Parametric over the argument type to enable [`ClientMessage::map_args`]. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct CallReducer { - /// The name of the reducer to call. - pub reducer: Box, - /// The arguments to the reducer. - /// - /// In the wire format, this will be a [`Bytes`], BSATN or JSON encoded according to the reducer's argument schema - /// and the enclosing message format. - pub args: Args, - /// An identifier for a client request. - /// - /// The server will include the same ID in the response [`TransactionUpdate`]. - pub request_id: u32, - /// Assorted flags that can be passed when calling a reducer. - /// - /// Currently accepts 0 or 1 where the latter means - /// that the caller does not want to be notified about the reducer - /// without being subscribed to any relevant queries. - pub flags: CallReducerFlags, -} - -#[derive(Clone, Copy, Default, PartialEq, Eq)] -pub enum CallReducerFlags { - /// The reducer's caller does want to be notified about the reducer completing successfully - /// regardless of whether the caller had subscribed to a relevant query. - /// - /// Note that updates to a reducer's caller are always sent as full updates - /// whether subscribed to a relevant query or not. - /// That is, the light tx mode setting does not apply to the reducer's caller. - /// - /// This is the default flag. - #[default] - FullUpdate, - /// The reducer's caller does not want to be notified about the reducer completing successfully - /// without having subscribed to any of the relevant queries. - NoSuccessNotify, -} - -impl_st!([] CallReducerFlags, AlgebraicType::U8); -impl_serialize!([] CallReducerFlags, (self, ser) => ser.serialize_u8(*self as u8)); -impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { - 0 => Ok(Self::FullUpdate), - 1 => Ok(Self::NoSuccessNotify), - x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), -}); - -/// An opaque id generated by the client to refer to a subscription. -/// This is used in Unsubscribe messages and errors. -#[derive(SpacetimeType, Copy, Clone, Debug, PartialEq, Eq, Hash)] -#[sats(crate = spacetimedb_lib)] -pub struct QueryId { - pub id: u32, -} - -impl QueryId { - pub fn new(id: u32) -> Self { - Self { id } - } -} - -/// Sent by client to database to register a set of queries, about which the client will -/// receive `TransactionUpdate`s. -/// -/// After issuing a `Subscribe` message, the client will receive a single -/// `SubscriptionUpdate` message containing every current row of every table which matches -/// the subscribed queries. Then, after each reducer run which updates one or more -/// subscribed rows, the client will receive a `TransactionUpdate` containing the updates. -/// -/// A `Subscribe` message sets or replaces the entire set of queries to which the client -/// is subscribed. If the client is previously subscribed to some set of queries `A`, and -/// then sends a `Subscribe` message to subscribe to a set `B`, afterwards, the client -/// will be subscribed to `B` but not `A`. In this case, the client will receive a -/// `SubscriptionUpdate` containing every existing row that matches `B`, even if some were -/// already in `A`. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct Subscribe { - /// A sequence of SQL queries. - pub query_strings: Box<[Box]>, - pub request_id: u32, -} - -/// Sent by client to register a subscription to single query, for which the client should receive -/// receive relevant `TransactionUpdate`s. -/// -/// After issuing a `SubscribeSingle` message, the client will receive a single -/// `SubscribeApplied` message containing every current row which matches the query. Then, any -/// time a reducer updates the query's results, the client will receive a `TransactionUpdate` -/// containing the relevant updates. -/// -/// If a client subscribes to queries with overlapping results, the client will receive -/// multiple copies of rows that appear in multiple queries. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscribeSingle { - /// A single SQL `SELECT` query to subscribe to. - pub query: Box, - /// An identifier for a client request. - pub request_id: u32, - - /// An identifier for this subscription, which should not be used for any other subscriptions on the same connection. - /// This is used to refer to this subscription in Unsubscribe messages from the client and errors sent from the server. - /// These only have meaning given a ConnectionId. - pub query_id: QueryId, -} - -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscribeMulti { - /// A single SQL `SELECT` query to subscribe to. - pub query_strings: Box<[Box]>, - /// An identifier for a client request. - pub request_id: u32, - - /// An identifier for this subscription, which should not be used for any other subscriptions on the same connection. - /// This is used to refer to this subscription in Unsubscribe messages from the client and errors sent from the server. - /// These only have meaning given a ConnectionId. - pub query_id: QueryId, -} - -/// Client request for removing a query from a subscription. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct Unsubscribe { - /// An identifier for a client request. - pub request_id: u32, - - /// The ID used in the corresponding `SubscribeSingle` message. - pub query_id: QueryId, -} - -/// Client request for removing a query from a subscription. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct UnsubscribeMulti { - /// An identifier for a client request. - pub request_id: u32, - - /// The ID used in the corresponding `SubscribeSingle` message. - pub query_id: QueryId, -} - -/// A one-off query submission. -/// -/// Query should be a "SELECT * FROM Table WHERE ...". Other types of queries will be rejected. -/// Multiple such semicolon-delimited queries are allowed. -/// -/// One-off queries are identified by a client-generated messageID. -/// To avoid data leaks, the server will NOT cache responses to messages based on UUID! -/// It also will not check for duplicate IDs. They are just a way to match responses to messages. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct OneOffQuery { - pub message_id: Box<[u8]>, - pub query_string: Box, -} - -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -/// Request a procedure run. -/// -/// Parametric over the argument type to enable [`ClientMessage::map_args`]. -pub struct CallProcedure { - /// The name of the procedure to call. - pub procedure: Box, - /// The arguments to the procedure. - /// - /// In the wire format, this will be a [`Bytes`], BSATN or JSON encoded according to the reducer's argument schema - /// and the enclosing message format. - pub args: Args, - /// An identifier for a client request. - /// - /// The server will include the same ID in the response [`ProcedureResult`]. - pub request_id: u32, - /// Reserved space for future extensions. - pub flags: CallProcedureFlags, -} - -#[derive(Clone, Copy, Default, PartialEq, Eq)] -pub enum CallProcedureFlags { - #[default] - Default, -} - -impl_st!([] CallProcedureFlags, AlgebraicType::U8); -impl_serialize!([] CallProcedureFlags, (self, ser) => ser.serialize_u8(*self as u8)); -impl_deserialize!([] CallProcedureFlags, de => match de.deserialize_u8()? { - 0 => Ok(Self::Default), - x => Err(D::Error::custom(format_args!("invalid call procedure flag {x}"))), -}); - -/// The tag recognized by the host and SDKs to mean no compression of a [`ServerMessage`]. -pub const SERVER_MSG_COMPRESSION_TAG_NONE: u8 = 0; - -/// The tag recognized by the host and SDKs to mean brotli compression of a [`ServerMessage`]. -pub const SERVER_MSG_COMPRESSION_TAG_BROTLI: u8 = 1; - -/// The tag recognized by the host and SDKs to mean brotli compression of a [`ServerMessage`]. -pub const SERVER_MSG_COMPRESSION_TAG_GZIP: u8 = 2; - -/// Messages sent from the server to the client. -#[derive(SpacetimeType, derive_more::From)] -#[sats(crate = spacetimedb_lib)] -pub enum ServerMessage { - /// Informs of changes to subscribed rows. - /// This will be removed when we switch to `SubscribeSingle`. - InitialSubscription(InitialSubscription), - /// Upon reducer run. - TransactionUpdate(TransactionUpdate), - /// Upon reducer run, but limited to just the table updates. - TransactionUpdateLight(TransactionUpdateLight), - /// After connecting, to inform client of its identity. - IdentityToken(IdentityToken), - /// Return results to a one off SQL query. - OneOffQueryResponse(OneOffQueryResponse), - /// Sent in response to a `SubscribeSingle` message. This contains the initial matching rows. - SubscribeApplied(SubscribeApplied), - /// Sent in response to an `Unsubscribe` message. This contains the matching rows. - UnsubscribeApplied(UnsubscribeApplied), - /// Communicate an error in the subscription lifecycle. - SubscriptionError(SubscriptionError), - /// Sent in response to a `SubscribeMulti` message. This contains the initial matching rows. - SubscribeMultiApplied(SubscribeMultiApplied), - /// Sent in response to an `UnsubscribeMulti` message. This contains the matching rows. - UnsubscribeMultiApplied(UnsubscribeMultiApplied), - /// Sent in response to a [`CallProcedure`] message. This contains the return value. - ProcedureResult(ProcedureResult), -} - -/// The matching rows of a subscription query. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscribeRows { - /// The table ID of the query. - pub table_id: TableId, - /// The table name of the query. - pub table_name: Box, - /// The BSATN row values. - pub table_rows: TableUpdate, -} - -/// Response to [`Subscribe`] containing the initial matching rows. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscribeApplied { - /// The request_id of the corresponding `SubscribeSingle` message. - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, - /// An identifier for the subscribed query sent by the client. - pub query_id: QueryId, - /// The matching rows for this query. - pub rows: SubscribeRows, -} - -/// Server response to a client [`Unsubscribe`] request. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct UnsubscribeApplied { - /// Provided by the client via the `Subscribe` message. - /// TODO: switch to subscription id? - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, - /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. - pub query_id: QueryId, - /// The matching rows for this query. - /// Note, this makes unsubscribing potentially very expensive. - /// To remove this in the future, we would need to send query_ids with rows in transaction updates, - /// and we would need clients to track which rows exist in which queries. - pub rows: SubscribeRows, -} - -/// Server response to an error at any point of the subscription lifecycle. -/// If this error doesn't have a request_id, the client should drop all subscriptions. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscriptionError { - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, - /// Provided by the client via a [`Subscribe`] or [`Unsubscribe`] message. - /// [`None`] if this occurred as the result of a [`TransactionUpdate`]. - pub request_id: Option, - /// Provided by the client via a [`Subscribe`] or [`Unsubscribe`] message. - /// [`None`] if this occurred as the result of a [`TransactionUpdate`]. - pub query_id: Option, - /// The return table of the query in question. - /// The server is not required to set this field. - /// It has been added to avoid a breaking change post 1.0. - /// - /// If unset, an error results in the entire subscription being dropped. - /// Otherwise only queries of this table type must be dropped. - pub table_id: Option, - /// An error message describing the failure. - /// - /// This should reference specific fragments of the query where applicable, - /// but should not include the full text of the query, - /// as the client can retrieve that from the `request_id`. - /// - /// This is intended for diagnostic purposes. - /// It need not have a predictable/parseable format. - pub error: Box, -} - -/// Response to [`Subscribe`] containing the initial matching rows. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscribeMultiApplied { - /// The request_id of the corresponding `SubscribeSingle` message. - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, - /// An identifier for the subscribed query sent by the client. - pub query_id: QueryId, - /// The matching rows for this query. - pub update: DatabaseUpdate, -} - -/// Server response to a client [`Unsubscribe`] request. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct UnsubscribeMultiApplied { - /// Provided by the client via the `Subscribe` message. - /// TODO: switch to subscription id? - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, - /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. - pub query_id: QueryId, - /// The matching rows for this query set. - /// Note, this makes unsubscribing potentially very expensive. - /// To remove this in the future, we would need to send query_ids with rows in transaction updates, - /// and we would need clients to track which rows exist in which queries. - pub update: DatabaseUpdate, -} - -/// Response to [`Subscribe`] containing the initial matching rows. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct SubscriptionUpdate { - /// A [`DatabaseUpdate`] containing only inserts, the rows which match the subscription queries. - pub database_update: DatabaseUpdate, - /// An identifier sent by the client in requests. - /// The server will include the same request_id in the response. - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration_micros: u64, -} - -/// Response to [`Subscribe`] containing the initial matching rows. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct InitialSubscription { - /// A [`DatabaseUpdate`] containing only inserts, the rows which match the subscription queries. - pub database_update: DatabaseUpdate, - /// An identifier sent by the client in requests. - /// The server will include the same request_id in the response. - pub request_id: u32, - /// The overall time between the server receiving a request and sending the response. - pub total_host_execution_duration: TimeDuration, -} - -/// Received by database from client to inform of user's identity, token and client connection id. -/// -/// The database will always send an `IdentityToken` message -/// as the first message for a new WebSocket connection. -/// If the client is re-connecting with existing credentials, -/// the message will include those credentials. -/// If the client connected anonymously, -/// the database will generate new credentials to identify it. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct IdentityToken { - pub identity: Identity, - pub token: Box, - pub connection_id: ConnectionId, -} - -/// Received by client from database upon a reducer run. -/// -/// Clients receive `TransactionUpdate`s only for reducers -/// which update at least one of their subscribed rows, -/// or for their own `Failed` or `OutOfEnergy` reducer invocations. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct TransactionUpdate { - /// The status of the transaction. Contains the updated rows, if successful. - pub status: UpdateStatus, - /// The time when the reducer started. - /// - /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. - pub timestamp: Timestamp, - /// The identity of the user who requested the reducer run. For event-driven and - /// scheduled reducers, it is the identity of the database owner. - pub caller_identity: Identity, - - /// The 16-byte [`ConnectionId`] of the user who requested the reducer run. - /// - /// The all-zeros id is a sentinel which denotes no meaningful value. - /// This can occur in the following situations: - /// - `init` and `update` reducers will have a `caller_connection_id` - /// if and only if one was provided to the `publish` HTTP endpoint. - /// - Scheduled reducers will never have a `caller_connection_id`. - /// - Reducers invoked by WebSocket or the HTTP API will always have a `caller_connection_id`. - pub caller_connection_id: ConnectionId, - /// The original CallReducer request that triggered this reducer. - pub reducer_call: ReducerCallInfo, - /// The amount of energy credits consumed by running the reducer. - pub energy_quanta_used: EnergyQuanta, - /// How long the reducer took to run. - pub total_host_execution_duration: TimeDuration, -} - -/// Received by client from database upon a reducer run. -/// -/// Clients receive `TransactionUpdateLight`s only for reducers -/// which update at least one of their subscribed rows. -/// Failed reducers result in full [`TransactionUpdate`]s -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct TransactionUpdateLight { - /// An identifier for a client request - pub request_id: u32, - - /// The reducer ran successfully and its changes were committed to the database. - /// The rows altered in the database/ are recorded in this `DatabaseUpdate`. - pub update: DatabaseUpdate, -} - -/// Contained in a [`TransactionUpdate`], metadata about a reducer invocation. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct ReducerCallInfo { - /// The name of the reducer that was called. - /// - /// NOTE(centril, 1.0): For bandwidth resource constrained clients - /// this can encourage them to have poor naming of reducers like `a`. - /// We should consider not sending this at all and instead - /// having a startup message where the name <-> id bindings - /// are established between the host and the client. - pub reducer_name: Box, - /// The numerical id of the reducer that was called. - pub reducer_id: u32, - /// The arguments to the reducer, encoded as BSATN or JSON according to the reducer's argument schema - /// and the client's requested protocol. - pub args: F::Single, - /// An identifier for a client request - pub request_id: u32, -} - -/// The status of a [`TransactionUpdate`]. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub enum UpdateStatus { - /// The reducer ran successfully and its changes were committed to the database. - /// The rows altered in the database/ will be recorded in the `DatabaseUpdate`. - Committed(DatabaseUpdate), - /// The reducer errored, and any changes it attempted to were rolled back. - /// This is the error message. - Failed(Box), - /// The reducer was interrupted due to insufficient energy/funds, - /// and any changes it attempted to make were rolled back. - OutOfEnergy, -} - -/// A collection of inserted and deleted rows, contained in a [`TransactionUpdate`] or [`SubscriptionUpdate`]. -#[derive(SpacetimeType, Debug, Clone, Default)] -#[sats(crate = spacetimedb_lib)] -pub struct DatabaseUpdate { - pub tables: Vec>, -} - -impl DatabaseUpdate { - pub fn is_empty(&self) -> bool { - self.tables.is_empty() - } - - pub fn num_rows(&self) -> usize { - self.tables.iter().map(|t| t.num_rows()).sum() - } -} - -impl FromIterator> for DatabaseUpdate { - fn from_iter>>(iter: T) -> Self { - DatabaseUpdate { - tables: iter.into_iter().collect(), - } - } -} - -/// Part of a [`DatabaseUpdate`] received by client from database for alterations to a single table. -/// -/// NOTE(centril): in 0.12 we added `num_rows` and `table_name` to the struct. -/// These inflate the size of messages, which for some customers is the wrong default. -/// We might want to consider `v1.spacetimedb.bsatn.lightweight` -#[derive(SpacetimeType, Debug, Clone)] -#[sats(crate = spacetimedb_lib)] -pub struct TableUpdate { - /// The id of the table. Clients should prefer `table_name`, as it is a stable part of a module's API, - /// whereas `table_id` may change between runs. - pub table_id: TableId, - /// The name of the table. - /// - /// NOTE(centril, 1.0): we might want to remove this and instead - /// tell clients about changes to table_name <-> table_id mappings. - pub table_name: Box, - /// The sum total of rows in `self.updates`, - pub num_rows: u64, - /// The actual insert and delete updates for this table. - pub updates: SmallVec<[F::QueryUpdate; 1]>, -} - -/// Computed update for a single query, annotated with the number of matching rows. -#[derive(Debug)] -pub struct SingleQueryUpdate { - pub update: F::QueryUpdate, - pub num_rows: u64, -} - -impl TableUpdate { - pub fn new(table_id: TableId, table_name: Box, update: SingleQueryUpdate) -> Self { - Self { - table_id, - table_name, - num_rows: update.num_rows, - updates: [update.update].into(), - } - } - - pub fn empty(table_id: TableId, table_name: Box) -> Self { - Self { - table_id, - table_name, - num_rows: 0, - updates: SmallVec::new(), - } - } - - pub fn push(&mut self, update: SingleQueryUpdate) { - self.updates.push(update.update); - self.num_rows += update.num_rows; - } - - pub fn num_rows(&self) -> usize { - self.num_rows as usize - } -} - -#[derive(SpacetimeType, Debug, Clone, EnumAsInner)] -#[sats(crate = spacetimedb_lib)] -pub enum CompressableQueryUpdate { - Uncompressed(QueryUpdate), - Brotli(Bytes), - Gzip(Bytes), -} - -#[derive(SpacetimeType, Debug, Clone)] -#[sats(crate = spacetimedb_lib)] -pub struct QueryUpdate { - /// When in a [`TransactionUpdate`], the matching rows of this table deleted by the transaction. - /// - /// Rows are encoded as BSATN or JSON according to the table's schema - /// and the client's requested protocol. - /// - /// Always empty when in an [`InitialSubscription`]. - pub deletes: F::List, - /// When in a [`TransactionUpdate`], the matching rows of this table inserted by the transaction. - /// When in an [`InitialSubscription`], the matching rows of this table in the entire committed state. - /// - /// Rows are encoded as BSATN or JSON according to the table's schema - /// and the client's requested protocol. - pub inserts: F::List, -} - -/// A response to a [`OneOffQuery`]. -/// Will contain either one error or some number of response rows. -/// At most one of these messages will be sent in reply to any query. -/// -/// The messageId will be identical to the one sent in the original query. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct OneOffQueryResponse { - pub message_id: Box<[u8]>, - /// If query compilation or evaluation errored, an error message. - pub error: Option>, - - /// If query compilation and evaluation succeeded, a set of resulting rows, grouped by table. - pub tables: Box<[OneOffTable]>, - - /// The total duration of query compilation and evaluation on the server, in microseconds. - pub total_host_execution_duration: TimeDuration, -} - -/// A table included as part of a [`OneOffQueryResponse`]. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct OneOffTable { - /// The name of the table. - pub table_name: Box, - /// The set of rows which matched the query, encoded as BSATN or JSON according to the table's schema - /// and the client's requested protocol. - /// - /// TODO(centril, 1.0): Evaluate whether we want to conditionally compress these. - pub rows: F::List, -} - -/// The result of running a procedure, -/// including the return value of the procedure on success. -/// -/// Sent in response to a [`CallProcedure`] message. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub struct ProcedureResult { - /// The status of the procedure run. - /// - /// Contains the return value if successful, or the error message if not. - pub status: ProcedureStatus, - /// The time when the reducer started. - /// - /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. - pub timestamp: Timestamp, - /// The time the procedure took to run. - pub total_host_execution_duration: TimeDuration, - /// The same same client-provided identifier as in the original [`ProcedureCall`] request. - /// - /// Clients use this to correlate the response with the original request. - pub request_id: u32, -} - -/// The status of a procedure call, -/// including the return value on success. -#[derive(SpacetimeType, Debug)] -#[sats(crate = spacetimedb_lib)] -pub enum ProcedureStatus { - /// The procedure ran and returned the enclosed value. - /// - /// All user error handling happens within here; - /// the returned value may be a `Result` or `Option`, - /// or any other type to which the user may ascribe arbitrary meaning. - Returned(F::Single), - /// The reducer was interrupted due to insufficient energy/funds. - /// - /// The procedure may have performed some observable side effects before being interrupted. - OutOfEnergy, - /// The call failed in the host, e.g. due to a type error or unknown procedure name. - InternalError(String), -} - -/// Used whenever different formats need to coexist. -#[derive(Debug, Clone)] -pub enum FormatSwitch { - Bsatn(B), - Json(J), -} - -impl FormatSwitch { - /// Zips together two switches. - pub fn zip_mut(&mut self, other: FormatSwitch) -> FormatSwitch<(&mut B1, B2), (&mut J1, J2)> { - match (self, other) { - (FormatSwitch::Bsatn(a), FormatSwitch::Bsatn(b)) => FormatSwitch::Bsatn((a, b)), - (FormatSwitch::Json(a), FormatSwitch::Json(b)) => FormatSwitch::Json((a, b)), - _ => panic!("format should be the same for both sides of the zip"), - } - } -} - -#[derive(Clone, Copy, Default, Debug, SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct JsonFormat; - -impl WebsocketFormat for JsonFormat { - type Single = ByteString; - type List = Vec; - type QueryUpdate = QueryUpdate; -} - -#[derive(Clone, Copy, Default, Debug, SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct BsatnFormat; - -impl WebsocketFormat for BsatnFormat { - type Single = Box<[u8]>; - type List = BsatnRowList; - type QueryUpdate = CompressableQueryUpdate; -} - -/// A specification of either a desired or decided compression algorithm. -#[derive(serde::Deserialize, Default, PartialEq, Eq, Clone, Copy, Hash, Debug)] -pub enum Compression { - /// No compression ever. - None, - /// Compress using brotli if a certain size threshold was met. - #[default] - Brotli, - /// Compress using gzip if a certain size threshold was met. - Gzip, -} - -pub type RowSize = u16; -pub type RowOffset = u64; - -/// A packed list of BSATN-encoded rows. -#[derive(SpacetimeType, Debug, Clone, Default)] -#[sats(crate = spacetimedb_lib)] -pub struct BsatnRowList { - /// A size hint about `rows_data` - /// intended to facilitate parallel decode purposes on large initial updates. - size_hint: RowSizeHint, - /// The flattened byte array for a list of rows. - rows_data: Bytes, -} - -impl BsatnRowList { - /// Returns a new row list where `rows_data` is the flattened byte array - /// containing the BSATN of each row, without any markers for where a row begins and end. - /// - /// The `size_hint` encodes the boundaries of each row in `rows_data`. - /// See [`RowSizeHint`] for more details on the encoding. - pub fn new(size_hint: RowSizeHint, rows_data: Bytes) -> Self { - Self { size_hint, rows_data } - } -} - -/// NOTE(centril, 1.0): We might want to add a `None` variant to this -/// where the client has to decode in a loop until `rows_data` has been exhausted. -/// The use-case for this is clients who are bandwidth limited and where every byte counts. -#[derive(SpacetimeType, Debug, Clone)] -#[sats(crate = spacetimedb_lib)] -pub enum RowSizeHint { - /// Each row in `rows_data` is of the same fixed size as specified here. - FixedSize(RowSize), - /// The offsets into `rows_data` defining the boundaries of each row. - /// Only stores the offset to the start of each row. - /// The ends of each row is inferred from the start of the next row, or `rows_data.len()`. - /// The behavior of this is identical to that of `PackedStr`. - RowOffsets(Arc<[RowOffset]>), -} - -impl Default for RowSizeHint { - fn default() -> Self { - Self::RowOffsets([].into()) - } -} - -impl RowSizeHint { - fn index_to_range(&self, index: usize, data_end: usize) -> Option> { - match self { - Self::FixedSize(size) => { - let size = *size as usize; - let start = index * size; - if start >= data_end { - // We've reached beyond `data_end`, - // so this is a row that doesn't exist, so we are beyond the count. - return None; - } - let end = (index + 1) * size; - Some(start..end) - } - Self::RowOffsets(offsets) => { - let offsets = offsets.as_ref(); - let start = *offsets.get(index)? as usize; - // The end is either the start of the next element or the end. - let end = offsets.get(index + 1).map(|e| *e as usize).unwrap_or(data_end); - Some(start..end) - } - } - } -} - -impl RowListLen for BsatnRowList { - fn len(&self) -> usize { - match &self.size_hint { - // `size != 0` is always the case for `FixedSize`. - RowSizeHint::FixedSize(size) => self.rows_data.as_ref().len() / *size as usize, - RowSizeHint::RowOffsets(offsets) => offsets.as_ref().len(), - } - } -} - -impl ByteListLen for BsatnRowList { - /// Returns the uncompressed size of the list in bytes - fn num_bytes(&self) -> usize { - self.rows_data.as_ref().len() - } -} - -impl BsatnRowList { - /// Returns the element at `index` in the list. - pub fn get(&self, index: usize) -> Option { - let data_end = self.rows_data.len(); - let data_range = self.size_hint.index_to_range(index, data_end)?; - Some(self.rows_data.slice(data_range)) - } - - /// Consumes the list and returns the parts. - pub fn into_inner(self) -> (RowSizeHint, Bytes) { - (self.size_hint, self.rows_data) - } -} - -/// An iterator over all the elements in a [`BsatnRowList`]. -pub struct BsatnRowListIter<'a> { - list: &'a BsatnRowList, - index: usize, -} - -impl<'a> IntoIterator for &'a BsatnRowList { - type IntoIter = BsatnRowListIter<'a>; - type Item = Bytes; - fn into_iter(self) -> Self::IntoIter { - BsatnRowListIter { list: self, index: 0 } - } -} - -impl Iterator for BsatnRowListIter<'_> { - type Item = Bytes; - fn next(&mut self) -> Option { - let index = self.index; - self.index += 1; - self.list.get(index) - } -} +pub mod common; +pub mod v1; +pub mod v2; diff --git a/crates/client-api-messages/src/websocket/common.rs b/crates/client-api-messages/src/websocket/common.rs new file mode 100644 index 00000000000..fdcd6305de9 --- /dev/null +++ b/crates/client-api-messages/src/websocket/common.rs @@ -0,0 +1,53 @@ +use spacetimedb_sats::{de::Error, impl_deserialize, impl_serialize, impl_st, AlgebraicType, SpacetimeType}; + +/// An opaque id generated by the client to refer to a subscription. +/// This is used in Unsubscribe messages and errors. +#[derive(SpacetimeType, Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[sats(crate = spacetimedb_lib)] +pub struct QuerySetId { + pub id: u32, +} + +impl QuerySetId { + pub fn new(id: u32) -> Self { + Self { id } + } +} + +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub enum CallReducerFlags { + /// The reducer's caller does want to be notified about the reducer completing successfully + /// regardless of whether the caller had subscribed to a relevant query. + /// + /// Note that updates to a reducer's caller are always sent as full updates + /// whether subscribed to a relevant query or not. + /// That is, the light tx mode setting does not apply to the reducer's caller. + /// + /// This is the default flag. + #[default] + FullUpdate, + /// The reducer's caller does not want to be notified about the reducer completing successfully + /// without having subscribed to any of the relevant queries. + NoSuccessNotify, +} + +impl_st!([] CallReducerFlags, AlgebraicType::U8); +impl_serialize!([] CallReducerFlags, (self, ser) => ser.serialize_u8(*self as u8)); +impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { + 0 => Ok(Self::FullUpdate), + 1 => Ok(Self::NoSuccessNotify), + x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), +}); + +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub enum CallProcedureFlags { + #[default] + Default, +} + +impl_st!([] CallProcedureFlags, AlgebraicType::U8); +impl_serialize!([] CallProcedureFlags, (self, ser) => ser.serialize_u8(*self as u8)); +impl_deserialize!([] CallProcedureFlags, de => match de.deserialize_u8()? { + 0 => Ok(Self::Default), + x => Err(D::Error::custom(format_args!("invalid call procedure flag {x}"))), +}); diff --git a/crates/client-api-messages/src/websocket/v1.rs b/crates/client-api-messages/src/websocket/v1.rs new file mode 100644 index 00000000000..a4dc0f109bd --- /dev/null +++ b/crates/client-api-messages/src/websocket/v1.rs @@ -0,0 +1,900 @@ +pub use super::common::{CallProcedureFlags, CallReducerFlags, QuerySetId as QueryId}; +use crate::energy::EnergyQuanta; +use bytes::Bytes; +use bytestring::ByteString; +use core::{ + fmt::Debug, + ops::{Deref, Range}, +}; +use enum_as_inner::EnumAsInner; +use smallvec::SmallVec; +use spacetimedb_lib::{ConnectionId, Identity, TimeDuration, Timestamp}; +use spacetimedb_primitives::TableId; +use spacetimedb_sats::{de::Deserialize, ser::Serialize, SpacetimeType}; +use std::sync::Arc; + +pub const TEXT_PROTOCOL: &str = "v1.json.spacetimedb"; +pub const BIN_PROTOCOL: &str = "v1.bsatn.spacetimedb"; + +pub trait RowListLen { + /// Returns the length, in number of rows, not bytes, of the row list. + fn len(&self) -> usize; + /// Returns whether the list is empty or not. + fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl> RowListLen for L { + fn len(&self) -> usize { + self.deref().len() + } + fn is_empty(&self) -> bool { + self.deref().is_empty() + } +} + +pub trait ByteListLen { + /// Returns the uncompressed size of the list in bytes + fn num_bytes(&self) -> usize; +} + +impl ByteListLen for Vec { + fn num_bytes(&self) -> usize { + self.iter().map(|str| str.len()).sum() + } +} + +/// A format / codec used by the websocket API. +/// +/// This can be e.g., BSATN, JSON. +pub trait WebsocketFormat: Sized { + /// The type used for the encoding of a single item. + type Single: SpacetimeType + for<'de> Deserialize<'de> + Serialize + Debug + Clone; + + /// The type used for the encoding of a list of items. + type List: SpacetimeType + + for<'de> Deserialize<'de> + + Serialize + + RowListLen + + ByteListLen + + Debug + + Clone + + Default; + + /// The type used to encode query updates. + /// This type exists so that some formats, e.g., BSATN, can compress an update. + type QueryUpdate: SpacetimeType + for<'de> Deserialize<'de> + Serialize + Debug + Clone + Send; +} + +/// Messages sent from the client to the server. +/// +/// Parametric over the reducer argument type to enable [`ClientMessage::map_args`]. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub enum ClientMessage { + /// Request a reducer run. + CallReducer(CallReducer), + /// Register SQL queries on which to receive updates. + Subscribe(Subscribe), + /// Send a one-off SQL query without establishing a subscription. + OneOffQuery(OneOffQuery), + /// Register a SQL query to to subscribe to updates. This does not affect other subscriptions. + SubscribeSingle(SubscribeSingle), + SubscribeMulti(SubscribeMulti), + /// Remove a subscription to a SQL query that was added with SubscribeSingle. + Unsubscribe(Unsubscribe), + UnsubscribeMulti(UnsubscribeMulti), + /// Request a procedure run. + CallProcedure(CallProcedure), +} + +impl ClientMessage { + pub fn map_args(self, f: impl FnOnce(Args) -> Args2) -> ClientMessage { + match self { + ClientMessage::CallReducer(CallReducer { + reducer, + args, + request_id, + flags, + }) => ClientMessage::CallReducer(CallReducer { + reducer, + args: f(args), + request_id, + flags, + }), + ClientMessage::OneOffQuery(x) => ClientMessage::OneOffQuery(x), + ClientMessage::SubscribeSingle(x) => ClientMessage::SubscribeSingle(x), + ClientMessage::Unsubscribe(x) => ClientMessage::Unsubscribe(x), + ClientMessage::Subscribe(x) => ClientMessage::Subscribe(x), + ClientMessage::SubscribeMulti(x) => ClientMessage::SubscribeMulti(x), + ClientMessage::UnsubscribeMulti(x) => ClientMessage::UnsubscribeMulti(x), + ClientMessage::CallProcedure(CallProcedure { + procedure, + args, + request_id, + flags, + }) => ClientMessage::CallProcedure(CallProcedure { + procedure, + args: f(args), + request_id, + flags, + }), + } + } +} + +/// Request a reducer run. +/// +/// Parametric over the argument type to enable [`ClientMessage::map_args`]. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct CallReducer { + /// The name of the reducer to call. + pub reducer: Box, + /// The arguments to the reducer. + /// + /// In the wire format, this will be a [`Bytes`], BSATN or JSON encoded according to the reducer's argument schema + /// and the enclosing message format. + pub args: Args, + /// An identifier for a client request. + /// + /// The server will include the same ID in the response [`TransactionUpdate`]. + pub request_id: u32, + /// Assorted flags that can be passed when calling a reducer. + /// + /// Currently accepts 0 or 1 where the latter means + /// that the caller does not want to be notified about the reducer + /// without being subscribed to any relevant queries. + pub flags: CallReducerFlags, +} + +/// Sent by client to database to register a set of queries, about which the client will +/// receive `TransactionUpdate`s. +/// +/// After issuing a `Subscribe` message, the client will receive a single +/// `SubscriptionUpdate` message containing every current row of every table which matches +/// the subscribed queries. Then, after each reducer run which updates one or more +/// subscribed rows, the client will receive a `TransactionUpdate` containing the updates. +/// +/// A `Subscribe` message sets or replaces the entire set of queries to which the client +/// is subscribed. If the client is previously subscribed to some set of queries `A`, and +/// then sends a `Subscribe` message to subscribe to a set `B`, afterwards, the client +/// will be subscribed to `B` but not `A`. In this case, the client will receive a +/// `SubscriptionUpdate` containing every existing row that matches `B`, even if some were +/// already in `A`. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct Subscribe { + /// A sequence of SQL queries. + pub query_strings: Box<[Box]>, + pub request_id: u32, +} + +/// Sent by client to register a subscription to single query, for which the client should receive +/// receive relevant `TransactionUpdate`s. +/// +/// After issuing a `SubscribeSingle` message, the client will receive a single +/// `SubscribeApplied` message containing every current row which matches the query. Then, any +/// time a reducer updates the query's results, the client will receive a `TransactionUpdate` +/// containing the relevant updates. +/// +/// If a client subscribes to queries with overlapping results, the client will receive +/// multiple copies of rows that appear in multiple queries. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeSingle { + /// A single SQL `SELECT` query to subscribe to. + pub query: Box, + /// An identifier for a client request. + pub request_id: u32, + + /// An identifier for this subscription, which should not be used for any other subscriptions on the same connection. + /// This is used to refer to this subscription in Unsubscribe messages from the client and errors sent from the server. + /// These only have meaning given a ConnectionId. + pub query_id: QueryId, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeMulti { + /// A single SQL `SELECT` query to subscribe to. + pub query_strings: Box<[Box]>, + /// An identifier for a client request. + pub request_id: u32, + + /// An identifier for this subscription, which should not be used for any other subscriptions on the same connection. + /// This is used to refer to this subscription in Unsubscribe messages from the client and errors sent from the server. + /// These only have meaning given a ConnectionId. + pub query_id: QueryId, +} + +/// Client request for removing a query from a subscription. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct Unsubscribe { + /// An identifier for a client request. + pub request_id: u32, + + /// The ID used in the corresponding `SubscribeSingle` message. + pub query_id: QueryId, +} + +/// Client request for removing a query from a subscription. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct UnsubscribeMulti { + /// An identifier for a client request. + pub request_id: u32, + + /// The ID used in the corresponding `SubscribeSingle` message. + pub query_id: QueryId, +} + +/// A one-off query submission. +/// +/// Query should be a "SELECT * FROM Table WHERE ...". Other types of queries will be rejected. +/// Multiple such semicolon-delimited queries are allowed. +/// +/// One-off queries are identified by a client-generated messageID. +/// To avoid data leaks, the server will NOT cache responses to messages based on UUID! +/// It also will not check for duplicate IDs. They are just a way to match responses to messages. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct OneOffQuery { + pub message_id: Box<[u8]>, + pub query_string: Box, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +/// Request a procedure run. +/// +/// Parametric over the argument type to enable [`ClientMessage::map_args`]. +pub struct CallProcedure { + /// The name of the procedure to call. + pub procedure: Box, + /// The arguments to the procedure. + /// + /// In the wire format, this will be a [`Bytes`], BSATN or JSON encoded according to the reducer's argument schema + /// and the enclosing message format. + pub args: Args, + /// An identifier for a client request. + /// + /// The server will include the same ID in the response [`ProcedureResult`]. + pub request_id: u32, + /// Reserved space for future extensions. + pub flags: CallProcedureFlags, +} + +/// The tag recognized by the host and SDKs to mean no compression of a [`ServerMessage`]. +pub const SERVER_MSG_COMPRESSION_TAG_NONE: u8 = 0; + +/// The tag recognized by the host and SDKs to mean brotli compression of a [`ServerMessage`]. +pub const SERVER_MSG_COMPRESSION_TAG_BROTLI: u8 = 1; + +/// The tag recognized by the host and SDKs to mean brotli compression of a [`ServerMessage`]. +pub const SERVER_MSG_COMPRESSION_TAG_GZIP: u8 = 2; + +/// Messages sent from the server to the client. +#[derive(SpacetimeType, derive_more::From)] +#[sats(crate = spacetimedb_lib)] +pub enum ServerMessage { + /// Informs of changes to subscribed rows. + /// This will be removed when we switch to `SubscribeSingle`. + InitialSubscription(InitialSubscription), + /// Upon reducer run. + TransactionUpdate(TransactionUpdate), + /// Upon reducer run, but limited to just the table updates. + TransactionUpdateLight(TransactionUpdateLight), + /// After connecting, to inform client of its identity. + IdentityToken(IdentityToken), + /// Return results to a one off SQL query. + OneOffQueryResponse(OneOffQueryResponse), + /// Sent in response to a `SubscribeSingle` message. This contains the initial matching rows. + SubscribeApplied(SubscribeApplied), + /// Sent in response to an `Unsubscribe` message. This contains the matching rows. + UnsubscribeApplied(UnsubscribeApplied), + /// Communicate an error in the subscription lifecycle. + SubscriptionError(SubscriptionError), + /// Sent in response to a `SubscribeMulti` message. This contains the initial matching rows. + SubscribeMultiApplied(SubscribeMultiApplied), + /// Sent in response to an `UnsubscribeMulti` message. This contains the matching rows. + UnsubscribeMultiApplied(UnsubscribeMultiApplied), + /// Sent in response to a [`CallProcedure`] message. This contains the return value. + ProcedureResult(ProcedureResult), +} + +/// The matching rows of a subscription query. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeRows { + /// The table ID of the query. + pub table_id: TableId, + /// The table name of the query. + pub table_name: Box, + /// The BSATN row values. + pub table_rows: TableUpdate, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeApplied { + /// The request_id of the corresponding `SubscribeSingle` message. + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, + /// An identifier for the subscribed query sent by the client. + pub query_id: QueryId, + /// The matching rows for this query. + pub rows: SubscribeRows, +} + +/// Server response to a client [`Unsubscribe`] request. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct UnsubscribeApplied { + /// Provided by the client via the `Subscribe` message. + /// TODO: switch to subscription id? + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, + /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. + pub query_id: QueryId, + /// The matching rows for this query. + /// Note, this makes unsubscribing potentially very expensive. + /// To remove this in the future, we would need to send query_ids with rows in transaction updates, + /// and we would need clients to track which rows exist in which queries. + pub rows: SubscribeRows, +} + +/// Server response to an error at any point of the subscription lifecycle. +/// If this error doesn't have a request_id, the client should drop all subscriptions. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscriptionError { + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, + /// Provided by the client via a [`Subscribe`] or [`Unsubscribe`] message. + /// [`None`] if this occurred as the result of a [`TransactionUpdate`]. + pub request_id: Option, + /// Provided by the client via a [`Subscribe`] or [`Unsubscribe`] message. + /// [`None`] if this occurred as the result of a [`TransactionUpdate`]. + pub query_id: Option, + /// The return table of the query in question. + /// The server is not required to set this field. + /// It has been added to avoid a breaking change post 1.0. + /// + /// If unset, an error results in the entire subscription being dropped. + /// Otherwise only queries of this table type must be dropped. + pub table_id: Option, + /// An error message describing the failure. + /// + /// This should reference specific fragments of the query where applicable, + /// but should not include the full text of the query, + /// as the client can retrieve that from the `request_id`. + /// + /// This is intended for diagnostic purposes. + /// It need not have a predictable/parseable format. + pub error: Box, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeMultiApplied { + /// The request_id of the corresponding `SubscribeSingle` message. + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, + /// An identifier for the subscribed query sent by the client. + pub query_id: QueryId, + /// The matching rows for this query. + pub update: DatabaseUpdate, +} + +/// Server response to a client [`Unsubscribe`] request. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct UnsubscribeMultiApplied { + /// Provided by the client via the `Subscribe` message. + /// TODO: switch to subscription id? + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, + /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. + pub query_id: QueryId, + /// The matching rows for this query set. + /// Note, this makes unsubscribing potentially very expensive. + /// To remove this in the future, we would need to send query_ids with rows in transaction updates, + /// and we would need clients to track which rows exist in which queries. + pub update: DatabaseUpdate, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscriptionUpdate { + /// A [`DatabaseUpdate`] containing only inserts, the rows which match the subscription queries. + pub database_update: DatabaseUpdate, + /// An identifier sent by the client in requests. + /// The server will include the same request_id in the response. + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration_micros: u64, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct InitialSubscription { + /// A [`DatabaseUpdate`] containing only inserts, the rows which match the subscription queries. + pub database_update: DatabaseUpdate, + /// An identifier sent by the client in requests. + /// The server will include the same request_id in the response. + pub request_id: u32, + /// The overall time between the server receiving a request and sending the response. + pub total_host_execution_duration: TimeDuration, +} + +/// Received by database from client to inform of user's identity, token and client connection id. +/// +/// The database will always send an `IdentityToken` message +/// as the first message for a new WebSocket connection. +/// If the client is re-connecting with existing credentials, +/// the message will include those credentials. +/// If the client connected anonymously, +/// the database will generate new credentials to identify it. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct IdentityToken { + pub identity: Identity, + pub token: Box, + pub connection_id: ConnectionId, +} + +/// Received by client from database upon a reducer run. +/// +/// Clients receive `TransactionUpdate`s only for reducers +/// which update at least one of their subscribed rows, +/// or for their own `Failed` or `OutOfEnergy` reducer invocations. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct TransactionUpdate { + /// The status of the transaction. Contains the updated rows, if successful. + pub status: UpdateStatus, + /// The time when the reducer started. + /// + /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. + pub timestamp: Timestamp, + /// The identity of the user who requested the reducer run. For event-driven and + /// scheduled reducers, it is the identity of the database owner. + pub caller_identity: Identity, + + /// The 16-byte [`ConnectionId`] of the user who requested the reducer run. + /// + /// The all-zeros id is a sentinel which denotes no meaningful value. + /// This can occur in the following situations: + /// - `init` and `update` reducers will have a `caller_connection_id` + /// if and only if one was provided to the `publish` HTTP endpoint. + /// - Scheduled reducers will never have a `caller_connection_id`. + /// - Reducers invoked by WebSocket or the HTTP API will always have a `caller_connection_id`. + pub caller_connection_id: ConnectionId, + /// The original CallReducer request that triggered this reducer. + pub reducer_call: ReducerCallInfo, + /// The amount of energy credits consumed by running the reducer. + pub energy_quanta_used: EnergyQuanta, + /// How long the reducer took to run. + pub total_host_execution_duration: TimeDuration, +} + +/// Received by client from database upon a reducer run. +/// +/// Clients receive `TransactionUpdateLight`s only for reducers +/// which update at least one of their subscribed rows. +/// Failed reducers result in full [`TransactionUpdate`]s +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct TransactionUpdateLight { + /// An identifier for a client request + pub request_id: u32, + + /// The reducer ran successfully and its changes were committed to the database. + /// The rows altered in the database/ are recorded in this `DatabaseUpdate`. + pub update: DatabaseUpdate, +} + +/// Contained in a [`TransactionUpdate`], metadata about a reducer invocation. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct ReducerCallInfo { + /// The name of the reducer that was called. + /// + /// NOTE(centril, 1.0): For bandwidth resource constrained clients + /// this can encourage them to have poor naming of reducers like `a`. + /// We should consider not sending this at all and instead + /// having a startup message where the name <-> id bindings + /// are established between the host and the client. + pub reducer_name: Box, + /// The numerical id of the reducer that was called. + pub reducer_id: u32, + /// The arguments to the reducer, encoded as BSATN or JSON according to the reducer's argument schema + /// and the client's requested protocol. + pub args: F::Single, + /// An identifier for a client request + pub request_id: u32, +} + +/// The status of a [`TransactionUpdate`]. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub enum UpdateStatus { + /// The reducer ran successfully and its changes were committed to the database. + /// The rows altered in the database/ will be recorded in the `DatabaseUpdate`. + Committed(DatabaseUpdate), + /// The reducer errored, and any changes it attempted to were rolled back. + /// This is the error message. + Failed(Box), + /// The reducer was interrupted due to insufficient energy/funds, + /// and any changes it attempted to make were rolled back. + OutOfEnergy, +} + +/// A collection of inserted and deleted rows, contained in a [`TransactionUpdate`] or [`SubscriptionUpdate`]. +#[derive(SpacetimeType, Debug, Clone, Default)] +#[sats(crate = spacetimedb_lib)] +pub struct DatabaseUpdate { + pub tables: Vec>, +} + +impl DatabaseUpdate { + pub fn is_empty(&self) -> bool { + self.tables.is_empty() + } + + pub fn num_rows(&self) -> usize { + self.tables.iter().map(|t| t.num_rows()).sum() + } +} + +impl FromIterator> for DatabaseUpdate { + fn from_iter>>(iter: T) -> Self { + DatabaseUpdate { + tables: iter.into_iter().collect(), + } + } +} + +/// Part of a [`DatabaseUpdate`] received by client from database for alterations to a single table. +/// +/// NOTE(centril): in 0.12 we added `num_rows` and `table_name` to the struct. +/// These inflate the size of messages, which for some customers is the wrong default. +/// We might want to consider `v1.spacetimedb.bsatn.lightweight` +#[derive(SpacetimeType, Debug, Clone)] +#[sats(crate = spacetimedb_lib)] +pub struct TableUpdate { + /// The id of the table. Clients should prefer `table_name`, as it is a stable part of a module's API, + /// whereas `table_id` may change between runs. + pub table_id: TableId, + /// The name of the table. + /// + /// NOTE(centril, 1.0): we might want to remove this and instead + /// tell clients about changes to table_name <-> table_id mappings. + pub table_name: Box, + /// The sum total of rows in `self.updates`, + pub num_rows: u64, + /// The actual insert and delete updates for this table. + pub updates: SmallVec<[F::QueryUpdate; 1]>, +} + +/// Computed update for a single query, annotated with the number of matching rows. +#[derive(Debug)] +pub struct SingleQueryUpdate { + pub update: F::QueryUpdate, + pub num_rows: u64, +} + +impl TableUpdate { + pub fn new(table_id: TableId, table_name: Box, update: SingleQueryUpdate) -> Self { + Self { + table_id, + table_name, + num_rows: update.num_rows, + updates: [update.update].into(), + } + } + + pub fn empty(table_id: TableId, table_name: Box) -> Self { + Self { + table_id, + table_name, + num_rows: 0, + updates: SmallVec::new(), + } + } + + pub fn push(&mut self, update: SingleQueryUpdate) { + self.updates.push(update.update); + self.num_rows += update.num_rows; + } + + pub fn num_rows(&self) -> usize { + self.num_rows as usize + } +} + +#[derive(SpacetimeType, Debug, Clone, EnumAsInner)] +#[sats(crate = spacetimedb_lib)] +pub enum CompressableQueryUpdate { + Uncompressed(QueryUpdate), + Brotli(Bytes), + Gzip(Bytes), +} + +#[derive(SpacetimeType, Debug, Clone)] +#[sats(crate = spacetimedb_lib)] +pub struct QueryUpdate { + /// When in a [`TransactionUpdate`], the matching rows of this table deleted by the transaction. + /// + /// Rows are encoded as BSATN or JSON according to the table's schema + /// and the client's requested protocol. + /// + /// Always empty when in an [`InitialSubscription`]. + pub deletes: F::List, + /// When in a [`TransactionUpdate`], the matching rows of this table inserted by the transaction. + /// When in an [`InitialSubscription`], the matching rows of this table in the entire committed state. + /// + /// Rows are encoded as BSATN or JSON according to the table's schema + /// and the client's requested protocol. + pub inserts: F::List, +} + +/// A response to a [`OneOffQuery`]. +/// Will contain either one error or some number of response rows. +/// At most one of these messages will be sent in reply to any query. +/// +/// The messageId will be identical to the one sent in the original query. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct OneOffQueryResponse { + pub message_id: Box<[u8]>, + /// If query compilation or evaluation errored, an error message. + pub error: Option>, + + /// If query compilation and evaluation succeeded, a set of resulting rows, grouped by table. + pub tables: Box<[OneOffTable]>, + + /// The total duration of query compilation and evaluation on the server, in microseconds. + pub total_host_execution_duration: TimeDuration, +} + +/// A table included as part of a [`OneOffQueryResponse`]. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct OneOffTable { + /// The name of the table. + pub table_name: Box, + /// The set of rows which matched the query, encoded as BSATN or JSON according to the table's schema + /// and the client's requested protocol. + /// + /// TODO(centril, 1.0): Evaluate whether we want to conditionally compress these. + pub rows: F::List, +} + +/// The result of running a procedure, +/// including the return value of the procedure on success. +/// +/// Sent in response to a [`CallProcedure`] message. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct ProcedureResult { + /// The status of the procedure run. + /// + /// Contains the return value if successful, or the error message if not. + pub status: ProcedureStatus, + /// The time when the reducer started. + /// + /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. + pub timestamp: Timestamp, + /// The time the procedure took to run. + pub total_host_execution_duration: TimeDuration, + /// The same same client-provided identifier as in the original [`ProcedureCall`] request. + /// + /// Clients use this to correlate the response with the original request. + pub request_id: u32, +} + +/// The status of a procedure call, +/// including the return value on success. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub enum ProcedureStatus { + /// The procedure ran and returned the enclosed value. + /// + /// All user error handling happens within here; + /// the returned value may be a `Result` or `Option`, + /// or any other type to which the user may ascribe arbitrary meaning. + Returned(F::Single), + /// The reducer was interrupted due to insufficient energy/funds. + /// + /// The procedure may have performed some observable side effects before being interrupted. + OutOfEnergy, + /// The call failed in the host, e.g. due to a type error or unknown procedure name. + InternalError(String), +} + +/// Used whenever different formats need to coexist. +#[derive(Debug, Clone)] +pub enum FormatSwitch { + Bsatn(B), + Json(J), +} + +impl FormatSwitch { + /// Zips together two switches. + pub fn zip_mut(&mut self, other: FormatSwitch) -> FormatSwitch<(&mut B1, B2), (&mut J1, J2)> { + match (self, other) { + (FormatSwitch::Bsatn(a), FormatSwitch::Bsatn(b)) => FormatSwitch::Bsatn((a, b)), + (FormatSwitch::Json(a), FormatSwitch::Json(b)) => FormatSwitch::Json((a, b)), + _ => panic!("format should be the same for both sides of the zip"), + } + } +} + +#[derive(Clone, Copy, Default, Debug, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct JsonFormat; + +impl WebsocketFormat for JsonFormat { + type Single = ByteString; + type List = Vec; + type QueryUpdate = QueryUpdate; +} + +#[derive(Clone, Copy, Default, Debug, SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct BsatnFormat; + +impl WebsocketFormat for BsatnFormat { + type Single = Box<[u8]>; + type List = BsatnRowList; + type QueryUpdate = CompressableQueryUpdate; +} + +/// A specification of either a desired or decided compression algorithm. +#[derive(serde::Deserialize, Default, PartialEq, Eq, Clone, Copy, Hash, Debug)] +pub enum Compression { + /// No compression ever. + None, + /// Compress using brotli if a certain size threshold was met. + #[default] + Brotli, + /// Compress using gzip if a certain size threshold was met. + Gzip, +} + +pub type RowSize = u16; +pub type RowOffset = u64; + +/// A packed list of BSATN-encoded rows. +#[derive(SpacetimeType, Debug, Clone, Default)] +#[sats(crate = spacetimedb_lib)] +pub struct BsatnRowList { + /// A size hint about `rows_data` + /// intended to facilitate parallel decode purposes on large initial updates. + size_hint: RowSizeHint, + /// The flattened byte array for a list of rows. + rows_data: Bytes, +} + +impl BsatnRowList { + /// Returns a new row list where `rows_data` is the flattened byte array + /// containing the BSATN of each row, without any markers for where a row begins and end. + /// + /// The `size_hint` encodes the boundaries of each row in `rows_data`. + /// See [`RowSizeHint`] for more details on the encoding. + pub fn new(size_hint: RowSizeHint, rows_data: Bytes) -> Self { + Self { size_hint, rows_data } + } +} + +/// NOTE(centril, 1.0): We might want to add a `None` variant to this +/// where the client has to decode in a loop until `rows_data` has been exhausted. +/// The use-case for this is clients who are bandwidth limited and where every byte counts. +#[derive(SpacetimeType, Debug, Clone)] +#[sats(crate = spacetimedb_lib)] +pub enum RowSizeHint { + /// Each row in `rows_data` is of the same fixed size as specified here. + FixedSize(RowSize), + /// The offsets into `rows_data` defining the boundaries of each row. + /// Only stores the offset to the start of each row. + /// The ends of each row is inferred from the start of the next row, or `rows_data.len()`. + /// The behavior of this is identical to that of `PackedStr`. + RowOffsets(Arc<[RowOffset]>), +} + +impl Default for RowSizeHint { + fn default() -> Self { + Self::RowOffsets([].into()) + } +} + +impl RowSizeHint { + fn index_to_range(&self, index: usize, data_end: usize) -> Option> { + match self { + Self::FixedSize(size) => { + let size = *size as usize; + let start = index * size; + if start >= data_end { + // We've reached beyond `data_end`, + // so this is a row that doesn't exist, so we are beyond the count. + return None; + } + let end = (index + 1) * size; + Some(start..end) + } + Self::RowOffsets(offsets) => { + let offsets = offsets.as_ref(); + let start = *offsets.get(index)? as usize; + // The end is either the start of the next element or the end. + let end = offsets.get(index + 1).map(|e| *e as usize).unwrap_or(data_end); + Some(start..end) + } + } + } +} + +impl RowListLen for BsatnRowList { + fn len(&self) -> usize { + match &self.size_hint { + // `size != 0` is always the case for `FixedSize`. + RowSizeHint::FixedSize(size) => self.rows_data.as_ref().len() / *size as usize, + RowSizeHint::RowOffsets(offsets) => offsets.as_ref().len(), + } + } +} + +impl ByteListLen for BsatnRowList { + /// Returns the uncompressed size of the list in bytes + fn num_bytes(&self) -> usize { + self.rows_data.as_ref().len() + } +} + +impl BsatnRowList { + /// Returns the element at `index` in the list. + pub fn get(&self, index: usize) -> Option { + let data_end = self.rows_data.len(); + let data_range = self.size_hint.index_to_range(index, data_end)?; + Some(self.rows_data.slice(data_range)) + } + + /// Consumes the list and returns the parts. + pub fn into_inner(self) -> (RowSizeHint, Bytes) { + (self.size_hint, self.rows_data) + } +} + +/// An iterator over all the elements in a [`BsatnRowList`]. +pub struct BsatnRowListIter<'a> { + list: &'a BsatnRowList, + index: usize, +} + +impl<'a> IntoIterator for &'a BsatnRowList { + type IntoIter = BsatnRowListIter<'a>; + type Item = Bytes; + fn into_iter(self) -> Self::IntoIter { + BsatnRowListIter { list: self, index: 0 } + } +} + +impl Iterator for BsatnRowListIter<'_> { + type Item = Bytes; + fn next(&mut self) -> Option { + let index = self.index; + self.index += 1; + self.list.get(index) + } +} diff --git a/crates/client-api-messages/src/websocket/v2.rs b/crates/client-api-messages/src/websocket/v2.rs new file mode 100644 index 00000000000..5d66c22eb2a --- /dev/null +++ b/crates/client-api-messages/src/websocket/v2.rs @@ -0,0 +1,294 @@ +pub use super::common::{CallProcedureFlags, CallReducerFlags, QuerySetId}; +use bytes::Bytes; +use spacetimedb_lib::{ConnectionId, Identity, Timestamp}; +pub use spacetimedb_sats::SpacetimeType; + +pub const BIN_PROTOCOL: &str = "v2.bsatn.spacetimedb"; + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub enum ClientMessage { + Subscribe(Subscribe), + Unsubscribe(Unsubscribe), + OneOffQuery(OneOffQuery), + CallReducer(CallReducer), + CallProcedure(CallProcedure), +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct Subscribe { + /// An identifier for a client request. + pub request_id: u32, + + /// An identifier for this subscription, + /// which should not be used for any other subscriptions on the same connection. + /// + /// This is used to refer to this subscription in [`Unsubscribe`] messages from the client + /// and in various responses from the server. + /// These only have meaning given a [`ConnectionId`]; they are not global. + pub query_set_id: QuerySetId, + + /// A set of queries to subscribe to, each a single SQL `SELECT` statement. + pub query_strings: Box<[Box]>, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct Unsubscribe { + /// An identifier for a client request. + pub request_id: u32, + + /// The ID used in the corresponding `Single` message. + pub query_set_id: QuerySetId, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct OneOffQuery { + /// An identifier for a client request. + pub request_id: u32, + + /// A single SQL `SELECT` statement. + pub query_string: Box, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct CallReducer { + /// An identifier for a client request. + pub request_id: u32, + + /// Assorted flags that can be passed when calling a reducer. + /// + /// Currently accepts 0 or 1 where the latter means + /// that the caller does not want to be notified about the reducer + /// without being subscribed to any relevant queries. + pub flags: CallReducerFlags, + + /// The name of the reducer to call. + pub reducer: Box, + + /// The arguments to the reducer. + /// + /// A BSATN-encoded [`ProductValue`] which meets the reducer's argument schema. + pub args: Bytes, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct CallProcedure { + /// An identifier for a client request. + pub request_id: u32, + + /// Reserved 0. + pub flags: CallProcedureFlags, + + /// The name of the procedure to call. + pub procedure: Box, + + /// The arguments to the procedure. + /// + /// A BSATN-encoded [`ProductValue`] which meets the procedure's argument schema. + pub args: Bytes, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub enum ServerMessage { + InitialConnection(InitialConnection), + SubscribeApplied(SubscribeApplied), + UnsubscribeApplied(UnsubscribeApplied), + SubscriptionError(SubscriptionError), + TransactionUpdate(TransactionUpdate), + OneOffQueryResult(OneOffQueryResult), + ReducerResult(ReducerResult), + ProcedureResult(ProcedureResult), +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct InitialConnection { + pub identity: Identity, + pub connection_id: ConnectionId, + pub token: Box, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscribeApplied { + /// The request_id of the corresponding `SubscribeSingle` message. + pub request_id: u32, + /// An identifier for the subscribed query sent by the client. + pub query_set_id: QuerySetId, + /// The matching rows for this query. + pub rows: QueryRows, +} + +/// Server response to a client [`Unsubscribe`] request. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct UnsubscribeApplied { + /// Provided by the client via the `Subscribe` message. + /// TODO: switch to subscription id? + pub request_id: u32, + /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. + pub query_set_id: QuerySetId, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct QueryRows { + pub tables: Box<[SingleTableRows]>, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SingleTableRows { + pub table: Box, + pub rows: Box<[Bytes]>, +} + +/// Server response to an error at any point of the subscription lifecycle. +/// If this error doesn't have a request_id, the client should drop all subscriptions. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscriptionError { + /// Provided by the client via a [`Subscribe`] message. + /// [`None`] if this occurred as the result of a [`TransactionUpdate`]. + pub request_id: Option, + /// Provided by the client via a [`Subscribe`] message. + /// + /// After receiving this message, the client should drop all its rows from this [`QuerySetId`], + /// and should not expect to receive any additional updates for that query set. + pub query_set_id: QuerySetId, + /// An error message describing the failure. + /// + /// This should reference specific fragments of the query where applicable, + /// but should not include the full text of the query, + /// as the client can retrieve that from the `request_id` or `query_set_id`. + /// + /// This is intended for diagnostic purposes. + /// It need not have a predictable/parseable format. + pub error: Box, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct TransactionUpdate { + // TODO: Do we want a timestamp here? Or should we just tell users to emit an event with a timestamp if they want that. + pub query_sets: Box<[QuerySetUpdate]>, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct QuerySetUpdate { + pub query_set_id: QuerySetId, + pub tables: Box<[TableUpdate]>, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct TableUpdate { + pub table_name: Box, + pub rows: TableUpdateRows, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub enum TableUpdateRows { + PersistentTable(PersistentTableRows), + EventTable(EventTableRows), +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct PersistentTableRows { + pub inserts: Box<[Bytes]>, + pub deletes: Box<[Bytes]>, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct EventTableRows { + pub events: Box<[Bytes]>, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct OneOffQueryResult { + /// The request_id of the corresponding `SubscribeSingle` message. + pub request_id: u32, + /// The matching rows for this query, or an error message if computation failed. + /// + /// This error message should follow the same format as [`SubscriptionError::error`]. + pub result: Result>, +} + +/// Response to [`Subscribe`] containing the initial matching rows. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct ReducerResult { + /// The request_id of the corresponding `SubscribeSingle` message. + pub request_id: u32, + /// The time when the reducer started. + /// + /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. + pub timestamp: Timestamp, + pub result: ReducerOutcome, +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub enum ReducerOutcome { + Ok(SubscriptionOk), + Err(Bytes), + InternalError(Box), +} + +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct SubscriptionOk { + pub ret_value: Bytes, + pub transaction_update: TransactionUpdate, +} + +/// The result of running a procedure, +/// including the return value of the procedure on success. +/// +/// Sent in response to a [`CallProcedure`] message. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub struct ProcedureResult { + /// The status of the procedure run. + /// + /// Contains the return value if successful, or the error message if not. + pub status: ProcedureStatus, + /// The time when the reducer started. + /// + /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. + pub timestamp: Timestamp, + /// The time the procedure took to run. + pub total_host_execution_duration: TimeDuration, + /// The same same client-provided identifier as in the original [`ProcedureCall`] request. + /// + /// Clients use this to correlate the response with the original request. + pub request_id: u32, +} + +/// The status of a procedure call, +/// including the return value on success. +#[derive(SpacetimeType, Debug)] +#[sats(crate = spacetimedb_lib)] +pub enum ProcedureStatus { + /// The procedure ran and returned the enclosed value. + /// + /// All user error handling happens within here; + /// the returned value may be a `Result` or `Option`, + /// or any other type to which the user may ascribe arbitrary meaning. + Returned(Bytes), + /// The call failed in the host, e.g. due to a type error or unknown procedure name. + InternalError(Box), +} From 7a1258a38503e95ffd065c41dca91eca3255948b Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Wed, 14 Jan 2026 11:18:33 -0500 Subject: [PATCH 2/4] Fix some copy-paste typos --- crates/client-api-messages/src/websocket/v2.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/client-api-messages/src/websocket/v2.rs b/crates/client-api-messages/src/websocket/v2.rs index 5d66c22eb2a..cfcdd54c599 100644 --- a/crates/client-api-messages/src/websocket/v2.rs +++ b/crates/client-api-messages/src/websocket/v2.rs @@ -1,6 +1,6 @@ pub use super::common::{CallProcedureFlags, CallReducerFlags, QuerySetId}; use bytes::Bytes; -use spacetimedb_lib::{ConnectionId, Identity, Timestamp}; +use spacetimedb_lib::{ConnectionId, Identity, TimeDuration, Timestamp}; pub use spacetimedb_sats::SpacetimeType; pub const BIN_PROTOCOL: &str = "v2.bsatn.spacetimedb"; @@ -261,11 +261,11 @@ pub struct SubscriptionOk { /// Sent in response to a [`CallProcedure`] message. #[derive(SpacetimeType, Debug)] #[sats(crate = spacetimedb_lib)] -pub struct ProcedureResult { +pub struct ProcedureResult { /// The status of the procedure run. /// /// Contains the return value if successful, or the error message if not. - pub status: ProcedureStatus, + pub status: ProcedureStatus, /// The time when the reducer started. /// /// Note that [`Timestamp`] serializes as `i64` nanoseconds since the Unix epoch. From 05478c8be5c337381e00d2cd7c10946083bf7762 Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 15 Jan 2026 12:37:27 -0500 Subject: [PATCH 3/4] Nix `NoSuccessNotify`, add doc comments --- .../src/websocket/common.rs | 25 --- .../client-api-messages/src/websocket/v1.rs | 34 +++- .../client-api-messages/src/websocket/v2.rs | 178 +++++++++++++++--- 3 files changed, 185 insertions(+), 52 deletions(-) diff --git a/crates/client-api-messages/src/websocket/common.rs b/crates/client-api-messages/src/websocket/common.rs index fdcd6305de9..8eb9d4f571d 100644 --- a/crates/client-api-messages/src/websocket/common.rs +++ b/crates/client-api-messages/src/websocket/common.rs @@ -14,31 +14,6 @@ impl QuerySetId { } } -#[derive(Clone, Copy, Default, PartialEq, Eq)] -pub enum CallReducerFlags { - /// The reducer's caller does want to be notified about the reducer completing successfully - /// regardless of whether the caller had subscribed to a relevant query. - /// - /// Note that updates to a reducer's caller are always sent as full updates - /// whether subscribed to a relevant query or not. - /// That is, the light tx mode setting does not apply to the reducer's caller. - /// - /// This is the default flag. - #[default] - FullUpdate, - /// The reducer's caller does not want to be notified about the reducer completing successfully - /// without having subscribed to any of the relevant queries. - NoSuccessNotify, -} - -impl_st!([] CallReducerFlags, AlgebraicType::U8); -impl_serialize!([] CallReducerFlags, (self, ser) => ser.serialize_u8(*self as u8)); -impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { - 0 => Ok(Self::FullUpdate), - 1 => Ok(Self::NoSuccessNotify), - x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), -}); - #[derive(Clone, Copy, Default, PartialEq, Eq)] pub enum CallProcedureFlags { #[default] diff --git a/crates/client-api-messages/src/websocket/v1.rs b/crates/client-api-messages/src/websocket/v1.rs index 55d7eca3b1c..d4ad698d19f 100644 --- a/crates/client-api-messages/src/websocket/v1.rs +++ b/crates/client-api-messages/src/websocket/v1.rs @@ -1,4 +1,4 @@ -pub use super::common::{CallProcedureFlags, CallReducerFlags, QuerySetId as QueryId}; +pub use super::common::{CallProcedureFlags, QuerySetId as QueryId}; use crate::energy::EnergyQuanta; use bytes::Bytes; use bytestring::ByteString; @@ -10,7 +10,12 @@ use enum_as_inner::EnumAsInner; use smallvec::SmallVec; use spacetimedb_lib::{ConnectionId, Identity, TimeDuration, Timestamp}; use spacetimedb_primitives::TableId; -use spacetimedb_sats::{de::Deserialize, ser::Serialize, SpacetimeType}; +use spacetimedb_sats::{ + de::{Deserialize, Error}, + impl_deserialize, impl_serialize, impl_st, + ser::Serialize, + AlgebraicType, SpacetimeType, +}; use std::sync::Arc; pub const TEXT_PROTOCOL: &str = "v1.json.spacetimedb"; @@ -149,6 +154,31 @@ pub struct CallReducer { pub flags: CallReducerFlags, } +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub enum CallReducerFlags { + /// The reducer's caller does want to be notified about the reducer completing successfully + /// regardless of whether the caller had subscribed to a relevant query. + /// + /// Note that updates to a reducer's caller are always sent as full updates + /// whether subscribed to a relevant query or not. + /// That is, the light tx mode setting does not apply to the reducer's caller. + /// + /// This is the default flag. + #[default] + FullUpdate, + /// The reducer's caller does not want to be notified about the reducer completing successfully + /// without having subscribed to any of the relevant queries. + NoSuccessNotify, +} + +impl_st!([] CallReducerFlags, AlgebraicType::U8); +impl_serialize!([] CallReducerFlags, (self, ser) => ser.serialize_u8(*self as u8)); +impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { + 0 => Ok(Self::FullUpdate), + 1 => Ok(Self::NoSuccessNotify), + x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), +}); + /// Sent by client to database to register a set of queries, about which the client will /// receive `TransactionUpdate`s. /// diff --git a/crates/client-api-messages/src/websocket/v2.rs b/crates/client-api-messages/src/websocket/v2.rs index cfcdd54c599..71e5278da9e 100644 --- a/crates/client-api-messages/src/websocket/v2.rs +++ b/crates/client-api-messages/src/websocket/v2.rs @@ -1,20 +1,51 @@ -pub use super::common::{CallProcedureFlags, CallReducerFlags, QuerySetId}; +pub use super::common::{CallProcedureFlags, QuerySetId}; use bytes::Bytes; use spacetimedb_lib::{ConnectionId, Identity, TimeDuration, Timestamp}; pub use spacetimedb_sats::SpacetimeType; +use spacetimedb_sats::{de::Error, impl_deserialize, impl_serialize, impl_st, AlgebraicType}; pub const BIN_PROTOCOL: &str = "v2.bsatn.spacetimedb"; +/// Messages sent by the client to the server. +/// +/// Each client message contains a `request_id`, a client-supplied integer ID. +/// The server assigns no meaning to this value, but encloses the same value in its response [`ServerMessage`]. +/// Clients can use `request_id`s to correlate requests and responses. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub enum ClientMessage { + /// Add a new set of subscribed queries to construct a local materialized view of matching rows. Subscribe(Subscribe), + /// Remove a previously-registered set of subscribed queries to stop receiving updates on its view. Unsubscribe(Unsubscribe), + /// Run a query once and receive its results at a single point in time, without real-time updates. OneOffQuery(OneOffQuery), + /// Invoke a reducer, a transactional non-side-effecting function which runs in the database. CallReducer(CallReducer), + /// Invoke a procedure, a non-transactional side-effecting function which runs in the database. CallProcedure(CallProcedure), } +/// Sent by client to register a subscription to a new query set +/// for which the client should receive [`QuerySetUpdate`]s in its [`TransactionUpdate`]s. +/// +/// Each subscribed query set is identified by a client-supplied [`QuerySetId`], +/// which should be unique within that client's connection. +/// The server will include that [`QuerySetId`] in updates with the matching rows, +/// and the client can later send that [`QuerySetId`] in an [`Unsubscribe`] message to end the subscription. +/// +/// If the enclosed queries are valid and compute successfully, +/// the server will respond with a [`SubscribeApplied`] message marked with the same `request_id` and [`QuerySetId`] +/// containing the initial matching rows, +/// and will then send matching inserts and deletes in [`QuerySetUpdate`]s enclosed in [`TransactionUpdate`] messages +/// as the changes occur. +/// +/// If the enclosed queries are invalid or fail to compute, the server will respond with a [`SubscriptionError`] message. +/// If the queries become invalid after an initial successful application, +/// the server may send a [`SubscribeApplied`], some number of [`TransactionUpdate`]s, and then a [`SubscriptionError`]. +/// After receiving a [`SubscriptionError`], the client should discard all previously-received rows for that [`QuerySetId`] +/// and should not expect to receive updates for it in the future. +/// That [`QuerySetId`] may then be re-used at the client's discretion. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct Subscribe { @@ -33,16 +64,27 @@ pub struct Subscribe { pub query_strings: Box<[Box]>, } +/// Sent by client to end a subscription which was previously added in a [`Subscribe`] message. +/// +/// After the server processes an unsubscribe message, it will send an [`UnsubscribeApplied`] as confirmation. +/// Following the [`UnsubscribeApplied`], the server will not reference the enclosed [`QuerySetId`] again, +/// and so it may be reused. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct Unsubscribe { /// An identifier for a client request. pub request_id: u32, - /// The ID used in the corresponding `Single` message. + /// The ID used in the corresponding [`Subscribe`] message. pub query_set_id: QuerySetId, } +/// Sent by the client to perform a query at a single point in time. +/// +/// Unlike subscriptions registered by [`Subscribe`], this query will not receive real-time updates. +/// +/// The server will respond with a [`OneOffQueryResponse`] message containing the same `request_id` +/// and the status of the query, either the matching rows or an error message. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct OneOffQuery { @@ -53,17 +95,17 @@ pub struct OneOffQuery { pub query_string: Box, } +/// Sent by the client to invoke a reducer, a transactional non-side-effecting database function. +/// +/// After the reducer runs, the server will respond with a [`CallReducerResult`] message containing the same `request_id` +/// and the status of the run, either the return value and [`TransactionUpdate`] or an error. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct CallReducer { /// An identifier for a client request. pub request_id: u32, - /// Assorted flags that can be passed when calling a reducer. - /// - /// Currently accepts 0 or 1 where the latter means - /// that the caller does not want to be notified about the reducer - /// without being subscribed to any relevant queries. + /// Reserved 0. pub flags: CallReducerFlags, /// The name of the reducer to call. @@ -75,6 +117,23 @@ pub struct CallReducer { pub args: Bytes, } +#[derive(Clone, Copy, Default, PartialEq, Eq)] +pub enum CallReducerFlags { + #[default] + Default, +} + +impl_st!([] CallReducerFlags, AlgebraicType::U8); +impl_serialize!([] CallReducerFlags, (self, ser) => ser.serialize_u8(*self as u8)); +impl_deserialize!([] CallReducerFlags, de => match de.deserialize_u8()? { + 0 => Ok(Self::Default), + x => Err(D::Error::custom(format_args!("invalid call reducer flag {x}"))), +}); + +/// Sent by the client to invoke a procedure, a non-transactional side-effecting database function. +/// +/// After the procedure runs, the server will respond with a [`CallProcedureResult`] message containing the same `request_id` +/// and the status of the run, either the return value or an error. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct CallProcedure { @@ -93,16 +152,33 @@ pub struct CallProcedure { pub args: Bytes, } +/// Messages sent by the server to the client in response to requests or database events. +/// +/// Server messages which are responses to client messages will contain a `request_id`. +/// This will take the same value as the client supplied in their request. +/// Clients can use `request_id`s to correlate requests and responses. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub enum ServerMessage { + /// The first message sent upon a successful connection. + /// Contains information about the client's identity and authentication. InitialConnection(InitialConnection), + /// In response to a [`Subscribe`] message, after a new query set has been added, containing its initial matching rows. SubscribeApplied(SubscribeApplied), + /// In response to an [`Unsubscribe`] message, confirming that a query set has been removed. UnsubscribeApplied(UnsubscribeApplied), + /// Notifies the client that a subscription to a query set has failed, either during initial application + /// or when computing a [`QuerySetUpdate`] for a [`TransactionUpdate`]. SubscriptionError(SubscriptionError), + /// Sent after the database runs a transaction, to notify the client of any changes to its subscribed query sets + /// in [`QuerySetUpdate`]s. TransactionUpdate(TransactionUpdate), + /// Sent in response to a [`OneOffQuery`] message, containing the matching rows or error message. OneOffQueryResult(OneOffQueryResult), + /// Sent in response to a [`CallReducer`] message, containing the reducer's exit status and, if it committed, + /// the [`TransactionUpdate`] for that reducer's transaction. ReducerResult(ReducerResult), + /// Sent in response to a [`CallProcedure`] message, containing the procedure's exit status. ProcedureResult(ProcedureResult), } @@ -115,34 +191,31 @@ pub struct InitialConnection { } /// Response to [`Subscribe`] containing the initial matching rows. +/// +/// This message's `request_id` and `query_set_id` will match those the client provided in the [`Subscribe`] message. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct SubscribeApplied { - /// The request_id of the corresponding `SubscribeSingle` message. + /// The request_id of the corresponding [`Subscribe`] message. pub request_id: u32, - /// An identifier for the subscribed query sent by the client. + /// An identifier for the subscribed query set provided by the client. pub query_set_id: QuerySetId, /// The matching rows for this query. pub rows: QueryRows, } -/// Server response to a client [`Unsubscribe`] request. -#[derive(SpacetimeType)] -#[sats(crate = spacetimedb_lib)] -pub struct UnsubscribeApplied { - /// Provided by the client via the `Subscribe` message. - /// TODO: switch to subscription id? - pub request_id: u32, - /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. - pub query_set_id: QuerySetId, -} - +/// Matching rows resident in tables at the time a query ran, +/// used in contexts where we're not sending insert/delete deltas, +/// like [`SubscribeApplied`]. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct QueryRows { pub tables: Box<[SingleTableRows]>, } +/// Matching rows resident in a table at the time a query ran, +/// used in contexts where we're not sending insert/delete deltas, +/// like the [`QueryRows`] of a [`SubscribeApplied`], and [`OneOffQueryResponse`]. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct SingleTableRows { @@ -150,8 +223,37 @@ pub struct SingleTableRows { pub rows: Box<[Bytes]>, } +/// Server response to a client [`Unsubscribe`] request. +/// +/// This message's `request_id` and `query_set_id` will match those the client provided in the [`Unsubscribe`] message. +/// +/// After receiving this message, the client will no longer receive any [`QuerySetUpdate`]s for the included [`QuerySetId`]. +/// That [`QuerySetId`] may then be re-used at the client's discretion. +#[derive(SpacetimeType)] +#[sats(crate = spacetimedb_lib)] +pub struct UnsubscribeApplied { + /// Provided by the client via the `Subscribe` message. + /// TODO: switch to subscription id? + pub request_id: u32, + /// The ID included in the `SubscribeApplied` and `Unsubscribe` messages. + pub query_set_id: QuerySetId, +} + /// Server response to an error at any point of the subscription lifecycle. -/// If this error doesn't have a request_id, the client should drop all subscriptions. +/// +/// If initial compilation or computation of a query fails, the server will send this message +/// in lieu of a [`SubscribeApplied`]. +/// In that case, the `request_id` will be `Some` and will match the one the client supplied in the [`Subscribe`] message. +/// +/// If a query fails after being applied, e.g. during recompilation or incremental evaluation, +/// the server will send this message with `request_id` set to `None`. +/// +/// In either case, this message will have its `query_set_id` set to the one provided by the client +/// to identify the failed query set. +/// After receiving this message, the client should consider the subscription to that query set to have ended, +/// should discard all previously-received matching rows, +/// and should not expect to receive any further [`QuerySetUpdate`]s for that [`QuerySetId`]. +/// That [`QuerySetId`] may then be re-used at the client's discretion. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct SubscriptionError { @@ -174,10 +276,18 @@ pub struct SubscriptionError { pub error: Box, } +/// Sent by the server to the client after a transaction runs and commits successfully in the database, +/// containing [`QuerySetUpdate`]s for each of the client's subscribed query sets +/// whose results were affected by the transaction. +/// +/// If a transaction does not affect a particular query set, +/// the transaction update will not contain a [`QuerySetUpdate`] for that set. +/// +/// If none of a client's query sets were affected by a transaction, +/// they will not receive an empty [`TransactionUpdate`]. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct TransactionUpdate { - // TODO: Do we want a timestamp here? Or should we just tell users to emit an event with a timestamp if they want that. pub query_sets: Box<[QuerySetUpdate]>, } @@ -227,7 +337,8 @@ pub struct OneOffQueryResult { pub result: Result>, } -/// Response to [`Subscribe`] containing the initial matching rows. +/// The result of running a reducer, including its return value and [`TransactionUpdate`] on success, +/// or its error on failure. #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub struct ReducerResult { @@ -243,14 +354,31 @@ pub struct ReducerResult { #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] pub enum ReducerOutcome { - Ok(SubscriptionOk), + /// The reducer returned successfully and its transaction committed. + /// The return value and [`TransactionUpdate`] are included here. + Ok(ReducerOk), + /// The reducer returned successfully and its transaction committed, + /// but its return value was zero bytes and its [`TransactionUpdate`] contained zero [`QuerySetUpdate`]s. + /// + /// This variant is an optimization which saves 8 bytes of wire size, + /// due to the BSATN format's using 4 bytes for the length of a variable-length object, + /// such as the `ret_value` of [`ReducerOk`] and the `query_sets` of [`TransactionUpdate`]. + Okmpty, + /// The reducer returned an expected, structured error, + /// and its transaction did not commit. + /// + /// The payload is a BSATN-encoded value of the reducer's error return type. Err(Bytes), + /// The reducer panicked, returned an unexpected and unstructured error, or failed to run due to a SpacetimeDB internal error. + /// + /// The payload is an error message, which is intended for diagnostic purposes only, + /// and is not intended to have a stable or parseable format. InternalError(Box), } #[derive(SpacetimeType)] #[sats(crate = spacetimedb_lib)] -pub struct SubscriptionOk { +pub struct ReducerOk { pub ret_value: Bytes, pub transaction_update: TransactionUpdate, } From 4a4b10b1a07357caa661355fa132204a01f9a50a Mon Sep 17 00:00:00 2001 From: Phoebe Goldman Date: Thu, 15 Jan 2026 12:37:46 -0500 Subject: [PATCH 4/4] Update references to v1 WS format --- Cargo.lock | 1 + crates/cli/src/subcommands/subscribe.rs | 35 ++-- crates/client-api/src/routes/subscribe.rs | 8 +- crates/core/src/client/client_connection.rs | 40 ++-- crates/core/src/client/consume_each_list.rs | 25 +-- crates/core/src/client/message_handlers.rs | 23 +-- crates/core/src/client/messages.rs | 190 +++++++++--------- crates/core/src/host/module_host.rs | 26 ++- crates/core/src/messages/mod.rs | 3 - .../core/src/subscription/execution_unit.rs | 13 +- crates/core/src/subscription/mod.rs | 28 ++- .../subscription/module_subscription_actor.rs | 110 +++++----- .../module_subscription_manager.rs | 101 +++++----- crates/core/src/subscription/query.rs | 6 +- .../src/subscription/row_list_builder_pool.rs | 8 +- crates/core/src/subscription/subscription.rs | 9 +- .../src/subscription/websocket_building.rs | 51 +++-- crates/testing/Cargo.toml | 1 + crates/testing/src/modules.rs | 4 +- sdks/rust/src/compression.rs | 21 +- sdks/rust/src/db_connection.rs | 65 +++--- sdks/rust/src/event.rs | 10 +- sdks/rust/src/lib.rs | 6 +- sdks/rust/src/spacetime_module.rs | 16 +- sdks/rust/src/subscription.rs | 16 +- sdks/rust/src/websocket.rs | 25 ++- 26 files changed, 422 insertions(+), 419 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21c31d26452..88caef7fe73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8278,6 +8278,7 @@ dependencies = [ "serial_test", "spacetimedb-cli", "spacetimedb-client-api", + "spacetimedb-client-api-messages", "spacetimedb-core", "spacetimedb-data-structures", "spacetimedb-lib 1.11.2", diff --git a/crates/cli/src/subcommands/subscribe.rs b/crates/cli/src/subcommands/subscribe.rs index 7c269f6ea64..a11e1084b43 100644 --- a/crates/cli/src/subcommands/subscribe.rs +++ b/crates/cli/src/subcommands/subscribe.rs @@ -4,7 +4,7 @@ use futures::{Sink, SinkExt, TryStream, TryStreamExt}; use http::header; use reqwest::Url; use serde_json::Value; -use spacetimedb_client_api_messages::websocket::{self as ws, JsonFormat}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_data_structures::map::HashMap; use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9; use spacetimedb_lib::de::serde::{DeserializeWrapper, SeedWrapper}; @@ -71,16 +71,16 @@ pub fn cli() -> clap::Command { .arg(common_args::server().help("The nickname, host name or URL of the server hosting the database")) } -fn parse_msg_json(msg: &WsMessage) -> Option> { +fn parse_msg_json(msg: &WsMessage) -> Option> { let WsMessage::Text(msg) = msg else { return None }; - serde_json::from_str::>>(msg) + serde_json::from_str::>>(msg) .inspect_err(|e| eprintln!("couldn't parse message from server: {e}")) .map(|wrapper| wrapper.0) .ok() } fn reformat_update<'a>( - msg: &'a ws::DatabaseUpdate, + msg: &'a ws_v1::DatabaseUpdate, schema: &RawModuleDefV9, ) -> anyhow::Result> { msg.tables @@ -152,7 +152,7 @@ pub async fn exec(config: Config, args: &ArgMatches) -> Result<(), anyhow::Error let mut req = url.into_client_request()?; req.headers_mut().insert( header::SEC_WEBSOCKET_PROTOCOL, - http::HeaderValue::from_static(ws::TEXT_PROTOCOL), + http::HeaderValue::from_static(ws_v1::TEXT_PROTOCOL), ); // Add the authorization header, if any. if let Some(auth_header) = api.con.auth_header.to_header() { @@ -241,8 +241,8 @@ async fn subscribe(ws: &mut S, query_strings: Box<[Box]>) -> Result<(), where S: Sink + Unpin, { - let msg = serde_json::to_string(&SerializeWrapper::new(ws::ClientMessage::<()>::Subscribe( - ws::Subscribe { + let msg = serde_json::to_string(&SerializeWrapper::new(ws_v1::ClientMessage::<()>::Subscribe( + ws_v1::Subscribe { query_strings, request_id: 0, }, @@ -262,22 +262,22 @@ where while let Some(msg) = ws.try_next().await.map_err(|source| Error::Websocket { source })? { let Some(msg) = parse_msg_json(&msg) else { continue }; match msg { - ws::ServerMessage::InitialSubscription(sub) => { + ws_v1::ServerMessage::InitialSubscription(sub) => { if let Some(module_def) = module_def { let output = format_output_json(&sub.database_update, module_def)?; tokio::io::stdout().write_all(output.as_bytes()).await? } break; } - ws::ServerMessage::TransactionUpdate(ws::TransactionUpdate { status, .. }) => { + ws_v1::ServerMessage::TransactionUpdate(ws_v1::TransactionUpdate { status, .. }) => { return Err(match status { - ws::UpdateStatus::Failed(msg) => Error::TransactionFailure { reason: msg }, + ws_v1::UpdateStatus::Failed(msg) => Error::TransactionFailure { reason: msg }, _ => Error::Protocol { details: RECV_TX_UPDATE, }, }) } - ws::ServerMessage::TransactionUpdateLight(ws::TransactionUpdateLight { .. }) => { + ws_v1::ServerMessage::TransactionUpdateLight(ws_v1::TransactionUpdateLight { .. }) => { return Err(Error::Protocol { details: RECV_TX_UPDATE, }) @@ -310,14 +310,14 @@ where let Some(msg) = parse_msg_json(&msg) else { continue }; match msg { - ws::ServerMessage::InitialSubscription(_) => { + ws_v1::ServerMessage::InitialSubscription(_) => { return Err(Error::Protocol { details: "received a second initial subscription update", }) } - ws::ServerMessage::TransactionUpdateLight(ws::TransactionUpdateLight { update, .. }) - | ws::ServerMessage::TransactionUpdate(ws::TransactionUpdate { - status: ws::UpdateStatus::Committed(update), + ws_v1::ServerMessage::TransactionUpdateLight(ws_v1::TransactionUpdateLight { update, .. }) + | ws_v1::ServerMessage::TransactionUpdate(ws_v1::TransactionUpdate { + status: ws_v1::UpdateStatus::Committed(update), .. }) => { let output = format_output_json(&update, module_def)?; @@ -329,7 +329,10 @@ where } } -fn format_output_json(msg: &ws::DatabaseUpdate, schema: &RawModuleDefV9) -> Result { +fn format_output_json( + msg: &ws_v1::DatabaseUpdate, + schema: &RawModuleDefV9, +) -> Result { let formatted = reformat_update(msg, schema).map_err(|source| Error::Reformat { source })?; let output = serde_json::to_string(&formatted)? + "\n"; diff --git a/crates/client-api/src/routes/subscribe.rs b/crates/client-api/src/routes/subscribe.rs index d769a8e3284..c5a3d169733 100644 --- a/crates/client-api/src/routes/subscribe.rs +++ b/crates/client-api/src/routes/subscribe.rs @@ -36,7 +36,7 @@ use spacetimedb::subscription::row_list_builder_pool::BsatnRowListBuilderPool; use spacetimedb::util::spawn_rayon; use spacetimedb::worker_metrics::WORKER_METRICS; use spacetimedb::Identity; -use spacetimedb_client_api_messages::websocket::{self as ws_api, Compression}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_datastore::execution_context::WorkloadType; use spacetimedb_lib::connection_id::{ConnectionId, ConnectionIdForUrl}; use tokio::sync::{mpsc, watch}; @@ -56,9 +56,9 @@ use crate::util::{NameOrIdentity, XForwardedFor}; use crate::{log_and_500, Authorization, ControlStateDelegate, NodeDelegate}; #[allow(clippy::declare_interior_mutable_const)] -pub const TEXT_PROTOCOL: HeaderValue = HeaderValue::from_static(ws_api::TEXT_PROTOCOL); +pub const TEXT_PROTOCOL: HeaderValue = HeaderValue::from_static(ws_v1::TEXT_PROTOCOL); #[allow(clippy::declare_interior_mutable_const)] -pub const BIN_PROTOCOL: HeaderValue = HeaderValue::from_static(ws_api::BIN_PROTOCOL); +pub const BIN_PROTOCOL: HeaderValue = HeaderValue::from_static(ws_v1::BIN_PROTOCOL); pub trait HasWebSocketOptions { fn websocket_options(&self) -> WebSocketOptions; @@ -79,7 +79,7 @@ pub struct SubscribeParams { pub struct SubscribeQueryParams { pub connection_id: Option, #[serde(default)] - pub compression: Compression, + pub compression: ws_v1::Compression, /// Whether we want "light" responses, tailored to network bandwidth constrained clients. /// This knob works by setting other, more specific, knobs to the value. #[serde(default)] diff --git a/crates/core/src/client/client_connection.rs b/crates/core/src/client/client_connection.rs index a2caef5bdc1..dab9b9037db 100644 --- a/crates/core/src/client/client_connection.rs +++ b/crates/core/src/client/client_connection.rs @@ -13,7 +13,6 @@ use crate::db::relational_db::RelationalDB; use crate::error::DBError; use crate::host::module_host::ClientConnectedError; use crate::host::{CallProcedureReturn, FunctionArgs, ModuleHost, NoSuchModule, ReducerCallError, ReducerCallResult}; -use crate::messages::websocket::Subscribe; use crate::subscription::module_subscription_manager::BroadcastError; use crate::subscription::row_list_builder_pool::JsonRowListBuilderFakePool; use crate::util::asyncify; @@ -25,10 +24,7 @@ use derive_more::From; use futures::prelude::*; use prometheus::{Histogram, IntCounter, IntGauge}; use spacetimedb_auth::identity::{ConnectionAuthCtx, SpacetimeIdentityClaims}; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, CallReducerFlags, Compression, FormatSwitch, JsonFormat, SubscribeMulti, SubscribeSingle, Unsubscribe, - UnsubscribeMulti, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_durability::{DurableOffset, TxOffset}; use spacetimedb_lib::identity::{AuthCtx, RequestId}; use spacetimedb_lib::metrics::ExecutionMetrics; @@ -52,9 +48,9 @@ impl Protocol { } } - pub(crate) fn assert_matches_format_switch(self, fs: &FormatSwitch) { + pub(crate) fn assert_matches_format_switch(self, fs: &ws_v1::FormatSwitch) { match (self, fs) { - (Protocol::Text, FormatSwitch::Json(_)) | (Protocol::Binary, FormatSwitch::Bsatn(_)) => {} + (Protocol::Text, ws_v1::FormatSwitch::Json(_)) | (Protocol::Binary, ws_v1::FormatSwitch::Bsatn(_)) => {} _ => unreachable!("requested protocol does not match output format"), } } @@ -65,7 +61,7 @@ pub struct ClientConfig { /// The client's desired protocol (format) when the host replies. pub protocol: Protocol, /// The client's desired (conditional) compression algorithm, if any. - pub compression: Compression, + pub compression: ws_v1::Compression, /// Whether the client prefers full [`TransactionUpdate`]s /// rather than [`TransactionUpdateLight`]s on a successful update. // TODO(centril): As more knobs are added, make this into a bitfield (when there's time). @@ -826,13 +822,13 @@ impl ClientConnection { args: FunctionArgs, request_id: RequestId, timer: Instant, - flags: CallReducerFlags, + flags: ws_v1::CallReducerFlags, ) -> Result { let caller = match flags { - CallReducerFlags::FullUpdate => Some(self.sender()), + ws_v1::CallReducerFlags::FullUpdate => Some(self.sender()), // Setting `sender = None` causes `eval_updates` to skip sending to the caller // as it has no access to the caller other than by id/connection id. - CallReducerFlags::NoSuccessNotify => None, + ws_v1::CallReducerFlags::NoSuccessNotify => None, }; self.module() @@ -875,7 +871,7 @@ impl ClientConnection { pub async fn subscribe_single( &self, - subscription: SubscribeSingle, + subscription: ws_v1::SubscribeSingle, timer: Instant, ) -> Result, DBError> { let me = self.clone(); @@ -889,7 +885,11 @@ impl ClientConnection { .await? } - pub async fn unsubscribe(&self, request: Unsubscribe, timer: Instant) -> Result, DBError> { + pub async fn unsubscribe( + &self, + request: ws_v1::Unsubscribe, + timer: Instant, + ) -> Result, DBError> { let me = self.clone(); asyncify(move || { me.module() @@ -901,7 +901,7 @@ impl ClientConnection { pub async fn subscribe_multi( &self, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, ) -> Result, DBError> { let me = self.clone(); @@ -917,7 +917,7 @@ impl ClientConnection { pub async fn unsubscribe_multi( &self, - request: UnsubscribeMulti, + request: ws_v1::UnsubscribeMulti, timer: Instant, ) -> Result, DBError> { let me = self.clone(); @@ -930,7 +930,7 @@ impl ClientConnection { .await? } - pub async fn subscribe(&self, subscription: Subscribe, timer: Instant) -> Result { + pub async fn subscribe(&self, subscription: ws_v1::Subscribe, timer: Instant) -> Result { let me = self.clone(); self.module() .on_module_thread_async("subscribe", async move || { @@ -949,14 +949,14 @@ impl ClientConnection { timer: Instant, ) -> Result<(), anyhow::Error> { self.module() - .one_off_query::( + .one_off_query::( self.auth.clone(), query.to_owned(), self.sender.clone(), message_id.to_owned(), timer, JsonRowListBuilderFakePool, - |msg: OneOffQueryResponseMessage| msg.into(), + |msg: OneOffQueryResponseMessage| msg.into(), ) .await } @@ -969,14 +969,14 @@ impl ClientConnection { ) -> Result<(), anyhow::Error> { let bsatn_rlb_pool = self.module().replica_ctx().subscriptions.bsatn_rlb_pool.clone(); self.module() - .one_off_query::( + .one_off_query::( self.auth.clone(), query.to_owned(), self.sender.clone(), message_id.to_owned(), timer, bsatn_rlb_pool, - |msg: OneOffQueryResponseMessage| msg.into(), + |msg: OneOffQueryResponseMessage| msg.into(), ) .await } diff --git a/crates/core/src/client/consume_each_list.rs b/crates/core/src/client/consume_each_list.rs index 191b726c570..b7fe9b0618f 100644 --- a/crates/core/src/client/consume_each_list.rs +++ b/crates/core/src/client/consume_each_list.rs @@ -1,8 +1,5 @@ use bytes::Bytes; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, BsatnRowList, CompressableQueryUpdate, DatabaseUpdate, OneOffQueryResponse, QueryUpdate, - ServerMessage, TableUpdate, UpdateStatus, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; /// Moves each buffer in `self` into a closure. pub trait ConsumeEachBuffer { @@ -10,9 +7,9 @@ pub trait ConsumeEachBuffer { fn consume_each_list(self, each: &mut impl FnMut(Bytes)); } -impl ConsumeEachBuffer for ServerMessage { +impl ConsumeEachBuffer for ws_v1::ServerMessage { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { - use ServerMessage::*; + use ws_v1::ServerMessage::*; match self { InitialSubscription(x) => x.database_update.consume_each_list(each), TransactionUpdate(x) => x.status.consume_each_list(each), @@ -27,7 +24,7 @@ impl ConsumeEachBuffer for ServerMessage { } } -impl ConsumeEachBuffer for OneOffQueryResponse { +impl ConsumeEachBuffer for ws_v1::OneOffQueryResponse { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { Vec::from(self.tables) .into_iter() @@ -35,28 +32,28 @@ impl ConsumeEachBuffer for OneOffQueryResponse { } } -impl ConsumeEachBuffer for UpdateStatus { +impl ConsumeEachBuffer for ws_v1::UpdateStatus { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { match self { Self::Committed(x) => x.consume_each_list(each), - Self::Failed(_) | UpdateStatus::OutOfEnergy => {} + Self::Failed(_) | ws_v1::UpdateStatus::OutOfEnergy => {} } } } -impl ConsumeEachBuffer for DatabaseUpdate { +impl ConsumeEachBuffer for ws_v1::DatabaseUpdate { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { self.tables.into_iter().for_each(|x| x.consume_each_list(each)); } } -impl ConsumeEachBuffer for TableUpdate { +impl ConsumeEachBuffer for ws_v1::TableUpdate { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { self.updates.into_iter().for_each(|x| x.consume_each_list(each)); } } -impl ConsumeEachBuffer for CompressableQueryUpdate { +impl ConsumeEachBuffer for ws_v1::CompressableQueryUpdate { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { match self { Self::Uncompressed(x) => x.consume_each_list(each), @@ -65,14 +62,14 @@ impl ConsumeEachBuffer for CompressableQueryUpdate { } } -impl ConsumeEachBuffer for QueryUpdate { +impl ConsumeEachBuffer for ws_v1::QueryUpdate { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { self.deletes.consume_each_list(each); self.inserts.consume_each_list(each); } } -impl ConsumeEachBuffer for BsatnRowList { +impl ConsumeEachBuffer for ws_v1::BsatnRowList { fn consume_each_list(self, each: &mut impl FnMut(Bytes)) { let (_, buffer) = self.into_inner(); each(buffer); diff --git a/crates/core/src/client/message_handlers.rs b/crates/core/src/client/message_handlers.rs index 313b87d0281..ee1b94db62b 100644 --- a/crates/core/src/client/message_handlers.rs +++ b/crates/core/src/client/message_handlers.rs @@ -4,9 +4,8 @@ use crate::energy::EnergyQuanta; use crate::host::module_host::{EventStatus, ModuleEvent, ModuleFunctionCall}; use crate::host::{FunctionArgs, ReducerId}; use crate::identity::Identity; -use crate::messages::websocket::{CallReducer, ClientMessage, OneOffQuery}; use crate::worker_metrics::WORKER_METRICS; -use spacetimedb_client_api_messages::websocket::CallProcedure; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_datastore::execution_context::WorkloadType; use spacetimedb_lib::de::serde::DeserializeWrapper; use spacetimedb_lib::identity::RequestId; @@ -35,7 +34,7 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst DataMessage::Text(text) => { // TODO(breaking): this should ideally be &serde_json::RawValue, not json-nested-in-string let DeserializeWrapper(message) = - serde_json::from_str::>>>(&text)?; + serde_json::from_str::>>>(&text)?; message.map_args(|s| { FunctionArgs::Json(match s { Cow::Borrowed(s) => text.slice_ref(s), @@ -43,7 +42,7 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst }) }) } - DataMessage::Binary(message_buf) => bsatn::from_slice::>(&message_buf)? + DataMessage::Binary(message_buf) => bsatn::from_slice::>(&message_buf)? .map_args(|b| FunctionArgs::Bsatn(message_buf.slice_ref(b))), }; @@ -63,7 +62,7 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst let unsub_metrics = record_metrics(WorkloadType::Unsubscribe); let res = match message { - ClientMessage::CallReducer(CallReducer { + ws_v1::ClientMessage::CallReducer(ws_v1::CallReducer { ref reducer, args, request_id, @@ -82,42 +81,42 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst ) }) } - ClientMessage::SubscribeMulti(subscription) => { + ws_v1::ClientMessage::SubscribeMulti(subscription) => { let res = client.subscribe_multi(subscription, timer).await.map(sub_metrics); mod_metrics .request_round_trip_subscribe .observe(timer.elapsed().as_secs_f64()); res.map_err(|e| (None, None, e.into())) } - ClientMessage::UnsubscribeMulti(request) => { + ws_v1::ClientMessage::UnsubscribeMulti(request) => { let res = client.unsubscribe_multi(request, timer).await.map(unsub_metrics); mod_metrics .request_round_trip_unsubscribe .observe(timer.elapsed().as_secs_f64()); res.map_err(|e| (None, None, e.into())) } - ClientMessage::SubscribeSingle(subscription) => { + ws_v1::ClientMessage::SubscribeSingle(subscription) => { let res = client.subscribe_single(subscription, timer).await.map(sub_metrics); mod_metrics .request_round_trip_subscribe .observe(timer.elapsed().as_secs_f64()); res.map_err(|e| (None, None, e.into())) } - ClientMessage::Unsubscribe(request) => { + ws_v1::ClientMessage::Unsubscribe(request) => { let res = client.unsubscribe(request, timer).await.map(unsub_metrics); mod_metrics .request_round_trip_unsubscribe .observe(timer.elapsed().as_secs_f64()); res.map_err(|e| (None, None, e.into())) } - ClientMessage::Subscribe(subscription) => { + ws_v1::ClientMessage::Subscribe(subscription) => { let res = client.subscribe(subscription, timer).await.map(Some).map(sub_metrics); mod_metrics .request_round_trip_subscribe .observe(timer.elapsed().as_secs_f64()); res.map_err(|e| (None, None, e.into())) } - ClientMessage::OneOffQuery(OneOffQuery { + ws_v1::ClientMessage::OneOffQuery(ws_v1::OneOffQuery { query_string: query, message_id, }) => { @@ -130,7 +129,7 @@ pub async fn handle(client: &ClientConnection, message: DataMessage, timer: Inst .observe(timer.elapsed().as_secs_f64()); res.map_err(|err| (None, None, err)) } - ClientMessage::CallProcedure(CallProcedure { + ws_v1::ClientMessage::CallProcedure(ws_v1::CallProcedure { ref procedure, args, request_id, diff --git a/crates/core/src/client/messages.rs b/crates/core/src/client/messages.rs index 4511afb9ec2..cc50665bb09 100644 --- a/crates/core/src/client/messages.rs +++ b/crates/core/src/client/messages.rs @@ -2,16 +2,12 @@ use super::{ClientConfig, DataMessage, Protocol}; use crate::client::consume_each_list::ConsumeEachBuffer; use crate::host::module_host::{EventStatus, ModuleEvent, ProcedureCallError}; use crate::host::{ArgsTuple, ProcedureCallResult}; -use crate::messages::websocket as ws; use crate::subscription::row_list_builder_pool::BsatnRowListBuilderPool; use crate::subscription::websocket_building::{brotli_compress, decide_compression, gzip_compress}; use bytes::{BufMut, Bytes, BytesMut}; use bytestring::ByteString; use derive_more::From; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, Compression, FormatSwitch, JsonFormat, OneOffTable, RowListLen, WebsocketFormat, - SERVER_MSG_COMPRESSION_TAG_BROTLI, SERVER_MSG_COMPRESSION_TAG_GZIP, SERVER_MSG_COMPRESSION_TAG_NONE, -}; +use spacetimedb_client_api_messages::websocket::v1::{self as ws_v1, RowListLen as _}; use spacetimedb_datastore::execution_context::WorkloadType; use spacetimedb_lib::identity::RequestId; use spacetimedb_lib::ser::serde::SerializeWrapper; @@ -29,8 +25,10 @@ pub trait ToProtocol { fn to_protocol(self, protocol: Protocol) -> Self::Encoded; } -pub type SwitchedServerMessage = FormatSwitch, ws::ServerMessage>; -pub(super) type SwitchedDbUpdate = FormatSwitch, ws::DatabaseUpdate>; +pub type SwitchedServerMessage = + ws_v1::FormatSwitch, ws_v1::ServerMessage>; +pub(super) type SwitchedDbUpdate = + ws_v1::FormatSwitch, ws_v1::DatabaseUpdate>; /// The initial size of a `serialize` buffer. /// Currently 4k to align with the linux page size @@ -46,7 +44,8 @@ pub struct SerializeBuffer { impl SerializeBuffer { pub fn new(config: ClientConfig) -> Self { let uncompressed_capacity = SERIALIZE_BUFFER_INIT_CAP; - let compressed_capacity = if config.compression == Compression::None || config.protocol == Protocol::Text { + let compressed_capacity = if config.compression == ws_v1::Compression::None || config.protocol == Protocol::Text + { 0 } else { SERIALIZE_BUFFER_INIT_CAP @@ -130,7 +129,7 @@ impl InUseSerializeBuffer { } } -/// Serialize `msg` into a [`DataMessage`] containing a [`ws::ServerMessage`]. +/// Serialize `msg` into a [`DataMessage`] containing a [`ws_v1::ServerMessage`]. /// /// If `protocol` is [`Protocol::Binary`], /// the message will be conditionally compressed by this method according to `compression`. @@ -141,7 +140,7 @@ pub fn serialize( config: ClientConfig, ) -> (InUseSerializeBuffer, DataMessage) { match msg.to_protocol(config.protocol) { - FormatSwitch::Json(msg) => { + ws_v1::FormatSwitch::Json(msg) => { let out: BytesMutWriter<'_> = (&mut buffer.uncompressed).writer(); serde_json::to_writer(out, &SerializeWrapper::new(msg)) .expect("should be able to json encode a `ServerMessage`"); @@ -152,9 +151,9 @@ pub fn serialize( let msg_json = unsafe { ByteString::from_bytes_unchecked(out) }; (in_use, msg_json.into()) } - FormatSwitch::Bsatn(msg) => { + ws_v1::FormatSwitch::Bsatn(msg) => { // First write the tag so that we avoid shifting the entire message at the end. - let srv_msg = buffer.write_with_tag(SERVER_MSG_COMPRESSION_TAG_NONE, |w| { + let srv_msg = buffer.write_with_tag(ws_v1::SERVER_MSG_COMPRESSION_TAG_NONE, |w| { bsatn::to_writer(w.into_inner(), &msg).unwrap() }); @@ -164,9 +163,13 @@ pub fn serialize( // Conditionally compress the message. let (in_use, msg_bytes) = match decide_compression(srv_msg.len(), config.compression) { - Compression::None => buffer.uncompressed(), - Compression::Brotli => buffer.compress_with_tag(SERVER_MSG_COMPRESSION_TAG_BROTLI, brotli_compress), - Compression::Gzip => buffer.compress_with_tag(SERVER_MSG_COMPRESSION_TAG_GZIP, gzip_compress), + ws_v1::Compression::None => buffer.uncompressed(), + ws_v1::Compression::Brotli => { + buffer.compress_with_tag(ws_v1::SERVER_MSG_COMPRESSION_TAG_BROTLI, brotli_compress) + } + ws_v1::Compression::Gzip => { + buffer.compress_with_tag(ws_v1::SERVER_MSG_COMPRESSION_TAG_GZIP, gzip_compress) + } }; (in_use, msg_bytes.into()) } @@ -175,8 +178,8 @@ pub fn serialize( #[derive(Debug, From)] pub enum SerializableMessage { - QueryBinary(OneOffQueryResponseMessage), - QueryText(OneOffQueryResponseMessage), + QueryBinary(OneOffQueryResponseMessage), + QueryText(OneOffQueryResponseMessage), Identity(IdentityTokenMessage), Subscribe(SubscriptionUpdateMessage), Subscription(SubscriptionMessage), @@ -229,14 +232,14 @@ impl ToProtocol for SerializableMessage { } } -pub type IdentityTokenMessage = ws::IdentityToken; +pub type IdentityTokenMessage = ws_v1::IdentityToken; impl ToProtocol for IdentityTokenMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, protocol: Protocol) -> Self::Encoded { match protocol { - Protocol::Text => FormatSwitch::Json(ws::ServerMessage::IdentityToken(self)), - Protocol::Binary => FormatSwitch::Bsatn(ws::ServerMessage::IdentityToken(self)), + Protocol::Text => ws_v1::FormatSwitch::Json(ws_v1::ServerMessage::IdentityToken(self)), + Protocol::Binary => ws_v1::FormatSwitch::Bsatn(ws_v1::ServerMessage::IdentityToken(self)), } } } @@ -258,29 +261,32 @@ impl TransactionUpdateMessage { impl ToProtocol for TransactionUpdateMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, protocol: Protocol) -> Self::Encoded { - fn convert( + fn convert( event: Option>, request_id: u32, - update: ws::DatabaseUpdate, + update: ws_v1::DatabaseUpdate, conv_args: impl FnOnce(&ArgsTuple) -> F::Single, - ) -> ws::ServerMessage { + ) -> ws_v1::ServerMessage { let Some(event) = event else { - return ws::ServerMessage::TransactionUpdateLight(ws::TransactionUpdateLight { request_id, update }); + return ws_v1::ServerMessage::TransactionUpdateLight(ws_v1::TransactionUpdateLight { + request_id, + update, + }); }; let status = match &event.status { - EventStatus::Committed(_) => ws::UpdateStatus::Committed(update), - EventStatus::Failed(errmsg) => ws::UpdateStatus::Failed(errmsg.clone().into()), - EventStatus::OutOfEnergy => ws::UpdateStatus::OutOfEnergy, + EventStatus::Committed(_) => ws_v1::UpdateStatus::Committed(update), + EventStatus::Failed(errmsg) => ws_v1::UpdateStatus::Failed(errmsg.clone().into()), + EventStatus::OutOfEnergy => ws_v1::UpdateStatus::OutOfEnergy, }; let args = conv_args(&event.function_call.args); - let tx_update = ws::TransactionUpdate { + let tx_update = ws_v1::TransactionUpdate { timestamp: event.timestamp, status, caller_identity: event.caller_identity, - reducer_call: ws::ReducerCallInfo { + reducer_call: ws_v1::ReducerCallInfo { reducer_name: event.function_call.reducer.to_owned().into(), reducer_id: event.function_call.reducer_id.into(), args, @@ -291,7 +297,7 @@ impl ToProtocol for TransactionUpdateMessage { caller_connection_id: event.caller_connection_id.unwrap_or(ConnectionId::ZERO), }; - ws::ServerMessage::TransactionUpdate(tx_update) + ws_v1::ServerMessage::TransactionUpdate(tx_update) } let TransactionUpdateMessage { event, database_update } = self; @@ -299,11 +305,13 @@ impl ToProtocol for TransactionUpdateMessage { protocol.assert_matches_format_switch(&update); let request_id = database_update.request_id.unwrap_or(0); match update { - FormatSwitch::Bsatn(update) => FormatSwitch::Bsatn(convert(event, request_id, update, |args| { - Vec::from(args.get_bsatn().clone()).into() - })), - FormatSwitch::Json(update) => { - FormatSwitch::Json(convert(event, request_id, update, |args| args.get_json().clone())) + ws_v1::FormatSwitch::Bsatn(update) => { + ws_v1::FormatSwitch::Bsatn(convert(event, request_id, update, |args| { + Vec::from(args.get_bsatn().clone()).into() + })) + } + ws_v1::FormatSwitch::Json(update) => { + ws_v1::FormatSwitch::Json(convert(event, request_id, update, |args| args.get_json().clone())) } } } @@ -320,8 +328,8 @@ impl SubscriptionUpdateMessage { pub(crate) fn default_for_protocol(protocol: Protocol, request_id: Option) -> Self { Self { database_update: match protocol { - Protocol::Text => FormatSwitch::Json(<_>::default()), - Protocol::Binary => FormatSwitch::Bsatn(<_>::default()), + Protocol::Text => ws_v1::FormatSwitch::Json(<_>::default()), + Protocol::Binary => ws_v1::FormatSwitch::Bsatn(<_>::default()), }, request_id, timer: None, @@ -338,8 +346,8 @@ impl SubscriptionUpdateMessage { fn num_rows(&self) -> usize { match &self.database_update { - FormatSwitch::Bsatn(x) => x.num_rows(), - FormatSwitch::Json(x) => x.num_rows(), + ws_v1::FormatSwitch::Bsatn(x) => x.num_rows(), + ws_v1::FormatSwitch::Json(x) => x.num_rows(), } } } @@ -352,15 +360,15 @@ impl ToProtocol for SubscriptionUpdateMessage { protocol.assert_matches_format_switch(&self.database_update); match self.database_update { - FormatSwitch::Bsatn(database_update) => { - FormatSwitch::Bsatn(ws::ServerMessage::InitialSubscription(ws::InitialSubscription { + ws_v1::FormatSwitch::Bsatn(database_update) => { + ws_v1::FormatSwitch::Bsatn(ws_v1::ServerMessage::InitialSubscription(ws_v1::InitialSubscription { database_update, request_id, total_host_execution_duration, })) } - FormatSwitch::Json(database_update) => { - FormatSwitch::Json(ws::ServerMessage::InitialSubscription(ws::InitialSubscription { + ws_v1::FormatSwitch::Json(database_update) => { + ws_v1::FormatSwitch::Json(ws_v1::ServerMessage::InitialSubscription(ws_v1::InitialSubscription { database_update, request_id, total_host_execution_duration, @@ -372,14 +380,14 @@ impl ToProtocol for SubscriptionUpdateMessage { #[derive(Debug, Clone)] pub struct SubscriptionData { - pub data: FormatSwitch, ws::DatabaseUpdate>, + pub data: ws_v1::FormatSwitch, ws_v1::DatabaseUpdate>, } #[derive(Debug, Clone)] pub struct SubscriptionRows { pub table_id: TableId, pub table_name: Box, - pub table_rows: FormatSwitch, ws::TableUpdate>, + pub table_rows: ws_v1::FormatSwitch, ws_v1::TableUpdate>, } #[derive(Debug, Clone)] @@ -401,21 +409,21 @@ pub enum SubscriptionResult { pub struct SubscriptionMessage { pub timer: Option, pub request_id: Option, - pub query_id: Option, + pub query_id: Option, pub result: SubscriptionResult, } fn num_rows_in(rows: &SubscriptionRows) -> usize { match &rows.table_rows { - FormatSwitch::Bsatn(x) => x.num_rows(), - FormatSwitch::Json(x) => x.num_rows(), + ws_v1::FormatSwitch::Bsatn(x) => x.num_rows(), + ws_v1::FormatSwitch::Json(x) => x.num_rows(), } } fn subscription_data_rows(rows: &SubscriptionData) -> usize { match &rows.data { - FormatSwitch::Bsatn(x) => x.num_rows(), - FormatSwitch::Json(x) => x.num_rows(), + ws_v1::FormatSwitch::Bsatn(x) => x.num_rows(), + ws_v1::FormatSwitch::Json(x) => x.num_rows(), } } @@ -435,19 +443,19 @@ impl ToProtocol for SubscriptionMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, protocol: Protocol) -> Self::Encoded { let request_id = self.request_id.unwrap_or(0); - let query_id = self.query_id.unwrap_or(ws::QueryId::new(0)); + let query_id = self.query_id.unwrap_or(ws_v1::QueryId::new(0)); let total_host_execution_duration_micros = self.timer.map_or(0, |t| t.elapsed().as_micros() as u64); match self.result { SubscriptionResult::Subscribe(result) => { protocol.assert_matches_format_switch(&result.table_rows); match result.table_rows { - FormatSwitch::Bsatn(table_rows) => FormatSwitch::Bsatn( - ws::SubscribeApplied { + ws_v1::FormatSwitch::Bsatn(table_rows) => ws_v1::FormatSwitch::Bsatn( + ws_v1::SubscribeApplied { total_host_execution_duration_micros, request_id, query_id, - rows: ws::SubscribeRows { + rows: ws_v1::SubscribeRows { table_id: result.table_id, table_name: result.table_name, table_rows, @@ -455,12 +463,12 @@ impl ToProtocol for SubscriptionMessage { } .into(), ), - FormatSwitch::Json(table_rows) => FormatSwitch::Json( - ws::SubscribeApplied { + ws_v1::FormatSwitch::Json(table_rows) => ws_v1::FormatSwitch::Json( + ws_v1::SubscribeApplied { total_host_execution_duration_micros, request_id, query_id, - rows: ws::SubscribeRows { + rows: ws_v1::SubscribeRows { table_id: result.table_id, table_name: result.table_name, table_rows, @@ -473,12 +481,12 @@ impl ToProtocol for SubscriptionMessage { SubscriptionResult::Unsubscribe(result) => { protocol.assert_matches_format_switch(&result.table_rows); match result.table_rows { - FormatSwitch::Bsatn(table_rows) => FormatSwitch::Bsatn( - ws::UnsubscribeApplied { + ws_v1::FormatSwitch::Bsatn(table_rows) => ws_v1::FormatSwitch::Bsatn( + ws_v1::UnsubscribeApplied { total_host_execution_duration_micros, request_id, query_id, - rows: ws::SubscribeRows { + rows: ws_v1::SubscribeRows { table_id: result.table_id, table_name: result.table_name, table_rows, @@ -486,12 +494,12 @@ impl ToProtocol for SubscriptionMessage { } .into(), ), - FormatSwitch::Json(table_rows) => FormatSwitch::Json( - ws::UnsubscribeApplied { + ws_v1::FormatSwitch::Json(table_rows) => ws_v1::FormatSwitch::Json( + ws_v1::UnsubscribeApplied { total_host_execution_duration_micros, request_id, query_id, - rows: ws::SubscribeRows { + rows: ws_v1::SubscribeRows { table_id: result.table_id, table_name: result.table_name, table_rows, @@ -502,7 +510,7 @@ impl ToProtocol for SubscriptionMessage { } } SubscriptionResult::Error(error) => { - let msg = ws::SubscriptionError { + let msg = ws_v1::SubscriptionError { total_host_execution_duration_micros, request_id: self.request_id, // Pass Option through query_id: self.query_id.map(|x| x.id), // Pass Option through @@ -510,15 +518,15 @@ impl ToProtocol for SubscriptionMessage { error: error.message, }; match protocol { - Protocol::Binary => FormatSwitch::Bsatn(msg.into()), - Protocol::Text => FormatSwitch::Json(msg.into()), + Protocol::Binary => ws_v1::FormatSwitch::Bsatn(msg.into()), + Protocol::Text => ws_v1::FormatSwitch::Json(msg.into()), } } SubscriptionResult::SubscribeMulti(result) => { protocol.assert_matches_format_switch(&result.data); match result.data { - FormatSwitch::Bsatn(data) => FormatSwitch::Bsatn( - ws::SubscribeMultiApplied { + ws_v1::FormatSwitch::Bsatn(data) => ws_v1::FormatSwitch::Bsatn( + ws_v1::SubscribeMultiApplied { total_host_execution_duration_micros, request_id, query_id, @@ -526,8 +534,8 @@ impl ToProtocol for SubscriptionMessage { } .into(), ), - FormatSwitch::Json(data) => FormatSwitch::Json( - ws::SubscribeMultiApplied { + ws_v1::FormatSwitch::Json(data) => ws_v1::FormatSwitch::Json( + ws_v1::SubscribeMultiApplied { total_host_execution_duration_micros, request_id, query_id, @@ -540,8 +548,8 @@ impl ToProtocol for SubscriptionMessage { SubscriptionResult::UnsubscribeMulti(result) => { protocol.assert_matches_format_switch(&result.data); match result.data { - FormatSwitch::Bsatn(data) => FormatSwitch::Bsatn( - ws::UnsubscribeMultiApplied { + ws_v1::FormatSwitch::Bsatn(data) => ws_v1::FormatSwitch::Bsatn( + ws_v1::UnsubscribeMultiApplied { total_host_execution_duration_micros, request_id, query_id, @@ -549,8 +557,8 @@ impl ToProtocol for SubscriptionMessage { } .into(), ), - FormatSwitch::Json(data) => FormatSwitch::Json( - ws::UnsubscribeMultiApplied { + ws_v1::FormatSwitch::Json(data) => ws_v1::FormatSwitch::Json( + ws_v1::UnsubscribeMultiApplied { total_host_execution_duration_micros, request_id, query_id, @@ -565,36 +573,36 @@ impl ToProtocol for SubscriptionMessage { } #[derive(Debug)] -pub struct OneOffQueryResponseMessage { +pub struct OneOffQueryResponseMessage { pub message_id: Vec, pub error: Option, - pub results: Vec>, + pub results: Vec>, pub total_host_execution_duration: TimeDuration, } -impl OneOffQueryResponseMessage { +impl OneOffQueryResponseMessage { fn num_rows(&self) -> usize { self.results.iter().map(|table| table.rows.len()).sum() } } -impl ToProtocol for OneOffQueryResponseMessage { +impl ToProtocol for OneOffQueryResponseMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, _: Protocol) -> Self::Encoded { - FormatSwitch::Bsatn(convert(self)) + ws_v1::FormatSwitch::Bsatn(convert(self)) } } -impl ToProtocol for OneOffQueryResponseMessage { +impl ToProtocol for OneOffQueryResponseMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, _: Protocol) -> Self::Encoded { - FormatSwitch::Json(convert(self)) + ws_v1::FormatSwitch::Json(convert(self)) } } -fn convert(msg: OneOffQueryResponseMessage) -> ws::ServerMessage { - ws::ServerMessage::OneOffQueryResponse(ws::OneOffQueryResponse { +fn convert(msg: OneOffQueryResponseMessage) -> ws_v1::ServerMessage { + ws_v1::ServerMessage::OneOffQueryResponse(ws_v1::OneOffQueryResponse { message_id: msg.message_id.into(), error: msg.error.map(Into::into), tables: msg.results.into_boxed_slice(), @@ -657,10 +665,10 @@ impl ToProtocol for ProcedureResultMessage { type Encoded = SwitchedServerMessage; fn to_protocol(self, protocol: Protocol) -> Self::Encoded { - fn convert( + fn convert( msg: ProcedureResultMessage, serialize_value: impl Fn(AlgebraicValue) -> F::Single, - ) -> ws::ServerMessage { + ) -> ws_v1::ServerMessage { let ProcedureResultMessage { status, timestamp, @@ -668,11 +676,11 @@ impl ToProtocol for ProcedureResultMessage { request_id, } = msg; let status = match status { - ProcedureStatus::InternalError(msg) => ws::ProcedureStatus::InternalError(msg), - ProcedureStatus::OutOfEnergy => ws::ProcedureStatus::OutOfEnergy, - ProcedureStatus::Returned(val) => ws::ProcedureStatus::Returned(serialize_value(val)), + ProcedureStatus::InternalError(msg) => ws_v1::ProcedureStatus::InternalError(msg), + ProcedureStatus::OutOfEnergy => ws_v1::ProcedureStatus::OutOfEnergy, + ProcedureStatus::Returned(val) => ws_v1::ProcedureStatus::Returned(serialize_value(val)), }; - ws::ServerMessage::ProcedureResult(ws::ProcedureResult { + ws_v1::ServerMessage::ProcedureResult(ws_v1::ProcedureResult { status, timestamp, total_host_execution_duration, @@ -683,12 +691,12 @@ impl ToProtocol for ProcedureResultMessage { // Note that procedure returns are sent only to the caller, not broadcast to all subscribers, // so we don't have to bother with memoizing the serialization the way we do for reducer args. match protocol { - Protocol::Binary => FormatSwitch::Bsatn(convert(self, |val| { + Protocol::Binary => ws_v1::FormatSwitch::Bsatn(convert(self, |val| { bsatn::to_vec(&val) .expect("Procedure return value failed to serialize to BSATN") .into() })), - Protocol::Text => FormatSwitch::Json(convert(self, |val| { + Protocol::Text => ws_v1::FormatSwitch::Json(convert(self, |val| { serde_json::to_string(&SerializeWrapper(val)) .expect("Procedure return value failed to serialize to JSON") .into() diff --git a/crates/core/src/host/module_host.rs b/crates/core/src/host/module_host.rs index ba06a25e57d..2119eef98f2 100644 --- a/crates/core/src/host/module_host.rs +++ b/crates/core/src/host/module_host.rs @@ -39,9 +39,7 @@ use prometheus::{Histogram, IntGauge}; use scopeguard::ScopeGuard; use spacetimedb_auth::identity::ConnectionAuthCtx; use spacetimedb_client_api_messages::energy::FunctionBudget; -use spacetimedb_client_api_messages::websocket::{ - ByteListLen, Compression, OneOffTable, QueryUpdate, Subscribe, SubscribeMulti, SubscribeSingle, -}; +use spacetimedb_client_api_messages::websocket::v1::{self as ws_v1, ByteListLen as _, RowListLen as _}; use spacetimedb_data_structures::error_stream::ErrorStream; use spacetimedb_data_structures::map::{HashCollectionExt as _, IntMap}; use spacetimedb_datastore::error::DatastoreError; @@ -164,12 +162,12 @@ impl UpdatesRelValue<'_> { let (inserts, nr_ins) = F::encode_list(rlb_pool.take_row_list_builder(), self.inserts.iter()); let num_rows = nr_del + nr_ins; let num_bytes = deletes.num_bytes() + inserts.num_bytes(); - let qu = QueryUpdate { deletes, inserts }; + let qu = ws_v1::QueryUpdate { deletes, inserts }; // We don't compress individual table updates. // Previously we were, but the benefits, if any, were unclear. // Note, each message is still compressed before being sent to clients, // but we no longer have to hold a tx lock when doing so. - let cqu = F::into_query_update(qu, Compression::None); + let cqu = F::into_query_update(qu, ws_v1::Compression::None); (cqu, num_rows, num_bytes) } } @@ -651,19 +649,19 @@ pub enum ViewCommand { AddSingleSubscription { sender: Arc, auth: AuthCtx, - request: SubscribeSingle, + request: ws_v1::SubscribeSingle, timer: Instant, }, AddMultiSubscription { sender: Arc, auth: AuthCtx, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, }, AddLegacySubscription { sender: Arc, auth: AuthCtx, - subscribe: Subscribe, + subscribe: ws_v1::Subscribe, timer: Instant, }, Sql { @@ -1560,7 +1558,7 @@ impl ModuleHost { &self, sender: Arc, auth: AuthCtx, - request: SubscribeSingle, + request: ws_v1::SubscribeSingle, timer: Instant, ) -> Result, DBError> { let cmd = ViewCommand::AddSingleSubscription { @@ -1593,7 +1591,7 @@ impl ModuleHost { &self, sender: Arc, auth: AuthCtx, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, ) -> Result, DBError> { let cmd = ViewCommand::AddMultiSubscription { @@ -1626,7 +1624,7 @@ impl ModuleHost { &self, sender: Arc, auth: AuthCtx, - subscribe: spacetimedb_client_api_messages::websocket::Subscribe, + subscribe: ws_v1::Subscribe, timer: Instant, ) -> Result, DBError> { let cmd = ViewCommand::AddLegacySubscription { @@ -2052,7 +2050,7 @@ impl ModuleHost { // We wrap the actual query in a closure so we can use ? to handle errors without making // the entire transaction abort with an error. - let result: Result<(OneOffTable, ExecutionMetrics), anyhow::Error> = (|| { + let result: Result<(ws_v1::OneOffTable, ExecutionMetrics), anyhow::Error> = (|| { let tx = SchemaViewer::new(&*tx, &auth); let ( @@ -2101,13 +2099,13 @@ impl ModuleHost { .collect::>(); // Execute the union and return the results return execute_plan_for_view::(&optimized, &DeltaTx::from(&*tx), &rlb_pool) - .map(|(rows, _, metrics)| (OneOffTable { table_name, rows }, metrics)) + .map(|(rows, _, metrics)| (ws_v1::OneOffTable { table_name, rows }, metrics)) .context("One-off queries are not allowed to modify the database"); } // Execute the union and return the results execute_plan::(&optimized, &DeltaTx::from(&*tx), &rlb_pool) - .map(|(rows, _, metrics)| (OneOffTable { table_name, rows }, metrics)) + .map(|(rows, _, metrics)| (ws_v1::OneOffTable { table_name, rows }, metrics)) .context("One-off queries are not allowed to modify the database") })(); diff --git a/crates/core/src/messages/mod.rs b/crates/core/src/messages/mod.rs index 61f7859905d..c4586f7ad01 100644 --- a/crates/core/src/messages/mod.rs +++ b/crates/core/src/messages/mod.rs @@ -2,6 +2,3 @@ pub mod control_db; pub mod control_worker_api; pub mod instance_db_trace_log; pub mod worker_db; -pub mod websocket { - pub use spacetimedb_client_api_messages::websocket::*; -} diff --git a/crates/core/src/subscription/execution_unit.rs b/crates/core/src/subscription/execution_unit.rs index daf071133f7..6bfa9b812e9 100644 --- a/crates/core/src/subscription/execution_unit.rs +++ b/crates/core/src/subscription/execution_unit.rs @@ -4,11 +4,10 @@ use crate::db::relational_db::{RelationalDB, Tx}; use crate::error::DBError; use crate::estimation; use crate::host::module_host::{DatabaseTableUpdate, DatabaseTableUpdateRelValue, UpdatesRelValue}; -use crate::messages::websocket::TableUpdate; use crate::subscription::websocket_building::{BuildableWebsocketFormat, RowListBuilderSource}; use crate::util::slow::SlowQueryLogger; use crate::vm::{build_query, TxMode}; -use spacetimedb_client_api_messages::websocket::{Compression, QueryUpdate, RowListLen as _, SingleQueryUpdate}; +use spacetimedb_client_api_messages::websocket::v1::{self as ws_v1, RowListLen as _}; use spacetimedb_datastore::locking_tx_datastore::TxId; use spacetimedb_lib::identity::AuthCtx; use spacetimedb_lib::Identity; @@ -243,8 +242,8 @@ impl ExecutionUnit { rlb_pool: &impl RowListBuilderSource, sql: &str, slow_query_threshold: Option, - compression: Compression, - ) -> Option> { + compression: ws_v1::Compression, + ) -> Option> { let _slow_query = SlowQueryLogger::new(sql, slow_query_threshold, tx.ctx.workload()).log_guard(); // Build & execute the query and then encode it to a row list. @@ -255,12 +254,12 @@ impl ExecutionUnit { (!inserts.is_empty()).then(|| { let deletes = F::List::default(); - let qu = QueryUpdate { deletes, inserts }; + let qu = ws_v1::QueryUpdate { deletes, inserts }; let update = F::into_query_update(qu, compression); - TableUpdate::new( + ws_v1::TableUpdate::new( self.return_table(), self.return_name(), - SingleQueryUpdate { update, num_rows }, + ws_v1::SingleQueryUpdate { update, num_rows }, ) }) } diff --git a/crates/core/src/subscription/mod.rs b/crates/core/src/subscription/mod.rs index 32abd7fec9d..bdb62e52dd3 100644 --- a/crates/core/src/subscription/mod.rs +++ b/crates/core/src/subscription/mod.rs @@ -5,9 +5,7 @@ use metrics::QueryMetrics; use module_subscription_manager::Plan; use prometheus::IntCounter; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use spacetimedb_client_api_messages::websocket::{ - ByteListLen, Compression, DatabaseUpdate, QueryUpdate, SingleQueryUpdate, TableUpdate, -}; +use spacetimedb_client_api_messages::websocket::v1::{self as ws_v1, ByteListLen as _}; use spacetimedb_datastore::{ db_metrics::DB_METRICS, execution_context::WorkloadType, locking_tx_datastore::datastore::MetricsRecorder, }; @@ -178,7 +176,7 @@ pub fn collect_table_update_for_view( tx: &Tx, update_type: TableUpdateType, rlb_pool: &impl RowListBuilderSource, -) -> Result<(TableUpdate, ExecutionMetrics)> +) -> Result<(ws_v1::TableUpdate, ExecutionMetrics)> where Tx: Datastore + DeltaStore, F: BuildableWebsocketFormat, @@ -186,11 +184,11 @@ where execute_plan_for_view::(plan_fragments, tx, rlb_pool).map(|(rows, num_rows, metrics)| { let empty = F::List::default(); let qu = match update_type { - TableUpdateType::Subscribe => QueryUpdate { + TableUpdateType::Subscribe => ws_v1::QueryUpdate { deletes: empty, inserts: rows, }, - TableUpdateType::Unsubscribe => QueryUpdate { + TableUpdateType::Unsubscribe => ws_v1::QueryUpdate { deletes: rows, inserts: empty, }, @@ -198,9 +196,9 @@ where // We will compress the outer server message, // after we release the tx lock. // There's no need to compress the inner table update too. - let update = F::into_query_update(qu, Compression::None); + let update = F::into_query_update(qu, ws_v1::Compression::None); ( - TableUpdate::new(table_id, table_name, SingleQueryUpdate { update, num_rows }), + ws_v1::TableUpdate::new(table_id, table_name, ws_v1::SingleQueryUpdate { update, num_rows }), metrics, ) }) @@ -214,15 +212,15 @@ pub fn collect_table_update( tx: &(impl Datastore + DeltaStore), update_type: TableUpdateType, rlb_pool: &impl RowListBuilderSource, -) -> Result<(TableUpdate, ExecutionMetrics)> { +) -> Result<(ws_v1::TableUpdate, ExecutionMetrics)> { execute_plan::(plan_fragments, tx, rlb_pool).map(|(rows, num_rows, metrics)| { let empty = F::List::default(); let qu = match update_type { - TableUpdateType::Subscribe => QueryUpdate { + TableUpdateType::Subscribe => ws_v1::QueryUpdate { deletes: empty, inserts: rows, }, - TableUpdateType::Unsubscribe => QueryUpdate { + TableUpdateType::Unsubscribe => ws_v1::QueryUpdate { deletes: rows, inserts: empty, }, @@ -230,9 +228,9 @@ pub fn collect_table_update( // We will compress the outer server message, // after we release the tx lock. // There's no need to compress the inner table update too. - let update = F::into_query_update(qu, Compression::None); + let update = F::into_query_update(qu, ws_v1::Compression::None); ( - TableUpdate::new(table_id, table_name, SingleQueryUpdate { update, num_rows }), + ws_v1::TableUpdate::new(table_id, table_name, ws_v1::SingleQueryUpdate { update, num_rows }), metrics, ) }) @@ -245,7 +243,7 @@ pub fn execute_plans( tx: &(impl Datastore + DeltaStore + Sync), update_type: TableUpdateType, rlb_pool: &(impl Sync + RowListBuilderSource), -) -> Result<(DatabaseUpdate, ExecutionMetrics, Vec), DBError> { +) -> Result<(ws_v1::DatabaseUpdate, ExecutionMetrics, Vec), DBError> { plans .par_iter() .flat_map_iter(|plan| plan.plans_fragments().map(|fragment| (plan.sql(), fragment))) @@ -327,6 +325,6 @@ pub fn execute_plans( query_metrics_vec.push(qm); } } - (DatabaseUpdate { tables }, aggregated_metrics, query_metrics_vec) + (ws_v1::DatabaseUpdate { tables }, aggregated_metrics, query_metrics_vec) }) } diff --git a/crates/core/src/subscription/module_subscription_actor.rs b/crates/core/src/subscription/module_subscription_actor.rs index 38df7cea554..8f88ff22a36 100644 --- a/crates/core/src/subscription/module_subscription_actor.rs +++ b/crates/core/src/subscription/module_subscription_actor.rs @@ -17,7 +17,6 @@ use crate::error::DBError; use crate::estimation::estimate_rows_scanned; use crate::host::module_host::{DatabaseUpdate, EventStatus, ModuleEvent, RefInstance, WasmInstance}; use crate::host::{self, ModuleHost}; -use crate::messages::websocket::Subscribe; use crate::subscription::query::is_subscribe_to_all_tables; use crate::subscription::row_list_builder_pool::{BsatnRowListBuilderPool, JsonRowListBuilderFakePool}; use crate::subscription::{collect_table_update_for_view, execute_plans}; @@ -27,10 +26,7 @@ use crate::worker_metrics::WORKER_METRICS; use parking_lot::RwLock; use prometheus::{Histogram, HistogramTimer, IntCounter, IntGauge}; use scopeguard::ScopeGuard; -use spacetimedb_client_api_messages::websocket::{ - self as ws, BsatnFormat, FormatSwitch, JsonFormat, SubscribeMulti, SubscribeSingle, TableUpdate, Unsubscribe, - UnsubscribeMulti, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_datastore::db_metrics::DB_METRICS; use spacetimedb_datastore::execution_context::{Workload, WorkloadType}; use spacetimedb_datastore::locking_tx_datastore::datastore::TxMetrics; @@ -199,8 +195,10 @@ pub(crate) fn commit_and_broadcast_event( } type AssertTxFn = Arc; -type SubscriptionUpdate = FormatSwitch, TableUpdate>; -type FullSubscriptionUpdate = FormatSwitch, ws::DatabaseUpdate>; +type SubscriptionUpdate = + ws_v1::FormatSwitch, ws_v1::TableUpdate>; +type FullSubscriptionUpdate = + ws_v1::FormatSwitch, ws_v1::DatabaseUpdate>; /// A utility for sending an error message to a client and returning early macro_rules! return_on_err { @@ -372,7 +370,7 @@ impl ModuleSubscriptions { update_type, &self.bsatn_rlb_pool, ) - .map(|(table_update, metrics)| (FormatSwitch::Bsatn(table_update), metrics)) + .map(|(table_update, metrics)| (ws_v1::FormatSwitch::Bsatn(table_update), metrics)) } (Protocol::Binary, None) => { let plans = plans.into_iter().map(PipelinedProject::from).collect::>(); @@ -384,7 +382,7 @@ impl ModuleSubscriptions { update_type, &self.bsatn_rlb_pool, ) - .map(|(table_update, metrics)| (FormatSwitch::Bsatn(table_update), metrics)) + .map(|(table_update, metrics)| (ws_v1::FormatSwitch::Bsatn(table_update), metrics)) } (Protocol::Text, Some(view_info)) => { let plans = plans @@ -400,7 +398,7 @@ impl ModuleSubscriptions { update_type, &JsonRowListBuilderFakePool, ) - .map(|(table_update, metrics)| (FormatSwitch::Json(table_update), metrics)) + .map(|(table_update, metrics)| (ws_v1::FormatSwitch::Json(table_update), metrics)) } (Protocol::Text, None) => { let plans = plans.into_iter().map(PipelinedProject::from).collect::>(); @@ -412,7 +410,7 @@ impl ModuleSubscriptions { update_type, &JsonRowListBuilderFakePool, ) - .map(|(table_update, metrics)| (FormatSwitch::Json(table_update), metrics)) + .map(|(table_update, metrics)| (ws_v1::FormatSwitch::Json(table_update), metrics)) } }?) } @@ -443,12 +441,12 @@ impl ModuleSubscriptions { Protocol::Binary => { let (update, metrics, query_metrics) = execute_plans(auth, queries, &tx, update_type, &self.bsatn_rlb_pool)?; - (FormatSwitch::Bsatn(update), metrics, query_metrics) + (ws_v1::FormatSwitch::Bsatn(update), metrics, query_metrics) } Protocol::Text => { let (update, metrics, query_metrics) = execute_plans(auth, queries, &tx, update_type, &JsonRowListBuilderFakePool)?; - (FormatSwitch::Json(update), metrics, query_metrics) + (ws_v1::FormatSwitch::Json(update), metrics, query_metrics) } }; @@ -470,7 +468,7 @@ impl ModuleSubscriptions { host: Option<&ModuleHost>, sender: Arc, auth: AuthCtx, - request: SubscribeSingle, + request: ws_v1::SubscribeSingle, timer: Instant, _assert: Option, ) -> Result, DBError> { @@ -495,7 +493,7 @@ impl ModuleSubscriptions { instance: &mut RefInstance, sender: Arc, auth: AuthCtx, - request: SubscribeSingle, + request: ws_v1::SubscribeSingle, timer: Instant, _assert: Option, ) -> Result<(Option, bool), DBError> { @@ -507,7 +505,7 @@ impl ModuleSubscriptions { instance: Option<&mut RefInstance>, sender: Arc, auth: AuthCtx, - request: SubscribeSingle, + request: ws_v1::SubscribeSingle, timer: Instant, _assert: Option, ) -> Result<(Option, bool), DBError> { @@ -603,7 +601,7 @@ impl ModuleSubscriptions { &self, sender: Arc, auth: AuthCtx, - request: Unsubscribe, + request: ws_v1::Unsubscribe, timer: Instant, ) -> Result, DBError> { // Send an error message to the client @@ -677,7 +675,7 @@ impl ModuleSubscriptions { &self, sender: Arc, auth: AuthCtx, - request: UnsubscribeMulti, + request: ws_v1::UnsubscribeMulti, timer: Instant, ) -> Result, DBError> { // Send an error message to the client @@ -879,7 +877,7 @@ impl ModuleSubscriptions { host: Option<&ModuleHost>, sender: Arc, auth: AuthCtx, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, _assert: Option, ) -> Result, DBError> { @@ -903,7 +901,7 @@ impl ModuleSubscriptions { instance: &mut RefInstance, sender: Arc, auth: AuthCtx, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, _assert: Option, ) -> Result<(Option, bool), DBError> { @@ -915,7 +913,7 @@ impl ModuleSubscriptions { instance: Option<&mut RefInstance>, sender: Arc, auth: AuthCtx, - request: SubscribeMulti, + request: ws_v1::SubscribeMulti, timer: Instant, _assert: Option, ) -> Result<(Option, bool), DBError> { @@ -1033,7 +1031,7 @@ impl ModuleSubscriptions { host: Option<&ModuleHost>, sender: Arc, auth: AuthCtx, - subscription: Subscribe, + subscription: ws_v1::Subscribe, timer: Instant, _assert: Option, ) -> Result { @@ -1062,7 +1060,7 @@ impl ModuleSubscriptions { instance: &mut RefInstance, sender: Arc, auth: AuthCtx, - subscription: Subscribe, + subscription: ws_v1::Subscribe, timer: Instant, _assert: Option, ) -> Result<(ExecutionMetrics, bool), DBError> { @@ -1074,7 +1072,7 @@ impl ModuleSubscriptions { instance: Option<&mut RefInstance>, sender: Arc, auth: AuthCtx, - subscription: Subscribe, + subscription: ws_v1::Subscribe, timer: Instant, _assert: Option, ) -> Result<(ExecutionMetrics, bool), DBError> { @@ -1113,7 +1111,7 @@ impl ModuleSubscriptions { let (database_update, metrics, query_metrics) = match sender.config.protocol { Protocol::Binary => execute_plans(&auth, &queries, &tx, TableUpdateType::Subscribe, &self.bsatn_rlb_pool) .map(|(table_update, metrics, query_metrics)| { - (FormatSwitch::Bsatn(table_update), metrics, query_metrics) + (ws_v1::FormatSwitch::Bsatn(table_update), metrics, query_metrics) })?, Protocol::Text => execute_plans( &auth, @@ -1122,7 +1120,9 @@ impl ModuleSubscriptions { TableUpdateType::Subscribe, &JsonRowListBuilderFakePool, ) - .map(|(table_update, metrics, query_metrics)| (FormatSwitch::Json(table_update), metrics, query_metrics))?, + .map(|(table_update, metrics, query_metrics)| { + (ws_v1::FormatSwitch::Json(table_update), metrics, query_metrics) + })?, }; record_query_metrics(&self.relational_db.database_identity(), query_metrics); @@ -1447,7 +1447,6 @@ mod tests { use crate::db::relational_db::{Persistence, RelationalDB, Txdata}; use crate::error::DBError; use crate::host::module_host::{DatabaseUpdate, EventStatus, ModuleEvent, ModuleFunctionCall}; - use crate::messages::websocket as ws; use crate::sql::execute::run; use crate::subscription::module_subscription_actor::commit_and_broadcast_event; use crate::subscription::module_subscription_manager::{spawn_send_worker, SubscriptionManager}; @@ -1459,10 +1458,7 @@ mod tests { use itertools::Itertools; use pretty_assertions::assert_matches; use spacetimedb_client_api_messages::energy::EnergyQuanta; - use spacetimedb_client_api_messages::websocket::{ - CompressableQueryUpdate, Compression, FormatSwitch, QueryId, Subscribe, SubscribeMulti, SubscribeSingle, - TableUpdate, Unsubscribe, UnsubscribeMulti, - }; + use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_commitlog::{commitlog, repo}; use spacetimedb_data_structures::map::{HashCollectionExt as _, HashMap}; use spacetimedb_datastore::system_tables::{StRowLevelSecurityRow, ST_ROW_LEVEL_SECURITY_ID}; @@ -1502,7 +1498,7 @@ mod tests { ); let auth = AuthCtx::new(owner, sender.auth.claims.identity); - let subscribe = Subscribe { + let subscribe = ws_v1::Subscribe { query_strings: [sql.into()].into(), request_id: 0, }; @@ -1622,39 +1618,39 @@ mod tests { } /// A [SubscribeSingle] message for testing - fn single_subscribe(sql: &str, query_id: u32) -> SubscribeSingle { - SubscribeSingle { + fn single_subscribe(sql: &str, query_id: u32) -> ws_v1::SubscribeSingle { + ws_v1::SubscribeSingle { query: sql.into(), request_id: 0, - query_id: QueryId::new(query_id), + query_id: ws_v1::QueryId::new(query_id), } } /// A [SubscribeMulti] message for testing - fn multi_subscribe(query_strings: &[&'static str], query_id: u32) -> SubscribeMulti { - SubscribeMulti { + fn multi_subscribe(query_strings: &[&'static str], query_id: u32) -> ws_v1::SubscribeMulti { + ws_v1::SubscribeMulti { query_strings: query_strings .iter() .map(|sql| String::from(*sql).into_boxed_str()) .collect(), request_id: 0, - query_id: QueryId::new(query_id), + query_id: ws_v1::QueryId::new(query_id), } } /// A [SubscribeMulti] message for testing - fn multi_unsubscribe(query_id: u32) -> UnsubscribeMulti { - UnsubscribeMulti { + fn multi_unsubscribe(query_id: u32) -> ws_v1::UnsubscribeMulti { + ws_v1::UnsubscribeMulti { request_id: 0, - query_id: QueryId::new(query_id), + query_id: ws_v1::QueryId::new(query_id), } } /// An [Unsubscribe] message for testing - fn single_unsubscribe(query_id: u32) -> Unsubscribe { - Unsubscribe { + fn single_unsubscribe(query_id: u32) -> ws_v1::Unsubscribe { + ws_v1::Unsubscribe { request_id: 0, - query_id: QueryId::new(query_id), + query_id: ws_v1::QueryId::new(query_id), } } @@ -1706,7 +1702,7 @@ mod tests { fn client_connection_with_compression( client_id: ClientActorId, db: &Arc, - compression: Compression, + compression: ws_v1::Compression, ) -> (Arc, ClientConnectionReceiver) { client_connection_with_config( client_id, @@ -1725,7 +1721,7 @@ mod tests { client_id: ClientActorId, db: &Arc, ) -> (Arc, ClientConnectionReceiver) { - client_connection_with_compression(client_id, db, Compression::None) + client_connection_with_compression(client_id, db, ws_v1::Compression::None) } /// Instantiate a client connection with confirmed reads turned on or off. @@ -1739,7 +1735,7 @@ mod tests { db, ClientConfig { protocol: Protocol::Binary, - compression: Compression::None, + compression: ws_v1::Compression::None, tx_update_full: true, confirmed_reads, }, @@ -1846,7 +1842,7 @@ mod tests { Some(SerializableMessage::TxUpdate(TransactionUpdateMessage { database_update: SubscriptionUpdateMessage { - database_update: FormatSwitch::Bsatn(ws::DatabaseUpdate { mut tables }), + database_update: ws_v1::FormatSwitch::Bsatn(ws_v1::DatabaseUpdate { mut tables }), .. }, .. @@ -1865,7 +1861,7 @@ mod tests { let mut rows_received: HashMap = HashMap::new(); for uncompressed in table_update.updates { - let CompressableQueryUpdate::Uncompressed(table_update) = uncompressed else { + let ws_v1::CompressableQueryUpdate::Uncompressed(table_update) = uncompressed else { panic!("expected an uncompressed table update") }; @@ -2511,7 +2507,7 @@ mod tests { let client_id = client_id_from_u8(1); // Establish a client connection with compression - let (tx, mut rx) = client_connection_with_compression(client_id, &db, Compression::Brotli); + let (tx, mut rx) = client_connection_with_compression(client_id, &db, ws_v1::Compression::Brotli); let auth = AuthCtx::new(db.owner_identity(), client_id.identity); let subs = ModuleSubscriptions::for_test_enclosing_runtime(db.clone()); @@ -2536,13 +2532,13 @@ mod tests { Some(SerializableMessage::Subscription(SubscriptionMessage { result: SubscriptionResult::SubscribeMulti(SubscriptionData { - data: FormatSwitch::Bsatn(ws::DatabaseUpdate { tables }), + data: ws_v1::FormatSwitch::Bsatn(ws_v1::DatabaseUpdate { tables }), }), .. })) => { - assert!(tables.iter().all(|TableUpdate { updates, .. }| updates + assert!(tables.iter().all(|ws_v1::TableUpdate { updates, .. }| updates .iter() - .all(|query_update| matches!(query_update, CompressableQueryUpdate::Uncompressed(_))))); + .all(|query_update| matches!(query_update, ws_v1::CompressableQueryUpdate::Uncompressed(_))))); } Some(_) => panic!("unexpected message from subscription"), None => panic!("channel unexpectedly closed"), @@ -2626,7 +2622,7 @@ mod tests { // Establish a client connection with compression let client_id = client_id_from_u8(1); - let (tx, mut rx) = client_connection_with_compression(client_id, &db, Compression::Brotli); + let (tx, mut rx) = client_connection_with_compression(client_id, &db, ws_v1::Compression::Brotli); let auth = AuthCtx::new(db.owner_identity(), client_id.identity); let subs = ModuleSubscriptions::for_test_enclosing_runtime(db.clone()); @@ -2654,14 +2650,14 @@ mod tests { Some(SerializableMessage::TxUpdate(TransactionUpdateMessage { database_update: SubscriptionUpdateMessage { - database_update: FormatSwitch::Bsatn(ws::DatabaseUpdate { tables }), + database_update: ws_v1::FormatSwitch::Bsatn(ws_v1::DatabaseUpdate { tables }), .. }, .. })) => { - assert!(tables.iter().all(|TableUpdate { updates, .. }| updates + assert!(tables.iter().all(|ws_v1::TableUpdate { updates, .. }| updates .iter() - .all(|query_update| matches!(query_update, CompressableQueryUpdate::Uncompressed(_))))); + .all(|query_update| matches!(query_update, ws_v1::CompressableQueryUpdate::Uncompressed(_))))); } Some(_) => panic!("unexpected message from subscription"), None => panic!("channel unexpectedly closed"), diff --git a/crates/core/src/subscription/module_subscription_manager.rs b/crates/core/src/subscription/module_subscription_manager.rs index 6b93e996ce4..b6662f91076 100644 --- a/crates/core/src/subscription/module_subscription_manager.rs +++ b/crates/core/src/subscription/module_subscription_manager.rs @@ -7,7 +7,6 @@ use crate::client::messages::{ use crate::client::{ClientConnectionSender, Protocol}; use crate::error::DBError; use crate::host::module_host::{DatabaseTableUpdate, ModuleEvent, UpdatesRelValue}; -use crate::messages::websocket::{self as ws, TableUpdate}; use crate::subscription::delta::eval_delta; use crate::subscription::row_list_builder_pool::{BsatnRowListBuilderPool, JsonRowListBuilderFakePool}; use crate::subscription::websocket_building::{BuildableWebsocketFormat, RowListBuilderSource}; @@ -15,9 +14,7 @@ use crate::worker_metrics::WORKER_METRICS; use core::mem; use parking_lot::RwLock; use prometheus::IntGauge; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, CompressableQueryUpdate, FormatSwitch, JsonFormat, QueryId, QueryUpdate, SingleQueryUpdate, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_data_structures::map::HashCollectionExt as _; use spacetimedb_data_structures::map::{ hash_map::{Entry, OccupiedError}, @@ -42,11 +39,13 @@ use tokio::sync::{mpsc, oneshot}; type ClientId = (Identity, ConnectionId); type Query = Arc; type Client = Arc; -type SwitchedTableUpdate = FormatSwitch, TableUpdate>; -type SwitchedDbUpdate = FormatSwitch, ws::DatabaseUpdate>; +type SwitchedTableUpdate = + ws_v1::FormatSwitch, ws_v1::TableUpdate>; +type SwitchedDbUpdate = + ws_v1::FormatSwitch, ws_v1::DatabaseUpdate>; /// ClientQueryId is an identifier for a query set by the client. -type ClientQueryId = QueryId; +type ClientQueryId = ws_v1::QueryId; /// SubscriptionId is a globally unique identifier for a subscription. type SubscriptionId = (ClientId, ClientQueryId); @@ -515,7 +514,8 @@ struct ClientUpdate { id: ClientId, table_id: TableId, table_name: TableName, - update: FormatSwitch, SingleQueryUpdate>, + update: + ws_v1::FormatSwitch, ws_v1::SingleQueryUpdate>, } /// The computed incremental update queries with sufficient information @@ -1146,7 +1146,7 @@ impl SubscriptionManager { event: Arc, caller: Option>, ) -> ExecutionMetrics { - use FormatSwitch::{Bsatn, Json}; + use ws_v1::FormatSwitch::{Bsatn, Json}; let tables = &event.status.database_update().unwrap().tables; @@ -1225,15 +1225,15 @@ impl SubscriptionManager { // the risks of holding the tx lock for longer than necessary, // as well as additional the message processing overhead on the client, // outweighed the benefit of reduced cpu with the former approach. - let mut ops_bin_uncompressed: Option<(CompressableQueryUpdate, _, _)> = None; - let mut ops_json: Option<(QueryUpdate, _, _)> = None; + let mut ops_bin_uncompressed: Option<(ws_v1::CompressableQueryUpdate, _, _)> = None; + let mut ops_json: Option<(ws_v1::QueryUpdate, _, _)> = None; fn memo_encode( updates: &UpdatesRelValue<'_>, memory: &mut Option<(F::QueryUpdate, u64, usize)>, metrics: &mut ExecutionMetrics, rlb_pool: &impl RowListBuilderSource, - ) -> SingleQueryUpdate { + ) -> ws_v1::SingleQueryUpdate { let (update, num_rows, num_bytes) = memory .get_or_insert_with(|| { // TODO(centril): consider pushing the encoding of each row into @@ -1251,7 +1251,7 @@ impl SubscriptionManager { // Therefore every time we call this function, // we update the `bytes_sent_to_clients` metric. metrics.bytes_sent_to_clients += num_bytes; - SingleQueryUpdate { update, num_rows } + ws_v1::SingleQueryUpdate { update, num_rows } } let clients_for_query = qstate.all_clients(); @@ -1279,13 +1279,13 @@ impl SubscriptionManager { let row_iter = clients_for_query.map(|id| { let client = &self.clients[id].outbound_ref; let update = match client.config.protocol { - Protocol::Binary => Bsatn(memo_encode::( + Protocol::Binary => Bsatn(memo_encode::( &delta_updates, &mut ops_bin_uncompressed, &mut acc.metrics, bsatn_rlb_pool, )), - Protocol::Text => Json(memo_encode::( + Protocol::Text => Json(memo_encode::( &delta_updates, &mut ops_json, &mut acc.metrics, @@ -1515,7 +1515,7 @@ impl SendWorker { caller, }: ComputedQueries, ) { - use FormatSwitch::{Bsatn, Json}; + use ws_v1::FormatSwitch::{Bsatn, Json}; let clients_with_errors = errs.iter().map(|(id, _)| id).collect::>(); @@ -1544,8 +1544,10 @@ impl SendWorker { Json((tbl_upd, update)) => tbl_upd.push(update), }, Entry::Vacant(entry) => drop(entry.insert(match upd.update { - Bsatn(update) => Bsatn(TableUpdate::new(upd.table_id, (&*upd.table_name).into(), update)), - Json(update) => Json(TableUpdate::new(upd.table_id, (&*upd.table_name).into(), update)), + Bsatn(update) => { + Bsatn(ws_v1::TableUpdate::new(upd.table_id, (&*upd.table_name).into(), update)) + } + Json(update) => Json(ws_v1::TableUpdate::new(upd.table_id, (&*upd.table_name).into(), update)), })), } tables @@ -1647,7 +1649,7 @@ fn send_to_client( mod tests { use std::{sync::Arc, time::Duration}; - use spacetimedb_client_api_messages::websocket::QueryId; + use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_lib::AlgebraicValue; use spacetimedb_lib::{error::ResultTest, identity::AuthCtx, AlgebraicType, ConnectionId, Identity, Timestamp}; use spacetimedb_primitives::{ColId, TableId}; @@ -1741,7 +1743,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1764,7 +1766,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1790,7 +1792,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1799,7 +1801,9 @@ mod tests { subscriptions.add_subscription(client.clone(), plan.clone(), query_id)?; let client_id = (client.id.identity, client.id.connection_id); - assert!(subscriptions.remove_subscription(client_id, QueryId::new(2)).is_err()); + assert!(subscriptions + .remove_subscription(client_id, ws_v1::QueryId::new(2)) + .is_err()); Ok(()) } @@ -1815,14 +1819,14 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); let mut subscriptions = SubscriptionManager::for_test_without_metrics(); subscriptions.add_subscription(client.clone(), plan.clone(), query_id)?; - subscriptions.add_subscription(client.clone(), plan.clone(), QueryId::new(2))?; + subscriptions.add_subscription(client.clone(), plan.clone(), ws_v1::QueryId::new(2))?; let client_id = (client.id.identity, client.id.connection_id); subscriptions.remove_subscription(client_id, query_id)?; @@ -1844,7 +1848,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1853,7 +1857,8 @@ mod tests { let added_query = subscriptions.add_subscription_multi(client.clone(), vec![plan.clone()], query_id)?; assert!(added_query.len() == 1); assert_eq!(added_query[0].hash, hash); - let second_one = subscriptions.add_subscription_multi(client.clone(), vec![plan.clone()], QueryId::new(2))?; + let second_one = + subscriptions.add_subscription_multi(client.clone(), vec![plan.clone()], ws_v1::QueryId::new(2))?; assert!(second_one.is_empty()); let client_id = (client.id.identity, client.id.connection_id); @@ -1861,7 +1866,7 @@ mod tests { assert!(removed_queries.is_empty()); assert!(subscriptions.query_reads_from_table(&hash, &table_id)); - let removed_queries = subscriptions.remove_subscription(client_id, QueryId::new(2))?; + let removed_queries = subscriptions.remove_subscription(client_id, ws_v1::QueryId::new(2))?; assert!(removed_queries.len() == 1); assert_eq!(removed_queries[0].hash, hash); @@ -1882,7 +1887,7 @@ mod tests { let clients = (0..3).map(|i| Arc::new(client(i, &db))).collect::>(); // All of the clients are using the same query id. - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1923,7 +1928,7 @@ mod tests { let clients = (0..3).map(|i| Arc::new(client(i, &db))).collect::>(); // All of the clients are using the same query id. - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -1977,15 +1982,15 @@ mod tests { let _rt = runtime.enter(); let mut subscriptions = SubscriptionManager::for_test_without_metrics(); - subscriptions.add_subscription(client.clone(), queries[0].clone(), QueryId::new(1))?; - subscriptions.add_subscription(client.clone(), queries[1].clone(), QueryId::new(2))?; - subscriptions.add_subscription(client.clone(), queries[2].clone(), QueryId::new(3))?; + subscriptions.add_subscription(client.clone(), queries[0].clone(), ws_v1::QueryId::new(1))?; + subscriptions.add_subscription(client.clone(), queries[1].clone(), ws_v1::QueryId::new(2))?; + subscriptions.add_subscription(client.clone(), queries[2].clone(), ws_v1::QueryId::new(3))?; for i in 0..3 { assert!(subscriptions.query_reads_from_table(&queries[i].hash(), &table_ids[i])); } let client_id = (client.id.identity, client.id.connection_id); - subscriptions.remove_subscription(client_id, QueryId::new(1))?; + subscriptions.remove_subscription(client_id, ws_v1::QueryId::new(1))?; assert!(!subscriptions.query_reads_from_table(&queries[0].hash(), &table_ids[0])); // Assert that the rest are there. for i in 1..3 { @@ -2022,13 +2027,16 @@ mod tests { let _rt = runtime.enter(); let mut subscriptions = SubscriptionManager::for_test_without_metrics(); - let added = subscriptions.add_subscription_multi(client.clone(), vec![queries[0].clone()], QueryId::new(1))?; + let added = + subscriptions.add_subscription_multi(client.clone(), vec![queries[0].clone()], ws_v1::QueryId::new(1))?; assert_eq!(added.len(), 1); assert_eq!(added[0].hash, queries[0].hash()); - let added = subscriptions.add_subscription_multi(client.clone(), vec![queries[1].clone()], QueryId::new(2))?; + let added = + subscriptions.add_subscription_multi(client.clone(), vec![queries[1].clone()], ws_v1::QueryId::new(2))?; assert_eq!(added.len(), 1); assert_eq!(added[0].hash, queries[1].hash()); - let added = subscriptions.add_subscription_multi(client.clone(), vec![queries[2].clone()], QueryId::new(3))?; + let added = + subscriptions.add_subscription_multi(client.clone(), vec![queries[2].clone()], ws_v1::QueryId::new(3))?; assert_eq!(added.len(), 1); assert_eq!(added[0].hash, queries[2].hash()); for i in 0..3 { @@ -2036,7 +2044,7 @@ mod tests { } let client_id = (client.id.identity, client.id.connection_id); - let removed = subscriptions.remove_subscription(client_id, QueryId::new(1))?; + let removed = subscriptions.remove_subscription(client_id, ws_v1::QueryId::new(1))?; assert_eq!(removed.len(), 1); assert_eq!(removed[0].hash, queries[0].hash()); assert!(!subscriptions.query_reads_from_table(&queries[0].hash(), &table_ids[0])); @@ -2074,8 +2082,11 @@ mod tests { .collect::>>()?; for (i, query) in queries.iter().enumerate().take(5) { - let added = - subscriptions.add_subscription_multi(client.clone(), vec![query.clone()], QueryId::new(i as u32))?; + let added = subscriptions.add_subscription_multi( + client.clone(), + vec![query.clone()], + ws_v1::QueryId::new(i as u32), + )?; assert_eq!(added.len(), 1); assert_eq!(added[0].hash, queries[i].hash); } @@ -2091,7 +2102,7 @@ mod tests { } // Remove one of the subscriptions - let query_id = QueryId::new(2); + let query_id = ws_v1::QueryId::new(2); let client_id = (client.id.identity, client.id.connection_id); let removed = subscriptions.remove_subscription(client_id, query_id)?; assert_eq!(removed.len(), 1); @@ -2147,7 +2158,7 @@ mod tests { .collect::>>()?; for (i, query) in queries.iter().enumerate() { - subscriptions.add_subscription_multi(client.clone(), vec![query.clone()], QueryId::new(i as u32))?; + subscriptions.add_subscription_multi(client.clone(), vec![query.clone()], ws_v1::QueryId::new(i as u32))?; } let hash_for_2 = queries[2].hash; @@ -2213,7 +2224,7 @@ mod tests { let plan = compile_plan(&db, "select t.* from t join s on t.id = s.id where s.a = 1")?; let hash = plan.hash; - subscriptions.add_subscription_multi(client.clone(), vec![plan], QueryId::new(0))?; + subscriptions.add_subscription_multi(client.clone(), vec![plan], ws_v1::QueryId::new(0))?; // Do we need to evaluate the above join query for this table update? // Yes, because the above query does not filter on `t`. @@ -2277,7 +2288,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); @@ -2302,7 +2313,7 @@ mod tests { let client = Arc::new(client(0, &db)); - let query_id: ClientQueryId = QueryId::new(1); + let query_id: ClientQueryId = ws_v1::QueryId::new(1); let runtime = tokio::runtime::Runtime::new().unwrap(); let _rt = runtime.enter(); diff --git a/crates/core/src/subscription/query.rs b/crates/core/src/subscription/query.rs index 968a092e5d2..c9829385e8d 100644 --- a/crates/core/src/subscription/query.rs +++ b/crates/core/src/subscription/query.rs @@ -162,7 +162,7 @@ mod tests { use crate::vm::tests::create_table_with_rows; use crate::vm::DbProgram; use itertools::Itertools; - use spacetimedb_client_api_messages::websocket::{BsatnFormat, CompressableQueryUpdate, Compression}; + use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_datastore::execution_context::Workload; use spacetimedb_lib::bsatn; use spacetimedb_lib::db::auth::{StAccess, StTableType}; @@ -357,7 +357,7 @@ mod tests { rows: &[ProductValue], ) -> ResultTest<()> { let result = s - .eval::(db, tx, &BsatnRowListBuilderPool::new(), None, Compression::None) + .eval::(db, tx, &BsatnRowListBuilderPool::new(), None, ws_v1::Compression::None) .tables; assert_eq!( result.len(), @@ -369,7 +369,7 @@ mod tests { .into_iter() .flat_map(|x| x.updates) .map(|x| match x { - CompressableQueryUpdate::Uncompressed(x) => x, + ws_v1::CompressableQueryUpdate::Uncompressed(x) => x, _ => unreachable!(), }) .flat_map(|x| { diff --git a/crates/core/src/subscription/row_list_builder_pool.rs b/crates/core/src/subscription/row_list_builder_pool.rs index 3deab7016fb..1199a0f0b43 100644 --- a/crates/core/src/subscription/row_list_builder_pool.rs +++ b/crates/core/src/subscription/row_list_builder_pool.rs @@ -2,7 +2,7 @@ use crate::subscription::websocket_building::{BsatnRowListBuilder, BuildableWebs use bytes::{Bytes, BytesMut}; use core::sync::atomic::{AtomicUsize, Ordering}; use derive_more::Deref; -use spacetimedb_client_api_messages::websocket::{BsatnFormat, JsonFormat}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_data_structures::object_pool::{Pool, PooledObject}; use spacetimedb_memory_usage::MemoryUsage; @@ -70,7 +70,7 @@ impl BsatnRowListBuilderPool { } } -impl RowListBuilderSource for BsatnRowListBuilderPool { +impl RowListBuilderSource for BsatnRowListBuilderPool { fn take_row_list_builder(&self) -> BsatnRowListBuilder { let PooledBuffer(buffer) = self.pool.take( |buffer| buffer.0.clear(), @@ -83,8 +83,8 @@ impl RowListBuilderSource for BsatnRowListBuilderPool { /// The "pool" for the builder for the [`JsonFormat`]. pub(crate) struct JsonRowListBuilderFakePool; -impl RowListBuilderSource for JsonRowListBuilderFakePool { - fn take_row_list_builder(&self) -> ::ListBuilder { +impl RowListBuilderSource for JsonRowListBuilderFakePool { + fn take_row_list_builder(&self) -> ::ListBuilder { Vec::new() } } diff --git a/crates/core/src/subscription/subscription.rs b/crates/core/src/subscription/subscription.rs index d96aee8fc93..d979de4d755 100644 --- a/crates/core/src/subscription/subscription.rs +++ b/crates/core/src/subscription/subscription.rs @@ -26,13 +26,12 @@ use super::query; use crate::db::relational_db::{RelationalDB, Tx}; use crate::error::{DBError, SubscriptionError}; use crate::host::module_host::{DatabaseTableUpdate, DatabaseUpdateRelValue, UpdatesRelValue}; -use crate::messages::websocket as ws; use crate::sql::ast::SchemaViewer; use crate::subscription::websocket_building::{BuildableWebsocketFormat, RowListBuilderSource}; use crate::vm::{build_query, TxMode}; use anyhow::Context; use itertools::Either; -use spacetimedb_client_api_messages::websocket::Compression; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_data_structures::map::HashSet; use spacetimedb_datastore::locking_tx_datastore::state_view::StateView; use spacetimedb_datastore::locking_tx_datastore::TxId; @@ -519,8 +518,8 @@ impl ExecutionSet { tx: &Tx, rlb_pool: &impl RowListBuilderSource, slow_query_threshold: Option, - compression: Compression, - ) -> ws::DatabaseUpdate { + compression: ws_v1::Compression, + ) -> ws_v1::DatabaseUpdate { // evaluate each of the execution units in this ExecutionSet in parallel let tables = self .exec_units @@ -528,7 +527,7 @@ impl ExecutionSet { .iter() .filter_map(|unit| unit.eval(db, tx, rlb_pool, &unit.sql, slow_query_threshold, compression)) .collect(); - ws::DatabaseUpdate { tables } + ws_v1::DatabaseUpdate { tables } } #[tracing::instrument(level = "trace", skip_all)] diff --git a/crates/core/src/subscription/websocket_building.rs b/crates/core/src/subscription/websocket_building.rs index 43944e2df34..597423e8b31 100644 --- a/crates/core/src/subscription/websocket_building.rs +++ b/crates/core/src/subscription/websocket_building.rs @@ -1,10 +1,7 @@ use bytes::BytesMut; use bytestring::ByteString; use core::mem; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, BsatnRowList, CompressableQueryUpdate, Compression, JsonFormat, QueryUpdate, RowOffset, RowSize, - RowSizeHint, WebsocketFormat, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_sats::bsatn::{self, ToBsatn}; use spacetimedb_sats::ser::serde::SerializeWrapper; use spacetimedb_sats::Serialize; @@ -28,7 +25,7 @@ pub trait RowListBuilder: Default { fn finish(self) -> Self::FinishedList; } -pub trait BuildableWebsocketFormat: WebsocketFormat { +pub trait BuildableWebsocketFormat: ws_v1::WebsocketFormat { /// The builder for [`WebsocketFormat::List`]. type ListBuilder: RowListBuilder; @@ -49,13 +46,13 @@ pub trait BuildableWebsocketFormat: WebsocketFormat { /// Convert a `QueryUpdate` into [`WebsocketFormat::QueryUpdate`]. /// This allows some formats to e.g., compress the update. - fn into_query_update(qu: QueryUpdate, compression: Compression) -> Self::QueryUpdate; + fn into_query_update(qu: ws_v1::QueryUpdate, compression: ws_v1::Compression) -> Self::QueryUpdate; } -impl BuildableWebsocketFormat for JsonFormat { +impl BuildableWebsocketFormat for ws_v1::JsonFormat { type ListBuilder = Self::List; - fn into_query_update(qu: QueryUpdate, _: Compression) -> Self::QueryUpdate { + fn into_query_update(qu: ws_v1::QueryUpdate, _: ws_v1::Compression) -> Self::QueryUpdate { qu } } @@ -91,12 +88,12 @@ pub enum RowSizeHintBuilder { FixedSizeDyn(usize), /// Each row in `rows_data` is of the same fixed size as specified here /// and we know that this will be the case for future rows as well. - FixedSizeStatic(RowSize), + FixedSizeStatic(ws_v1::RowSize), /// The offsets into `rows_data` defining the boundaries of each row. /// Only stores the offset to the start of each row. /// The ends of each row is inferred from the start of the next row, or `rows_data.len()`. /// The behavior of this is identical to that of `PackedStr`. - RowOffsets(Vec), + RowOffsets(Vec), } impl BsatnRowListBuilder { @@ -114,7 +111,7 @@ impl Default for RowSizeHintBuilder { } impl RowListBuilder for BsatnRowListBuilder { - type FinishedList = BsatnRowList; + type FinishedList = ws_v1::BsatnRowList; fn push(&mut self, row: impl ToBsatn + Serialize) { use RowSizeHintBuilder::*; @@ -161,16 +158,18 @@ impl RowListBuilder for BsatnRowListBuilder { fn finish(self) -> Self::FinishedList { let Self { size_hint, rows_data } = self; let size_hint = match size_hint { - RowSizeHintBuilder::Empty => RowSizeHint::RowOffsets([].into()), - RowSizeHintBuilder::FixedSizeStatic(fs) => RowSizeHint::FixedSize(fs), + RowSizeHintBuilder::Empty => ws_v1::RowSizeHint::RowOffsets([].into()), + RowSizeHintBuilder::FixedSizeStatic(fs) => ws_v1::RowSizeHint::FixedSize(fs), RowSizeHintBuilder::FixedSizeDyn(fs) => match u16::try_from(fs) { - Ok(fs) => RowSizeHint::FixedSize(fs), - Err(_) => RowSizeHint::RowOffsets(collect_offsets_from_num_rows(rows_data.len() / fs, fs).into()), + Ok(fs) => ws_v1::RowSizeHint::FixedSize(fs), + Err(_) => { + ws_v1::RowSizeHint::RowOffsets(collect_offsets_from_num_rows(rows_data.len() / fs, fs).into()) + } }, - RowSizeHintBuilder::RowOffsets(ro) => RowSizeHint::RowOffsets(ro.into()), + RowSizeHintBuilder::RowOffsets(ro) => ws_v1::RowSizeHint::RowOffsets(ro.into()), }; let rows_data = rows_data.into(); - BsatnRowList::new(size_hint, rows_data) + ws_v1::BsatnRowList::new(size_hint, rows_data) } } @@ -178,31 +177,31 @@ fn collect_offsets_from_num_rows(num_rows: usize, size: usize) -> Vec { (0..num_rows).map(|i| i * size).map(|o| o as u64).collect() } -impl BuildableWebsocketFormat for BsatnFormat { +impl BuildableWebsocketFormat for ws_v1::BsatnFormat { type ListBuilder = BsatnRowListBuilder; - fn into_query_update(qu: QueryUpdate, compression: Compression) -> Self::QueryUpdate { + fn into_query_update(qu: ws_v1::QueryUpdate, compression: ws_v1::Compression) -> Self::QueryUpdate { let qu_len_would_have_been = bsatn::to_len(&qu).unwrap(); match decide_compression(qu_len_would_have_been, compression) { - Compression::None => CompressableQueryUpdate::Uncompressed(qu), - Compression::Brotli => { + ws_v1::Compression::None => ws_v1::CompressableQueryUpdate::Uncompressed(qu), + ws_v1::Compression::Brotli => { let bytes = bsatn::to_vec(&qu).unwrap(); let mut out = Vec::new(); brotli_compress(&bytes, &mut out); - CompressableQueryUpdate::Brotli(out.into()) + ws_v1::CompressableQueryUpdate::Brotli(out.into()) } - Compression::Gzip => { + ws_v1::Compression::Gzip => { let bytes = bsatn::to_vec(&qu).unwrap(); let mut out = Vec::new(); gzip_compress(&bytes, &mut out); - CompressableQueryUpdate::Gzip(out.into()) + ws_v1::CompressableQueryUpdate::Gzip(out.into()) } } } } -pub fn decide_compression(len: usize, compression: Compression) -> Compression { +pub fn decide_compression(len: usize, compression: ws_v1::Compression) -> ws_v1::Compression { /// The threshold beyond which we start to compress messages. /// 1KiB was chosen without measurement. /// TODO(perf): measure! @@ -211,7 +210,7 @@ pub fn decide_compression(len: usize, compression: Compression) -> Compression { if len > COMPRESS_THRESHOLD { compression } else { - Compression::None + ws_v1::Compression::None } } diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index 2d06a8dddce..e9728f5866b 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -12,6 +12,7 @@ spacetimedb-lib.workspace = true spacetimedb-core.workspace = true spacetimedb-standalone.workspace = true spacetimedb-client-api.workspace = true +spacetimedb-client-api-messages.workspace = true spacetimedb-paths.workspace = true spacetimedb-schema.workspace = true diff --git a/crates/testing/src/modules.rs b/crates/testing/src/modules.rs index 2f12b1d690e..099eb2d083f 100644 --- a/crates/testing/src/modules.rs +++ b/crates/testing/src/modules.rs @@ -21,8 +21,8 @@ use spacetimedb::client::{ClientActorId, ClientConfig, ClientConnection, DataMes use spacetimedb::database_logger::DatabaseLogger; use spacetimedb::db::{Config, Storage}; use spacetimedb::host::FunctionArgs; -use spacetimedb::messages::websocket::CallReducerFlags; use spacetimedb_client_api::{ControlStateReadAccess, ControlStateWriteAccess, DatabaseDef, NodeDelegate}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_lib::{bsatn, sats}; pub use spacetimedb::database_logger::LogLevel; @@ -59,7 +59,7 @@ impl ModuleHandle { async fn call_reducer(&self, reducer: &str, args: FunctionArgs) -> anyhow::Result<()> { let result = self .client - .call_reducer(reducer, args, 0, Instant::now(), CallReducerFlags::FullUpdate) + .call_reducer(reducer, args, 0, Instant::now(), ws_v1::CallReducerFlags::FullUpdate) .await; let result = match result { Ok(result) => result.into(), diff --git a/sdks/rust/src/compression.rs b/sdks/rust/src/compression.rs index e190cbcb709..ac191a42873 100644 --- a/sdks/rust/src/compression.rs +++ b/sdks/rust/src/compression.rs @@ -1,8 +1,5 @@ use crate::websocket::WsError; -use spacetimedb_client_api_messages::websocket::{ - BsatnFormat, CompressableQueryUpdate, QueryUpdate, SERVER_MSG_COMPRESSION_TAG_BROTLI, - SERVER_MSG_COMPRESSION_TAG_GZIP, SERVER_MSG_COMPRESSION_TAG_NONE, -}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_sats::bsatn; use std::borrow::Cow; use std::io::{self, Read as _}; @@ -20,14 +17,16 @@ fn gzip_decompress(bytes: &[u8]) -> Result, io::Error> { Ok(decompressed) } -pub(crate) fn maybe_decompress_cqu(cqu: CompressableQueryUpdate) -> QueryUpdate { +pub(crate) fn maybe_decompress_cqu( + cqu: ws_v1::CompressableQueryUpdate, +) -> ws_v1::QueryUpdate { match cqu { - CompressableQueryUpdate::Uncompressed(qu) => qu, - CompressableQueryUpdate::Brotli(bytes) => { + ws_v1::CompressableQueryUpdate::Uncompressed(qu) => qu, + ws_v1::CompressableQueryUpdate::Brotli(bytes) => { let bytes = brotli_decompress(&bytes).unwrap(); bsatn::from_slice(&bytes).unwrap() } - CompressableQueryUpdate::Gzip(bytes) => { + ws_v1::CompressableQueryUpdate::Gzip(bytes) => { let bytes = gzip_decompress(&bytes).unwrap(); bsatn::from_slice(&bytes).unwrap() } @@ -45,11 +44,11 @@ pub(crate) fn decompress_server_message(raw: &[u8]) -> Result, WsE }; match raw { [] => Err(WsError::EmptyMessage), - [SERVER_MSG_COMPRESSION_TAG_NONE, bytes @ ..] => Ok(Cow::Borrowed(bytes)), - [SERVER_MSG_COMPRESSION_TAG_BROTLI, bytes @ ..] => brotli_decompress(bytes) + [ws_v1::SERVER_MSG_COMPRESSION_TAG_NONE, bytes @ ..] => Ok(Cow::Borrowed(bytes)), + [ws_v1::SERVER_MSG_COMPRESSION_TAG_BROTLI, bytes @ ..] => brotli_decompress(bytes) .map(Cow::Owned) .map_err(err_decompress("brotli")), - [SERVER_MSG_COMPRESSION_TAG_GZIP, bytes @ ..] => { + [ws_v1::SERVER_MSG_COMPRESSION_TAG_GZIP, bytes @ ..] => { gzip_decompress(bytes).map(Cow::Owned).map_err(err_decompress("gzip")) } [c, ..] => Err(WsError::UnknownCompressionScheme { scheme: *c }), diff --git a/sdks/rust/src/db_connection.rs b/sdks/rust/src/db_connection.rs index 6ad86a45ba3..adf371bf5a1 100644 --- a/sdks/rust/src/db_connection.rs +++ b/sdks/rust/src/db_connection.rs @@ -36,8 +36,7 @@ use bytes::Bytes; use futures::StreamExt; use futures_channel::mpsc; use http::Uri; -use spacetimedb_client_api_messages::websocket as ws; -use spacetimedb_client_api_messages::websocket::{BsatnFormat, CallReducerFlags, Compression}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_lib::{bsatn, ser::Serialize, ConnectionId, Identity}; use spacetimedb_sats::Deserialize; use std::{ @@ -63,7 +62,7 @@ pub struct DbContextImpl { pub(crate) inner: SharedCell>, /// None if we have disconnected. - pub(crate) send_chan: SharedCell>>>, + pub(crate) send_chan: SharedCell>>>, /// The client cache, which stores subscribed rows. cache: SharedCell>, @@ -85,12 +84,12 @@ pub struct DbContextImpl { /// This connection's `Identity`. /// /// May be `None` if we connected anonymously - /// and have not yet received the [`ws::IdentityToken`] message. + /// and have not yet received the [`ws_v1::IdentityToken`] message. identity: SharedCell>, /// This connection's `ConnectionId`. /// - /// This may be none if we have not yet received the [`ws::IdentityToken`] message. + /// This may be none if we have not yet received the [`ws_v1::IdentityToken`] message. connection_id: SharedCell>, } @@ -314,7 +313,7 @@ impl DbContextImpl { .unwrap() .as_mut() .ok_or(crate::Error::Disconnected)? - .unbounded_send(ws::ClientMessage::Subscribe(ws::Subscribe { + .unbounded_send(ws_v1::ClientMessage::Subscribe(ws_v1::Subscribe { query_strings: queries, request_id: sub_id, })) @@ -332,7 +331,7 @@ impl DbContextImpl { .unwrap() .as_mut() .ok_or(crate::Error::Disconnected)? - .unbounded_send(ws::ClientMessage::SubscribeMulti(msg)) + .unbounded_send(ws_v1::ClientMessage::SubscribeMulti(msg)) .expect("Unable to send subscribe message: WS sender loop has dropped its recv channel"); } // else, the handle was already cancelled. @@ -356,7 +355,7 @@ impl DbContextImpl { .unwrap() .as_mut() .ok_or(crate::Error::Disconnected)? - .unbounded_send(ws::ClientMessage::UnsubscribeMulti(m)) + .unbounded_send(ws_v1::ClientMessage::UnsubscribeMulti(m)) .expect("Unable to send unsubscribe message: WS sender loop has dropped its recv channel"); } } @@ -367,7 +366,7 @@ impl DbContextImpl { let inner = &mut *self.inner.lock().unwrap(); let flags = inner.call_reducer_flags.get_flags(reducer); - let msg = ws::ClientMessage::CallReducer(ws::CallReducer { + let msg = ws_v1::ClientMessage::CallReducer(ws_v1::CallReducer { reducer: reducer.into(), args: args_bsatn.into(), // We could call `next_request_id` to get a unique ID to include here, @@ -398,11 +397,11 @@ impl DbContextImpl { .procedure_callbacks .insert(request_id, callback); - let msg = ws::ClientMessage::CallProcedure(ws::CallProcedure { + let msg = ws_v1::ClientMessage::CallProcedure(ws_v1::CallProcedure { procedure: procedure.into(), args: args.into(), request_id, - flags: ws::CallProcedureFlags::Default, + flags: ws_v1::CallProcedureFlags::Default, }); self.send_chan .lock() @@ -705,7 +704,7 @@ impl DbContextImpl { } /// Called by autogenerated on `reducer_config` methods. - pub fn set_call_reducer_flags(&self, reducer: &'static str, flags: CallReducerFlags) { + pub fn set_call_reducer_flags(&self, reducer: &'static str, flags: ws_v1::CallReducerFlags) { self.queue_mutation(PendingMutation::SetCallReducerFlags { reducer, flags }); } @@ -805,17 +804,17 @@ pub(crate) struct DbContextImplInner { struct CallReducerFlagsMap { // TODO(centril): consider replacing the string with a type-id based map // where each reducer is associated with a marker type. - map: HashMap<&'static str, CallReducerFlags>, + map: HashMap<&'static str, ws_v1::CallReducerFlags>, } impl CallReducerFlagsMap { /// Returns the [`CallReducerFlags`] for `reducer_name`. - fn get_flags(&self, reducer_name: &str) -> CallReducerFlags { + fn get_flags(&self, reducer_name: &str) -> ws_v1::CallReducerFlags { self.map.get(reducer_name).copied().unwrap_or_default() } /// Sets the [`CallReducerFlags`] for `reducer_name` to `flags`. - pub fn set_flags(&mut self, reducer_name: &'static str, flags: CallReducerFlags) { + pub fn set_flags(&mut self, reducer_name: &'static str, flags: ws_v1::CallReducerFlags) { if flags == <_>::default() { self.map.remove(reducer_name) } else { @@ -1017,7 +1016,7 @@ but you must call one of them, or else the connection will never progress. /// The current threshold used by the host is 1KiB for the entire server message /// and for individual query updates. /// Note however that this threshold is not guaranteed and may change without notice. - pub fn with_compression(mut self, compression: Compression) -> Self { + pub fn with_compression(mut self, compression: ws_v1::Compression) -> Self { self.params.compression = compression; self } @@ -1163,7 +1162,7 @@ enum ParsedMessage { } fn spawn_parse_loop( - raw_message_recv: mpsc::UnboundedReceiver>, + raw_message_recv: mpsc::UnboundedReceiver>, handle: &runtime::Handle, ) -> (tokio::task::JoinHandle<()>, mpsc::UnboundedReceiver>) { let (parsed_message_send, parsed_message_recv) = mpsc::unbounded(); @@ -1174,12 +1173,12 @@ fn spawn_parse_loop( /// A loop which reads raw WS messages from `recv`, parses them into domain types, /// and pushes the [`ParsedMessage`]s into `send`. async fn parse_loop( - mut recv: mpsc::UnboundedReceiver>, + mut recv: mpsc::UnboundedReceiver>, send: mpsc::UnboundedSender>, ) { while let Some(msg) = recv.next().await { send.unbounded_send(match msg { - ws::ServerMessage::InitialSubscription(sub) => M::DbUpdate::try_from(sub.database_update) + ws_v1::ServerMessage::InitialSubscription(sub) => M::DbUpdate::try_from(sub.database_update) .map(|update| ParsedMessage::InitialSubscription { db_update: update, sub_id: sub.request_id, @@ -1191,7 +1190,7 @@ async fn parse_loop( .into(), ) }), - ws::ServerMessage::TransactionUpdate(ws::TransactionUpdate { + ws_v1::ServerMessage::TransactionUpdate(ws_v1::TransactionUpdate { status, timestamp, caller_identity, @@ -1221,7 +1220,7 @@ async fn parse_loop( ParsedMessage::TransactionUpdate(event, db_update) } }, - ws::ServerMessage::TransactionUpdateLight(ws::TransactionUpdateLight { update, request_id: _ }) => { + ws_v1::ServerMessage::TransactionUpdateLight(ws_v1::TransactionUpdateLight { update, request_id: _ }) => { match M::DbUpdate::parse_update(update) { Err(e) => ParsedMessage::Error( InternalError::failed_parse("DbUpdate", "TransactionUpdateLight") @@ -1231,15 +1230,15 @@ async fn parse_loop( Ok(db_update) => ParsedMessage::TransactionUpdate(Event::UnknownTransaction, Some(db_update)), } } - ws::ServerMessage::IdentityToken(ws::IdentityToken { + ws_v1::ServerMessage::IdentityToken(ws_v1::IdentityToken { identity, token, connection_id, }) => ParsedMessage::IdentityToken(identity, token, connection_id), - ws::ServerMessage::OneOffQueryResponse(_) => { + ws_v1::ServerMessage::OneOffQueryResponse(_) => { unreachable!("The Rust SDK does not implement one-off queries") } - ws::ServerMessage::SubscribeMultiApplied(subscribe_applied) => { + ws_v1::ServerMessage::SubscribeMultiApplied(subscribe_applied) => { let db_update = subscribe_applied.update; let query_id = subscribe_applied.query_id.id; match M::DbUpdate::parse_update(db_update) { @@ -1254,7 +1253,7 @@ async fn parse_loop( }, } } - ws::ServerMessage::UnsubscribeMultiApplied(unsubscribe_applied) => { + ws_v1::ServerMessage::UnsubscribeMultiApplied(unsubscribe_applied) => { let db_update = unsubscribe_applied.update; let query_id = unsubscribe_applied.query_id.id; match M::DbUpdate::parse_update(db_update) { @@ -1269,18 +1268,18 @@ async fn parse_loop( }, } } - ws::ServerMessage::SubscriptionError(e) => ParsedMessage::SubscriptionError { + ws_v1::ServerMessage::SubscriptionError(e) => ParsedMessage::SubscriptionError { query_id: e.query_id, error: e.error.to_string(), }, - ws::ServerMessage::SubscribeApplied(_) => unreachable!("Rust client SDK never sends `SubscribeSingle`, but received a `SubscribeApplied` from the host... huh?"), - ws::ServerMessage::UnsubscribeApplied(_) => unreachable!("Rust client SDK never sends `UnsubscribeSingle`, but received a `UnsubscribeApplied` from the host... huh?"), - ws::ServerMessage::ProcedureResult(procedure_result) => ParsedMessage::ProcedureResult { + ws_v1::ServerMessage::SubscribeApplied(_) => unreachable!("Rust client SDK never sends `SubscribeSingle`, but received a `SubscribeApplied` from the host... huh?"), + ws_v1::ServerMessage::UnsubscribeApplied(_) => unreachable!("Rust client SDK never sends `UnsubscribeSingle`, but received a `UnsubscribeApplied` from the host... huh?"), + ws_v1::ServerMessage::ProcedureResult(procedure_result) => ParsedMessage::ProcedureResult { request_id: procedure_result.request_id, result: match procedure_result.status { - ws::ProcedureStatus::InternalError(msg) => Err(InternalError::new(msg)), - ws::ProcedureStatus::OutOfEnergy => Err(InternalError::new("Procedure execution aborted due to insufficient energy")), - ws::ProcedureStatus::Returned(val) => Ok(val), + ws_v1::ProcedureStatus::InternalError(msg) => Err(InternalError::new(msg)), + ws_v1::ProcedureStatus::OutOfEnergy => Err(InternalError::new("Procedure execution aborted due to insufficient energy")), + ws_v1::ProcedureStatus::Returned(val) => Ok(val), } }, }) @@ -1347,7 +1346,7 @@ pub(crate) enum PendingMutation { Disconnect, SetCallReducerFlags { reducer: &'static str, - flags: CallReducerFlags, + flags: ws_v1::CallReducerFlags, }, InvokeProcedureWithCallback { procedure: &'static str, diff --git a/sdks/rust/src/event.rs b/sdks/rust/src/event.rs index 8765de6aa1f..031f1efc289 100644 --- a/sdks/rust/src/event.rs +++ b/sdks/rust/src/event.rs @@ -11,7 +11,7 @@ //! to determine what change in your connection's state caused the callback to run. use crate::spacetime_module::{DbUpdate as _, SpacetimeModule}; -use spacetimedb_client_api_messages::websocket as ws; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_lib::{ConnectionId, Identity, Timestamp}; #[non_exhaustive] @@ -101,12 +101,12 @@ pub enum Status { impl Status { pub(crate) fn parse_status_and_update( - status: ws::UpdateStatus, + status: ws_v1::UpdateStatus, ) -> crate::Result<(Self, Option)> { Ok(match status { - ws::UpdateStatus::Committed(update) => (Self::Committed, Some(M::DbUpdate::parse_update(update)?)), - ws::UpdateStatus::Failed(errmsg) => (Self::Failed(errmsg), None), - ws::UpdateStatus::OutOfEnergy => (Self::OutOfEnergy, None), + ws_v1::UpdateStatus::Committed(update) => (Self::Committed, Some(M::DbUpdate::parse_update(update)?)), + ws_v1::UpdateStatus::Failed(errmsg) => (Self::Failed(errmsg), None), + ws_v1::UpdateStatus::OutOfEnergy => (Self::OutOfEnergy, None), }) } } diff --git a/sdks/rust/src/lib.rs b/sdks/rust/src/lib.rs index b79a7111ac1..02a34e696bb 100644 --- a/sdks/rust/src/lib.rs +++ b/sdks/rust/src/lib.rs @@ -32,7 +32,7 @@ pub use event::{Event, ReducerEvent, Status}; pub use table::{Table, TableWithPrimaryKey}; pub use spacetime_module::SubscriptionHandle; -pub use spacetimedb_client_api_messages::websocket::Compression; +pub use spacetimedb_client_api_messages::websocket::v1::Compression; pub use spacetimedb_lib::{ConnectionId, Identity, ScheduleAt, TimeDuration, Timestamp, Uuid}; pub use spacetimedb_sats::{i256, u256}; @@ -44,7 +44,7 @@ pub mod __codegen { //! These may change incompatibly without a major version bump. pub use http; pub use log; - pub use spacetimedb_client_api_messages::websocket as __ws; + pub use spacetimedb_client_api_messages::websocket::v1 as __ws; pub use spacetimedb_lib as __lib; pub use spacetimedb_sats as __sats; @@ -71,5 +71,5 @@ pub mod unstable { //! These may change incompatibly without a major version bump. pub use crate::db_connection::set_connection_id; pub use crate::metrics::{ClientMetrics, CLIENT_METRICS}; - pub use spacetimedb_client_api_messages::websocket::CallReducerFlags; + pub use spacetimedb_client_api_messages::websocket::v1::CallReducerFlags; } diff --git a/sdks/rust/src/spacetime_module.rs b/sdks/rust/src/spacetime_module.rs index 7b8e3fba176..bd3b0ee1960 100644 --- a/sdks/rust/src/spacetime_module.rs +++ b/sdks/rust/src/spacetime_module.rs @@ -12,7 +12,7 @@ use crate::{ compression::maybe_decompress_cqu, }; use bytes::Bytes; -use spacetimedb_client_api_messages::websocket::{self as ws, RowListLen as _}; +use spacetimedb_client_api_messages::websocket::v1::{self as ws_v1, RowListLen as _}; use spacetimedb_lib::{bsatn, de::DeserializeOwned}; use std::fmt::Debug; @@ -56,7 +56,7 @@ pub trait SpacetimeModule: Send + Sync + 'static { /// Return type of [`crate::DbContext::set_reducer_flags`]. type SetReducerFlags: InModule + Send + 'static; - /// Parsed and typed analogue of [`crate::ws::DatabaseUpdate`]. + /// Parsed and typed analogue of [`crate::ws_v1::DatabaseUpdate`]. type DbUpdate: DbUpdate; /// The result of applying `Self::DbUpdate` to the client cache. @@ -71,9 +71,9 @@ pub trait SpacetimeModule: Send + Sync + 'static { } /// Implemented by the autogenerated `DbUpdate` type, -/// which is a parsed and typed analogue of [`crate::ws::DatabaseUpdate`]. +/// which is a parsed and typed analogue of [`crate::ws_v1::DatabaseUpdate`]. pub trait DbUpdate: - TryFrom, Error = crate::Error> + InModule + Send + 'static + TryFrom, Error = crate::Error> + InModule + Send + 'static where Self::Module: SpacetimeModule, { @@ -82,7 +82,7 @@ where cache: &mut ClientCache, ) -> ::AppliedDiff<'_>; - fn parse_update(update: ws::DatabaseUpdate) -> crate::Result { + fn parse_update(update: ws_v1::DatabaseUpdate) -> crate::Result { Self::try_from(update).map_err(|source| { InternalError::failed_parse(std::any::type_name::(), "DatabaseUpdate") .with_cause(source) @@ -167,7 +167,7 @@ where /// This will be the type parameter to [`Event`] and [`crate::ReducerEvent`]. pub trait Reducer: InModule - + TryFrom, Error = crate::Error> + + TryFrom, Error = crate::Error> + std::fmt::Debug + Clone + Send @@ -234,7 +234,7 @@ impl TableUpdate { impl TableUpdate { /// Parse `raw_updates` into a [`TableUpdate`]. - pub fn parse_table_update(raw_updates: ws::TableUpdate) -> crate::Result> { + pub fn parse_table_update(raw_updates: ws_v1::TableUpdate) -> crate::Result> { let mut inserts = Vec::new(); let mut deletes = Vec::new(); for update in raw_updates.updates { @@ -245,7 +245,7 @@ impl TableUpdate { Ok(Self { inserts, deletes }) } - fn parse_from_row_list(sink: &mut Vec>, raw_rows: &ws::BsatnRowList) -> crate::Result<()> { + fn parse_from_row_list(sink: &mut Vec>, raw_rows: &ws_v1::BsatnRowList) -> crate::Result<()> { sink.reserve(raw_rows.len()); for raw_row in raw_rows { sink.push(Self::parse_row(raw_row)?); diff --git a/sdks/rust/src/subscription.rs b/sdks/rust/src/subscription.rs index 93bf09b667b..4436da6ee5d 100644 --- a/sdks/rust/src/subscription.rs +++ b/sdks/rust/src/subscription.rs @@ -8,7 +8,7 @@ use crate::{ spacetime_module::{SpacetimeModule, SubscriptionHandle}, }; use futures_channel::mpsc; -use spacetimedb_client_api_messages::websocket::{self as ws}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_data_structures::map::HashMap; use std::sync::{atomic::AtomicU32, Arc, Mutex}; @@ -39,7 +39,7 @@ pub type OnEndedCallback = Box::Subscripti /// When handling a pending unsubscribe, there are three cases the caller must handle. pub(crate) enum PendingUnsubscribeResult { // The unsubscribe message should be sent to the server. - SendUnsubscribe(ws::UnsubscribeMulti), + SendUnsubscribe(ws_v1::UnsubscribeMulti), // The subscription is immediately being cancelled, so the callback should be run. RunCallback(OnEndedCallback), // No action is required. @@ -137,8 +137,8 @@ impl SubscriptionManager { self.new_subscriptions.remove(&sub_id); return PendingUnsubscribeResult::DoNothing; } - PendingUnsubscribeResult::SendUnsubscribe(ws::UnsubscribeMulti { - query_id: ws::QueryId::new(sub_id), + PendingUnsubscribeResult::SendUnsubscribe(ws_v1::UnsubscribeMulti { + query_id: ws_v1::QueryId::new(sub_id), request_id: next_request_id(), }) } @@ -367,7 +367,7 @@ impl SubscriptionState { /// Start the subscription. /// This updates the state in the handle, and returns the message to be sent to the server. /// The caller is responsible for sending the message to the server. - pub(crate) fn start(&mut self) -> Option { + pub(crate) fn start(&mut self) -> Option { if self.unsubscribe_called { // This means that the subscription was cancelled before it was started. // We skip sending the subscription start message. @@ -380,8 +380,8 @@ impl SubscriptionState { unreachable!("Subscription already started"); } self.status = SubscriptionServerState::Sent; - Some(ws::SubscribeMulti { - query_id: ws::QueryId::new(self.query_id), + Some(ws_v1::SubscribeMulti { + query_id: ws_v1::QueryId::new(self.query_id), query_strings: self.query_sql.clone(), request_id: next_request_id(), }) @@ -483,7 +483,7 @@ impl SubscriptionHandleImpl { } } - pub(crate) fn start(&self) -> Option { + pub(crate) fn start(&self) -> Option { let mut inner = self.inner.lock().unwrap(); inner.start() } diff --git a/sdks/rust/src/websocket.rs b/sdks/rust/src/websocket.rs index e0372a53dbb..b7ab90a9d85 100644 --- a/sdks/rust/src/websocket.rs +++ b/sdks/rust/src/websocket.rs @@ -10,8 +10,7 @@ use bytes::Bytes; use futures::{SinkExt, StreamExt as _, TryStreamExt}; use futures_channel::mpsc; use http::uri::{InvalidUri, Scheme, Uri}; -use spacetimedb_client_api_messages::websocket::{BsatnFormat, Compression, BIN_PROTOCOL}; -use spacetimedb_client_api_messages::websocket::{ClientMessage, ServerMessage}; +use spacetimedb_client_api_messages::websocket::v1 as ws_v1; use spacetimedb_lib::{bsatn, ConnectionId}; use thiserror::Error; use tokio::task::JoinHandle; @@ -105,7 +104,7 @@ fn parse_scheme(scheme: Option) -> Result { #[derive(Clone, Copy, Default)] pub(crate) struct WsParams { - pub compression: Compression, + pub compression: ws_v1::Compression, pub light: bool, /// `Some(true)` to enable confirmed reads for the connection, /// `Some(false)` to disable them. @@ -137,11 +136,11 @@ fn make_uri(host: Uri, db_name: &str, connection_id: Option, param // Specify the desired compression for host->client replies. match params.compression { - Compression::None => path.push_str("?compression=None"), - Compression::Gzip => path.push_str("?compression=Gzip"), + ws_v1::Compression::None => path.push_str("?compression=None"), + ws_v1::Compression::Gzip => path.push_str("?compression=Gzip"), // The host uses the same default as the sdk, // but in case this changes, we prefer to be explicit now. - Compression::Brotli => path.push_str("?compression=Brotli"), + ws_v1::Compression::Brotli => path.push_str("?compression=Brotli"), }; // Provide the connection ID if the client provided one. @@ -199,7 +198,7 @@ fn make_request( fn request_insert_protocol_header(req: &mut http::Request<()>) { req.headers_mut().insert( http::header::SEC_WEBSOCKET_PROTOCOL, - const { http::HeaderValue::from_static(BIN_PROTOCOL) }, + const { http::HeaderValue::from_static(ws_v1::BIN_PROTOCOL) }, ); } @@ -253,19 +252,19 @@ impl WsConnection { }) } - pub(crate) fn parse_response(bytes: &[u8]) -> Result, WsError> { + pub(crate) fn parse_response(bytes: &[u8]) -> Result, WsError> { let bytes = &*decompress_server_message(bytes)?; bsatn::from_slice(bytes).map_err(|source| WsError::DeserializeMessage { source }) } - pub(crate) fn encode_message(msg: ClientMessage) -> WebSocketMessage { + pub(crate) fn encode_message(msg: ws_v1::ClientMessage) -> WebSocketMessage { WebSocketMessage::Binary(bsatn::to_vec(&msg).unwrap().into()) } async fn message_loop( mut self, - incoming_messages: mpsc::UnboundedSender>, - outgoing_messages: mpsc::UnboundedReceiver>, + incoming_messages: mpsc::UnboundedSender>, + outgoing_messages: mpsc::UnboundedReceiver>, ) { let websocket_received = CLIENT_METRICS.websocket_received.with_label_values(&self.db_name); let websocket_received_msg_size = CLIENT_METRICS @@ -404,8 +403,8 @@ impl WsConnection { runtime: &runtime::Handle, ) -> ( JoinHandle<()>, - mpsc::UnboundedReceiver>, - mpsc::UnboundedSender>, + mpsc::UnboundedReceiver>, + mpsc::UnboundedSender>, ) { let (outgoing_send, outgoing_recv) = mpsc::unbounded(); let (incoming_send, incoming_recv) = mpsc::unbounded();