feat: Add SSE subscription support to GraphiQL fetcher#4218
Conversation
🦋 Changeset detectedLatest commit: 1bf4314 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
used codex to comb for edge cases and and ensuring no breaking changes |
1892d82 to
5c2f056
Compare
5c2f056 to
05a62ea
Compare
|
@dimaMachina I know you have some context here (#3750) in case you'd like to weigh in. I'll check in on this in a few days otherwise, most likely. @rushrs would you mind sharing a repo where I can demo this? I haven't messed with SSE at all yet. |
… [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>
… [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>
Let me throw something together during my AM |
## 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 the `graphql-sse` UMD client and wires a custom fetcher: subscriptions go through `graphqlSse.iterate()`, queries through `fetch`. CDN deps pinned; subscription detection uses `parse` + `getOperationAST` so it handles multi-operation documents with an explicit `operationName`. Once #4218 lands, this collapses to a one-line `createGraphiQLFetcher({ url, sseUrl })` call. ## Test plan How did you test the new or updated feature? - unit + e2e tests - Start local GraphQL Server ## Stack - #26019 - #26094 - #26117 - #26170 - #26194 - #26202 - #26414 - #26453 - #26476 - #26487 - #26495 - #26425 --- ## 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: Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds GraphQL over SSE support to
createGraphiQLFetcherin@graphiql/toolkit.sseUrl,sseClient, andsseClientOptionsfetcher optionsgraphql-wssubscriptionUrl,wsClient,legacyClient, andlegacyWsClientbehavior as the fallback pathsseClientinstances under caller ownershipgraphql-sseas an optional peer dependencygraphql-sseimport-map entryReasoning
Some GraphQL servers expose subscriptions over GraphQL over SSE rather than websockets. GraphiQL already has a transport-neutral fetcher contract: the UI calls a fetcher and can consume plain results, observables, or async iterables. This change keeps that contract intact and adds SSE at the
@graphiql/toolkitfetcher layer where websocket support already lives.The compatibility strategy is intentionally additive:
createGraphiQLFetcher(...)entrypoint are unchanged.wsClient,subscriptionUrl, or legacy websocket clients.sseClientorsseUrlis provided.graphql-sseis dynamically imported only forsseUrl, and is marked optional so non-SSE consumers do not need to install it.sseUrlcan mapgraphql-sseexplicitly, matching how optional transport peers work in browser import-map setups.Validation
node .yarn/releases/yarn-4.9.1.cjs exec vitest run --config packages/graphiql-toolkit/vitest.config.mts packages/graphiql-toolkit/src/create-fetcher/__tests__node .yarn/releases/yarn-4.9.1.cjs workspace @graphiql/toolkit types:checknode .yarn/releases/yarn-4.9.1.cjs workspace @graphiql/react types:checknode .yarn/releases/yarn-4.9.1.cjs workspace graphiql types:checknode .yarn/releases/yarn-4.9.1.cjs workspace @graphiql/toolkit buildnode .yarn/releases/yarn-4.9.1.cjs exec oxfmt --check package.json packages/graphiql-toolkit/package.json packages/graphiql-toolkit/src/create-fetcher packages/graphiql-toolkit/docs/create-fetcher.md packages/graphiql-toolkit/README.md .changeset/silent-lemons-learn.mdgit diff --check origin/main...HEAD