Skip to content

Commit c1b4c79

Browse files
authored
feat: Implement client (#3)
1 parent ba252e7 commit c1b4c79

File tree

10 files changed

+1021
-2
lines changed

10 files changed

+1021
-2
lines changed

README.md

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,267 @@ fastify.listen(4000);
205205
console.log('Listening to port 4000');
206206
```
207207

208+
#### Use the client
209+
210+
```js
211+
import { createClient } from 'graphql-http';
212+
213+
const client = createClient({
214+
url: 'http://localhost:4000/graphql',
215+
});
216+
217+
(async () => {
218+
let cancel = () => {
219+
/* abort the request if it is in-flight */
220+
};
221+
222+
const result = await new Promise((resolve, reject) => {
223+
let result;
224+
cancel = client.subscribe(
225+
{
226+
query: '{ hello }',
227+
},
228+
{
229+
next: (data) => (result = data),
230+
error: reject,
231+
complete: () => resolve(result),
232+
},
233+
);
234+
});
235+
236+
expect(result).toEqual({ hello: 'world' });
237+
})();
238+
```
239+
208240
## Recipes
209241

242+
<details id="promise">
243+
<summary><a href="#promise">🔗</a> Client usage with Promise</summary>
244+
245+
```ts
246+
import { ExecutionResult } from 'graphql';
247+
import { createClient, RequestParams } from 'graphql-http';
248+
import { getSession } from './my-auth';
249+
250+
const client = createClient({
251+
url: 'http://hey.there:4000/graphql',
252+
headers: async () => {
253+
const session = await getSession();
254+
if (session) {
255+
return {
256+
Authorization: `Bearer ${session.token}`,
257+
};
258+
}
259+
},
260+
});
261+
262+
function execute<Data, Extensions>(
263+
params: RequestParams,
264+
): [request: Promise<ExecutionResult<Data, Extensions>>, cancel: () => void] {
265+
let cancel!: () => void;
266+
const request = new Promise<ExecutionResult<Data, Extensions>>(
267+
(resolve, reject) => {
268+
let result: ExecutionResult<Data, Extensions>;
269+
cancel = client.subscribe<Data, Extensions>(params, {
270+
next: (data) => (result = data),
271+
error: reject,
272+
complete: () => resolve(result),
273+
});
274+
},
275+
);
276+
return [request, cancel];
277+
}
278+
279+
(async () => {
280+
const [request, cancel] = execute({
281+
query: '{ hello }',
282+
});
283+
284+
// just an example, not a real function
285+
onUserLeavePage(() => {
286+
cancel();
287+
});
288+
289+
const result = await request;
290+
291+
expect(result).toBe({ data: { hello: 'world' } });
292+
})();
293+
```
294+
295+
</details>
296+
297+
</details>
298+
299+
<details id="observable">
300+
<summary><a href="#observable">🔗</a> Client usage with <a href="https://github.com/tc39/proposal-observable">Observable</a></summary>
301+
302+
```js
303+
import { Observable } from 'relay-runtime';
304+
// or
305+
import { Observable } from '@apollo/client/core';
306+
// or
307+
import { Observable } from 'rxjs';
308+
// or
309+
import Observable from 'zen-observable';
310+
// or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
311+
import { createClient } from 'graphql-http';
312+
import { getSession } from './my-auth';
313+
314+
const client = createClient({
315+
url: 'http://graphql.loves:4000/observables',
316+
headers: async () => {
317+
const session = await getSession();
318+
if (session) {
319+
return {
320+
Authorization: `Bearer ${session.token}`,
321+
};
322+
}
323+
},
324+
});
325+
326+
const observable = new Observable((observer) =>
327+
client.subscribe({ query: '{ hello }' }, observer),
328+
);
329+
330+
const subscription = observable.subscribe({
331+
next: (result) => {
332+
expect(result).toBe({ data: { hello: 'world' } });
333+
},
334+
});
335+
336+
// unsubscribe will cancel the request if it is pending
337+
subscription.unsubscribe();
338+
```
339+
340+
</details>
341+
342+
<details id="relay">
343+
<summary><a href="#relay">🔗</a> Client usage with <a href="https://relay.dev">Relay</a></summary>
344+
345+
```ts
346+
import { GraphQLError } from 'graphql';
347+
import {
348+
Network,
349+
Observable,
350+
RequestParameters,
351+
Variables,
352+
} from 'relay-runtime';
353+
import { createClient } from 'graphql-http';
354+
import { getSession } from './my-auth';
355+
356+
const client = createClient({
357+
url: 'http://i.love:4000/graphql',
358+
headers: async () => {
359+
const session = await getSession();
360+
if (session) {
361+
return {
362+
Authorization: `Bearer ${session.token}`,
363+
};
364+
}
365+
},
366+
});
367+
368+
function fetch(operation: RequestParameters, variables: Variables) {
369+
return Observable.create((sink) => {
370+
if (!operation.text) {
371+
return sink.error(new Error('Operation text cannot be empty'));
372+
}
373+
return client.subscribe(
374+
{
375+
operationName: operation.name,
376+
query: operation.text,
377+
variables,
378+
},
379+
sink,
380+
);
381+
});
382+
}
383+
384+
export const network = Network.create(fetch);
385+
```
386+
387+
</details>
388+
389+
<details id="apollo-client">
390+
<summary><a href="#apollo-client">🔗</a> Client usage with <a href="https://www.apollographql.com">Apollo</a></summary>
391+
392+
```ts
393+
import {
394+
ApolloLink,
395+
Operation,
396+
FetchResult,
397+
Observable,
398+
} from '@apollo/client/core';
399+
import { print, GraphQLError } from 'graphql';
400+
import { createClient, ClientOptions, Client } from 'graphql-http';
401+
import { getSession } from './my-auth';
402+
403+
class HTTPLink extends ApolloLink {
404+
private client: Client;
405+
406+
constructor(options: ClientOptions) {
407+
super();
408+
this.client = createClient(options);
409+
}
410+
411+
public request(operation: Operation): Observable<FetchResult> {
412+
return new Observable((sink) => {
413+
return this.client.subscribe<FetchResult>(
414+
{ ...operation, query: print(operation.query) },
415+
{
416+
next: sink.next.bind(sink),
417+
complete: sink.complete.bind(sink),
418+
error: sink.error.bind(sink),
419+
},
420+
);
421+
});
422+
}
423+
}
424+
425+
const link = new HTTPLink({
426+
url: 'http://where.is:4000/graphql',
427+
headers: async () => {
428+
const session = await getSession();
429+
if (session) {
430+
return {
431+
Authorization: `Bearer ${session.token}`,
432+
};
433+
}
434+
},
435+
});
436+
```
437+
438+
</details>
439+
440+
<details id="request-retries">
441+
<summary><a href="#request-retries">🔗</a> Client usage with request retries</summary>
442+
443+
```ts
444+
import { createClient, NetworkError } from 'graphql-http';
445+
446+
const client = createClient({
447+
url: 'http://unstable.service:4000/graphql',
448+
shouldRetry: async (err: NetworkError, retries: number) => {
449+
if (retries > 3) {
450+
// max 3 retries and then report service down
451+
return false;
452+
}
453+
454+
// try again when service unavailable, could be temporary
455+
if (err.response?.status === 503) {
456+
// wait one second (you can alternatively time the promise resolution to your preference)
457+
await new Promise((resolve) => setTimeout(resolve, 1000));
458+
return true;
459+
}
460+
461+
// otherwise report error immediately
462+
return false;
463+
},
464+
});
465+
```
466+
467+
</details>
468+
210469
<details id="auth">
211470
<summary><a href="#auth">🔗</a> Server handler usage with authentication</summary>
212471

docs/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ graphql-http
44

55
## Table of contents
66

7+
### Classes
8+
9+
- [NetworkError](classes/NetworkError.md)
10+
711
### Interfaces
812

13+
- [Client](interfaces/Client.md)
14+
- [ClientOptions](interfaces/ClientOptions.md)
915
- [HandlerOptions](interfaces/HandlerOptions.md)
1016
- [Headers](interfaces/Headers.md)
1117
- [Request](interfaces/Request.md)
@@ -22,9 +28,29 @@ graphql-http
2228

2329
### Functions
2430

31+
- [createClient](README.md#createclient)
2532
- [createHandler](README.md#createhandler)
2633
- [isResponse](README.md#isresponse)
2734

35+
## Client
36+
37+
### createClient
38+
39+
**createClient**(`options`): [`Client`](interfaces/Client.md)
40+
41+
Creates a disposable GraphQL over HTTP client to transmit
42+
GraphQL operation results.
43+
44+
#### Parameters
45+
46+
| Name | Type |
47+
| :------ | :------ |
48+
| `options` | [`ClientOptions`](interfaces/ClientOptions.md) |
49+
50+
#### Returns
51+
52+
[`Client`](interfaces/Client.md)
53+
2854
## Common
2955

3056
### Response

docs/classes/NetworkError.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
[graphql-http](../README.md) / NetworkError
2+
3+
# Class: NetworkError<Response\>
4+
5+
A network error caused by the client or an unexpected response from the server.
6+
7+
To avoid bundling DOM typings (because the client can run in Node env too),
8+
you should supply the `Response` generic depending on your Fetch implementation.
9+
10+
## Type parameters
11+
12+
| Name | Type |
13+
| :------ | :------ |
14+
| `Response` | extends `ResponseLike` = `ResponseLike` |
15+
16+
## Hierarchy
17+
18+
- `Error`
19+
20+
**`NetworkError`**
21+
22+
## Table of contents
23+
24+
### Constructors
25+
26+
- [constructor](NetworkError.md#constructor)
27+
28+
### Properties
29+
30+
- [response](NetworkError.md#response)
31+
32+
## Constructors
33+
34+
### constructor
35+
36+
**new NetworkError**<`Response`\>(`msgOrErrOrResponse`)
37+
38+
#### Type parameters
39+
40+
| Name | Type |
41+
| :------ | :------ |
42+
| `Response` | extends `ResponseLike` = `ResponseLike` |
43+
44+
#### Parameters
45+
46+
| Name | Type |
47+
| :------ | :------ |
48+
| `msgOrErrOrResponse` | `string` \| `Response` \| `Error` |
49+
50+
#### Overrides
51+
52+
Error.constructor
53+
54+
## Properties
55+
56+
### response
57+
58+
**response**: `undefined` \| `Response`
59+
60+
The underlyig response thats considered an error.
61+
62+
Will be undefined when no response is received,
63+
instead an unexpected network error.

0 commit comments

Comments
 (0)