Update Rust client SDK for V2 WebSocket format#4257
Merged
cloutiertyler merged 100 commits intomasterfrom Feb 13, 2026
Merged
Conversation
Incl. mention of future possibility of PK-ful partial updates.
This compiles, but absolutely won't run, and is filled with a ton of TODOs.
Resolve merge conflicts and fix compilation errors: - Recreate message_handlers_v1.rs and message_handlers_v2.rs (v1/v2 dispatch) - Adapt for Option<ReducerName> change from master - Convert TableName to Box<str> for TableUpdate::new calls - Fix async closure annotations in call_view_add_v2_subscription - Fix BTreeMap key type (TableName doesn't impl Ord)
Both types wrap Identifier which already implements Ord. Revert the Box<str> workaround in module_subscription_manager.
RowListLen is defined in websocket::common but was not re-exported from websocket::v1. Import it directly from common instead.
…-ws-v2 I manually fixed conflicts in code-generated files, probably wrong. I will follow up with a commit that re-generates them.
See comments in `sdks/rust/tests/test.rs` for justification.
jsdt
approved these changes
Feb 12, 2026
Contributor
jsdt
left a comment
There was a problem hiding this comment.
I didn't give it the deepest review, but it looks good to me.
The codegen emits __ws::v2::* references, but __ws was aliased to websocket::v1, so __ws::v2 didn't resolve. Point it to the parent websocket module instead.
gefjon
added a commit
that referenced
this pull request
Feb 13, 2026
At some point, a rebase on this branch appears to have accidentally overwritten the Rust codegen changes in #4257 . This commit fixes that, to integrate the event tables codegen changes with the prior WS V2 codegen changes. I've also taken the minor liberty of re-ordering the emitted top-level forms in a table definition file, in order to allow merging the definitions of `register_table` and `parse_table_update`, which this branch previously had duplicated into the `if` and `else` cases of the conditional on whether the table was an event table or not. Plus I added a few comments.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 associatedctx.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 inctx.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 toReducerOutcome::OkorReducerOutcome::Okmptyin the new WS format.Ok(Err(message))means the reducer returned an "expected" or "user" error. This corresponds toReducerOutcome::Errin the new WS format.Err(internal_error)means something went wrong with host execution. This corresponds toReducerOutcome::InternalErrorin the new WS format.Internally, the SDK stores the callbacks in its
ReducerCallbacksmap. This is keyed onrequest_id: u32, a number that is generated for each reducer call (from anAtomicU32that we increment each time), and included in theClientMessage::CallReducerrequest. TheServerMessage::ReducerResultincludes the samerequest_id, so the SDK pops out of theReducerCallbacksand invokes the appropriate callback when processing that message.These new callbacks are very similar to the existing procedure callbacks.
The
Eventexposed to row callbacksRow 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 aServerMessage::ReducerResultwithReducerOutcome::Ok. In order to expose the reducer name and arguments to this event, the client stores them in itsReducerCallbacksmap, 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 oldEvent::UnknownTransaction.Less metadata in
ReducerEventSome metadata is removed from
ReducerEvent, as the V2 WebSocket format no longer publishes it, even to the caller.CallReducerFlagsare removedAll 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 responseServerMessage::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
DbUpdateis changed so that:TransactionUpdateinto the generatedDbUpdatetype, 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 functiontransaction_update_iter_table_updateswhich encapsulates this nested loop in an iterator.QueryRowsinto the generatedDbUpdatetype, one for when they come from aSubscribeApplied, and the other when they come from anUnsubscribeApplied.QueryRowsfromSubscribeAppliedtranslate to aDbUpdateof all inserts, while one fromUnsubscribeAppliedwill 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-implementsubscribe_to_all_tablesby code-generating a list of all known tables, and having it subscribe toselect * from {table}for every table in that list.subscribe_to_all_tablesvia a listPreviously,
subscribe_to_all_tablesworked by sending a legacy subscription with the querySELECT * 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-generateSpacetimeModule::ALL_TABLE_NAMES, a list of all the known table names.subscribe_to_all_tablesthen maps across this list to construct a list of queries in the formSELECT * 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
RowSizeHintThe Rust implementation of
RowSizeHintinBsatnRowListgot 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
subscribe_all_select_star, which is currently broken because it's trying to subscribe to rows from private tables. [2.0 Breaking] Add --include-private and default private tables to not generate #4241 will fix this.