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

Client to server ping message #117

Closed
michelepra opened this issue Feb 16, 2021 · 54 comments · Fixed by #201
Closed

Client to server ping message #117

michelepra opened this issue Feb 16, 2021 · 54 comments · Fixed by #201
Labels
enhancement New feature or request help wanted Extra attention is needed released Has been released and published

Comments

@michelepra
Copy link
Contributor

Browser is very lazy to detect websocket disconnect, it's responsive only when client try to send some frame to server.
To do this with current graphl-ws protocol i need query exposed in * .graphl service that is only need for websocket transport and only for keep alive. This is a small hack use of graphql due to protocol absence of this feature.
subscriptions-transport-ws has similar concept but managed from server to client

This issue is for discussion of new message type that client or server can periodically sent (with payload to make more complete) similar to connection_init. The response that client or server send can be customized with payload, similar to connection_ack

@andyrichardson
Copy link

andyrichardson commented Feb 16, 2021

@michelepra I'm 100% with you on the need for this 🚀

This kind of heartbeat/ping-pong looks to be a recommended approach.

Edit: Bad link - see application layer example here

@michelepra
Copy link
Contributor Author

This kind of heartbeat/ping-pong looks to be a recommended approach.

This kind is only available on server side and is already mentioned in #10
What I have proposed is not related to websocket (that ping/pong is) but in application layer transport protocol (that graphl-ws is)

@andyrichardson
Copy link

My bad - yeah we are talking about the same thing, I linked to the wrong resource 👍

Application layer ping/pong added to the graphql-ws protocol so we can determine whether messages are being sent/received (without consideration for TCP timeouts)

@pranaypratyush
Copy link

This thing is SUPER important for me as well.

@enisdenjo enisdenjo added the question Further information about the library is requested label Feb 16, 2021
@enisdenjo
Copy link
Owner

For keeping the client alive, you should rely on the Pings and Pongs: The Heartbeat of WebSockets. Pong frames are automatically sent in response to ping messages as required by the spec. This feature is a mandatory built-in, all browsers that don't support Ping and Pong are simply not RFC6455 compliant.

Also, it is a part of this lib already:

graphql-ws/src/use/ws.ts

Lines 64 to 87 in 94ea423

// keep alive through ping-pong messages
let pongWait: NodeJS.Timeout | null = null;
const pingInterval =
keepAlive > 0 && isFinite(keepAlive)
? setInterval(() => {
// ping pong on open sockets only
if (socket.readyState === socket.OPEN) {
// terminate the connection after pong wait has passed because the client is idle
pongWait = setTimeout(() => {
socket.terminate();
}, keepAlive);
// listen for client's pong and stop socket termination
socket.once('pong', () => {
if (pongWait) {
clearTimeout(pongWait);
pongWait = null;
}
});
socket.ping();
}
}, keepAlive)
: null;

Please beware, these ping-pong frames are built-in! Most browsers probably wont show them in the network developer tools. If you do indeed want to see them: use Firefox (it does expose these WS Protocol frames).

Application layer ping/pong added to the graphql-ws protocol so we can determine whether messages are being sent/received (without consideration for TCP timeouts)

@andyrichardson why would you need a message to determine this? If a WS connection is open, you're guaranteed to have the messages flow through. Also, you'd have to send that "are-your-there?" message in the first place - so you're checking if messages are sent/received by sending a message.

Browser is very lazy to detect websocket disconnect, it's responsive only when client try to send some frame to server.

@michelepra if WS connections close prematurely at any point in time, graphql-ws will reconnect silently and automatically. On the other hand, the servers pings should keep the browser around.

Nevertheless, these types of checks go well beyond the Protocol (and thus graphql-ws because it is a minimal reference implementation).

P.S. We had a concrete message before, but it was cut off in favour of the specified ping-pongs. Check the PR out: #11.

@andyrichardson
Copy link

andyrichardson commented Feb 16, 2021

@enisdenjo thanks for the response 🙏

If a WS connection is open, you're guaranteed to have the messages flow through

I can't speak for everyone else but this is what I'm struggling to guarantee.

If you spin up this sandbox and disconnect from the network (wifi off, unplug ethernet, etc) you'll see that onClose doesn't get called... at least not for 10 minutes 😅

The lack of pong from the client will allow the server to know that the client is unresponsive, but the same can't be said the other way round. The client will consider the socket to be alive until the TCP timeout is reached (even though the connection may have been closed minutes earlier by the server).

My guess here is that we need some kind of mechanism for the client to know that the server is responsive (which doesn't look to be an option on the ws layer - at least not in JS land)

Edit: There's a good explanation here about the need for application layer pinging for client side aliveness checks

@michelepra
Copy link
Contributor Author

P.S. We had a concrete message before, but it was cut off in favour of the specified ping-pongs. Check the PR out: #11.

Server side ping/pong can be managed and is that is implemented in #11

Client side ws !== js websocket and here we cannot send ping frame.
Browser cannot detectet network failure until we try to put some frame on opened connection.
Using concrete message is the only thing that solve network failure detection in client side.
This can be reached with specific graphql query, that is out of graphl-ws protocol and is a bit evil use of graphql, or can be managed by protocol using specific message type like for example

interface ConnectionPingMessage {
  type: 'connection_ping';
  payload?: Record<string, unknown>;
}
interface ConnectionPongMessage {
  type: 'connection_pong';
  payload?: Record<string, unknown>;
}

@enisdenjo
Copy link
Owner

enisdenjo commented Feb 16, 2021

Edit: There's a good explanation here about the need for application layer pinging for client side aliveness checks

@andyrichardson the comment you referenced simply describes the lack of a JS API for ping-pongs. This is intentional because the browser should be handling the connections, not the applications.

The main reason for delayed connection checks is resource conservation (idling browsers have their JS paused). Adding to your example, if you tried issuing a GQL operation (send a WS message) the browser will re-evaluate the connection and notice that the socket is no more. It will then report the closure and graphql-ws will handle the rest (reconnect silently) - the user will notice nothing.

Even if we add the "ping-pong" concrete message, it will suffer from the exact same problem. The browser will go in idling state (which you cant and shouldnt control), you'll try pinging a server, the browser will notice the connection is gone and graphql-ws will kick in again in the exact same way.

Given the current set of laid out arguments, a ping-pong message is something I am not considering adding. Sorry to be blunt guys, but - you're overcomplicating it!

If you would like to discuss this in further detail, or you would like to raise more arguments: please consider leaving a comment over at the PR for standardising this GraphQL over WebSocket Protocol and have the rest of the brilliant developers engage.

@enisdenjo enisdenjo added the wontfix This will not be worked on label Feb 16, 2021
@michelepra
Copy link
Contributor Author

Even if we add the "ping-pong" concrete message, it will suffer from the exact same problem. The browser will go in idling state (which you cant and shouldnt control), you'll try pinging a server, the browser will notice the connection is gone and graphql-ws will kick in again in the exact same way.

This is the purpose of this issue. If the browser will not notice the connection is gone we never kick in againg until we explicitly call some operation.

User experience in this scenario is very poor because we cannot tell the connection is gone and data that we expect to receive from subscription is never receiving.
Ther's more use case when subscription is call once and then put aside until it receives data.

Is more elegant and minor data consumption that this is done by protocol instead of user specific operation.

PS.
I see in PR one screenshot of spotify websocket like an example, and notice that ping/pong is also exposed in application layer.
WTF why they do this!?!? If connection lost they can tell as soon as possibile to user that connection is broken.
This shows how this necessity is more common than one would like to believe

@enisdenjo
Copy link
Owner

@michelepra, @andyrichardson, I think I am getting the hang of it now. Let me try summarising:

The client might take too long to notice that the connection is gone (for whatever reason), this is bad UX because the user will have to wait for the "browsers time" to reconnect (which might take 10mins+ as @andyrichardson points out).

In this case, a client ping-pong does indeed make sense. Sorry for taking me too long to realise this, glad you guys stuck around! 😅

I want to do a bit more research before I approach this enhancement. But, I will most certainly consider this!

@enisdenjo enisdenjo added enhancement New feature or request and removed question Further information about the library is requested wontfix This will not be worked on labels Feb 17, 2021
@enisdenjo
Copy link
Owner

@michelepra, I see you'd like to have a payload accompany the ping/pong messages. Could you please elaborate a bit more on how and why would this payload be helpful?

@michelepra
Copy link
Contributor Author

michelepra commented Feb 17, 2021

The client might take too long to notice that the connection is gone (for whatever reason), this is bad UX because the user will have to wait for the "browsers time" to reconnect (which might take 10mins+ as @andyrichardson points out).

Even worse, user cannot know that must wait reconnecting, because in it's point of view all is ok until websocket is closed, and this is done only when try to send some frame. This is the reason of having ping/pong in graphql-ws protocol

@michelepra, I see you'd like to have a payload accompany the ping/pong messages. Could you please elaborate a bit more on how and why would this payload be helpful?

I think in more than one use case of payload.... the simplest is for logging, the hardest is for event driven ping/pong.
Payload must be optional because it's purpose is in application layer logic, not in protocol

@andyrichardson
Copy link

Sorry to be blunt guys, but - you're overcomplicating it!

While savage, I totally agree

The client might take too long to notice that the connection is gone (for whatever reason), this is bad UX because the user will have to wait for the "browsers time" to reconnect (which might take 10mins+ as @andyrichardson points out).

Yup this is exactly the problem I'm stuck with.

My current workaround is to use the online state and assume that if online is true and the socket is open, we will be receiving messages in real time.

If we go offline or the socket is closed, then we communicate to the user that they won't be receiving updates.

Does this cover all use cases? That's what I'm not so confident about 🤔

@enisdenjo
Copy link
Owner

enisdenjo commented Feb 17, 2021

Yeah, I always favour good UX and DX; but, I had a hard time understanding the needs for this issue.

Nevertheless, it is pretty clear now and after I did my research I'll come back with a PR (or more questions).

@enisdenjo
Copy link
Owner

Doing further research I feel like I will indeed stay behind my initial decision - NOT supporting client->server pings. The reason is quite simple, please bear with me.

Network loss does not necessarily mean that the socket needs to be closed, as mentioned in a chromium issue report comment, simply because:

... temporal disconnected sockets might recover and continue to send and receive subsequent packets without any packet loss. Timeout depends on OS.

For example, disable WLAN and re-enable it with the same IP address before it become timeout. Your dammed sockets will be back to send and receive. At that time, both of client and server could not detect closed connection. They can continue communication without any additional opening handshake or session recovery.

In essence, the connection might recover on its own and continue as nothing had happened. Since the lack of (or delayed) connection close events, you guys are mentioning, can occur really only on network disconnects - adding a client->server ping for that case exclusively is not something I want to cover because of the aforementioned arguments.

Furthermore, doing a ws.send while the network is down WILL NOT report the error right away. It will still wait for the OS to settle and decide that the underlaying TCP connection is really down before reporting it through the browser. So, our solution (pinging server from the client) to catch network disruptions as soon as possible WONT work either, you're still stuck with having the OS decide that the socket is closed...

What will happen actually is that the send packets will get queued, and once the network is up again, they'll be sent through. However, if the network never recovers, the OS/browser will report a close event in a timely fashon.

A demonstration of a temporal network down without the socket actually getting closed is below. Please note how doing a ws.send while offline will NOT report a close event and the queued messages will go through once the network recovers.

temporal-network-down

@michelepra
Copy link
Contributor Author

michelepra commented Feb 24, 2021

Network loss does not necessarily mean that the socket needs to be closed, as mentioned in a chromium issue report comment

This is not the purpose of this issue. Closing socket must not be fired by graphql-ws client out of application logic due to missing pong response, but by browser/OS that detect tcp connection is down. The behaviour of force close websocket and run reconnecting must be in application logic if for application this is requested and managed.

... temporal disconnected sockets might recover and continue to send and receive subsequent packets without any packet loss. Timeout depends on OS.

Exactly. Reported issue of chrome (that is very old) tell behave differently depending on the OS and that's why the state has been forced WontFix because is not a bug of websocket spec implementation.
For example on Windows it throws close event after 60-70 seconds (now with chrome 88 on windows 10 is more short), on linux (??), chromeOS never fired... etc etc

In essence, the connection might recover on its own and continue as nothing had happened. Since the lack of (or delayed) connection close events, you guys are mentioning, can occur really only on network disconnects - adding a client->server ping for that case exclusively is not something I want to cover because of the aforementioned arguments.

This is not quoted here but ping/pong in application layer logic is not only exclusively for detect network disconnects but also for keep connection active as toyoshim say in comment in chrome issue.

If you want to keep the connection in active, you can implement application-level ping-pong or heartbeats checker with your favorite intervals.

For example a few years ago i developed web application where communication with server is based on websocket. Client do some requests that on server was managed similar as graphql subscription. Server was write in java and based on jetty websocket that implement and manage ping/pong websocket spec. Whithout interval messages in application layer like ping from client the socket on server was closed (and session associated closed) and client must continuosly reopen new websocket and rerun previous requests.

So, our solution (pinging server from the client) to catch network disruptions as soon as possible WONT work either, you're still stuck with having the OS decide that the socket is closed...

If application based on graphql-ws wait only close event to catch network disruptions it's partly true due to different OS behaviour. With this in mind client application can settle down some logic based on ping/pong (that now is not possible due to the lack of this in the graphql-ws protocol, this only can be simulated with graphql query made specifically for this purpose)

What will happen actually is that the send packets will get queued, and once the network is up again, they'll be sent through. However, if the network never recovers, the OS/browser will report a close event in a timely fashon.

As quoted above this is based on OS where browser run

@andyrichardson
Copy link

andyrichardson commented Feb 24, 2021

Thanks for looking into this @enisdenjo

So, our solution (pinging server from the client) to catch network disruptions as soon as possible WONT work either, you're still stuck with having the OS decide that the socket is closed...

I think sending ping/pong messages would still be a valid approach.

The example flow I would have expected would be:

Alive

  • Client sends ping message (we don't care whether the server gets it or not)
  • setTimeout is introduced which closes the socket after X seconds
  • pong message is received and timeout is cleared
  • repeat

Dead

  • Client sends ping message (we don't care whether the server gets it or not)
  • setTimeout is introduced which closes the socket after X seconds
  • pong message is not received within X seconds, close socket and state is now updated

@michelepra
Copy link
Contributor Author

michelepra commented Feb 24, 2021

Show `example`

Alive

  • Client sends ping message (we don't care whether the server gets it or not)
  • setTimeout is introduced which closes the socket after X seconds
  • pong message is received and timeout is cleared
  • repeat

Dead

  • Client sends ping message (we don't care whether the server gets it or not)
  • setTimeout is introduced which closes the socket after X seconds
  • pong message is not received within X seconds, close socket and state is now updated

@andyrichardson the example can be ok but be careful of chained timers

@enisdenjo
Copy link
Owner

Guys, you're missing one key point, the connection might recover. Terminating it early will stop all subscriptions and discard all messages that the server might have sent (or the client) in the meantime because it does not know that the client went away yet. By allowing the OS/browser to control the connection, you will receive all queued up messages if the connection recovers; and if not, you'll get a close event and the client will reconnect silently.

Another crucial thing you're overlooking is that you want to close the connection early while the user is offline. No retries or ping-pongs will put the user back online... You might even experience downsides, like for example closing the connection unnecessarily (OS/browser would've recovered), having the client retries get exceeded and then fail for good.

Why do you want to retry while the user is offline? What do you gain by reporting an error early (and even unnecessarly)?

On the other hand, if the OS/browser manages to recover the connection when the user comes back online - messages from both sides will be flushed; and if it does not manage to recover, it will report a close event AFTER the user came back online - allowing the client to retry exactly when it should.

This is not quoted here but ping/pong in application layer logic is not only exclusively for detect network disconnects but also for keep connection active as toyoshim say in comment in chrome issue.

@michelepra the connection will be kept alive because of the server pings as long as the client is online.

@michelepra
Copy link
Contributor Author

Guys, you're missing one key point, the connection might recover. Terminating it early will stop all subscriptions and discard all messages that the server might have sent (or the client) in the meantime because it does not know that the client went away yet. By allowing the OS/browser to control the connection, you will receive all queued up messages if the connection recovers; and if not, you'll get a close event and the client will reconnect silently.

Server side is not browser, here connection lost can be detected with websocket ping/pong frame, when connection is closed all subscriptions complete and new messages are discarded
Client side will receive all queued up messages (if server side not detect connection lost) when connection recovers, but we will never had close event when necessary or detecting server is down until we send ping message to server

Another crucial thing you're overlooking is that you want to close the connection early while the user is offline. No retries or ping-pongs will put the user back online... You might even experience downsides, like for example closing the connection unnecessarily (OS/browser would've recovered)

Close the connection is only one of possibilities. This must be in client side application logic, not in graphql-ws.
For example you consider unnecessarily to closing the connection because browser would've automatically recovered. This point of view can be true for one application but not for other, use case can be very different. Therefore it would be better not to consider the examples when defining a transport protocol such as graphql-ws. What people do with client side ping/pong must be out of protocol definition

having the client retries get exceeded and then fail for good.

This means severe network problems (like network devices broken or ISP down) and this must be managed in application, How to do this if we never had close event or we never receiving pong message? Easy, now we cannot because we haven't ping/pong client message

Why do you want to retry while the user is offline? What do you gain by reporting an error early (and even unnecessarly)?

If client application is real time data based, we must consider to notify user as soon as possible that is not online.

On the other hand, if the OS/browser manages to recover the connection when the user comes back online - messages from both sides will be flushed; and if it does not manage to recover, it will report a close event AFTER the user came back online - allowing the client to retry exactly when it should.

And if client never, or very long time, came back online? Now we must considering we cannot show user that is not online due to the lack of client side ping/pong

@michelepra the connection will be kept alive because of the server pings as long as the client is online.

I reported this as example, some year ago (and chrome issue is 10 year) this happend on server side where ping/pong of websocket spec was correctly implemented. I will not exclude this was bug of jetty websocket servlet, but this is not the subject of this issue

@asmeikal
Copy link

I'm leaving my two cents here based on my experience: the need for early detection of connection loss, and thus for some kind of ping from the client, is useful when a connection loss triggers a fallback. In the project I'm working on subscriptions update continuously the resources that the user is working on, and if we detect a socket disconnection we start polling these resources, as other services may still be working. Detecting earlier that the connection is closed even when it could be re-established (without message loss) is something that can improve the user experience: instead of seeing the client not updating, the fallback can kick in and update the resources.

But I believe that this should not be a protocol concern, and it can be implemented relatively easily with queries at regular intervals using the same client as the subscriptions, or a subscription that sends messages regularly from the server paired with a timeout on the client.

@michelepra
Copy link
Contributor Author

michelepra commented Feb 24, 2021

But I believe that this should not be a protocol concern, and it can be implemented relatively easily with queries at regular intervals using the same client as the subscriptions, or a subscription that sends messages regularly from the server paired with a timeout on the client.

@asmeikal Is exactly that i said above with "this only can be simulated with graphql query made specifically for this purpose" (and that's what I did because I have no alternative, and the reason why I opened this issue)

I do not agree with "this should not be a protocol concern" because expose and use query (that is message protocol) only to monitoring websocket is an abuse due to the lack on graphql-ws (that is transport protocol) of concrete message type to gain this result.

@enisdenjo
Copy link
Owner

enisdenjo commented Feb 24, 2021

Client side will receive all queued up messages (if server side not detect connection lost) when connection recovers, but we will never had close event when necessary or detecting server is down until we send ping message to server

@michelepra Actually, until the pong message receive times out. Server downs will be detected immediately, only network disruptions of the client will stall. And for a good reason, they might recover and the messages flush.

Close the connection is only one of possibilities. This must be in client side application logic, not in graphql-ws.

@michelepra You are right, this is application, subjective, logic - something graphql-ws shouldnt be responsible for.

For example you consider unnecessarily to closing the connection because browser would've automatically recovered. This point of view can be true for one application but not for other, use case can be very different. Therefore it would be better not to consider the examples when defining a transport protocol such as graphql-ws. What people do with client side ping/pong must be out of protocol definition

@michelepra Can you give me one other example? Just for the example sake.

This means severe network problems (like network devices broken or ISP down) and this must be managed in application, How to do this if we never had close event or we never receiving pong message? Easy, now we cannot because we haven't ping/pong client message

@michelepra Simple, outside of graphql-ws. Just listen for the online and offline events. You absolutely should not rely on a transport implementation (or protocol) for detecting online/offline statuses.

If client application is real time data based, we must consider to notify user as soon as possible that is not online.

@michelepra Again, outside of graphql-ws. There are other primitives that detect online status, like online and offline events for browsers, and other utilities for NodeJS.

And if client never, or very long time, came back online? Now we must considering we cannot show user that is not online due to the lack of client side ping/pong

@michelepra still a use case for online and offline events.

I am not in favour of adding an additional protocol requirement just to detect the online status... If you want absolutely want to kick the client off as soon as the browser goes offline, you can simply do:

import { createClient } from 'graphql-ws';

let activeSocket;
const client = createClient({
  url: 'wss://i.close/on/offline/event',
  on: {
    connected: (socket) => {
      activeSocket = socket;
    },
    closed: () => {
      activeSocket = undefined;
    },
  },
});

window.addEventListener('offline', () => {
  activeSocket?.close(1001, 'Going Away');
});

This 👆 would be more reliable than any ping-pong message a protocol (or an implementation) could offer. You can even go one step further and setTimeout on the activeSocket.close to apply a delay. Note that closing the connection will also toggle the clients silent retries, so you're not loosing any subscriptions or state.

However, I would absolutely not recommend this, you should leave the heavy lifting to the OS/browser.

But I believe that this should not be a protocol concern, and it can be implemented relatively easily with queries at regular intervals using the same client as the subscriptions, or a subscription that sends messages regularly from the server paired with a timeout on the client.

@asmeikal You dont even need queries, the example above achieves exactly what this issue is describing.

@enisdenjo
Copy link
Owner

enisdenjo commented Jun 8, 2021

I think e43154f is just enough. The only thing that the client configures is the keep-alive timeout (when to issue a ping), all other logic is up to the implementor.

For example, a close-on-timeout + latency metrics implementation would look like this:

import { createClient } from 'graphql-ws';

let activeSocket,
  timedOut,
  pingSentAt = 0,
  latency = 0;
createClient({
  url: 'ws://i.time.out:4000/and-measure/latency',
  keepAlive: 10_000, // ping server every 10 seconds
  on: {
    connected: (socket) => (activeSocket = socket),
    ping: (received) => {
      if (!received /* sent */) {
        pingSentAt = Date.now();
        timedOut = setTimeout(() => {
          if (activeSocket.readyState === WebSocket.OPEN)
            activeSocket.close(4408, 'Request Timeout');
        }, 5_000); // wait 5 seconds for the pong and then close the connection
      }
    },
    pong: (received) => {
      if (received) {
        latency = Date.now() - pingSentAt;
        clearTimeout(timedOut); // pong is received, clear connection close timeout
      }
    },
  },
});

@michelepra, @andyrichardson, @pranaypratyush, @asmeikal, @idevelop, @backbone87 would love to hear your thoughts on this. 😄

@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 5.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Jun 8, 2021
@enisdenjo
Copy link
Owner

Summary behind the decision of adding the ping/pongs in the Protocol: graphql/graphql-over-http#140 (comment).

@michelepra
Copy link
Contributor Author

🤟 a small step has been taken, i have some points to clarify:

  1. client ping must be send according to application logic, with your implementation this is out of client control. keepAlive parameter is only one point of this logic
  2. why you don't consider optional payload like ping/pong websocket protocol?
  3. message event must be emitted after check message type is not ping/pong because this type have specific event handler

@enisdenjo
Copy link
Owner

Hey hey, straight to the point:

  1. The idea behind the keep-alive is sending it on an interval. I felt like having something like client.ping is an overkill (for now at least). Do you have an example usage in mind?
  2. Keeping it simply, I dont think passing a payload through the ping and pong message is really necessary. The main use case is connection stability, latency measurement and, in general, network probing. Still, suggested example usage?
  3. As stated in the documentation: the message event is called for all valid messages received by the client. Mainly useful for debugging and logging received messages.

@michelepra
Copy link
Contributor Author

  1. The idea behind the keep-alive is sending it on an interval. I felt like having something like client.ping is an overkill (for now at least). Do you have an example usage in mind?

i can fancy some example

  • monitor idle connection, where client ping must be sent after N milliseconds of receiving last message from websocket, because i can correctly assume that if i receive messages the socket is active.
  • Can also be browser limitations on some condition
  • ping keepAlive interval can be variable by some logic (example after do http request to other place)
  • sending ping can be enable/disable by some conditions
  1. Keeping it simply, I dont think passing a payload through the ping and pong message is really necessary. The main use case is connection stability, latency measurement and, in general, network probing. Still, suggested example usage?

Most easy example is for logging purpose, server can logging receiving ping X from client Y
Other example can be in client by sending ping with payload containing progression number, If all is ok (client <- network -> server) i expect to receive pong with this number after ping was sent, If network is unstable message is sent as soon network going on (as you correctly report). With payload i can do more sophisticated thing than reporting "receiving N consecutive pong"

  1. As stated in the documentation: the message event is called for all valid messages received by the client. Mainly useful for debugging and logging received messages.

Ok, thanks for explanation

@enisdenjo
Copy link
Owner

enisdenjo commented Jun 9, 2021

i can fancy some example

  • monitor idle connection, where client ping must be sent after N milliseconds of receiving last message from websocket, because i can correctly assume that if i receive messages the socket is active.
  • Can also be browser limitations on some condition
  • ping keepAlive interval can be variable by some logic (example after do http request to other place)
  • sending ping can be enable/disable by some conditions

Awesome! Makes perfect sense. I'll introduce a client.ping soon. Please read: #117 (comment).

Most easy example is for logging purpose, server can logging receiving ping X from client Y
Other example can be in client by sending ping with payload containing progression number, If all is ok (client <- network -> server) i expect to receive pong with this number after ping was sent, If network is unstable message is sent as soon network going on (as you correctly report). With payload i can do more sophisticated thing than reporting "receiving N consecutive pong"

Agreed. A payload does indeed make sense.

@enisdenjo
Copy link
Owner

enisdenjo commented Jun 9, 2021

@michelepra I realised that a manual (and super extensible) client.ping is already possible by using the socket directly (same for client.ping). I'd like to avoid baking it in if not absolutely necessary.

The benefit of this approach is that you can customise your pinger to fit your exact needs (async ping/pong, throw error on closed connection, integrated responses, logging, and much more...).

This is how a super simple manual pinger client would look like:

import {
  createClient,
  Client,
  ClientOptions,
  stringifyMessage,
  PingMessage,
  PongMessage,
  MessageType,
} from 'graphql-ws';

interface PingerClient extends Client {
  ping(payload?: PingMessage['payload']): void;
  pong(payload?: PongMessage['payload']): void;
}

function createPingerClient(options: ClientOptions): PingerClient {
  let activeSocket;

  const client = createClient({
    disablePong: true,
    ...options,
    on: {
      connected: (socket) => {
        options.on?.connected?.(socket);
        activeSocket = socket;
      },
    },
  });

  return {
    ...client,
    ping: (payload) => {
      if (activeSocket.readyState === WebSocket.OPEN)
        activeSocket.send(
          stringifyMessage({
            type: MessageType.Ping,
            payload,
          }),
        );
    },
    pong: (payload) => {
      if (activeSocket.readyState === WebSocket.OPEN)
        activeSocket.send(
          stringifyMessage({
            type: MessageType.Pong,
            payload,
          }),
        );
    },
  };
}

However, what the client needs is a disablePong option so that you can respond manually when necessary and optionally attach a payload.

@michelepra
Copy link
Contributor Author

looks nice, i think this can cover almost all use cases

enisdenjo added a commit that referenced this issue Jun 9, 2021
enisdenjo pushed a commit that referenced this issue Jun 9, 2021
# [5.1.0](v5.0.0...v5.1.0) (2021-06-09)

### Features

* **client:** `disablePong` option for when implementing a custom pinger ([6510360](6510360)), closes [#117](#117)
* Optional `payload` for ping/pong message types ([2fe0345](2fe0345)), closes [#117](#117)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 5.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo
Copy link
Owner

Both optional ping/pong payloads and the disablePong client option, together with a recipe of a custom pinger, is available starting from v5.1.0.

michelepra added a commit to michelepra/graphql-ws that referenced this issue Jun 9, 2021
@michelepra
Copy link
Contributor Author

@enisdenjo server don't send payload, see fix

@enisdenjo
Copy link
Owner

Hmm, I didnt realise you wanted the server to pong with the same payload as the ping. My original thinking was that you would implement the server response.

But, I think that the default behaviour for having the payload do a round trip makes sense. Let me see what I can do.

enisdenjo pushed a commit that referenced this issue Jun 9, 2021
## [5.1.1](v5.1.0...v5.1.1) (2021-06-09)

### Bug Fixes

* **server:** Return ping's payload through the response pong ([47730a9](47730a9)), closes [#117](#117)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 5.1.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo
Copy link
Owner

v5.1.1 returns ping's payload through the response pong.

@asmeikal
Copy link

Hi @enisdenjo, I got around to trying this! Kudos for making the change retrocompatible, we're upgrading all subscription servers before any client and it's working perfectly! I found out we needed this with an electron app, since it didn't reliably detect when the socket dies.

@andyrichardson
Copy link

Massive thanks for getting this supported!

I'm a little late to the game but I had a quick thought about the necessity of bi-directional ping/pong.

client->server ping/pong solves the problem where, without this mechanism, a socket timeout has taken place on the server and the client has no way of knowing. The ideal scenario is that both the client and the server acknowledge a socket timeout at the same time.

If this is the case, I think this could be done without the need for bi-directional ping/pong.

Client acknowledgment of a server-side timeout could work as follows:

  • Client sends connection_init
  • Server sends connection_ack, payload includes socket_timeout (the socket timeout value + delay between pong receipt and subsequent ping)
  • Server sends ping to client
  • Client sends pong to server

Following a pong to the server, the client would expect a subsequent ping from the server within socket_timeout milliseconds. If this is not the case, it's safe to assume that the server attempted to send a pong but it never arrived.

This approach could have a few advantages:

  • Socket aliveness is mutually assured (both client + server use same timeout)
  • Socket timeout identification by both parties would be much closer together (identical acknowledgment - connection_ack transport overhead)

@enisdenjo
Copy link
Owner

enisdenjo commented Jun 23, 2021

@andyrichardson already possible with minimal effort. 😄

On the client, you can use the EventConnectedListener to read out the accompanying payload (or the EventPingListener if you wish to transmit the socket_timout on the first ping or every ping).

Then, combine the payload with the "Client usage with manual pings and pongs" recipe and implement your own pinger following whatever logic you find necessary.

On the server, you can use the "Server usage with ws and subprotocol pings and pongs" recipe for customising the server-side pinger logic.

Please note that the defaults of graphql-ws will not change and it will never use the subprotocol ping/pongs on its own. The subprotocol ping/pongs are considered helpers for cases where using WS protocol level pings and pongs is not possible. Having said that, it is up to the implementor to invest effort in achieving the wished keep-alive logic.

@andyrichardson
Copy link

I knew this must have been considered! Thanks for explaining 🔥

github-actions bot pushed a commit to ilijaNL/graphql-transport-ws that referenced this issue Mar 16, 2023
# 1.0.0 (2023-03-16)

### Bug Fixes

* Add `browser` export map ([ea306db](ea306db))
* Add `package.json` to exports map ([enisdenjo#119](https://github.com/ilijaNL/graphql-ws/issues/119)) ([1f09863](1f09863)), closes [enisdenjo#118](https://github.com/ilijaNL/graphql-ws/issues/118)
* Add `uWebSockets` exports path ([36247cb](36247cb)), closes [enisdenjo#155](https://github.com/ilijaNL/graphql-ws/issues/155)
* Add support for `graphql@v16` ([ad5aea2](ad5aea2))
* add the sink to the subscribed map AFTER emitting a subscribe message ([814f46c](814f46c))
* Add types path to package.json `exports` ([enisdenjo#375](https://github.com/ilijaNL/graphql-ws/issues/375)) ([9f394d7](9f394d7))
* **client:** `complete` should not be called after subscription `error` ([1fba419](1fba419))
* **client:** `ConnectionInit` payload is absent if `connectionParams` returns nothing ([98f8265](98f8265))
* **client:** `isFatalConnectionProblem` defaults to undefined for using `shouldRetry` ([9d5c573](9d5c573))
* **client:** Accept nullish values for `operationName` and `variables` ([2d60dda](2d60dda))
* **client:** cant read the `CloseEvent.reason` after bundling so just pass the whole event to the sink error and let the user handle it ([9ccb46b](9ccb46b))
* **client:** Close event's `wasClean` is not necessary ([2c65f0e](2c65f0e)), closes [enisdenjo#81](https://github.com/ilijaNL/graphql-ws/issues/81)
* **client:** Close with error message during connecting issues ([f8ecdd7](f8ecdd7))
* **client:** Connection locks dont increment on retries ([1e7bd97](1e7bd97)), closes [enisdenjo#153](https://github.com/ilijaNL/graphql-ws/issues/153)
* **client:** Debounce close by `lazyCloseTimeout` ([c332837](c332837)), closes [enisdenjo#388](https://github.com/ilijaNL/graphql-ws/issues/388)
* **client:** Dispose of subscription on complete or error messages ([enisdenjo#23](https://github.com/ilijaNL/graphql-ws/issues/23)) ([fb4d8e9](fb4d8e9))
* **client:** Distinguish client connection closes ([ed4d9db](ed4d9db))
* **client:** Don't complete after connection error ([5f829c3](5f829c3))
* **client:** Export relevant elements from the browser bundle ([b106dbe](b106dbe)), closes [enisdenjo#97](https://github.com/ilijaNL/graphql-ws/issues/97)
* **client:** Lazy connects after successful reconnects are not retries ([99b85a3](99b85a3))
* **client:** Limit client emitted error close message size ([2d959f6](2d959f6))
* **client:** New `error` event listener for handling connection errors ([enisdenjo#136](https://github.com/ilijaNL/graphql-ws/issues/136)) ([127b69f](127b69f)), closes [enisdenjo#135](https://github.com/ilijaNL/graphql-ws/issues/135)
* **client:** No retries when disposed ([0d5e6c2](0d5e6c2))
* **client:** One cleanup per subscription ([enisdenjo#67](https://github.com/ilijaNL/graphql-ws/issues/67)) ([5a5ae4d](5a5ae4d))
* **client:** Only `query` is required in the subscribe payload ([e892530](e892530))
* **client:** Reduce WebSocket event listeners and add new client `message` event ([enisdenjo#104](https://github.com/ilijaNL/graphql-ws/issues/104)) ([68d0e20](68d0e20)), closes [enisdenjo#102](https://github.com/ilijaNL/graphql-ws/issues/102)
* **client:** Report close causing internal errors to error listeners ([4e7e389](4e7e389))
* **client:** Report close error even if `Complete` message followed ([27754b2](27754b2)), closes [enisdenjo#245](https://github.com/ilijaNL/graphql-ws/issues/245)
* **client:** Return ping's payload through the response pong ([ee6193a](ee6193a))
* **client:** send complete message and close only if socket is still open ([49b75ce](49b75ce))
* **client:** Should emit `closed` event when disposing ([5800de8](5800de8)), closes [enisdenjo#108](https://github.com/ilijaNL/graphql-ws/issues/108)
* **client:** Shouldn't reconnect if all subscriptions complete while waiting for retry ([2826c10](2826c10)), closes [enisdenjo#163](https://github.com/ilijaNL/graphql-ws/issues/163)
* **client:** Shouldn’t send the `Complete` message if socket is not open ([cd12024](cd12024))
* **client:** Some close events are not worth retrying ([4d9134b](4d9134b))
* **client:** Specify and fail on fatal internal WebSocket close codes ([a720125](a720125))
* **client:** Stabilize and simplify internals ([enisdenjo#100](https://github.com/ilijaNL/graphql-ws/issues/100)) ([5ff8f1d](5ff8f1d)), closes [enisdenjo#99](https://github.com/ilijaNL/graphql-ws/issues/99) [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Stop execution if `connectionParams` took too long and the server kicked the client off ([1e94e45](1e94e45)), closes [enisdenjo#331](https://github.com/ilijaNL/graphql-ws/issues/331)
* **client:** Subscribes even if socket is in CLOSING state due to all subscriptions being completed ([3e3b8b7](3e3b8b7)), closes [enisdenjo#173](https://github.com/ilijaNL/graphql-ws/issues/173) [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscription can be disposed only once ([abd9c28](abd9c28)), closes [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscriptions acquire locks ([eb6cb2a](eb6cb2a))
* **client:** Time retries and socket change waits ([7c707db](7c707db)), closes [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Wait for server acknowledgement indefinitely ([a4bd602](a4bd602)), closes [enisdenjo#98](https://github.com/ilijaNL/graphql-ws/issues/98)
* Close the details tag in the README ([84144c4](84144c4))
* correctly detect WebSocket server ([eab29dc](eab29dc))
* Define entry points through the `exports`  field and use `.mjs` suffixed ESM imports ([enisdenjo#110](https://github.com/ilijaNL/graphql-ws/issues/110)) ([4196238](4196238))
* Define graphql execution results ([a64c91b](a64c91b))
* Drop TypeScript DOM lib dependency ([a81e8c1](a81e8c1))
* export both the client and the server from index ([29923b1](29923b1))
* Export useful types ([e4cc4d4](e4cc4d4))
* **fastify-websocket:** Handle connection and socket emitted errors ([71e9586](71e9586))
* **fastify-websocket:** Handle server emitted errors ([3fa17a7](3fa17a7))
* http and ws have no default exports ([5c01ed9](5c01ed9))
* include `types` file holding important types ([f3e4edf](f3e4edf))
* Main entrypoint in `exports` is just `"."` ([8f70b02](8f70b02))
* **message:** Allow `data` field to be of any type ([533248e](533248e)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* **message:** Allow `payload` field to be of any type for `NextMessage` ([7cebbfe](7cebbfe)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* Node 10 is the min supported version ([19844d7](19844d7))
* notify only relevant sinks about errors or completions ([62155ba](62155ba))
* Only UMD build has side effects ([66ed43f](66ed43f))
* Reorder types paths in package.json for better import resolution ([enisdenjo#406](https://github.com/ilijaNL/graphql-ws/issues/406)) ([37263c5](37263c5))
* reset connected/connecting state when disconnecting and disposing ([2eb3cd5](2eb3cd5))
* **server:** `handleProtocols` accepts arrays too and gracefully rejects other types ([98dec1a](98dec1a)), closes [enisdenjo#318](https://github.com/ilijaNL/graphql-ws/issues/318)
* **server:** `onDisconnect` is called exclusively if the connection is acknowledged ([33ed5f2](33ed5f2))
* **server:** `return` instead of `break` at switch case ends ([e9447e4](e9447e4)), closes [enisdenjo#140](https://github.com/ilijaNL/graphql-ws/issues/140)
* **server:** `subscription` operations are distinct on the message ID ([enisdenjo#24](https://github.com/ilijaNL/graphql-ws/issues/24)) ([dfffb05](dfffb05))
* **server:** allow skipping init message wait with zero values ([a7419df](a7419df))
* **server:** Async iterator must implement `return` ([d99982b](d99982b)), closes [enisdenjo#149](https://github.com/ilijaNL/graphql-ws/issues/149)
* **server:** Client can complete/cancel any operation ([0ad1c4c](0ad1c4c))
* **server:** Close socket if `onSubscribe` returns invalid array ([enisdenjo#53](https://github.com/ilijaNL/graphql-ws/issues/53)) ([0464a54](0464a54))
* **server:** Consistently set `rootValue` and `contextValue`, if not overridden ([enisdenjo#49](https://github.com/ilijaNL/graphql-ws/issues/49)) ([7aa3bcd](7aa3bcd))
* **server:** Distribute server error to all clients even if one error listener throws ([enisdenjo#56](https://github.com/ilijaNL/graphql-ws/issues/56)) ([b96dbb9](b96dbb9))
* **server:** Don't surface bad request error details in production ([enisdenjo#55](https://github.com/ilijaNL/graphql-ws/issues/55)) ([70317b2](70317b2))
* **server:** Enforce ID uniqueness across all operations and during the whole subscription life ([enisdenjo#96](https://github.com/ilijaNL/graphql-ws/issues/96)) ([65d1bfa](65d1bfa))
* **server:** Handle upgrade requests with multiple subprotocols and omit `Sec-WebSocket-Protocol` header if none supported ([9bae064](9bae064))
* **server:** Hide internal server error messages from the client in production ([36fe405](36fe405)), closes [enisdenjo#31](https://github.com/ilijaNL/graphql-ws/issues/31)
* **server:** Init context first on connection open ([a80e753](a80e753)), closes [enisdenjo#181](https://github.com/ilijaNL/graphql-ws/issues/181)
* **server:** Limit internal server error close message size ([8479f76](8479f76))
* **server:** Log internal errors to the console ([6ddf0d1](6ddf0d1))
* **server:** Make sure to use `onSubscribe` result exclusively ([51fdb7c](51fdb7c))
* **server:** No need to bind `this` scope ([f76ac73](f76ac73))
* **server:** Operation result can be async generator or iterable ([b1fb883](b1fb883))
* **server:** Receiving more than one `ConnectionInit` message closes the socket immediately ([757c6e9](757c6e9))
* **server:** Respect completed subscriptions even if `subscribe` or `onOperation` didnt resolve yet ([4700154](4700154))
* **server:** Return ping's payload through the response pong ([47730a9](47730a9)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **server:** scoped execution result formatter from `onConnect` ([f91fadb](f91fadb))
* **server:** Should clean up subscription reservations on abrupt errors without relying on connection close ([611c223](611c223))
* **server:** Shouldn't send a complete message if client sent it ([331fe47](331fe47)), closes [enisdenjo#403](https://github.com/ilijaNL/graphql-ws/issues/403)
* **server:** store the intial request in the context ([6927ee0](6927ee0))
* **server:** Use `subscribe` from the config ([6fbd47c](6fbd47c))
* **server:** use subscription specific formatter for queries and mutations too ([5672a04](5672a04))
* Sink's next callback always receives an `ExecutionResult` ([045b402](045b402))
* Stop sending messages after receiving complete ([enisdenjo#65](https://github.com/ilijaNL/graphql-ws/issues/65)) ([3f4f836](3f4f836))
* Support more `graphql` versions ([de69b4e](de69b4e))
* Support more Node versions by not using `globalThis` ([79c2ed2](79c2ed2))
* Use `4406` close code for unsupported subprotocol (`1002` is an internal WebSocket close code) ([df85281](df85281))
* Use `4500` close code for internal server errors (`1011` is an internal WebSocket close code) ([3c0316d](3c0316d))
* Use `ID` type for message id field ([87ebd35](87ebd35))
* **uWebSockets:** Handle premature and abrupt socket closes ([9d3ff52](9d3ff52)), closes [enisdenjo#186](https://github.com/ilijaNL/graphql-ws/issues/186)
* Warn about subscriptions-transport-ws clients and provide migration link ([e080739](e080739)), closes [enisdenjo#339](https://github.com/ilijaNL/graphql-ws/issues/339) [enisdenjo#325](https://github.com/ilijaNL/graphql-ws/issues/325)
* **ws,fastify-websocket:** Send only on ready socket ([8d13c9e](8d13c9e))
* **ws,uWebSockets,@fastify/websocket:** Handle internal errors that are not instances of `Error` ([enisdenjo#442](https://github.com/ilijaNL/graphql-ws/issues/442)) ([9884889](9884889)), closes [enisdenjo#441](https://github.com/ilijaNL/graphql-ws/issues/441)
* **ws:** Handle socket emitted errors ([a22c00f](a22c00f))
* **ws:** Limit server emitted error close message size ([50620df](50620df))
* **ws:** Log server emitted errors to the console ([0826b0a](0826b0a))
* yarn engine is not required ([enisdenjo#34](https://github.com/ilijaNL/graphql-ws/issues/34)) ([89484b8](89484b8))

### Features

* `cjs`, `esm` and `umd` builds with minification and compression for the browser ([enisdenjo#58](https://github.com/ilijaNL/graphql-ws/issues/58)) ([ebb8dfe](ebb8dfe))
* Add `extensions` field to the subscribe message payload ([d86a8e4](d86a8e4))
* Allow null payloads in messages ([enisdenjo#456](https://github.com/ilijaNL/graphql-ws/issues/456)) ([eeb0265](eeb0265)), closes [enisdenjo#455](https://github.com/ilijaNL/graphql-ws/issues/455)
* Bidirectional ping/pong message types ([enisdenjo#201](https://github.com/ilijaNL/graphql-ws/issues/201)) ([1efaf83](1efaf83))
* Centralise expected close codes in `CloseCode` enum ([d10a75c](d10a75c))
* **client:** `connectionParams` can return `undefined` ([a543187](a543187))
* **client:** `connectionParams` may return a promise ([enisdenjo#71](https://github.com/ilijaNL/graphql-ws/issues/71)) ([33f210c](33f210c))
* **client:** `disablePong` option for when implementing a custom pinger ([6510360](6510360)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **client:** `isFatalConnectionProblem` option for deciding if the connect error should be immediately reported or the connection retried ([enisdenjo#126](https://github.com/ilijaNL/graphql-ws/issues/126)) ([8115871](8115871)), closes [enisdenjo#122](https://github.com/ilijaNL/graphql-ws/issues/122)
* **client:** `onNonLazyError` allows you to catch errors reported in non-lazy mode ([cd1e7df](cd1e7df))
* **client:** `url` option accepts a function or a Promise ([enisdenjo#143](https://github.com/ilijaNL/graphql-ws/issues/143)) ([76f522f](76f522f)), closes [enisdenjo#145](https://github.com/ilijaNL/graphql-ws/issues/145) [enisdenjo#146](https://github.com/ilijaNL/graphql-ws/issues/146)
* **client:** Add `connectionAckWaitTimeout` option ([enisdenjo#228](https://github.com/ilijaNL/graphql-ws/issues/228)) ([35ce054](35ce054))
* **client:** Add `opened` event for when a WebSocket opens ([9053224](9053224))
* **client:** Allow keeping the connection alive for some time before lazy closing ([enisdenjo#69](https://github.com/ilijaNL/graphql-ws/issues/69)) ([555c2c3](555c2c3))
* **client:** Deprecate `isFatalConnectionProblem` option in favour of `shouldRetry` ([d8dcf21](d8dcf21))
* **client:** Emit events for `connecting`, `connected` and `closed` ([627775b](627775b))
* **client:** Implement silent-reconnects ([c6f7872](c6f7872)), closes [enisdenjo#7](https://github.com/ilijaNL/graphql-ws/issues/7)
* **client:** introduce Socky 🧦 - the nifty internal socket state manager ([enisdenjo#8](https://github.com/ilijaNL/graphql-ws/issues/8)) ([a4bee6f](a4bee6f))
* **client:** Lazy option can be changed ([fb0ec14](fb0ec14))
* **client:** Optional `generateID` to provide subscription IDs ([enisdenjo#22](https://github.com/ilijaNL/graphql-ws/issues/22)) ([9a3f54a](9a3f54a)), closes [enisdenjo#21](https://github.com/ilijaNL/graphql-ws/issues/21)
* **client:** Provide subscribe payload in `generateID` ([d0bc6e1](d0bc6e1)), closes [enisdenjo#398](https://github.com/ilijaNL/graphql-ws/issues/398)
* **client:** Re-implement following the new transport protocol ([#6](#6)) ([5191a35](5191a35))
* **client:** Rename `keepAlive` option to `lazyCloseTimeout` ([3c1f13c](3c1f13c))
* **client:** Retry with randomised exponential backoff or provide your own strategy ([enisdenjo#84](https://github.com/ilijaNL/graphql-ws/issues/84)) ([d3e7a17](d3e7a17))
* **client:** Support providing custom WebSocket implementations ([enisdenjo#18](https://github.com/ilijaNL/graphql-ws/issues/18)) ([1515fe2](1515fe2))
* **client:** Terminate the WebSocket abruptly and immediately ([53ad515](53ad515)), closes [enisdenjo#290](https://github.com/ilijaNL/graphql-ws/issues/290)
* Descriptive invalid message errors ([b46379e](b46379e)), closes [enisdenjo#366](https://github.com/ilijaNL/graphql-ws/issues/366)
* Optional `payload` for ping/pong message types ([2fe0345](2fe0345)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* Package ECMAScript Modules too ([enisdenjo#87](https://github.com/ilijaNL/graphql-ws/issues/87)) ([2108174](2108174))
* Package rename `@enisdenjo/graphql-transport-ws` 👉 `graphql-transport-ws`. ([494f676](494f676))
* Rewrite GraphQL over WebSocket Protocol ([#2](#2)) ([42045c5](42045c5))
* Send optional payload with the `ConnectionAck` message ([enisdenjo#60](https://github.com/ilijaNL/graphql-ws/issues/60)) ([1327e77](1327e77))
* **server:** `context` may return a promise ([cd5c2f8](cd5c2f8)), closes [enisdenjo#74](https://github.com/ilijaNL/graphql-ws/issues/74)
* **server:** `execute` and `subscribe` are optional ([enisdenjo#148](https://github.com/ilijaNL/graphql-ws/issues/148)) ([af748b0](af748b0))
* **server:** Add `onClose` callback for closures at _any_ point in time ([dd0d4fa](dd0d4fa))
* **server:** Add `onDisconnect` callback ([enisdenjo#94](https://github.com/ilijaNL/graphql-ws/issues/94)) ([2a61268](2a61268))
* **server:** Add support for `ws@v8` ([9119153](9119153))
* **server:** Define execution/subscription `context` in creation options ([5b3d253](5b3d253)), closes [enisdenjo#13](https://github.com/ilijaNL/graphql-ws/issues/13)
* **server:** Dynamic `schema` support by accepting a function or a Promise ([enisdenjo#147](https://github.com/ilijaNL/graphql-ws/issues/147)) ([6a0bf94](6a0bf94)), closes [enisdenjo#127](https://github.com/ilijaNL/graphql-ws/issues/127)
* **server:** For dynamic usage, `context` option can be a function too ([enisdenjo#46](https://github.com/ilijaNL/graphql-ws/issues/46)) ([149b582](149b582))
* **server:** Implement following the new transport protocol ([#1](#1)) ([a412d25](a412d25))
* **server:** Log a warning for unsupported subprotocols ([88a12ef](88a12ef)), closes [enisdenjo#92](https://github.com/ilijaNL/graphql-ws/issues/92)
* **server:** Make and use with your own flavour ([enisdenjo#64](https://github.com/ilijaNL/graphql-ws/issues/64)) ([38bde87](38bde87)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61) [enisdenjo#73](https://github.com/ilijaNL/graphql-ws/issues/73) [enisdenjo#75](https://github.com/ilijaNL/graphql-ws/issues/75)
* **server:** More callbacks, clearer differences and higher extensibility ([enisdenjo#40](https://github.com/ilijaNL/graphql-ws/issues/40)) ([507a222](507a222))
* **server:** Optional `onPing` and `onPong` message type listeners ([f36066f](f36066f))
* **server:** Pass roots for operation fields as an option ([dcb5ed4](dcb5ed4))
* **server:** Support returning multiple results from `execute` ([enisdenjo#28](https://github.com/ilijaNL/graphql-ws/issues/28)) ([dbbd88b](dbbd88b))
* **server:** Use `@fastify/websocket` ([enisdenjo#382](https://github.com/ilijaNL/graphql-ws/issues/382)) ([dd755b0](dd755b0)), closes [enisdenjo#381](https://github.com/ilijaNL/graphql-ws/issues/381)
* **server:** Use `fastify-websocket` ([enisdenjo#200](https://github.com/ilijaNL/graphql-ws/issues/200)) ([b62fc95](b62fc95))
* **server:** Use `validate` option for custom GraphQL validation ([b68d56c](b68d56c))
* **server:** Use uWebSockets ([enisdenjo#89](https://github.com/ilijaNL/graphql-ws/issues/89)) ([45d08fc](45d08fc)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61)
* Subscribe message `query` must be a string ([enisdenjo#45](https://github.com/ilijaNL/graphql-ws/issues/45)) ([60d9cd5](60d9cd5))
* Support custom JSON message `reviver` and `replacer` ([enisdenjo#172](https://github.com/ilijaNL/graphql-ws/issues/172)) ([0a9894e](0a9894e))
* TypeScript generic for connection init payload (`connectionParams`) ([enisdenjo#311](https://github.com/ilijaNL/graphql-ws/issues/311)) ([e67cf80](e67cf80))
* **use:** Generic for extending the context extras ([401cd4c](401cd4c)), closes [enisdenjo#189](https://github.com/ilijaNL/graphql-ws/issues/189)
* **uWebSockets:** Add `persistedRequest` to context extra and deprecate uWS's stack allocated `request` ([enisdenjo#196](https://github.com/ilijaNL/graphql-ws/issues/196)) ([736e6ed](736e6ed))
* **uWebSockets:** Drop deprecated `request` context extra ([02ea5ee](02ea5ee))
* WebSocket Ping and Pong as keep-alive ([enisdenjo#11](https://github.com/ilijaNL/graphql-ws/issues/11)) ([16ae316](16ae316))

### Performance Improvements

* **client:** Await timeouts only in recursive connects ([55c8fc8](55c8fc8))
* **client:** Focus subscription message listeners on `id` ([enisdenjo#150](https://github.com/ilijaNL/graphql-ws/issues/150)) ([32c2268](32c2268))
* **client:** Memoize message parsing for each subscriber ([2a7ba46](2a7ba46))
* Easier message parser ([d44c6f1](d44c6f1))
* Reduce runtime prototype traversal for hasOwnProperty ([enisdenjo#52](https://github.com/ilijaNL/graphql-ws/issues/52)) ([1bb9218](1bb9218))

### Reverts

* Revert "refactor: emit client connect in next tick during testing" ([c10d0bf](c10d0bf))

### BREAKING CHANGES

* Because of the Protocol's strictness, an instant connection termination will happen whenever an invalid message is identified; meaning, all previous implementations will fail when receiving the new subprotocol ping/pong messages.

**Beware,** the client will NOT ping the server by default. Please make sure to upgrade your stack in order to support the new ping/pong message types.

A simple recipe showcasing a client that times out if no pong is received and measures latency, looks like this:
```js
import { createClient } from 'graphql-ws';

let activeSocket,
  timedOut,
  pingSentAt = 0,
  latency = 0;
createClient({
  url: 'ws://i.time.out:4000/and-measure/latency',
  keepAlive: 10_000, // ping server every 10 seconds
  on: {
    connected: (socket) => (activeSocket = socket),
    ping: (received) => {
      if (!received /* sent */) {
        pingSentAt = Date.now();
        timedOut = setTimeout(() => {
          if (activeSocket.readyState === WebSocket.OPEN)
            activeSocket.close(4408, 'Request Timeout');
        }, 5_000); // wait 5 seconds for the pong and then close the connection
      }
    },
    pong: (received) => {
      if (received) {
        latency = Date.now() - pingSentAt;
        clearTimeout(timedOut); // pong is received, clear connection close timeout
      }
    },
  },
});
```
* **uWebSockets:** The deprecated uWebSockets `request` context extra field has been dropped because it is stack allocated and cannot be used ouside the internal `upgrade` callback.
* **client:** Client `keepAlive` option has been renamed to `lazyCloseTimeout` in order to eliminate ambiguity with the client to server pings keep-alive option.
* **server:** The return function of `server.opened` (`closed`) now requires the close event code and reason for reporting to the `onDisconnect` callback.
* **server:** The `Context.subscriptions` record value can be either an `AsyncIterator` or a `Promise`.
* **client:** Client `retryTimeout` option has been replaced with the new `retryWait`.

`retryWait` allows you to control the retry timeout strategy by resolving the returned promise when ready. The default implements the randomised exponential backoff like so:
```ts
// this is the default
const retryWait = async function randomisedExponentialBackoff(retries: number) {
  let retryDelay = 1000; // start with 1s delay
  for (let i = 0; i < retries; i++) {
    retryDelay *= 2; // square `retries` times
  }
  await new Promise((resolve) =>
    setTimeout(
      // resolve pending promise with added random timeout from 300ms to 3s
      resolve,
      retryDelay + Math.floor(Math.random() * (3000 - 300) + 300),
    ),
  );
};
```
* **server:** You now "make" a ready-to-use server that can be used with _any_ WebSocket implementation!

Summary of breaking changes:
- No more `keepAlive`. The user should provide its own keep-alive implementation. _(I highly recommend [WebSocket Ping and Pongs](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets))_
- No more HTTP `request` in the server context.
- No more WebSocket in the server context (you're the one that creates it).
- You use your own WebSocket server
- Server exports only `makeServer` _(no more `createServer`)_

### Benefits
- You're responsible for the server (_any_ optimisation or adjustment can be applied)
- Any WebSocket server can be used (or even mocked if necessary)
- You control the disposal of the server (close or transfer clients however you wish)
- New `extra` field in the `Context` for storing custom values useful for callbacks
- Full control of authentication flow
- Full control over error handling
- True zero-dependency

### Migrating from v1

**Only the server has to be migrated.** Since this release allows you to use your favourite WebSocket library (or your own implementation), using [ws](https://github.com/websockets/ws) is just one way of using `graphql-ws`. This is how to use the implementation shipped with the lib:

```ts
/**
 * ❌ instead of the lib creating a WebSocket server internally with the provided arguments
 */
import https from 'https';
import { createServer } from 'graphql-ws';

const server = https.createServer(...);

createServer(
  {
    onConnect(ctx) {
      // were previously directly on the context
      ctx.request as IncomingRequest
      ctx.socket as WebSocket
    },
    ...rest,
  },
  {
    server,
    path: '/graphql',
  },
);

/**
 * ✅ you have to supply the server yourself
 */
import https from 'https';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws'; // notice the import path

const server = https.createServer(...);
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});

useServer(
  {
    onConnect(ctx) {
      // are now in the `extra` field
      ctx.extra.request as IncomingRequest
      ctx.extra.socket as WebSocket
    },
    ...rest,
  },
  wsServer,
  // optional keepAlive with ping pongs (defaults to 12 seconds)
);
```
* This lib is no longer compatible with [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws). It follows a redesigned transport protocol aiming to improve security, stability and reduce ambiguity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed released Has been released and published
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants