SubscriptionsTransportWS.Socket behaviour (SubscriptionsTransportWS v1.0.2) View Source

Implementation of the subscriptions-transport-ws graphql subscription protocol for Absinthe. Instead of using Absinthe subscriptions over Phoenix channels it exposes a websocket directly. This allows you to use the Apollo and Urql Graphql clients without using a translation layer to Phoenix channels such as @absinthe/socket.

Has been tested with Apollo iOS and Urql with subscriptions-transport-ws.

Installation

If available in Hex, the package can be installed by adding subscriptions_transport_ws to your list of dependencies in mix.exs:

def deps do
  [
    {:subscriptions_transport_ws, "~> 1.0.0"}
  ]
end

Usage

There are several steps to use this library.

You need to have a working phoenix pubsub configured. Here is what the default looks like if you create a new phoenix project:

config :my_app, MyAppWeb.Endpoint,
  # ... other config
  pubsub_server: MyApp.PubSub

In your application supervisor add a line AFTER your existing endpoint supervision line:

[
  # other children ...
  MyAppWeb.Endpoint, # this line should already exist
  {Absinthe.Subscription, MyAppWeb.Endpoint}, # add this line
  # other children ...
]

Where MyAppWeb.Endpoint is the name of your application's phoenix endpoint.

Add a module in your app lib/web/channels/absinthe_socket.ex

defmodule AbsintheSocket do
  # App.GraphqlSchema is your graphql schema
  use SubscriptionsTransportWS.Socket, schema: App.GraphqlSchema, keep_alive: 1000

  # Callback similar to default Phoenix UserSocket
  @impl true
  def connect(params, socket) do
    {:ok, socket}
  end

  # Callback to authenticate the user
  @impl true
  def gql_connection_init(message, socket) do
    {:ok, socket}
  end
end

In your MyAppWeb.Endpoint module add:

  defmodule MyAppWeb.Endpoint do
    use Phoenix.Endpoint, otp_app: :my_app
    use Absinthe.Phoenix.Endpoint

    socket("/absinthe-ws", AbsintheSocket, websocket: [subprotocols: ["graphql-ws"]])
    # ...
  end

Now if you start your app you can connect to the socket on ws://localhost:4000/absinthe-ws/websocket

Example with Apollo JS

import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useSubscription,
} from "@apollo/client";
import { split, HttpLink } from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
import { WebSocketLink } from "@apollo/client/link/ws";

const wsLink = new WebSocketLink({
  uri: "ws://localhost:4000/absinthe-ws/websocket",
  options: {
    reconnect: true,
  },
});
const httpLink = new HttpLink({
  uri: "http://localhost:4000/api",
});

const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === "OperationDefinition" &&
      definition.operation === "subscription"
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  uri: "http://localhost:4000/api",
  cache: new InMemoryCache(),
  link: splitLink,
});
See the [Apollo documentation](https://www.apollographql.com/docs/react/data/subscriptions/) for more information

Example with Urql

import { SubscriptionClient } from "subscriptions-transport-ws";
import {
  useSubscription,
  Provider,
  defaultExchanges,
  subscriptionExchange,
} from "urql";

const subscriptionClient = new SubscriptionClient(
  "ws://localhost:4000/absinthe-ws/websocket",
  {
    reconnect: true,
  }
);

const client = new Client({
  url: "http://localhost:4000/api",
  exchanges: [
    subscriptionExchange({
      forwardSubscription(operation) {
        return subscriptionClient.request(operation);
      },
    }),
    ...defaultExchanges,
  ],
});

See the Urql documentation for more information.

Example with Swift Apollo

import Apollo
import ApolloSQLite
import ApolloWebSocket
import Foundation
import Combine

class ApolloService {
    static let shared = ApolloService()
    static let url = Config.host.appendingPathComponent("api")
  
    private(set) lazy var client: ApolloClient = {

        let store = ApolloStore()
        
        let requestChainTransport = RequestChainNetworkTransport(
            interceptorProvider: DefaultInterceptorProvider(store: store),
            endpointURL: "https://localhost:4000/api"
        )
        
        // The Normal Apollo Web Socket Implementation which uses an Apollo adapter server side
        let wsUrl = "wss://localhost:4000/absinthe-ws/websocket"
        let wsRequest = URLRequest(url: wsUrl)
        let wsClient = WebSocket(request: wsRequest)
        let apolloWebSocketTransport =  WebSocketTransport(websocket: wsClient)

        let splitNetworkTransport = SplitNetworkTransport(
            uploadingNetworkTransport: requestChainTransport,
            webSocketNetworkTransport: apolloWebSocketTransport
          )

        // Remember to give the store you already created to the client so it
        // doesn't create one on its own
        let client =  ApolloClient(
            networkTransport: splitNetworkTransport,
            store: store
        )

        return client
    }()
}

Or see here https://www.apollographql.com/docs/ios/subscriptions/#subscriptions-and-authorization-tokens

Link to this section Summary

Types

t()

When using this module there are several options available

Callbacks

Receives the socket params and authenticates the connection.

Callback for the connection_init message. The client sends this message after plain websocket connection to start the communication with the server.

Functions

Adds key value pairs to socket assigns. A single key value pair may be passed, a keyword list or map of assigns may be provided to be merged into existing socket assigns.

Adds key-value pairs into Absinthe context.

Same as assign_context/2 except one key-value pair is assigned.

Default pipeline to use for Absinthe graphql document execution

Sets the options for a given GraphQL document execution.

Link to this section Types

Specs

control() :: :ping | :pong

Specs

frame() :: {opcode(), message()}

Specs

message() :: binary()

Specs

opcode() :: :text | :binary | control()

Specs

t() :: %SubscriptionsTransportWS.Socket{
  assigns: map(),
  endpoint: atom(),
  handler: atom(),
  json_module: atom(),
  keep_alive: integer() | nil,
  operations: map(),
  ping_interval: integer() | nil,
  pubsub_server: atom(),
  serializer: atom()
}

When using this module there are several options available

  • json_module - defaults to Jason
  • schema - refers to the Absinthe schema (required)
  • pipeline - refers to the Absinthe pipeline to use, defaults to {SubscriptionsTransportWS.Socket, :default_pipeline}
  • keep_alive period in ms to send keep alive messages over the socket, defaults to 10000
  • ping_interval period in ms to send keep pings to the client, the client should respond with pong to keep the connection alive

Example

use SubscriptionsTransportWS.Socket, schema: App.GraphqlSchema, keep_alive: 1000

Link to this section Callbacks

Link to this callback

connect(params, t)

View Source (optional)

Specs

connect(params :: map(), t()) :: {:ok, t()} | :error

Receives the socket params and authenticates the connection.

Socket params and assigns

Socket params are passed from the client and can be used to verify and authenticate a user. After verification, you can put default assigns into the socket that will be set for all channels, ie

{:ok, assign(socket, :user_id, verified_user_id)}

To deny connection, return :error.

See Phoenix.Token documentation for examples in performing token verification on connect.

Link to this callback

connect(params, t, connect_info)

View Source (optional)

Specs

connect(params :: map(), t(), connect_info :: map()) :: {:ok, t()} | :error
Link to this callback

gql_connection_init(connection_params, t)

View Source

Specs

gql_connection_init(connection_params :: map(), t()) ::
  {:ok, t()} | {:error, any()}

Callback for the connection_init message. The client sends this message after plain websocket connection to start the communication with the server.

In the subscriptions-transport-ws protocol this is usually used to set the user on the socket.

Should return {:ok, socket} on success, and {:error, payload} to deny.

Receives the a map of connection_params, see

or similar in other clients.

Link to this callback

handle_message(params, t)

View Source (optional)

Specs

handle_message(params :: term(), t()) ::
  {:ok, t()} | {:push, frame(), t()} | {:stop, term(), t()}

Link to this section Functions

Link to this function

assign(socket, key, value)

View Source

Adds key value pairs to socket assigns. A single key value pair may be passed, a keyword list or map of assigns may be provided to be merged into existing socket assigns.

Examples

iex> assign(socket, :name, "Elixir")
iex> assign(socket, name: "Elixir", logo: "💧")
Link to this function

assign_context(socket, context)

View Source

Adds key-value pairs into Absinthe context.

Examples

iex> Socket.assign_context(socket, current_user: user)
%Socket{}
Link to this function

assign_context(socket, key, value)

View Source

Same as assign_context/2 except one key-value pair is assigned.

Link to this function

default_pipeline(schema, options)

View Source

Default pipeline to use for Absinthe graphql document execution

Link to this function

put_options(socket, opts)

View Source

Sets the options for a given GraphQL document execution.

Examples

iex> SubscriptionsTransportWS.Socket.put_options(socket, context: %{current_user: user})
%SubscriptionsTransportWS.Socket{}