Skip to content

feat: Add SSE subscription support to GraphiQL fetcher#4218

Open
rushrs wants to merge 2 commits into
graphql:mainfrom
rushrs:codex/add-sse-subscription-fetcher
Open

feat: Add SSE subscription support to GraphiQL fetcher#4218
rushrs wants to merge 2 commits into
graphql:mainfrom
rushrs:codex/add-sse-subscription-fetcher

Conversation

@rushrs
Copy link
Copy Markdown

@rushrs rushrs commented Apr 27, 2026

Summary

Adds GraphQL over SSE support to createGraphiQLFetcher in @graphiql/toolkit.

  • Adds sseUrl, sseClient, and sseClientOptions fetcher options
  • Introduces a generic subscription-client adapter shared by SSE and graphql-ws
  • Keeps existing subscriptionUrl, wsClient, legacyClient, and legacyWsClient behavior as the fallback path
  • Disposes toolkit-created SSE clients when subscription iterators close, while leaving caller-owned sseClient instances under caller ownership
  • Adds graphql-sse as an optional peer dependency
  • Documents npm and ESM CDN usage, including the optional graphql-sse import-map entry

Reasoning

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/toolkit fetcher layer where websocket support already lives.

The compatibility strategy is intentionally additive:

  • Existing imports and the createGraphiQLFetcher(...) entrypoint are unchanged.
  • Query, mutation, introspection, and multipart incremental delivery behavior are unchanged.
  • Existing websocket consumers continue to use wsClient, subscriptionUrl, or legacy websocket clients.
  • SSE is only selected when sseClient or sseUrl is provided.
  • graphql-sse is dynamically imported only for sseUrl, and is marked optional so non-SSE consumers do not need to install it.
  • CDN users who opt into sseUrl can map graphql-sse explicitly, 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:check
  • node .yarn/releases/yarn-4.9.1.cjs workspace @graphiql/react types:check
  • node .yarn/releases/yarn-4.9.1.cjs workspace graphiql types:check
  • node .yarn/releases/yarn-4.9.1.cjs workspace @graphiql/toolkit build
  • node .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.md
  • git diff --check origin/main...HEAD

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 27, 2026

🦋 Changeset detected

Latest commit: 1bf4314

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@graphiql/toolkit Minor
@graphiql/plugin-history Patch
@graphiql/react Patch

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

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented Apr 27, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

@rushrs rushrs marked this pull request as ready for review April 27, 2026 18:58
@rushrs
Copy link
Copy Markdown
Author

rushrs commented Apr 27, 2026

used codex to comb for edge cases and and ensuring no breaking changes

@rushrs rushrs changed the title [codex] Add SSE subscription support to GraphiQL fetcher feat: Add SSE subscription support to GraphiQL fetcher Apr 27, 2026
@rushrs rushrs force-pushed the codex/add-sse-subscription-fetcher branch from 1892d82 to 5c2f056 Compare April 27, 2026 19:15
@rushrs rushrs force-pushed the codex/add-sse-subscription-fetcher branch from 5c2f056 to 05a62ea Compare April 27, 2026 19:40
@trevor-scheer
Copy link
Copy Markdown
Contributor

@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.

tpham-mysten added a commit to MystenLabs/sui that referenced this pull request May 11, 2026
… [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 added a commit to MystenLabs/sui that referenced this pull request May 11, 2026
… [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>
@rushrs
Copy link
Copy Markdown
Author

rushrs commented May 11, 2026

@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.

Let me throw something together during my AM

tpham-mysten added a commit to MystenLabs/sui that referenced this pull request May 12, 2026
## 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>
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