-
Notifications
You must be signed in to change notification settings - Fork 991
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support GraphQL Subscriptions in Apollo Client using SSE links (#…
…9009) This PR adds support for GraphQL SSE (Server Sent Events) in both the Redwood GraphQL Server and the Apollo Client web side. GraphQL SSE has two options: distinct connections mode and single connection mode. Yoga supports distinct connection mode out of the box and is the one configured in the PR. There is a known "gotcha" with distinct mode: * [Maximum open connections limit](https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x) (when not using http/2) * But we can work to support http2 in Fastify and also see how deploy providers handle http2 See also: https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md#distinct-connections-mode Note: I have testing setting up single connection mode but this requires: * two graphql endpoints/servers at "graphql" for non subscriptions and "graphql/stream" for subs * some extra termination code as seen here: https://github.com/dotansimha/graphql-yoga/blob/main/examples/graphql-sse/src/app.ts We can revisit single connection mode configuration if in beta testing we see the distinct connection issue being significant. At least we now know how to configure and setup both modes. The PR: * Adds the graphql sse plugin to yoga if subscriptions are enabled * Adds a new SSELink to Apollo client following https://the-guild.dev/graphql/sse/recipes#with-apollo * In Apollo client, it needs to now use the SSELink for subs and the HTTPLink for other operations, so "directional link composition" is employed. See: https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition * This is now the "terminatingLink" and the client uses one or the other depending on the operation * Fixes out-of-date realtime templates that now need to use the realtime package
- Loading branch information
1 parent
71888e3
commit 20a1fd1
Showing
12 changed files
with
244 additions
and
190 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
packages/cli/src/commands/experimental/templates/liveQueries/auctions/auctions.ts.template
This file contains 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
2 changes: 1 addition & 1 deletion
2
...s/cli/src/commands/experimental/templates/subscriptions/newMessage/newMessage.ts.template
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import type { HttpOptions } from '@apollo/client' | ||
import { | ||
ApolloLink, | ||
Operation, | ||
FetchResult, | ||
Observable, | ||
} from '@apollo/client/core' | ||
import { print } from 'graphql' | ||
import { createClient, ClientOptions, Client } from 'graphql-sse' | ||
interface SSELinkOptions extends Partial<ClientOptions> { | ||
url: string | ||
auth: { authProviderType: string; tokenFn: () => Promise<null | string> } | ||
httpLinkConfig?: HttpOptions | ||
headers?: Record<string, string> | ||
} | ||
|
||
const mapCredentialsHeader = ( | ||
httpLinkCredentials?: string | ||
): 'omit' | 'same-origin' | 'include' | undefined => { | ||
if (!httpLinkCredentials) { | ||
return undefined | ||
} | ||
switch (httpLinkCredentials) { | ||
case 'omit': | ||
case 'same-origin': | ||
case 'include': | ||
return httpLinkCredentials | ||
default: | ||
return undefined | ||
} | ||
} | ||
|
||
const mapReferrerPolicyHeader = ( | ||
referrerPolicy?: string | ||
): | ||
| 'no-referrer' | ||
| 'no-referrer-when-downgrade' | ||
| 'same-origin' | ||
| 'origin' | ||
| 'strict-origin' | ||
| 'origin-when-cross-origin' | ||
| 'strict-origin-when-cross-origin' | ||
| 'unsafe-url' | ||
| undefined => { | ||
if (!referrerPolicy) { | ||
return undefined | ||
} | ||
switch (referrerPolicy) { | ||
case 'no-referrer': | ||
case 'no-referrer-when-downgrade': | ||
case 'same-origin': | ||
case 'origin': | ||
case 'strict-origin': | ||
case 'origin-when-cross-origin': | ||
case 'strict-origin-when-cross-origin': | ||
case 'unsafe-url': | ||
return referrerPolicy | ||
default: | ||
return undefined | ||
} | ||
} | ||
|
||
/** | ||
* GraphQL over Server-Sent Events (SSE) spec link for Apollo Client | ||
*/ | ||
export class SSELink extends ApolloLink { | ||
private client: Client | ||
|
||
constructor(options: SSELinkOptions) { | ||
super() | ||
|
||
const { url, auth, headers, httpLinkConfig } = options | ||
const { credentials, referrer, referrerPolicy } = | ||
httpLinkConfig?.headers || {} | ||
|
||
this.client = createClient({ | ||
url, | ||
headers: async () => { | ||
const token = await auth.tokenFn() | ||
|
||
// Only add auth headers when there's a token. `token` is `null` when `!isAuthenticated`. | ||
if (!token) { | ||
return { ...headers } | ||
} | ||
return { | ||
Authorization: `Bearer ${token}`, | ||
'auth-provider': auth.authProviderType, | ||
...headers, | ||
} | ||
}, | ||
credentials: mapCredentialsHeader(credentials), | ||
referrer, | ||
referrerPolicy: mapReferrerPolicyHeader(referrerPolicy), | ||
}) | ||
} | ||
|
||
public request(operation: Operation): Observable<FetchResult> { | ||
return new Observable((sink) => { | ||
return this.client.subscribe<FetchResult>( | ||
{ ...operation, query: print(operation.query) }, | ||
{ | ||
next: sink.next.bind(sink), | ||
complete: sink.complete.bind(sink), | ||
error: sink.error.bind(sink), | ||
} | ||
) | ||
}) | ||
} | ||
} |
Oops, something went wrong.