[graphql-stream] SSE transport + SSE-aware GraphiQL [11/n] - #26425
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
tpham-mysten
temporarily deployed
to
sui-typescript-aws-kms-test-env
April 29, 2026 16:46 — with
GitHub Actions
Inactive
tpham-mysten
force-pushed
the
poc-graphql-sse-graphiql
branch
from
May 11, 2026 02:12
be094dd to
71b276e
Compare
tpham-mysten
temporarily deployed
to
sui-typescript-aws-kms-test-env
May 11, 2026 02:12 — with
GitHub Actions
Inactive
tpham-mysten
marked this pull request as ready for review
May 11, 2026 02:26
tpham-mysten
requested review from
amnn,
emmazzz,
evan-wall-mysten,
nickvikeras and
wlmyng
May 11, 2026 02:26
tpham-mysten
temporarily deployed
to
sui-typescript-aws-kms-test-env
May 11, 2026 02:26 — with
GitHub Actions
Inactive
… [12/n] Replace the WebSocket subscription transport with Server-Sent Events. SSE is the standard transport for GraphQL subscriptions over HTTP and is what the rest of the JS/Rust GraphQL ecosystem (graphql-yoga, graphql-helix, the graphql-sse reference protocol) is converging on. The previous WebSocket route was added in 1/n as a working starting point but is not in production yet, so this PR removes it outright rather than running the two transports in parallel. Server changes (`sui-indexer-alt-graphql/src/lib.rs`) * New endpoint: `POST /graphql/subscriptions`. Accepts a GraphQL request with `Accept: text/event-stream`, returns a graphql-sse "distinct connections" stream (`event: next` frames carrying GraphQL responses). Implemented by `graphql_subscriptions` handler which wraps `schema.execute_stream()` in `axum::response::sse::Sse` with a 15 s keep-alive comment. * Removed: `handle_ws`, the GET-multiplexer `graphql_get`, and the imports `ALL_WEBSOCKET_PROTOCOLS`, `GraphQLProtocol`, `GraphQLWebSocket`, `WebSocketUpgrade`. GET on `/graphql` now serves the GraphiQL IDE directly via the `graphiql` handler (gated on `IdeEnabled`), matching the pre-1/n shape. GraphiQL host page (`sui-indexer-alt-graphql/assets/graphiql.html`) `async_graphql::http::GraphiQLSource::build()` only knows how to wire the WebSocket subscription URL, so we ship a small static HTML template that loads upstream GraphiQL via UMD plus the `graphql-sse` UMD client. The template substitutes endpoint paths at serve time via two `.replace()` calls in the `graphiql` handler. * All CDN dependencies pinned to specific patch versions (`graphiql@3.9.0`, `graphql-sse@2.6.0`, `react@18.3.1`, `react-dom@18.3.1`, `graphql@16.14.0`) so the served page does not break when upstreams cut a major. * Subscription detection uses `parse` + `getOperationAST` from the `graphql` ESM module, so multi-operation documents with an explicit `operationName` route correctly. Falls back to non-subscription on parse error. * Subscriptions are routed through `graphqlSse.iterate(params)`; queries and mutations go through a plain `fetch` POST to `/graphql`. This matches the shape that graphql/graphiql#4218 will eventually land upstream, so the future swap to `createGraphiQLFetcher({ url, sseUrl })` will be a one-line change once a UMD build of `@graphiql/toolkit` is available. Test infrastructure (`sui-indexer-alt-e2e-tests/tests/graphql_subscription/testing/harness.rs`) Migrated `SubscriptionTestCluster::subscribe_with_variables` from `tokio-tungstenite` (WebSocket + `graphql-transport-ws` protocol) to `reqwest` HTTP POST + a small inline SSE event-stream parser. The parser reads `event: next` frames, parses their `data:` field as JSON, and yields each one to the test until the server sends `event: complete` or closes the connection. `subscription_url` now points at `http://.../graphql/subscriptions` instead of `ws://.../graphql`. Test bodies (`checkpoint_subscription.rs`, `transaction_subscription.rs`, `event_subscription.rs`) are unchanged; they only consume the public `subscribe()` / `subscribe_with_variables()` methods. Dependency changes: * `sui-indexer-alt-e2e-tests/Cargo.toml`: drop `tokio-tungstenite`, add `async-stream` and `bytes` as dev-deps for the SSE parser. Verified locally * `cargo nextest run -p sui-indexer-alt-graphql --features staging --lib`: 158/158 * `cargo nextest run -p sui-indexer-alt-e2e-tests --features staging --test graphql_subscription`: 15/15 * `cargo clippy -p sui-indexer-alt-graphql -p sui-indexer-alt-e2e-tests --features staging --all-targets -- -D warnings`: clean * `cargo fmt --check`: clean * Manual smoke: ran the local server against testnet via `--checkpoint-stream-url https://fullnode.testnet.sui.io:443`, opened GraphiQL in the browser, confirmed `subscription { checkpoints { sequenceNumber } }` streams real testnet checkpoints over SSE while a regular query (`{ chainIdentifier }`) routes through `fetch`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tpham-mysten
force-pushed
the
poc-graphql-sse-graphiql
branch
from
May 11, 2026 12:40
71b276e to
3417cb7
Compare
tpham-mysten
temporarily deployed
to
sui-typescript-aws-kms-test-env
May 11, 2026 12:40 — with
GitHub Actions
Inactive
bmwill
approved these changes
May 11, 2026
amnn
approved these changes
May 11, 2026
| <div id="graphiql" style="height: 100vh;"></div> | ||
| <script crossorigin src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script> | ||
| <script crossorigin src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script> | ||
| <script src="https://unpkg.com/graphiql@3.9.0/graphiql.min.js"></script> |
Contributor
There was a problem hiding this comment.
Note for follow-up. As long as we're customising this, we might as well upgrade it to the latest version (5.2.2)
8 tasks
This was referenced May 20, 2026
tpham-mysten
added a commit
that referenced
this pull request
May 20, 2026
## Description
Streaming subscribers want to take an object ID that surfaces inside an
event payload and immediately ask "how was this object referenced by the
transaction that emitted the event?" without round-tripping to the
indexed Query API. The object is also sometimes an unchanged shared
input (e.g. a clock or registry), so it never appears in
`objectChanges`; the resolver therefore needs to expose both the change
case and the read-only consensus input case.
This PR adds:
```graphql
type Address {
asTransactionObject(transactionDigest: String): TransactionObjectRef
}
union TransactionObjectRef = ObjectChange | ConsensusObjectRead
```
`ObjectChange` and `ConsensusObjectRead` are reused as-is from the
schema; no duplication. The resolver scans `effects.object_changes`
first, then `effects.unchanged_consensus_objects` filtered to the
ReadOnlyRoot variant. Other unchanged-input markers (cancelled,
stream-ended, per-epoch) resolve to null since they do not house an
object the user can navigate to.
In an events subscription, the transactionDigest argument may be
omitted; the field then resolves against the transaction that emitted
the parent event, via a new Scope::tx_digest_viewed_at() helper. Passing
an explicit transactionDigest other than the parent event's transaction
in subscription context is intentionally not supported (the document
states this); use the indexed Query API for arbitrary transaction
lookups.
## Test plan
How did you test the new or updated feature?
- unit + e2e tests
## Stack
- #26019
- #26094
- #26117
- #26170
- #26194
- #26202
- #26414
- #26453
- #26476
- #26487
- #26425
- #26495
---
## Release notes
Check each box that your changes affect. If none of the boxes relate to
your changes, release notes aren't required.
For each box you select, include information after the relevant heading
that describes the impact of your changes that a user might notice and
any actions they must take to implement updates.
- [ ] Protocol:
- [ ] Nodes (Validators and Full nodes):
- [ ] gRPC:
- [ ] JSON-RPC:
- [x] GraphQL: add asTransactionObject which allows user to query the
status of address that is involved as an object in a particular
transaction
- [ ] CLI:
- [ ] Rust SDK:
- [ ] Indexing Framework:
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
tpham-mysten
added a commit
that referenced
this pull request
Jun 25, 2026
## Description - Expose cursor: String! on Checkpoint — encoded JsonCursor<u64> of the sequence number, matching the format Query.checkpoints pagination already uses. - Clients (especially streaming subscribers) read this on each yielded checkpoint and pass it back as afterCursor to resume from this point. ## Test plan How did you test the new or updated feature? - e2e + unit tests --- ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26425 - #26495 - #26714 - #26731 ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] gRPC: - [ ] JSON-RPC: - [x] GraphQL: Add cursor field to Checkpoint object - [ ] CLI: - [ ] Rust SDK: - [ ] Indexing Framework:
tpham-mysten
added a commit
that referenced
this pull request
Jul 2, 2026
## Description - Add `afterCursor` and `afterCheckpoint` to the checkpoints subscription so clients can resume from a known point. On subscribe, a LedgerService scan covers the gap to the live tip in parallel batches, then the live broadcast takes over. - Gaps or Lagged events during the live phase re-enter the scan, bounded by `resume_max_recovery_attempts` consecutive retries before disconnecting the subscriber. ## Test plan How did you test the new or updated feature? - cargo nextest run --features staging -p sui-indexer-alt-graphql --lib - Schema snapshot regenerated and checked in. - Manual: connect to a streaming server with afterCheckpoint set --- ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26425 - #26495 - #26731 - #26714 ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] gRPC: - [ ] JSON-RPC: - [x] GraphQL: Support resumable checkpoints subscription in features `staging` - [ ] CLI: - [ ] Rust SDK: - [ ] Indexing Framework:
8 tasks
8 tasks
tpham-mysten
added a commit
that referenced
this pull request
Jul 31, 2026
## Description Part of the GraphQL streaming series (follows #26714, checkpoints [14/n]). Adds a resumable `transactions` subscription: it backfills the filter's matching transactions from a resume point via the scanning API, then hands off to the live checkpoint broadcast at a pinned seam, so delivery is contiguous with no gap or duplicate. The backfill scan retries transient errors (e.g. a rolling indexer deploy) with bounded exponential backoff before it gives up and disconnects. Staging-gated and not yet exposed. Each payload is a batch of transaction edges (`[TransactionEdge!]!`) rather than a single edge: the backfill packs matches up to a fixed batch size so a deep scan coalesces its navigation reads, while live delivers each checkpoint's matches as one batch. Per-transaction cursors keep resume exact even when a batch splits a checkpoint. The scan-then-live handoff, sparse-filter coverage, cursor unification, batching, and retry are documented at the top of `api/subscription/transactions.rs`. ## Test plan - e2e parity test (`test_transaction_subscription_live_backfill_parity`): the same transactions resolve identically whether delivered live or through the backfill scan, plus the existing streaming/ordering/resume subscription tests. - Batching e2e tests: `test_transaction_subscription_backfill_batches_matches` (a deep backfill packs matches into multi-edge payloads) and `test_transaction_subscription_live_batches_per_checkpoint` (each live payload is one checkpoint's matches). - Unit tests for the scan retry: recovers within the budget, gives up once it is exhausted. --- ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26425 - #26495 - #26731 - #26714 - #27140 ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] gRPC: - [ ] JSON-RPC: - [x] GraphQL: Support transaction subscription from historical cursor - [ ] CLI: - [ ] Rust SDK: - [ ] Indexing Framework:
This was referenced Aug 4, 2026
tpham-mysten
added a commit
that referenced
this pull request
Aug 5, 2026
…27537) ## Description Resolve subscription payloads concurrently rather than one at a time. A single `max_concurrent_resolutions` config drives both the resolve window (async-graphql's `subscription_resolution_concurrency`, now [merged](amnn/async-graphql#2)) and the backfill scan page, so a batch's matches resolve within the concurrency budget and coalesce their content reads into one `KvLoader` round trip. ## Test plan - e2e parity test (`test_transaction_subscription_live_backfill_parity`): the same transactions resolve identically whether delivered live or through the backfill scan, plus the existing streaming/ordering/resume subscription tests. - Batching e2e tests: `test_transaction_subscription_backfill_batches_matches` (a deep backfill packs matches into multi-edge payloads) and `test_transaction_subscription_live_batches_per_checkpoint` (each live payload is one checkpoint's matches). - Unit tests for the scan retry: recovers within the budget, gives up once it is exhausted. --- ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26425 - #26495 - #26731 - #26714 - #27140 - #27537 ## Release notes Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required. For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates. - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] gRPC: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] Indexing Framework:
tpham-mysten
added a commit
that referenced
this pull request
Aug 5, 2026
…17/n] (#27564) ## Description A per-subscriber delivery throttle for GraphQL subscriptions. The SSE handler paces each payload by its cost (`cost / rate` seconds), so a subscriber's sustained delivery stays within `per_subscriber_max_output_nodes_per_second` output nodes per second (default ~off, an operator opts in by lowering it). A payload's cost is its output nodes plus a query-depth surcharge. The throttle lives in the handler, so it wraps every subscription type identically. ## Test plan Unit tests cover the cost model and `Throttle::wrap` pacing (deterministic via tokio's paused clock). `throttle_subscription.rs` adds e2e for checkpoint pacing/ordering, the disable path, richer-payload cost, and the transaction backfill path. ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26425 - #26495 - #26731 - #26714 - #27140 - #27537 - #27564 ## Release notes - [ ] Protocol: - [ ] Nodes (Validators and Full nodes): - [ ] gRPC: - [ ] JSON-RPC: - [ ] GraphQL: - [ ] CLI: - [ ] Rust SDK: - [ ] Indexing Framework:
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.
Summary
Replace the WebSocket subscription transport with Server-Sent Events. SSE is the standard transport for GraphQL subscriptions over HTTP. WebSocket was added in 1/n (#26019) as a starting point and is not in production yet, so this PR removes it outright.
Why
For one-way streaming (server pushes events, client mostly listens — exactly our subscription shape), SSE is closer to current industry best practice than WebSocket: simpler infra (no upgrade dance, plays naturally with HTTP/2/3, standard HTTP caching/proxy semantics), debuggable with curl, native browser support. The same pattern is used by OpenAI/Anthropic completion streaming, Cloudflare Workers AI, GitHub live activity, Vercel AI SDK, etc.
GraphiQL story
Upstream GraphiQL has no SSE fetcher today. Rather than block on the in-flight upstream PR (graphql/graphiql#4218), this PR ships a small static HTML template (
assets/graphiql.html) that loads GraphiQL via UMD plus thegraphql-sseUMD client and wires a custom fetcher: subscriptions go throughgraphqlSse.iterate(), queries throughfetch. CDN deps pinned; subscription detection usesparse+getOperationASTso it handles multi-operation documents with an explicitoperationName. Once #4218 lands, this collapses to a one-linecreateGraphiQLFetcher({ url, sseUrl })call.Test plan
How did you test the new or updated feature?
Stack
Release notes
Check each box that your changes affect. If none of the boxes relate to your changes, release notes aren't required.
For each box you select, include information after the relevant heading that describes the impact of your changes that a user might notice and any actions they must take to implement updates.