Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thin Client Integration packages #2066

Merged
merged 3 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/giant-spoons-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-yoga/apollo-link': patch
'@graphql-yoga/urql-exchange': patch
---

Thinner Client integration packages
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ if (process.env.INTEGRATION_TEST === 'true') {

// tests that leak due to external dependencies
if (process.env.LEAKS_TEST === 'true') {
testMatch.push('!**/hackernews.spec.ts')
testMatch.push(
'!**/hackernews.spec.ts',
'!**/urql-exchange.spec.ts',
'!**/apollo-link.spec.ts',
)
}

testMatch.push('!**/dist/**', '!**/.bob/**')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe.skip('Yoga Apollo Link', () => {
client = new ApolloClient({
link: new YogaLink({
endpoint: url,
customFetch: yoga.fetch as WindowOrWorkerGlobalScope['fetch'],
fetch: yoga.fetch as WindowOrWorkerGlobalScope['fetch'],
}),
cache: new InMemoryCache(),
})
Expand Down
4 changes: 2 additions & 2 deletions packages/client/apollo-link/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"access": "public"
},
"dependencies": {
"@graphql-tools/url-loader": "7.16.12",
"@graphql-tools/utils": "9.1.0",
"@graphql-tools/executor-http": "0.0.2",
"@graphql-tools/executor-apollo-link": "0.0.2",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand Down
63 changes: 7 additions & 56 deletions packages/client/apollo-link/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,13 @@
import * as apolloImport from '@apollo/client'
import { ExecutorLink } from '@graphql-tools/executor-apollo-link'
import {
LoadFromUrlOptions,
SubscriptionProtocol,
UrlLoader,
} from '@graphql-tools/url-loader'
import { ExecutionRequest, isAsyncIterable } from '@graphql-tools/utils'
HTTPExecutorOptions,
buildHTTPExecutor,
} from '@graphql-tools/executor-http'

export type YogaLinkOptions = LoadFromUrlOptions & { endpoint: string }
export type YogaLinkOptions = HTTPExecutorOptions

const apollo: typeof apolloImport =
(apolloImport as any)?.default ?? apolloImport

function createYogaApolloRequestHandler(
options: YogaLinkOptions,
): apolloImport.RequestHandler {
const urlLoader = new UrlLoader()
const executor = urlLoader.getExecutorAsync(options.endpoint, {
subscriptionsProtocol: SubscriptionProtocol.SSE,
multipart: true,
...options,
})
return function graphQLYogaApolloRequestHandler(
operation: apolloImport.Operation,
): apolloImport.Observable<apolloImport.FetchResult> {
return new apollo.Observable((observer) => {
const executionRequest: ExecutionRequest = {
document: operation.query,
variables: operation.variables,
operationName: operation.operationName,
extensions: operation.extensions,
context: operation.getContext(),
}
executor(executionRequest)
.then(async (results) => {
if (isAsyncIterable(results)) {
for await (const result of results) {
if (observer.closed) {
return
}
observer.next(result)
}
observer.complete()
} else if (!observer.closed) {
observer.next(results)
observer.complete()
}
})
.catch((error) => {
if (!observer.closed) {
observer.error(error)
}
})
})
}
}

export class YogaLink extends apollo.ApolloLink {
export class YogaLink extends ExecutorLink {
constructor(options: YogaLinkOptions) {
super(createYogaApolloRequestHandler(options))
super(buildHTTPExecutor(options as any))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe.skip('URQL Yoga Exchange', () => {
url,
exchanges: [
yogaExchange({
customFetch: yoga.fetch as WindowOrWorkerGlobalScope['fetch'],
fetch: yoga.fetch as WindowOrWorkerGlobalScope['fetch'],
}),
],
})
Expand Down
4 changes: 2 additions & 2 deletions packages/client/urql-exchange/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
"access": "public"
},
"dependencies": {
"@graphql-tools/url-loader": "7.16.12",
"@graphql-tools/utils": "9.1.0",
"@graphql-tools/executor-http": "0.0.2",
"@graphql-tools/executor-urql-exchange": "0.0.2",
"tslib": "^2.4.0"
},
"devDependencies": {
Expand Down
149 changes: 8 additions & 141 deletions packages/client/urql-exchange/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,12 @@
import {
Source,
pipe,
share,
filter,
takeUntil,
mergeMap,
merge,
make,
} from 'wonka'
buildHTTPExecutor,
HTTPExecutorOptions,
} from '@graphql-tools/executor-http'
import { executorExchange } from '@graphql-tools/executor-urql-exchange'
import { Exchange } from '@urql/core'

import {
Exchange,
ExecutionResult,
makeResult,
makeErrorResult,
mergeResultPatch,
Operation,
OperationResult,
getOperationName,
OperationContext,
ExchangeIO,
AnyVariables,
} from '@urql/core'

import { ExecutionRequest, isAsyncIterable } from '@graphql-tools/utils'
import {
LoadFromUrlOptions,
SubscriptionProtocol,
UrlLoader,
} from '@graphql-tools/url-loader'
import { OperationTypeNode } from 'graphql'

export type YogaExchangeOptions = LoadFromUrlOptions

export function yogaExchange(options?: YogaExchangeOptions): Exchange {
const urlLoader = new UrlLoader()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function makeYogaSource<TData extends Record<string, any>>(
operation: Operation<TData>,
): Source<OperationResult<TData>> {
const operationName = getOperationName(operation.query)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const executionRequest: ExecutionRequest<any, OperationContext> = {
document: operation.query,
operationName,
operationType: operation.kind as OperationTypeNode,
variables: operation.variables,
context: operation.context,
extensions: {
endpoint: operation.context.url,
headers: operation.context.headers,
},
}
const extraFetchOptions =
typeof operation.context.fetchOptions === 'function'
? operation.context.fetchOptions()
: operation.context.fetchOptions
const executor = urlLoader.getExecutorAsync(
options?.endpoint || operation.context.url,
{
subscriptionsProtocol: SubscriptionProtocol.SSE,
multipart: true,
customFetch: operation.context.fetch,
useGETForQueries: !!operation.context.preferGetMethod,
headers: extraFetchOptions?.headers as Record<string, string>,
method: extraFetchOptions?.method as 'GET' | 'POST',
credentials: extraFetchOptions?.credentials,
...options,
},
)
return make<OperationResult<TData>>((observer) => {
let ended = false
executor(executionRequest)
.then(
async (result: ExecutionResult | AsyncIterable<ExecutionResult>) => {
if (ended || !result) {
return
}
if (!isAsyncIterable(result)) {
observer.next(makeResult(operation, result))
} else {
let prevResult: OperationResult<TData, AnyVariables> | null = null

for await (const value of result) {
if (value) {
prevResult = prevResult
? mergeResultPatch(prevResult, value)
: makeResult(operation, value)
observer.next(prevResult)
}
if (ended) {
break
}
}
}
observer.complete()
},
)
.catch((error) => {
observer.next(makeErrorResult(operation, error))
})
.finally(() => {
ended = true
observer.complete()
})
return () => {
ended = true
}
})
}
return function yogaExchangeFn({ forward }): ExchangeIO {
return function yogaExchangeIO<TData, TVariables extends AnyVariables>(
ops$: Source<Operation<TData, TVariables>>,
): Source<OperationResult<TData>> {
const sharedOps$ = share(ops$)

const executedOps$ = pipe(
sharedOps$,
filter(
(operation) =>
operation.kind === 'query' ||
operation.kind === 'mutation' ||
operation.kind === 'subscription',
),
mergeMap((operation) => {
const teardown$ = pipe(
sharedOps$,
filter((op) => op.kind === 'teardown' && op.key === operation.key),
)

return pipe(makeYogaSource(operation), takeUntil(teardown$))
}),
)

const forwardedOps$ = pipe(
sharedOps$,
filter((operation) => operation.kind === 'teardown'),
forward,
)
export type YogaExchangeOptions = HTTPExecutorOptions

return merge([executedOps$, forwardedOps$])
}
}
export function yogaExchange(options?: HTTPExecutorOptions): Exchange {
return executorExchange(buildHTTPExecutor(options as any))
}
Loading