Skip to content

Commit

Permalink
docs(recipe): ping from client with latency [skip ci]
Browse files Browse the repository at this point in the history
  • Loading branch information
enisdenjo committed Mar 23, 2021
1 parent cbd8810 commit 62f2089
Showing 1 changed file with 144 additions and 0 deletions.
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,150 @@ const client = createClient({

</details>

<details id="ping-from-client">
<summary><a href="#ping-from-client">🔗</a> <a href="https://github.com/websockets/ws">ws</a> server and client with client to server pings and latency</summary>

```typescript
// 🛸 server

import {
parse,
validate,
execute,
subscribe,
GraphQLSchema,
GraphQLObjectType,
GraphQLNonNull,
GraphQLString,
} from 'graphql';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws';
import { schema } from 'my-graphql-schema';

// a custom graphql schema that holds just the ping query.
// used exclusively when the client sends a ping to the server.
// if you want to send/receive more details, simply adjust the pinger schema.
const pinger = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
ping: {
type: new GraphQLNonNull(GraphQLString),
resolve: () => 'pong',
},
},
}),
});

const wsServer = new WebSocket.Server({
port: 443,
path: '/graphql',
});

useServer(
{
execute,
subscribe,
onSubscribe: (_ctx, msg) => {
const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
};

// if the query is just a ping, use the pinger schema instead
if (msg.payload.query === '{ ping }') {
args.schema = pinger;
}

// dont forget to validate
const errors = validate(args.schema, args.document);
if (errors.length > 0) {
// returning `GraphQLError[]` sends an `ErrorMessage` and stops the subscription
return errors;
}

return args;
},
},
wsServer,
);
```

```typescript
// 📺 client

import { createClient } from 'graphql-ws';

let connection: WebSocket;
const client = createClient({
url: 'wss://client.can/send-pings/too',
on: {
connected: (socket) => (connection = socket as WebSocket),
},
});

async function ping() {
// record the ping sent at moment for calculating latency
const pinged = Date.now();

// if the client went offline or the server is unresponsive
// close the active WebSocket connection as soon as the pong
// wait timeout expires and have the client silently reconnect.
// there is no need to dispose of the subscription since it
// will eventually settle because either:
// - the client reconnected and a new pong is received
// - the retry attempts were exceeded and the close is reported
// because if this, the latency accounts for retry waits too.
// if you do not want this, simply dispose of the ping subscription
// as soon as the pong timeout is exceeded
const pongTimeout = setTimeout(
() => {
if (connection?.readyState === WebSocket.OPEN)
connection.close(4408, 'Pong Timeout');
},
2000, // expect a pong within 2 seconds of the ping
);

// wait for the pong. the promise is guaranteed to settle
await new Promise<void>((resolve, reject) => {
client.subscribe<{ data: { ping: string } }>(
{ query: '{ ping }' },
{
next: () => {
/* not interested in the pong */
},
error: reject,
complete: resolve,
},
);
});

// pong has been received, clear the timeout
const ponged = Date.now();
clearTimeout(pongTimeout);

// how long it took for the pong to arrive after sending the ping
return ponged - pinged;
}

// keep pinging until a fatal problem occurs
(async () => {
for (;;) {
const latency = await ping();

// or send to your favourite logger - the user
console.info('GraphQL WebSocket connection latency', latency);

// ping every 3 seconds
await new Promise((resolve) => setTimeout(resolve, 3000));
}
})();
```

</details>

## [Documentation](docs/)

Check the [docs folder](docs/) out for [TypeDoc](https://typedoc.org) generated documentation.
Expand Down

0 comments on commit 62f2089

Please sign in to comment.