Repository: apollographql/apollo-client

Problem statement:
Why use WebSocketLink  from '@apollo/client/link/ws'
1) Sorry for my english.
2) thanks for your time.

In your documentation (https://www.apollographql.com/docs/react/api/link/apollo-link-ws/) you encourage to use WebSocket which depends on subscriptions-transport-ws npm package. but apollographql/subscriptions-transport-ws repo says 

> "The subscriptions-transport-ws library is not being actively maintained. It is recommended that you use the graphql-ws library instead."

Same case here https://www.apollographql.com/docs/graphql-subscriptions/authentication/ .




By other side in the next link you implement difference approach to the same solution, will be great or unify then, or explain pros/cons, or when use what:

https://www.apollographql.com/docs/graphql-subscriptions/express/
https://www.apollographql.com/docs/apollo-server/data/subscriptions/#example-authentication-with-onconnect


Finally I would like to ask you about an example to how implement graphql-ws with apollo server.






Hints:
+1, I think it would be better to link to the new `graphql-ws` library which is being actively maintained.

To the maintainers: was there a particular reason for using `subscriptions-transport-ws` in the documentation example? If not, happy to work on getting the documentation updated for the replacement library
I tried to implement graphql-ws with apollo (following the example in graphql-ws documentation) but i had no success.
Will be great have a oficial documentation to how to implement.
+1
Example is here: https://github.com/enisdenjo/graphql-ws#apollo-client

Patch:
diff --git a/config/entryPoints.js b/config/entryPoints.js
--- a/config/entryPoints.js
+++ b/config/entryPoints.js
@@ -12,6 +12,7 @@ const entryPoints = [
   { dirs: ['link', 'persisted-queries'] },
   { dirs: ['link', 'retry'] },
   { dirs: ['link', 'schema'] },
+  { dirs: ['link', 'subscriptions'] },
   { dirs: ['link', 'utils'] },
   { dirs: ['link', 'ws'] },
   { dirs: ['react'] },
diff --git a/docs/gatsby-config.js b/docs/gatsby-config.js
--- a/docs/gatsby-config.js
+++ b/docs/gatsby-config.js
@@ -109,6 +109,7 @@ module.exports = {
             'api/link/apollo-link-rest',
             'api/link/apollo-link-retry',
             'api/link/apollo-link-schema',
+            'api/link/apollo-link-subscriptions',
             'api/link/apollo-link-ws',
             'api/link/community-links'
           ],
diff --git a/src/link/subscriptions/index.ts b/src/link/subscriptions/index.ts
new file mode 100644
--- /dev/null
+++ b/src/link/subscriptions/index.ts
@@ -0,0 +1,86 @@
+// This file is adapted from the graphql-ws npm package:
+// https://github.com/enisdenjo/graphql-ws
+//
+// Most of the file comes from that package's README; some other parts (such as
+// isLikeCloseEvent) come from its source.
+//
+// Here's the license of the original code:
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2020-2021 Denis Badurina
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+import { print } from "graphql";
+import type { Client } from "graphql-ws";
+
+import { ApolloLink, Operation, FetchResult } from "../core";
+import { isNonNullObject, Observable } from "../../utilities";
+import { ApolloError } from "../../errors";
+
+interface LikeCloseEvent {
+  /** Returns the WebSocket connection close code provided by the server. */
+  readonly code: number;
+  /** Returns the WebSocket connection close reason provided by the server. */
+  readonly reason: string;
+}
+
+function isLikeCloseEvent(val: unknown): val is LikeCloseEvent {
+  return isNonNullObject(val) && 'code' in val && 'reason' in val;
+}
+
+
+export class GraphQLWsLink extends ApolloLink {
+  constructor(public readonly client: Client) {
+    super();
+  }
+
+  public request(operation: Operation): Observable<FetchResult> {
+    return new Observable((observer) => {
+      return this.client.subscribe<FetchResult>(
+        { ...operation, query: print(operation.query) },
+        {
+          next: observer.next.bind(observer),
+          complete: observer.complete.bind(observer),
+          error: (err) => {
+            if (err instanceof Error) {
+              return observer.error(err);
+            }
+
+            if (isLikeCloseEvent(err)) {
+              return observer.error(
+                // reason will be available on clean closes
+                new Error(
+                  `Socket closed with event ${err.code} ${err.reason || ""}`
+                )
+              );
+            }
+
+            return observer.error(
+              new ApolloError({
+                graphQLErrors: Array.isArray(err) ? err : [err],
+              })
+            );
+          },
+        }
+      );
+    });
+  }
+}


Test patch:
diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap
--- a/src/__tests__/__snapshots__/exports.ts.snap
+++ b/src/__tests__/__snapshots__/exports.ts.snap
@@ -210,6 +210,12 @@ Array [
 ]
 `;
 
+exports[`exports of public entry points @apollo/client/link/subscriptions 1`] = `
+Array [
+  "GraphQLWsLink",
+]
+`;
+
 exports[`exports of public entry points @apollo/client/link/utils 1`] = `
 Array [
   "createOperation",
diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts
--- a/src/__tests__/exports.ts
+++ b/src/__tests__/exports.ts
@@ -11,6 +11,7 @@ import * as linkHTTP from "../link/http";
 import * as linkPersistedQueries from "../link/persisted-queries";
 import * as linkRetry from "../link/retry";
 import * as linkSchema from "../link/schema";
+import * as linkSubscriptions from "../link/subscriptions";
 import * as linkUtils from "../link/utils";
 import * as linkWS from "../link/ws";
 import * as react from "../react";
@@ -52,6 +53,7 @@ describe('exports of public entry points', () => {
   check("@apollo/client/link/persisted-queries", linkPersistedQueries);
   check("@apollo/client/link/retry", linkRetry);
   check("@apollo/client/link/schema", linkSchema);
+  check("@apollo/client/link/subscriptions", linkSubscriptions);
   check("@apollo/client/link/utils", linkUtils);
   check("@apollo/client/link/ws", linkWS);
   check("@apollo/client/react", react);
diff --git a/src/link/subscriptions/__tests__/graphqlWsLink.ts b/src/link/subscriptions/__tests__/graphqlWsLink.ts
new file mode 100644
--- /dev/null
+++ b/src/link/subscriptions/__tests__/graphqlWsLink.ts
@@ -0,0 +1,106 @@
+import { Client } from "graphql-ws";
+import { ExecutionResult } from "graphql";
+import gql from "graphql-tag";
+
+import { Observable } from "../../../utilities";
+import { execute } from "../../core";
+import { GraphQLWsLink } from "..";
+
+const query = gql`
+  query SampleQuery {
+    stub {
+      id
+    }
+  }
+`;
+
+const mutation = gql`
+  mutation SampleMutation {
+    stub {
+      id
+    }
+  }
+`;
+
+const subscription = gql`
+  subscription SampleSubscription {
+    stub {
+      id
+    }
+  }
+`;
+
+function mockClient(subscribe: Client["subscribe"]): Client {
+  return {
+    subscribe,
+    // GraphQLWsLink doesn't use these methods
+    on: () => () => {},
+    dispose: () => {},
+  };
+}
+
+async function observableToArray<T>(o: Observable<T>): Promise<T[]> {
+  const out: T[] = [];
+  await o.forEach((v) => out.push(v));
+  return out;
+}
+
+describe("GraphQLWSlink", () => {
+  it("constructs", () => {
+    const client = mockClient(() => () => {});
+    expect(() => new GraphQLWsLink(client)).not.toThrow();
+  });
+
+  // TODO some sort of dependency injection
+
+  // it('should pass the correct initialization parameters to the Subscription Client', () => {
+  // });
+
+  it("should call subscribe on the client for a query", async () => {
+    const result = { data: { data: "result" } } as ExecutionResult<any, any>;
+    const subscribe: Client["subscribe"] = (_, sink) => {
+      sink.next(result);
+      sink.complete();
+      return () => {};
+    };
+    const client = mockClient(subscribe);
+    const link = new GraphQLWsLink(client);
+
+    const obs = execute(link, { query });
+    await expect(observableToArray(obs)).resolves.toEqual([result]);
+  });
+
+  it("should call subscribe on the client for a mutation", async () => {
+    const result = { data: { data: "result" } } as ExecutionResult<any, any>;
+    const subscribe: Client["subscribe"] = (_, sink) => {
+      sink.next(result);
+      sink.complete();
+      return () => {};
+    };
+    const client = mockClient(subscribe);
+    const link = new GraphQLWsLink(client);
+
+    const obs = execute(link, { query: mutation });
+    await expect(observableToArray(obs)).resolves.toEqual([result]);
+  });
+
+  it("should call next with multiple results for subscription", async () => {
+    const results = [
+      { data: { data: "result1" } },
+      { data: { data: "result2" } },
+    ] as ExecutionResult<any, any>[];
+    const subscribe: Client["subscribe"] = (_, sink) => {
+      const copy = [...results];
+      for (const r of copy) {
+        sink.next(r);
+      }
+      sink.complete();
+      return () => {};
+    };
+    const client = mockClient(subscribe);
+    const link = new GraphQLWsLink(client);
+
+    const obs = execute(link, { query: subscription });
+    await expect(observableToArray(obs)).resolves.toEqual(results);
+  });
+});
diff --git a/src/link/ws/__tests__/webSocketLink.ts b/src/link/ws/__tests__/webSocketLink.ts
--- a/src/link/ws/__tests__/webSocketLink.ts
+++ b/src/link/ws/__tests__/webSocketLink.ts
@@ -88,7 +88,7 @@ describe('WebSocketLink', () => {
     client.request.mockReturnValueOnce(observable);
     const link = new WebSocketLink(client);
 
-    const obs = execute(link, { query: mutation });
+    const obs = execute(link, { query: subscription });
     expect(obs).toEqual(observable);
     obs.subscribe(data => {
       expect(data).toEqual(result);