View Source SubscriptionsTransportWS.Socket behaviour (SubscriptionsTransportWS v1.0.3)
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/ Apollo JS and Urql with subscriptions-transport-ws.
Subscriptions-Transport-WS vs Graphql-WS
subscriptions-transport-ws is an older protocol. A newer one has been written named graphql_ws. The grapqhl_ws protocol is more robust, and the way to go in the future.
At the time of writing the major libraries support one, the other or both. E.g. Apollo Swift currently only supports subscriptions-transport-ws, v3 of Apollo Android supports graphql_ws. The Urlq/Apollo JS libraries support either one.
If you need to support graphql_ws on the backend in Elixir, you can use the absinthe_graphql_ws library. You can set up multiple websocket endpoints to support both protocols.
Installation
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"}
]
endUsage
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.PubSubIn 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
endIn 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"]])
# ...
endNow 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 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
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
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 Jasonschema- refers to the Absinthe schema (required)pipeline- refers to the Absinthe pipeline to use, defaults to{SubscriptionsTransportWS.Socket, :default_pipeline}keep_aliveperiod in ms to send keep alive messages over the socket, defaults to 10000ping_intervalperiod 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
Specs
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.
Specs
Specs
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
- connectionParams in Apollo javascript client
- connectingPayload in Apollo iOS client
or similar in other clients.
Specs
Link to this section 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.
Examples
iex> assign(socket, :name, "Elixir")
iex> assign(socket, name: "Elixir", logo: "💧")
Adds key-value pairs into Absinthe context.
Examples
iex> Socket.assign_context(socket, current_user: user)
%Socket{}
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.
Examples
iex> SubscriptionsTransportWS.Socket.put_options(socket, context: %{current_user: user})
%SubscriptionsTransportWS.Socket{}