2 Commits

Author SHA1 Message Date
Phoebe Goldman 1592dec8af Update Rust client SDK for V2 WebSocket format (#4257)
# 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>
2026-02-13 01:08:37 +00:00
Phoebe Goldman ab3554576b Add a nix flake (#3422)
# Description of Changes

Recently, my hacked-together env stopped working, probably due to our
switching linkers. This pushed me to write a proper Nix packaging for
SpacetimeDB, complete with flake and development environment.

Some things I haven't been able to figure out (or in some cases, just
haven't bothered with):

- Unreal.
- C# WASI SDK.
- Exposing Git commit hash for `spacetime --version`.
- Making the Rust SDK's reauth test work. This test tries to write a
file in the home directory, which is inaccessible in the Nix sandbox.

# API and ABI breaking changes


It's a new thing we have to maintain if we're exposing it to users, but
I need to maintain it anyways to be able to develop Spacetime, so...

# Expected complexity level and risk

1

# Testing

- [x] Ran `nix flake check` locally.
- [x] Ran `nix build` locally and then did `spacetime start`, got an
apparently-responsive SpacetimeDB.
- [x] Ran `nix develop` locally, then `cargo build` and `cargo test` in
the dev shell.

---------

Co-authored-by: Jeffrey Dallatezza <jeffreydallatezza@gmail.com>
2025-10-20 17:44:50 +00:00