Skip to content
This repository has been archived by the owner on Apr 14, 2023. It is now read-only.

allow using custom WebSocket server implementation #374

Merged
merged 13 commits into from
Jun 13, 2018

Conversation

lgandecki
Copy link
Contributor

@lgandecki lgandecki commented Mar 17, 2018

Hello, this will allow me to write isolated integration tests for subscriptions.
My next step will be to create apollo-link-schema-with-subscriptions (open to a better name! :) ), so people can simply do:

new ApolloClient({
    link: new SchemaLinkWithSubscriptions({
      schema,
      context
    }),
    cache: new InMemoryCache()
  });

And be able to use apolloClient as if it was connected to a websocket server (so they can do subscriptions as well)

Currently, with my fork of subscriptions-transport-ws in my test I do:

const mockServer = require("mock-socket-with-protocol").Server;
const mockWebSocket = require("mock-socket-with-protocol").WebSocket;
const gqClient = () => {
  const port = Math.random(); 
// using incorrect port here - it doesn't matter 
// since the mocked implementation does string matching to connect client/server anyway,
// there are no actual ports being opened, which is the point ;-) 
  SubscriptionServer.create(
    {
      schema,
      execute,
      subscribe
    },
    { host: "ws://localhost", port },
    mockServer
  );
  
  const wsLink = new WebSocketLink({
    uri: `ws://localhost:${port}`,
    webSocketImpl: mockWebSocket
  });

  return new ApolloClient({
    link: wsLink,
    cache: new InMemoryCache()
  });
};

and it works like a charm :)

  • Make sure all of the significant new logic is covered by tests
  • Rebase your changes on master so that they can be merged easily
  • Make sure all tests and linter rules pass
  • Update CHANGELOG.md with your change

Copy link
Contributor

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely see the value in being able to pass a custom websocket server! 💯

The API is kinda awkward, as you end up having to pass socketOptions even though it's not used since you also pass your own server:

new SubscriptionServer({}, {}, customServer);

What about instead letting the second argument be either socketOptions or a custom server?

new SubscriptionServer(options: ServerOptions, socket: WebSocket.ServerOptions | WebSocket)

That seems much nicer from the user side: either just pass options and it'll work, or if you need more control you can also pass your own server instead(!).

@lgandecki
Copy link
Contributor Author

Hey Max! You are right. Let me tweak this tomorrow morning :)
Happy to hear you are not opposed to the idea - meaning I (most possibly!) won't have to use a fork.

Copy link
Contributor

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, much better! Now it'd be great if you could add a note about this to the docs and the readme and we should be good to go.

@lgandecki
Copy link
Contributor Author

@mxstbr I added some docs and examples - please let me know if this is a good direction. I can continue on this tomorrow. :) Thanks!

@mxstbr
Copy link
Contributor

mxstbr commented Mar 21, 2018

The "works with custom WebSocket implementation" test fails on that latest commit, any clues why?

Copy link
Contributor

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readme doc update is 💯, but let's work on the website a bit more.

Currently it's a bit too personal, and also the example code shouldn't be everything that's needed ever, it should only demonstrate the core concepts. (creating a custom websocket server with mock-socket-with-protocol, passing that to the subscription server and writing the test) Mind revising that?

@lgandecki
Copy link
Contributor Author

lgandecki commented Mar 21, 2018

As per the failure.. looks like it's a rare timing issue, looks like 200 ms between firing the events and the assertion that the subscription data came back wasn't enough. I only updated Changelog so that couldn't affect the test. I can increase the timeout a bit. A nicer way of doing this might be to instead of having an assertion in a timeout to check that both subscriptions finished:

if (result.data) {
    numTriggers += 1;
    assert.property(result.data, 'userFiltered'); (..)
    if(numTriggers === 2) {
        done()
    }
}

in both of them. This way the test would end the moment both subscriptions came back, instead of waiting a particular amount of time to verify that numOfTriggers is equal 2. I reused the pattern that's been established in the previous tests, but I don't think it's ideal to be honest.

Website part - would you rather it not speak about apollo and have a test that's almost a copy paste of the one that I talked about above?
Or maybe not even have a page about integration testing, but an example of creating a server and connecting a client to it?

I thought that the most typical use case would be nice to cover :) but I could change that into a blog post, and have more minimal info here.

Thanks!

Copy link
Contributor

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ~what I'd like to see in terms of the example in the docs:

// we are not opening any ports - this example of WebSocket client/server uses string matching to know to what server connect a given client
// we should use different string for every test to not share state
  const RANDOM_WS_PORT = Math.floor(Math.random() * 100000);

  const customServer = new Server(`ws://localhost:${RANDOM_WS_PORT}`);

  // We pass customServer instead of typical configuration of a default WebSocket server
  SubscriptionServer.create(
    {
      schema,
      execute,
      subscribe
    },
    customServer
  );

  const wsLink = new WebSocketLink({
    uri: `ws://localhost:${RANDOM_WS_PORT}`,
    webSocketImpl: WebSocket
  });

  return new ApolloClient({
    link: wsLink,
    cache: new InMemoryCache()
  });
};

test(
  "subscription",
  async done => {
    gqClient()
      .subscribe({
        query: gql`
          subscription {
            somethingChanged
          }
        `
      })
      .subscribe({
        next({ data }) {
          expect(data.somethingChanged).toEqual("passedId");
          done();
        },
        error(err) {
          throw new Error(err);
        }
      });

    setTimeout(() => {
      pubsub.publish(SOMETHING_CHANGED_TOPIC, "passedId");
    }, 100);
  },
  1000
);

Maybe?

The text should specify when to even use that code in the first place, rather than the "I" stuff.

@lgandecki
Copy link
Contributor Author

I see, thanks for the feedback. I will take a look tomorrow. I get your point. I was torn here, obviously there is a lot of boiler code that's not related to the new functionality and it introduces a lot of noise.. On the other hand this is a piece of code that you can copy/paste and it works (assuming you install all the dependencies, but that's the easy part). Otherwise it might be a bit tricky to understand what all the parts are and how they connect with each other.

@lgandecki
Copy link
Contributor Author

I finally got some time to clean it up a bit. I hope that's better.

On a different note - I'm also tempted to work on the tests, they are slow, and can't be parallelized. In mocha watch mode they take ~25 seconds to run, which is a rather suboptimal developer experience. :-)

If it was up to me I'd:

  • split client/server to separate files (so they can run in parallel)
  • introduce jest for its fantastic watch mode
  • use waitForExpect instead of arbitrary setTimeouts that introduce unnecessary long waits in some places.

Is this something you guys would consider merging?

@NeoPhi
Copy link
Contributor

NeoPhi commented Mar 27, 2018

I'd be happy moving to jest as the testing framework.

@mxstbr
Copy link
Contributor

mxstbr commented Mar 27, 2018

Same, I'd love to actually, but please do it in a separate PR!

Copy link
Contributor

@mxstbr mxstbr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM, @NeoPhi?

@NeoPhi NeoPhi self-requested a review March 27, 2018 14:27
@NeoPhi
Copy link
Contributor

NeoPhi commented Mar 27, 2018

@mxstbr Yes, looks good.

@lgandecki
Copy link
Contributor Author

Ping :-)

@lgandecki
Copy link
Contributor Author

I have people asking me about this, even on apollo slack - is there any reason why this is not merged yet? Is there a different pattern for doing integration testing for subscriptions?
I personally use my fork, but would definitely prefer to just be able to install and use the main package..

@NeoPhi NeoPhi merged commit fafc524 into apollographql:master Jun 13, 2018
@NeoPhi
Copy link
Contributor

NeoPhi commented Jun 13, 2018

@lgandecki Fell off the radar. Merging and releasing now.

chilinh added a commit to chilinh/subscriptions-transport-ws that referenced this pull request Jun 18, 2018
* commit '71dd1041242ac20553af0adad8ee001a25edfec3':
  release 0.9.11
  allow using custom WebSocket server implementation (apollographql#374)
  update changelog header
  update dependencies (apollographql#427)
  Fix constructor signature (apollographql#419)
  Typo (apollographql#423)
  chore(deps): update dependency meteor-theme-hexo to v1.0.13
  chore(deps): update dependency meteor-theme-hexo to v1.0.12
  chore(deps): update dependency meteor-theme-hexo to v1.0.10
  fix issue with @types/[email protected]
  chore(deps): update dependency hexo-server to v0.3.2
  chore(deps): update dependency meteor-theme-hexo to v1.0.9

# Conflicts:
#	src/client.ts
@danfernand
Copy link

danfernand commented Mar 6, 2019

Question please, how do you pass in the constant to publish for in

pubsub.publish(SOMETHING_CHANGED_TOPIC, "passedId");

example subsscription server

SubscriptionServer.create(
    {
      schema: gql`
        type SomethingLog {
          log: String
        }

        type Subscription {
          onSomethingLog(
            someId: ID!
          ): SomethingLog
        }
      `,
      execute,
      subscribe
    },
    customServer
  );

Im trying to trigger a publish via pubsub but I am not piecing it all together

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature New addition or enhancement to existing solutions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants