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

feat!: add signal support #303

Merged
merged 12 commits into from
Jan 8, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules
dist
.DS_Store
*.log
coverage
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps
- [Browser](#browser)
- [Node](#node)
- [Batching](#batching)
- [Cancellation](#cancellation)
- [FAQ](#faq)
- [Why do I have to install `graphql`?](#why-do-i-have-to-install-graphql)
- [Do I need to wrap my GraphQL documents inside the `gql` template exported by `graphql-request`?](#do-i-need-to-wrap-my-graphql-documents-inside-the-gql-template-exported-by-graphql-request)
Expand Down Expand Up @@ -539,6 +540,31 @@ import { batchRequests } from 'graphql-request';
})().catch((error) => console.error(error))
```

### Cancellation

It is possible to cancel a request using an `AbortController` signal.

You can define the `signal` in the `GraphQLClient` constructor:

```ts
const abortController = new AbortController()

const client = new GraphQLClient(endpoint, { signal: abortController.signal })
client.request(query)

abortController.abort()
```

You can also set the signal per request (this will override an existing GraphQLClient signal):

```ts
const abortController = new AbortController()

const client = new GraphQLClient(endpoint)
client.request({ document: query, signal: abortController.signal })

abortController.abort()
```

## FAQ

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"test:node": "jest --testEnvironment node",
"test:dom": "jest --testEnvironment jsdom",
"test": "yarn test:node && yarn test:dom",
"test:coverage": "yarn test --coverage",
"release:stable": "dripip stable",
"release:preview": "dripip preview",
"release:pr": "dripip pr"
Expand Down
126 changes: 112 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,69 @@ import crossFetch, * as CrossFetch from 'cross-fetch'
import { OperationDefinitionNode } from 'graphql/language/ast'
import { print } from 'graphql/language/printer'
import createRequestBody from './createRequestBody'
import { BatchRequestDocument, ClientError, RequestDocument, Variables } from './types'
import {
BatchRequestDocument,
BatchRequestOptions,
ClientError,
RawRequestOptions,
RequestDocument,
RequestOptions,
Variables,
} from './types'
import * as Dom from './types.dom'

export { BatchRequestDocument, ClientError, RequestDocument, Variables }
export {
BatchRequestDocument,
BatchRequestOptions,
ClientError,
RequestDocument,
Variables,
RawRequestOptions,
RequestOptions,
}

function parseRequestArgs<V = Variables>(
arg1: RequestDocument | RequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
) {
return (arg1 as RequestOptions).document
? (arg1 as RequestOptions<V>)
: {
document: arg1 as RequestDocument,
variables: arg2,
requestHeaders: arg3,
signal: undefined,
}
}

function parseRawRequestArgs<V = Variables>(
arg1: RequestDocument | RawRequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
) {
return (arg1 as RawRequestOptions).query
? (arg1 as RawRequestOptions<V>)
: {
query: arg1 as string,
variables: arg2,
requestHeaders: arg3,
signal: undefined,
}
}

function parseBatchRequestArgs<V = Variables>(
arg1: BatchRequestDocument<V>[] | BatchRequestOptions,
arg2?: Dom.RequestInit['headers']
) {
return (arg1 as BatchRequestOptions).documents
? (arg1 as BatchRequestOptions<V>)
: {
documents: arg1 as BatchRequestDocument<V>[],
requestHeaders: arg2,
signal: undefined,
}
}

/**
* Convert the given headers configuration into a plain object.
Expand Down Expand Up @@ -152,7 +211,7 @@ const get = async <V = Variables>({
}

/**
* todo
* GraphQL Client.
*/
export class GraphQLClient {
private url: string
Expand All @@ -163,21 +222,37 @@ export class GraphQLClient {
this.options = options || {}
}

rawRequest<T = any, V = Variables>(
/**
* Send a GraphQL query to the server.
*/
async rawRequest<T = any, V = Variables>(
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
async rawRequest<T = any, V = Variables>(
options: RawRequestOptions
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }>
async rawRequest<T = any, V = Variables>(
arg1: RequestDocument | RawRequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
const rawRequestOptions = parseRawRequestArgs(arg1, arg2, arg3)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (rawRequestOptions.signal !== undefined) {
fetchOptions.signal = rawRequestOptions.signal
}

return makeRequest<T, V>({
url,
query,
variables,
query: rawRequestOptions.query,
variables: rawRequestOptions.variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(rawRequestOptions.requestHeaders),
},
operationName: undefined,
fetch,
Expand All @@ -193,19 +268,30 @@ export class GraphQLClient {
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
async request<T = any, V = Variables>(options: RequestOptions): Promise<T>
async request<T = any, V = Variables>(
arg1: RequestDocument | RequestOptions,
arg2?: V,
arg3?: Dom.RequestInit['headers']
): Promise<T> {
const requestOptions = parseRequestArgs(arg1, arg2, arg3)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (requestOptions.signal !== undefined) {
fetchOptions.signal = requestOptions.signal
}

const { query, operationName } = resolveRequestDocument(document)
const { query, operationName } = resolveRequestDocument(requestOptions.document)

const { data } = await makeRequest<T, V>({
url,
query,
variables,
variables: requestOptions.variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(requestOptions.requestHeaders),
},
operationName,
fetch,
Expand All @@ -217,25 +303,37 @@ export class GraphQLClient {
}

/**
* Send a GraphQL document to the server.
* Send GraphQL documents in batch to the server.
*/
async batchRequests<T extends any = any, V = Variables>(
documents: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
): Promise<T>
async batchRequests<T = any, V = Variables>(options: BatchRequestOptions): Promise<T>
async batchRequests<T = any, V = Variables>(
arg1: BatchRequestDocument<V>[] | BatchRequestOptions,
arg2?: Dom.RequestInit['headers']
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
): Promise<T> {
const batchRequestOptions = parseBatchRequestArgs(arg1, arg2)

let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (batchRequestOptions.signal !== undefined) {
fetchOptions.signal = batchRequestOptions.signal
}

const queries = documents.map(({ document }) => resolveRequestDocument(document).query)
const variables = documents.map(({ variables }) => variables)
const queries = batchRequestOptions.documents.map(
({ document }) => resolveRequestDocument(document).query
)
const variables = batchRequestOptions.documents.map(({ variables }) => variables)

const { data } = await makeRequest<T, (V | undefined)[]>({
url,
query: queries,
variables,
headers: {
...resolveHeaders(headers),
...resolveHeaders(requestHeaders),
...resolveHeaders(batchRequestOptions.requestHeaders),
},
operationName: undefined,
fetch,
Expand Down
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DocumentNode } from 'graphql/language/ast'
import * as Dom from './types.dom'

export type Variables = { [key: string]: any }

Expand Down Expand Up @@ -60,3 +61,23 @@ export type BatchRequestDocument<V = Variables> = {
document: RequestDocument
variables?: V
}

export type RawRequestOptions<V = Variables> = {
query: string
variables?: V
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}

export type RequestOptions<V = Variables> = {
document: RequestDocument
variables?: V
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}

export type BatchRequestOptions<V = Variables> = {
documents: BatchRequestDocument<V>[]
requestHeaders?: Dom.RequestInit['headers']
signal?: Dom.RequestInit['signal']
}
14 changes: 12 additions & 2 deletions tests/__helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type MockResult<Spec extends MockSpec | MockSpecBatch = MockSpec> = {
}[]
}

export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>(): Context<T> {
export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>(delay?: number): Context<T> {
const ctx = {} as Context<T>
beforeAll(async () => {
const port = await getPort()
Expand All @@ -58,7 +58,11 @@ export function setupTestServer<T extends MockSpec | MockSpecBatch = MockSpec>()
ctx.url = `http://localhost:${port}`
ctx.res = (spec?: T): MockResult<T> => {
const requests: CapturedRequest[] = []
ctx.server.use('*', function mock(req, res) {
ctx.server.use('*', async function mock(req, res) {
if (delay) {
await sleep(delay)
}

req.headers.host = 'DYNAMIC'
requests.push({
method: req.method,
Expand Down Expand Up @@ -130,3 +134,9 @@ export function createApolloServerContext({ typeDefs, resolvers }: ApolloServerC

return ctx
}

export function sleep(timeout: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, timeout)
})
}
Loading