Skip to content

Conversation

@gefjon
Copy link
Contributor

@gefjon gefjon commented Feb 10, 2026

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 ;: e rr 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.

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

  • Updated automated test suite.
    • Known failures:
      • fail_reducer, which depends on functionality that @jsdt hasn't yet implemented in the parent branch.
      • subscribe_all_select_star, which depends on the aforementioned subscribe_to_all_tables.

gefjon and others added 20 commits February 5, 2026 13:03
This compiles, but absolutely won't run, and is filled with a ton of TODOs.
Not actually update Rust codegen, manually rewrite quickstart-chat-rust template
to make it all compile.
…-ws-v2

Complete with lots of merge conflicts, and also updating the Nix flake.
Pass the generated request_id into the CallReducer message instead of
hardcoded 0, matching the procedure code pattern. Without this, reducer
callbacks never fire because the server echoes back request_id 0 which
doesn't match any stored callback.
- Replace on_<reducer>() callbacks with _then() invocation pattern
- Replace Status::Failed/OutOfEnergy with Status::Err/Panic
- Remove CallReducerFlags, set_reducer_flags, Event::UnknownTransaction
- Remove ReducerEvent.caller_identity/caller_connection_id field access
It no longer does anything
Zero-sized rows are well-defined, and we have tests that rely on them working
@gefjon gefjon requested review from cloutiertyler and jsdt February 10, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants