mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-09 09:10:03 -04:00
1592dec8af
# Description of Changes
Update the Rust client SDK to use the new V2 WebSocket format, and
present the V2 user-facing API.
## Reducer events
### Remove on-reducer callbacks
It's no longer possible to observe reducers called by other clients by
registering callbacks with `ctx.reducers.on_{my_reducer}`. We no longer
code-generate those methods, or the associated
`ctx.reducers.remove_on_{my_reducer}`. Internal plumbing for storing and
invoking those callbacks is also removed.
### Add specific reducer invocation callbacks
In addition to the previous way to invoke reducers,
`ctx.reducers.{my_reducer}(args...)`, we add a method that registers a
callback to run after the reducer is finished. This method has the
suffix `_then`, as in `ctx.reducers.{my_reducer}_then(args...,
callback)`.
The callback will accept two arguments:
- `ctx: &ReducerEventContext`, the same context as was previously passed
to on-reducer callbacks.
- `status: Result<Result<(), String>, InternalError>`, denoting the
outcome of the reducer.
- `Ok(Ok(())` means the reducer committed. This corresponds to
`ReducerOutcome::Ok` or `ReducerOutcome::Okmpty` in the new WS format.
- `Ok(Err(message))` means the reducer returned an "expected" or "user"
error. This corresponds to `ReducerOutcome::Err` in the new WS format.
- `Err(internal_error)` means something went wrong with host execution.
This corresponds to `ReducerOutcome::InternalError` in the new WS
format.
Internally, the SDK stores the callbacks in its `ReducerCallbacks` map.
This is keyed on `request_id: u32`, a number that is generated for each
reducer call (from an `AtomicU32` that we increment each time), and
included in the `ClientMessage::CallReducer` request. The
`ServerMessage::ReducerResult` includes the same `request_id`, so the
SDK pops out of the `ReducerCallbacks` and invokes the appropriate
callback when processing that message.
These new callbacks are very similar to the existing procedure
callbacks.
### The `Event` exposed to row callbacks
Row callbacks caused by a reducer invoked by this client will see
`Event::Reducer`, the same as they would prior to this PR. These
callbacks will be the result of a `ServerMessage::ReducerResult` with
`ReducerOutcome::Ok`. In order to expose the reducer name and arguments
to this event, the client stores them in its `ReducerCallbacks` map,
alongside the callback for when the reducer is complete.
Row callbacks caused by any other reducer, or any non-reducer
transaction, are now indistinguishable to the client. These will see
`Event::Transaction`, which is renamed from the old
`Event::UnknownTransaction`.
### Less metadata in `ReducerEvent`
Some metadata is removed from `ReducerEvent`, as the V2 WebSocket format
no longer publishes it, even to the caller.
## `CallReducerFlags` are removed
All machinery for setting, storing and applying call reducer flags is
removed from the SDK, as the new WS format does not have any non-default
flags.
## Requesting rows in unsubscribe
When sending a `ClientMessage::Unsubscribe`, we always request that the
server include the matching rows in its response
`ServerMessage::UnsubscribeApplied`. This saves us having to update the
SDK to store query sets separately, at least for now. (We'll do that
later.)
## Handling rows
The new SDK does some additional parsing to wrangle rows in the new
WebSocket format into the same internal data structures as before,
rather than re-writing the client cache. (We'll do that later.)
Specifically, parsing of `DbUpdate` is changed so that:
- We parse raw `TransactionUpdate` into the generated `DbUpdate` type,
which requires an additional loop compared to the previous version, to
cope with the new WS format's dividing updates by query set. We define a
function `transaction_update_iter_table_updates` which encapsulates this
nested loop in an iterator.
- We have two new functions for parsing raw `QueryRows` into the
generated `DbUpdate` type, one for when they come from a
`SubscribeApplied`, and the other when they come from an
`UnsubscribeApplied`. `QueryRows` from `SubscribeApplied` translate to a
`DbUpdate` of all inserts, while one from `UnsubscribeApplied` will be
all deletes.
## Legacy subscriptions
"Legacy subscriptions" are removed. These were only used for
`subscribe_to_all_tables`, which as of now is stubbed. I will follow up
with a change to re-implement `subscribe_to_all_tables` by
code-generating a list of all known tables, and having it subscribe to
`select * from {table}` for every table in that list.
## `subscribe_to_all_tables` via a list
Previously, `subscribe_to_all_tables` worked by sending a legacy
subscription with the query `SELECT * FROM *`, which the host had
special handling to expand to subscribing to all tables. As legacy
subscriptions are no longer usable in V2 clients, this can't work.
Instead, we code-generate `SpacetimeModule::ALL_TABLE_NAMES`, a list of
all the known table names. `subscribe_to_all_tables` then maps across
this list to construct a list of queries in the form `SELECT * FROM
{table_name}`, and subscribes to all of those queries. This has the
upside that defining a new table in the module without regenerating
client bindings will no longer result in the client seeing rows of
tables it does not know about and cannot parse.
## Light mode removed
Light mode is no longer meaningful in the V2 WS format, so all code
related to it is removed.
## Internal changes
### Renamed WS messages
The SDK's internal code is updated to account for various renames:
- `QueryId` -> `QuerySetId`, `query_id` -> `query_set_id`.
- `SubscribeMulti` -> `Subscribe`, `UnsubscribeMulti` -> `Unsubscribe`.
## Incidental changes in this PR, not necessary for other client SDKs
### Don't filter out empty ranges in `RowSizeHint`
The Rust implementation of `RowSizeHint` in `BsatnRowList` got regressed
in the base branch to not work with zero-sized rows. This change fixes
that.
# API and ABI breaking changes
Boy howdy is it!
# Expected complexity level and risk
3? Changes ended up being less complicated than I feared, but we do have
some fiddly code here, and we have internal dependencies on the SDK.
# Testing
<!-- Describe any testing you've done, and any testing you'd like your
reviewers to do,
so that you're confident that all the changes work as expected! -->
- [x] Updated automated test suite.
- Known failures:
- [ ] `subscribe_all_select_star`, which is currently broken because
it's trying to subscribe to rows from private tables. #4241 will fix
this.
---------
Co-authored-by: Jeffrey Dallatezza <jeffreydallatezza@gmail.com>
Co-authored-by: = <cloutiertyler@gmail.com>
78 lines
3.0 KiB
Rust
78 lines
3.0 KiB
Rust
//! The [`DbContext`] trait, which mediates access to a remote module.
|
|
//!
|
|
//! [`DbContext`] is implemented by `DbConnection` and `EventContext`,
|
|
//! both defined in your module-specific codegen.
|
|
|
|
use crate::{ConnectionId, Identity};
|
|
|
|
pub trait DbContext {
|
|
type DbView;
|
|
|
|
/// Access to tables in the client cache, which stores a read-only replica of the remote database state.
|
|
///
|
|
/// The returned `DbView` will have a method to access each table defined by the module.
|
|
///
|
|
/// `DbConnection` and `EventContext` also have a public field `db`,
|
|
/// so accesses to concrete-typed contexts don't need to use this method.
|
|
fn db(&self) -> &Self::DbView;
|
|
|
|
type Reducers;
|
|
|
|
/// Access to reducers defined by the module.
|
|
///
|
|
/// The returned `Reducers` will have a method to invoke each reducer defined by the module,
|
|
/// plus methods for adding and removing callbacks on each of those reducers.
|
|
///
|
|
/// `DbConnection` and `EventContext` also have a public field `reducers`,
|
|
/// so accesses to concrete-typed contexts don't need to use this method.
|
|
fn reducers(&self) -> &Self::Reducers;
|
|
|
|
type Procedures;
|
|
|
|
/// Access to procedures defined by the module.
|
|
///
|
|
/// The returned `Procedures` will have a method to invoke each procedure defined by the module.
|
|
///
|
|
/// `DbConnection` and `EventContext` also have a public field `procedures`,
|
|
/// so accesses to concrete-typed contexts don't need to use this method.
|
|
fn procedures(&self) -> &Self::Procedures;
|
|
|
|
/// Returns `true` if the connection is active, i.e. has not yet disconnected.
|
|
fn is_active(&self) -> bool;
|
|
|
|
/// Close the connection.
|
|
///
|
|
/// Returns an error if we are already disconnected.
|
|
fn disconnect(&self) -> crate::Result<()>;
|
|
|
|
type SubscriptionBuilder;
|
|
/// Get a builder-pattern constructor for subscribing to queries,
|
|
/// causing matching rows to be replicated into the client cache.
|
|
fn subscription_builder(&self) -> Self::SubscriptionBuilder;
|
|
|
|
/// Get the [`Identity`] of this connection.
|
|
///
|
|
/// This method panics if we have not yet received our [`Identity`] from the host.
|
|
/// For a non-panicking version, see [`Self::try_identity`].
|
|
fn identity(&self) -> Identity {
|
|
self.try_identity().unwrap()
|
|
}
|
|
|
|
/// Get the [`Identity`] of this connection.
|
|
///
|
|
/// This method returns `None` if we have not yet received our [`Identity`] from the host.
|
|
/// For a panicking version, see [`Self::identity`].
|
|
fn try_identity(&self) -> Option<Identity>;
|
|
|
|
/// Get this connection's [`ConnectionId`].
|
|
// Currently, all connections opened by the same process will have the same [`ConnectionId`],
|
|
// including connections to different modules.
|
|
// TODO: fix this.
|
|
// TODO: add `Self::try_connection_id`, for the same reason as `Self::try_identity`.
|
|
fn connection_id(&self) -> ConnectionId;
|
|
|
|
/// Get this connection's [`ConnectionId`].
|
|
/// This may return None if the connection has not been established.
|
|
fn try_connection_id(&self) -> Option<ConnectionId>;
|
|
}
|