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 2 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
25 changes: 25 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,30 @@ 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()

request(endpoint, query, undefined, undefined, abortController.signal)
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved

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
33 changes: 24 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,14 @@ export class GraphQLClient {
rawRequest<T = any, V = Variables>(
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (signal !== undefined) {
fetchOptions.signal = signal
}

return makeRequest<T, V>({
url,
Expand All @@ -192,10 +196,14 @@ export class GraphQLClient {
async request<T = any, V = Variables>(
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<T> {
let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (signal !== undefined) {
fetchOptions.signal = signal
}

const { query, operationName } = resolveRequestDocument(document)

Expand All @@ -221,10 +229,14 @@ export class GraphQLClient {
*/
async batchRequests<T extends any = any, V = Variables>(
documents: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<T> {
let { headers, fetch = crossFetch, method = 'POST', ...fetchOptions } = this.options
let { url } = this
if (signal !== undefined) {
fetchOptions.signal = signal
}

const queries = documents.map(({ document }) => resolveRequestDocument(document).query)
const variables = documents.map(({ variables }) => variables)
Expand Down Expand Up @@ -336,9 +348,10 @@ export async function rawRequest<T = any, V = Variables>(
url: string,
query: string,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<{ data: T; extensions?: any; headers: Dom.Headers; status: number }> {
const client = new GraphQLClient(url)
const client = new GraphQLClient(url, { signal })
return client.rawRequest<T, V>(query, variables, requestHeaders)
}

Expand Down Expand Up @@ -380,9 +393,10 @@ export async function request<T = any, V = Variables>(
url: string,
document: RequestDocument,
variables?: V,
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<T> {
const client = new GraphQLClient(url)
const client = new GraphQLClient(url, { signal })
return client.request<T, V>(document, variables, requestHeaders)
}

Expand Down Expand Up @@ -423,9 +437,10 @@ export async function request<T = any, V = Variables>(
export async function batchRequests<T extends any = any, V = Variables>(
url: string,
documents: BatchRequestDocument<V>[],
requestHeaders?: Dom.RequestInit['headers']
requestHeaders?: Dom.RequestInit['headers'],
signal?: Dom.RequestInit['signal']
): Promise<T> {
const client = new GraphQLClient(url)
const client = new GraphQLClient(url, { signal })
return client.batchRequests<T, V>(documents, requestHeaders)
}

Expand Down
133 changes: 133 additions & 0 deletions tests/signal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { batchRequests, GraphQLClient, rawRequest, request } from '../src'
import { setupTestServer } from './__helpers'

const ctx = setupTestServer()

it('should abort a request when the signal is defined using GraphQLClient constructor', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url, { signal: abortController.signal })

try {
await client.request(ctx.url, `{ me { id } }`)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort a raw request when the signal is defined using GraphQLClient constructor', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url, { signal: abortController.signal })

try {
await client.rawRequest(ctx.url, `{ me { id } }`)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort batch requests when the signal is defined using GraphQLClient constructor', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url, { signal: abortController.signal })

try {
await client.batchRequests([{ document: `{ me { id } }` }, { document: `{ me { id } }` }])
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort a request when the signal overrides GraphQLClient settings', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url)

try {
await client.request(ctx.url, `{ me { id } }`, undefined, abortController.signal)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort a raw request when the signal overrides GraphQLClient settings', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url)

try {
await client.rawRequest(ctx.url, `{ me { id } }`, undefined, abortController.signal)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort batch requests when the signal overrides GraphQLClient settings', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

const client = new GraphQLClient(ctx.url)

try {
await client.batchRequests(
[{ document: `{ me { id } }` }, { document: `{ me { id } }` }],
undefined,
abortController.signal
)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort a request', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

try {
await request(ctx.url, `{ me { id } }`, undefined, undefined, abortController.signal)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort a raw request', async () => {
const abortController = new AbortController()
abortController.abort()
expect.assertions(1)

try {
await rawRequest(ctx.url, `{ me { id } }`, undefined, undefined, abortController.signal)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})

it('should abort batch requests', async () => {
const abortController = new AbortController()
abortController.abort()
jasonkuhrt marked this conversation as resolved.
Show resolved Hide resolved
expect.assertions(1)

try {
await batchRequests(
ctx.url,
[{ document: `{ me { id } }` }, { document: `{ me { id } }` }],
undefined,
abortController.signal
)
} catch (error) {
expect((error as Error).message).toEqual('The user aborted a request.')
}
})