Skip to content

React: add flow control to reactive readers - #145

Draft
rjhuijsman wants to merge 4 commits into
mainfrom
rjh.reactive-flow-control
Draft

React: add flow control to reactive readers#145
rjhuijsman wants to merge 4 commits into
mainfrom
rjh.reactive-flow-control

Conversation

@rjhuijsman

Copy link
Copy Markdown
Contributor

Keeps clients of a reactive read on the latest state even when the state changes faster than they can consume it. Before this change the backend produced a response per state change and pushed it at the client regardless of whether the client had consumed the previous one, so responses queued up in the gRPC stream or in the browser's websocket buffer; a client that couldn't keep up worked its way through every state it had missed and fell arbitrarily far behind while still receiving a steady stream of (old) responses.

React.Query now stamps every response with a query_response_id and produces no further response until the client acknowledges that ID. Acknowledgements travel back over the websocket that carries the responses when there is one — a QueryRequest with acknowledge_query_response_id set, which costs no extra round trip — and over the new unary AcknowledgeQueryResponse RPC otherwise. Between an acknowledgement and the next response the backend catches up to the latest state, so a slow client skips the states it missed instead of replaying them.

BREAKING CHANGE: a frontend that doesn't acknowledge responses receives one reactive response and then never updates again, so frontends must be redeployed alongside (or before) this backend change. A frontend from this version does keep working against an older backend, since it only acknowledges responses that carry an ID.

rjhuijsman and others added 3 commits August 31, 2026 17:39
Keeps a client of a reactive read on the latest state even when
the state changes faster than the client can consume the
updates. Before this change the backend produced a response per
state change and pushed it at the client regardless of whether
the client had consumed the previous one, so responses queued up
in the gRPC stream or in the browser's websocket buffer: a
client that couldn't keep up worked its way through every state
it had missed, and fell arbitrarily far behind while still
receiving a steady stream of (old) responses.

`React.Query` now stamps every response with a
`query_response_id` and produces no further response until the
client acknowledges that ID. Acknowledgements travel back over
the websocket that carries the responses when there is one -- a
`QueryRequest` with `acknowledge_query_response_id` set, which
costs no extra round trip -- and over the new unary
`AcknowledgeQueryResponse` RPC otherwise. Between an
acknowledgement and the next response the backend catches up to
the _latest_ state, so a slow client skips the states it missed
instead of replaying them.

- A client only acknowledges a response that carries an ID, so a
  frontend built from this version keeps working against an
  older backend, which sends none.
- Transitive reactive reads acknowledge only once the response
  they hold has been used, so a reader that reads through
  another reader also gets the latest state rather than the next
  one in line.

BREAKING CHANGE: a frontend that doesn't acknowledge responses
receives one reactive response and then never updates again, so
frontends must be redeployed alongside (or before) this backend
change.

TESTED: `//tests/reboot:reactivity_test_py` gains
  `test_skip_to_latest` and `test_transitive_skip_to_latest`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMgyLgRtZXNAerHpSguK9k
Stops a mutation from staying pending forever now that the
backend skips states. Before this commit the generated React
client compared each query response only against the oldest of
the idempotency keys it was expecting. That was enough while
every state change produced its own response, but a response now
reports the aggregated idempotency keys of every state the
backend skipped on its way to the state that response reflects,
so a single response can be the only word a client ever gets
about several of its mutations -- and the ones it didn't compare
stayed `pending` forever.

Observe every expected mutation whose idempotency key the
response reports, rather than only the oldest one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMgyLgRtZXNAerHpSguK9k
Covers the failure that got the first attempt at reactive read
flow control reverted: an update that arrives while the browser
is too busy to consume it must not be lost, or the page shows
old data forever.

The app under test blocks the browser's main thread for a second
on every response it renders, so each round of state changes this
test makes lands on a client that cannot process it and has not
yet asked for it. The browser must still end each round showing
the last state written in that round. The test runs against a
local Envoy both with TLS, where responses arrive over an HTTP
stream and acknowledgements are requests of their own, and
without, where responses and acknowledgements share one
websocket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMgyLgRtZXNAerHpSguK9k
@aviator-app

aviator-app Bot commented Aug 31, 2026

Copy link
Copy Markdown

Current Aviator status

Aviator will automatically update this comment as the status of the PR changes.
Comment /aviator refresh to force Aviator to re-examine your PR (or learn about other /aviator commands).

This pull request is currently open (not queued).

How to merge

To merge this PR, comment /aviator merge or add the mergequeue-ready label.


See the real-time status of this PR on the Aviator webapp.
Use the Aviator Chrome Extension to see the status of your PR within GitHub.

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.

1 participant