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

Connection closed after onError() in the subscription #170

Closed
Lana11s opened this issue Apr 26, 2021 · 19 comments
Closed

Connection closed after onError() in the subscription #170

Lana11s opened this issue Apr 26, 2021 · 19 comments
Labels
bug Something isn't working released Has been released and published

Comments

@Lana11s
Copy link

Lana11s commented Apr 26, 2021

Hello,

I'm using graphql-ws with Apollo Client and lately updated from 4.2.1 to 4.2.2.

It seems that if there is an onError event in the subscription, a complete event is submitted and the subscription gets closed:

image

image

My question is whether this behavior is intended/expected?

In the 4.2.1, only error() was invoked, without complete()

@enisdenjo
Copy link
Owner

Hey there, yes, this is intended behaviour. Following the Observer pattern both the error or complete stop the observable/sink. One of them needs to be invoked to complete the execution (one for erroneous one for successful), but never both.

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

enisdenjo commented Apr 26, 2021

Furthermore, the Complete message should not be sent from the client if it receives an Error message. Hence the completed = true:

graphql-ws/src/client.ts

Lines 552 to 555 in cd70a49

case MessageType.Error: {
completed = true;
sink.error(message.payload);
releaser();

The releaser checks the completed flag, and if true - it doesnt dispatch the Complete message:

graphql-ws/src/client.ts

Lines 578 to 585 in cd70a49

if (!completed && socket.readyState === WebSocketImpl.OPEN)
// if not completed already and socket is open, send complete message to server on release
socket.send(
stringifyMessage<MessageType.Complete>({
id,
type: MessageType.Complete,
}),
);

@Lana11s
Copy link
Author

Lana11s commented Apr 26, 2021

Hey @enisdenjo,
thanks for the quick response and clarification!

I am trying to resubscribe when error() callback is triggered.
But now it results in the invocation of complete() of the new subscription.

Best regards

@enisdenjo
Copy link
Owner

Urm, I dont quite understand. Could you elaborate your issue a bit more? New subscriptions have nothing to do with old/completed/errored subscriptions. Note that resubscribing is not done automatically, the user/you have to resubscribe by calling client.subscribe again.

@Lana11s
Copy link
Author

Lana11s commented Apr 26, 2021

Simplified, there is following:

 // 1) initial call
function subscribe() {
    client.subscribe({ query: QUERY }).subscribe(createObserver()) // Apollo Client with WebSocketLink as in readme-examle.
}

function createObserver<T>(): ZenObservable.Observer<T> {
    return {
        error(error: any) {
            subscribe() // 2) onError: resubscribe
        },
        complete() {
            // 3) gets called for the newly created/subscribed observer
        }
    }
}

@enisdenjo
Copy link
Owner

It is possible that you're getting the complete from the first subscription. Can you confirm that it is not?

Also, can you share your WebSocket messages from the browser DevTools?

A repro would be the best and easiest way for me to check the behaviour.

@enisdenjo
Copy link
Owner

Note that, with the current version of the lib, both error and complete do get called. There is a TODO here:

graphql-ws/src/client.ts

Lines 552 to 559 in cd70a49

case MessageType.Error: {
completed = true;
sink.error(message.payload);
releaser();
// TODO-db-201025 calling releaser will complete the sink, meaning that both the `error` and `complete` will be
// called. neither promises or observables care; once they settle, additional calls to the resolvers will be ignored
return;
}

So, it is probably the first subscription calling the complete.

@Lana11s
Copy link
Author

Lana11s commented Apr 26, 2021

yes, complete comes from the second subscription. If I modify the observer, then I get:

function createObserver<T>(): ZenObservable.Observer<T> {
    return {
        error(error: any) {
            client.subscribe({ query: QUERY }).subscribe(createSecondObserver()) // 2) onError: resubscribe
        },
        complete() {
            // not called
        }
    }
}
function createSecondObserver<T>(): ZenObservable.Observer<T> {
    return {
        complete() {
            // 3) gets called for the newly created/subscribed observer
        }
    }
}

It is running within a test, where at first an error-event sent from server followed by next-event afterwards.

If a closed listener is registered on the WebSocketLink, it is notified right after resubscription:

wsLink.client.on("closed", (event: any) => {
    // invoked only if resubscription created
})

@enisdenjo
Copy link
Owner

Hmm, weird. You might be onto something. But, sadly, I cannot put a dot on what...

I'd be very appreciative if you could provide me with a reproduction that I can dig around. I dont know where to start really... It can be a factor of things, from the server, through the client, all the way to Apollo stack.

@Lana11s
Copy link
Author

Lana11s commented Apr 27, 2021

Hello @enisdenjo,

I found out that there is some timing issue (perhaps only with my test), so that with

error(error: any) {
    new Promise<void>((resolve, _) => {
                setTimeout(() => {
                    resolve()
                }, 300)
            }).then((_) => {
            client.subscribe({ query: QUERY }).subscribe(createSecondObserver()) // 2) onError: resubscribe
    })
}

it works fine. I'm currently on it.

This code currently not in a github repository yet, I will try to make an example project for it and let you know.

Thank you for your support!

@enisdenjo
Copy link
Owner

Ok, cool! Please keep me up-to-date, I'd like to fix this if coming from graphql-ws. 😄

@Lana11s Lana11s closed this as completed Apr 27, 2021
@enisdenjo enisdenjo added bug Something isn't working and removed question Further information about the library is requested labels Apr 27, 2021
enisdenjo added a commit that referenced this issue Apr 27, 2021
enisdenjo pushed a commit that referenced this issue Apr 27, 2021
## [4.4.3](v4.4.2...v4.4.3) (2021-04-27)

### Bug Fixes

* **client:** Subscribes even if socket is in CLOSING state due to all subscriptions being completed ([3e3b8b7](3e3b8b7)), closes [#173](#173) [#170](#170)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 4.4.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Apr 27, 2021
@enisdenjo
Copy link
Owner

Hey @Lana11s, good news - I think #173 was experiencing something similar. I pushed a fix and feel that this issue should be resolved with it too.

Can you please try v4.4.3 out with your original problem?

@Lana11s
Copy link
Author

Lana11s commented Apr 28, 2021

Hey @enisdenjo
thank you for the fix!
But I'm still not sure if this resolves my problem completely.

I was able to achieve the desired behavior by setting the 'keepAlive' property. Using it, the socket remains open and the re-subscription can be initialized.

I have also some other question:
Is it intended that 'locks' can have negative values?
I added some println and got:

    console.log
      locks: 1

      at node_modules/graphql-ws/lib/client.js:285:25

    console.log
      locks: 2

      at node_modules/graphql-ws/lib/client.js:285:25

    console.log
      locks: 1

      at releaser (node_modules/graphql-ws/lib/client.js:321:37)

    console.log
      locks: 0

      at releaser (node_modules/graphql-ws/lib/client.js:321:37)

    console.log
      locks: 0

      at node_modules/graphql-ws/lib/client.js:193:29

    console.log
      locks: -1

      at releaser (node_modules/graphql-ws/lib/client.js:321:37)

    console.log
      locks: -1

      at node_modules/graphql-ws/lib/client.js:193:29

@enisdenjo
Copy link
Owner

enisdenjo commented Apr 28, 2021

Great find! Seems like it is indeed possible for the locks to go negative if you dispose of a subscription multiple times. This behaviour is potentially causing your misbehaviour, this is how:

  1. Create 2 subscriptions
  2. Dispose/complete the 2nd subscription 2 times
  3. Locks decrement from 2 to 0
  4. Since lazy = true and locks = 0, the connection gracefully closes - invoking the complete on the 2nd subscription

enisdenjo pushed a commit that referenced this issue Apr 28, 2021
## [4.4.4](v4.4.3...v4.4.4) (2021-04-28)

### Bug Fixes

* **client:** `complete` should not be called after subscription `error` ([1fba419](1fba419))
* **client:** Subscription can be disposed only once ([abd9c28](abd9c28)), closes [#170](#170)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 4.4.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo
Copy link
Owner

Hey hey @Lana11s, I made sure subscriptions can be disposed only once (locks shouldnt go negative) in v4.4.4.

Could you please try again? 😄

@Lana11s
Copy link
Author

Lana11s commented Apr 28, 2021

Hey @enisdenjo,

I tested it and it works as expected, even without my workaround with 'keepAlive'.
After first subsription is terminated with an onError() event, the second one can be registered successfully, without getting closed :)
And the locks-counter remains positive ;)

Thank you!

@enisdenjo
Copy link
Owner

Wohoo, nice! Glad it works now without any workarounds in place. Thank you for sticking around and providing valuable insights. 😄

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
bug Something isn't working released Has been released and published
Projects
None yet
Development

No branches or pull requests

2 participants