@enisdenjo/graphql-transport-ws
Advanced tools
Comparing version 0.0.2 to 1.0.0
/** | ||
* | ||
* GraphQL subscriptions over the WebSocket Protocol | ||
* GraphQL over WebSocket Protocol | ||
* | ||
@@ -8,83 +8,12 @@ * Check out the `PROTOCOL.md` document for the transport specification. | ||
*/ | ||
/** | ||
* The shape of a GraphQL response as dictated by the | ||
* [spec](https://graphql.github.io/graphql-spec/June2018/#sec-Response-Format) | ||
*/ | ||
export interface GraphQLResponseWithData { | ||
data: Record<string, any>; | ||
errors?: { | ||
message: string; | ||
locations?: Array<{ | ||
line: number; | ||
column: number; | ||
}>; | ||
}[]; | ||
path?: string[] | number[]; | ||
} | ||
export interface GraphQLResponseWithoutData { | ||
data?: Record<string, any>; | ||
errors: { | ||
message: string; | ||
locations?: Array<{ | ||
line: number; | ||
column: number; | ||
}>; | ||
}[]; | ||
path?: Array<string | number>; | ||
} | ||
export interface GraphQLError { | ||
message: string; | ||
} | ||
export declare type GraphQLResponse = GraphQLResponseWithData | GraphQLResponseWithoutData | GraphQLError; | ||
/** Used to indicate that the requestId is missing. */ | ||
declare const NO_REQUEST_ID = "NRID"; | ||
/** | ||
* Is the raw message being sent through the WebSocket connection. | ||
* Since the ID generation is done automatically, we have 2 separate | ||
* types for the two possible messages. | ||
*/ | ||
export interface MessageWithoutID { | ||
type: MessageType; | ||
payload?: GraphQLResponse | null; | ||
} | ||
export interface Message extends MessageWithoutID { | ||
/** | ||
* The message ID (internally represented as the `requestId`). | ||
* Can be missing in cases when managing the subscription | ||
* connection itself. | ||
*/ | ||
id: string | typeof NO_REQUEST_ID; | ||
} | ||
/** Types of messages allowed to be sent by the client/server over the WS protocol. */ | ||
export declare enum MessageType { | ||
ConnectionInit = "connection_init", | ||
ConnectionAck = "connection_ack", | ||
ConnectionError = "connection_error", | ||
ConnectionKeepAlive = "ka", | ||
ConnectionTerminate = "connection_terminate", | ||
Start = "start", | ||
Data = "data", | ||
Error = "error", | ||
Complete = "complete", | ||
Stop = "stop" | ||
} | ||
/** The payload used for starting GraphQL subscriptions. */ | ||
export interface StartPayload { | ||
operationName?: string; | ||
query: string; | ||
variables: Record<string, any>; | ||
} | ||
/** The sink to communicate the subscription through. */ | ||
export interface Sink<T = any> { | ||
next(value: T): void; | ||
error(error: Error): void; | ||
complete(): void; | ||
readonly closed: boolean; | ||
} | ||
import { Sink, Disposable } from './types'; | ||
import { SubscribePayload } from './message'; | ||
/** Configuration used for the `create` client function. */ | ||
export interface Config { | ||
export interface ClientOptions { | ||
/** URL of the GraphQL server to connect. */ | ||
url: string; | ||
connectionParams?: Record<string, any> | (() => Record<string, any>); | ||
/** Optional parameters that the client specifies when establishing a connection with the server. */ | ||
connectionParams?: Record<string, unknown> | (() => Record<string, unknown>); | ||
} | ||
export interface Client { | ||
export interface Client extends Disposable { | ||
/** | ||
@@ -95,8 +24,5 @@ * Subscribes through the WebSocket following the config parameters. It | ||
*/ | ||
subscribe<T>(payload: StartPayload, sink: Sink<T>): () => void; | ||
/** Disposes of all active subscriptions, closes the WebSocket client and frees up memory. */ | ||
dispose(): Promise<void>; | ||
subscribe<T = unknown>(payload: SubscribePayload, sink: Sink<T>): () => void; | ||
} | ||
/** Creates a disposable GQL subscriptions client. */ | ||
export declare function createClient({ url, connectionParams }: Config): Client; | ||
export {}; | ||
export declare function createClient(options: ClientOptions): Client; |
"use strict"; | ||
/** | ||
* | ||
* GraphQL subscriptions over the WebSocket Protocol | ||
* GraphQL over WebSocket Protocol | ||
* | ||
@@ -9,241 +9,214 @@ * Check out the `PROTOCOL.md` document for the transport specification. | ||
*/ | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.createClient = exports.MessageType = void 0; | ||
const websocket_as_promised_1 = __importDefault(require("websocket-as-promised")); | ||
/** Used to indicate that the requestId is missing. */ | ||
const NO_REQUEST_ID = 'NRID'; | ||
function isNoRequestId(val) { | ||
return val === NO_REQUEST_ID; | ||
} | ||
/** Types of messages allowed to be sent by the client/server over the WS protocol. */ | ||
var MessageType; | ||
(function (MessageType) { | ||
MessageType["ConnectionInit"] = "connection_init"; | ||
MessageType["ConnectionAck"] = "connection_ack"; | ||
MessageType["ConnectionError"] = "connection_error"; | ||
// NOTE: The keep alive message type does not follow the standard due to connection optimizations | ||
MessageType["ConnectionKeepAlive"] = "ka"; | ||
MessageType["ConnectionTerminate"] = "connection_terminate"; | ||
MessageType["Start"] = "start"; | ||
MessageType["Data"] = "data"; | ||
MessageType["Error"] = "error"; | ||
MessageType["Complete"] = "complete"; | ||
MessageType["Stop"] = "stop"; | ||
})(MessageType = exports.MessageType || (exports.MessageType = {})); | ||
/** Checks if the value has a shape of a `Message`. */ | ||
function isMessage(val) { | ||
if (typeof val !== 'object' || val == null) { | ||
return false; | ||
} | ||
// TODO-db-200603 validate the type | ||
if ('type' in val && Boolean(val.type)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
/** Checks if the value has a shape of a `GraphQLResponse`. */ | ||
function isGraphQLResponse(val) { | ||
if (typeof val !== 'object' || val == null) { | ||
return false; | ||
} | ||
if ( | ||
// GraphQLResponseWithData | ||
'data' in val || | ||
// GraphQLResponseWithoutData | ||
'errors' in val || | ||
// GraphQLError | ||
('message' in val && Object.keys(val).length === 1)) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
exports.createClient = void 0; | ||
const protocol_1 = require("./protocol"); | ||
const message_1 = require("./message"); | ||
const utils_1 = require("./utils"); | ||
/** Creates a disposable GQL subscriptions client. */ | ||
function createClient({ url, connectionParams }) { | ||
const ws = new websocket_as_promised_1.default(url, { | ||
timeout: 2 * 1000, | ||
createWebSocket: (url) => new WebSocket(url, 'graphql-ws'), | ||
packMessage: (data) => JSON.stringify(data), | ||
unpackMessage: (data) => { | ||
if (typeof data !== 'string') { | ||
throw new Error(`Unsupported message data type ${typeof data}`); | ||
} | ||
return JSON.parse(data); | ||
}, | ||
// omits when receiving a no request id symbol to avoid confusion and reduce message size | ||
attachRequestId: (data, requestId) => { | ||
if (isNoRequestId(requestId)) { | ||
return data; | ||
} | ||
return Object.assign(Object.assign({}, data), { id: String(requestId) }); | ||
}, | ||
// injecting no request id symbol allows us to request/response on id-less messages | ||
extractRequestId: (data) => { var _a; return (_a = data === null || data === void 0 ? void 0 : data.id) !== null && _a !== void 0 ? _a : NO_REQUEST_ID; }, | ||
}); | ||
// connects on demand, already open connections are ignored | ||
let isConnected = false, isConnecting = false, isDisconnecting = false; | ||
async function waitForConnected() { | ||
let waitedTimes = 0; | ||
while (!isConnected) { | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
// 100ms * 100 = 10s | ||
if (waitedTimes >= 100) { | ||
throw new Error('Waited 10 seconds but socket never connected.'); | ||
} | ||
waitedTimes++; | ||
} | ||
function createClient(options) { | ||
const { url, connectionParams } = options; | ||
// holds all currently subscribed sinks, will use this map | ||
// to dispatch messages to the correct destination | ||
const subscribedSinks = {}; | ||
function errorAllSinks(err) { | ||
Object.entries(subscribedSinks).forEach(([, sink]) => sink.error(err)); | ||
} | ||
async function waitForDisconnected() { | ||
let waitedTimes = 0; | ||
while (isConnected) { | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
// 100ms * 100 = 10s | ||
if (waitedTimes >= 100) { | ||
throw new Error('Waited 10 seconds but socket never disconnected.'); | ||
} | ||
waitedTimes++; | ||
} | ||
function completeAllSinks() { | ||
Object.entries(subscribedSinks).forEach(([, sink]) => sink.complete()); | ||
} | ||
// Lazily uses the socket singleton to establishes a connection described by the protocol. | ||
let socket = null, connected = false, connecting = false; | ||
async function connect() { | ||
if (isConnected) | ||
if (connected) { | ||
return; | ||
if (isConnecting) { | ||
return waitForConnected(); | ||
} | ||
if (isDisconnecting) { | ||
await waitForDisconnected(); | ||
if (connecting) { | ||
let waitedTimes = 0; | ||
while (!connected) { | ||
await new Promise((resolve) => setTimeout(resolve, 100)); | ||
// 100ms * 50 = 5sec | ||
if (waitedTimes >= 50) { | ||
throw new Error('Waited 10 seconds but socket never connected'); | ||
} | ||
waitedTimes++; | ||
} | ||
// connected === true | ||
return; | ||
} | ||
// open and initialize a connection, send the start message and flag as connected | ||
isConnected = false; | ||
isConnecting = true; | ||
await ws.open(); | ||
const ack = await request(MessageType.ConnectionInit, connectionParams && typeof connectionParams === 'function' | ||
? connectionParams() | ||
: connectionParams, NO_REQUEST_ID); | ||
if (ack.type !== MessageType.ConnectionAck) { | ||
await ws.close(); | ||
throw new Error('Connection not acknowledged'); | ||
} | ||
isConnecting = false; | ||
isConnected = true; | ||
connected = false; | ||
connecting = true; | ||
return new Promise((resolve, reject) => { | ||
let done = false; // used to avoid resolving/rejecting the promise multiple times | ||
socket = new WebSocket(url, protocol_1.GRAPHQL_TRANSPORT_WS_PROTOCOL); | ||
/** | ||
* `onerror` handler is unnecessary because even if an error occurs, the `onclose` handler will be called | ||
* | ||
* From: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications | ||
* > If an error occurs while attempting to connect, first a simple event with the name error is sent to the | ||
* > WebSocket object (thereby invoking its onerror handler), and then the CloseEvent is sent to the WebSocket | ||
* > object (thereby invoking its onclose handler) to indicate the reason for the connection's closing. | ||
*/ | ||
socket.onclose = ({ code, reason }) => { | ||
const err = new Error(`Socket closed with event ${code}` + !reason ? '' : `: ${reason}`); | ||
if (code === 1000 || code === 1001) { | ||
// close event `1000: Normal Closure` is ok and so is `1001: Going Away` (maybe the server is restarting) | ||
completeAllSinks(); | ||
} | ||
else { | ||
// all other close events are considered erroneous | ||
errorAllSinks(err); | ||
} | ||
if (!done) { | ||
done = true; | ||
connecting = false; | ||
connected = false; // the connection is lost | ||
socket = null; | ||
reject(err); // we reject here bacause the close is not supposed to be called during the connect phase | ||
} | ||
}; | ||
socket.onopen = () => { | ||
try { | ||
if (!socket) { | ||
throw new Error('Opened a socket on nothing'); | ||
} | ||
socket.send(message_1.stringifyMessage({ | ||
type: message_1.MessageType.ConnectionInit, | ||
payload: typeof connectionParams === 'function' | ||
? connectionParams() | ||
: connectionParams, | ||
})); | ||
} | ||
catch (err) { | ||
errorAllSinks(err); | ||
if (!done) { | ||
done = true; | ||
connecting = false; | ||
if (socket) { | ||
socket.close(); | ||
socket = null; | ||
} | ||
reject(err); | ||
} | ||
} | ||
}; | ||
function handleMessage({ data }) { | ||
try { | ||
if (!socket) { | ||
throw new Error('Received a message on nothing'); | ||
} | ||
const message = message_1.parseMessage(data); | ||
if (message.type !== message_1.MessageType.ConnectionAck) { | ||
throw new Error(`First message cannot be of type ${message.type}`); | ||
} | ||
// message.type === MessageType.ConnectionAck | ||
if (!done) { | ||
done = true; | ||
connecting = false; | ||
connected = true; // only now is the connection ready | ||
resolve(); | ||
} | ||
} | ||
catch (err) { | ||
errorAllSinks(err); | ||
if (!done) { | ||
done = true; | ||
connecting = false; | ||
if (socket) { | ||
socket.close(); | ||
socket = null; | ||
} | ||
reject(err); | ||
} | ||
} | ||
finally { | ||
if (socket) { | ||
// this listener is not necessary anymore | ||
socket.removeEventListener('message', handleMessage); | ||
} | ||
} | ||
} | ||
socket.addEventListener('message', handleMessage); | ||
}); | ||
} | ||
// disconnects on demand, already closed connections are ignored | ||
async function disconnect() { | ||
isDisconnecting = true; | ||
if (isConnected) { | ||
// sends a terminate message, then closes the websocket | ||
send(MessageType.ConnectionTerminate); | ||
} | ||
await ws.close(); | ||
isDisconnecting = false; | ||
isConnected = false; | ||
} | ||
// holds all currently subscribed sinks, will use this map | ||
// to dispatch messages to the correct destination and | ||
// as a decision system on when to unsubscribe | ||
const requestIdSink = {}; | ||
function messageForSinkWithRequestId(requestId, message) { | ||
let hasCompleted = false; | ||
Object.entries(requestIdSink).some(([sinkRequestId, sink]) => { | ||
if (requestId === sinkRequestId) { | ||
return { | ||
subscribe: (payload, sink) => { | ||
const uuid = generateUUID(); | ||
if (subscribedSinks[uuid]) { | ||
sink.error(new Error(`Sink with ID ${uuid} already registered`)); | ||
return utils_1.noop; | ||
} | ||
subscribedSinks[uuid] = sink; | ||
function handleMessage({ data }) { | ||
const message = message_1.parseMessage(data); | ||
switch (message.type) { | ||
case MessageType.Data: { | ||
const err = checkServerPayload(message.payload); | ||
if (err) { | ||
sink.error(err); | ||
hasCompleted = true; | ||
return true; | ||
case message_1.MessageType.Next: { | ||
if (message.id === uuid) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
sink.next(message.payload); | ||
} | ||
sink.next(message.payload); | ||
break; | ||
} | ||
case MessageType.Error: { | ||
const err = checkServerPayload(message.payload); | ||
if (err) { | ||
sink.error(err); | ||
case message_1.MessageType.Error: { | ||
if (message.id === uuid) { | ||
sink.error(message.payload); | ||
} | ||
else { | ||
sink.error(new Error('Unkown error received from the subscription server')); | ||
} | ||
hasCompleted = true; | ||
break; | ||
} | ||
case MessageType.Complete: { | ||
sink.complete(); | ||
hasCompleted = true; | ||
case message_1.MessageType.Complete: { | ||
if (message.id === uuid) { | ||
sink.complete(); | ||
} | ||
break; | ||
} | ||
} | ||
return true; | ||
} | ||
return false; | ||
}); | ||
// if the sink got completed, remove it from the subscribed sinks | ||
if (hasCompleted) { | ||
delete requestIdSink[requestId]; | ||
} | ||
// if there are no subscriptions left over, disconnect | ||
if (Object.keys(requestIdSink).length === 0) { | ||
// TODO-db-200603 report possible errors on disconnect | ||
disconnect(); | ||
} | ||
} | ||
function errorAllSinks(error) { | ||
Object.entries(requestIdSink).forEach(([, sink]) => sink.error(error)); | ||
} | ||
// listens exclusively to messages with matching request ids | ||
function responseListener(data) { | ||
if (!isMessage(data)) { | ||
return errorAllSinks(new Error('Received an invalid message from the subscription server')); | ||
} | ||
messageForSinkWithRequestId(data.id, data); | ||
} | ||
ws.onResponse.addListener(responseListener); | ||
function subscribe(payload, sink) { | ||
// generate a unique request id for this subscription | ||
const requestId = randomString(); | ||
if (requestIdSink[requestId]) { | ||
sink.error(new Error(`Sink already registered for ID: ${requestId}`)); | ||
return () => { }; | ||
} | ||
requestIdSink[requestId] = sink; | ||
connect() | ||
// start the subscription on a connection | ||
.then(() => send(MessageType.Start, payload, requestId)) | ||
// will also error this sink because its added to the map above | ||
.catch(errorAllSinks); | ||
return () => { | ||
connect() | ||
// stop the subscription, after the server acknowledges this the sink will complete | ||
.then(() => send(MessageType.Stop, undefined, requestId)) | ||
// will also error this sink because its added to the map above | ||
.catch(errorAllSinks); | ||
}; | ||
} | ||
function send(type, payload, requestId) { | ||
if (requestId) { | ||
return ws.sendPacked({ id: requestId, type, payload }); | ||
} | ||
return ws.sendPacked({ type, payload }); | ||
} | ||
async function request(type, payload, requestId) { | ||
return await ws.sendRequest({ type, payload }, { requestId }); | ||
} | ||
return { | ||
subscribe(payload, sink) { | ||
return subscribe(payload, sink); | ||
(async () => { | ||
try { | ||
await connect(); | ||
if (!socket) { | ||
throw new Error('Socket connected but empty'); | ||
} | ||
socket.addEventListener('message', handleMessage); | ||
socket.send(message_1.stringifyMessage({ | ||
id: uuid, | ||
type: message_1.MessageType.Subscribe, | ||
payload, | ||
})); | ||
} | ||
catch (err) { | ||
sink.error(err); | ||
} | ||
})(); | ||
return () => { | ||
if (socket) { | ||
socket.send(message_1.stringifyMessage({ | ||
id: uuid, | ||
type: message_1.MessageType.Complete, | ||
})); | ||
socket.removeEventListener('message', handleMessage); | ||
// equal to 1 because this sink is the last one. | ||
// the deletion from the map happens afterwards | ||
if (Object.entries(subscribedSinks).length === 1) { | ||
socket.close(1000, 'Normal Closure'); | ||
socket = null; | ||
} | ||
} | ||
sink.complete(); | ||
delete subscribedSinks[uuid]; | ||
}; | ||
}, | ||
async dispose() { | ||
dispose: async () => { | ||
// complete all sinks | ||
Object.entries(requestIdSink).forEach(([, sink]) => sink.complete()); | ||
// remove all subscriptions | ||
Object.keys(requestIdSink).forEach((key) => { | ||
delete requestIdSink[key]; | ||
// TODO-db-200817 complete or error? the sinks should be completed BEFORE the client gets disposed | ||
completeAllSinks(); | ||
// delete all sinks | ||
Object.keys(subscribedSinks).forEach((uuid) => { | ||
delete subscribedSinks[uuid]; | ||
}); | ||
// remove all listeners | ||
ws.removeAllListeners(); | ||
// do disconnect | ||
return disconnect(); | ||
// if there is an active socket, close it with a normal closure | ||
if (socket && socket.readyState === WebSocket.OPEN) { | ||
// TODO-db-200817 decide if `1001: Going Away` should be used instead | ||
socket.close(1000, 'Normal Closure'); | ||
socket = null; | ||
} | ||
}, | ||
@@ -253,26 +226,16 @@ }; | ||
exports.createClient = createClient; | ||
/** | ||
* Takes in the payload received from the server, parses and validates it, | ||
* checks for errors and returns a single error for all problematic cases. | ||
*/ | ||
function checkServerPayload(payload) { | ||
if (!payload) { | ||
return new Error('Received empty payload from the subscription server'); | ||
/** Generates a new v4 UUID. Reference: https://stackoverflow.com/a/2117523/709884 */ | ||
function generateUUID() { | ||
if (!window.crypto) { | ||
// fallback to Math.random when crypto is not available | ||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { | ||
const r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3) | 0x8; | ||
return v.toString(16); | ||
}); | ||
} | ||
if (!isGraphQLResponse(payload)) { | ||
return new Error('Received invalid payload structure from the subscription server'); | ||
} | ||
if ('errors' in payload && payload.errors) { | ||
return new Error(payload.errors.map(({ message }) => message).join(', ')); | ||
} | ||
if (Object.keys(payload).length === 1 && | ||
'message' in payload && | ||
payload.message) { | ||
return new Error(payload.message); | ||
} | ||
return null; | ||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (s) => { | ||
const c = Number.parseInt(s, 10); | ||
return (c ^ | ||
(window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16); | ||
}); | ||
} | ||
/** randomString does exactly what the name says. */ | ||
function randomString() { | ||
return Math.random().toString(36).substr(2, 6); | ||
} |
{ | ||
"name": "@enisdenjo/graphql-transport-ws", | ||
"version": "0.0.2", | ||
"version": "1.0.0", | ||
"description": "A WebSocket client for GraphQL subscriptions", | ||
@@ -17,14 +17,24 @@ "license": "MIT", | ||
"README.md", | ||
"LICENSE.md" | ||
"LICENSE.md", | ||
"PROTOCOL.md" | ||
], | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"scripts": { | ||
"gendocs": "typedoc --options typedoc.js src/", | ||
"lint": "eslint 'src'", | ||
"type-check": "tsc --noEmit", | ||
"test": "jest", | ||
"build": "tsc -b" | ||
"test": "jest -i", | ||
"build": "tsc -b", | ||
"release": "semantic-release" | ||
}, | ||
"peerDependencies": { | ||
"graphql": ">=15.0.0" | ||
}, | ||
"dependencies": { | ||
"websocket-as-promised": "^1.0.1" | ||
"ws": "^7.3.1" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.11.0", | ||
"@babel/core": "^7.11.1", | ||
"@babel/plugin-proposal-class-properties": "^7.10.4", | ||
@@ -36,8 +46,21 @@ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", | ||
"@babel/preset-typescript": "^7.10.4", | ||
"@types/jest": "^26.0.8", | ||
"babel-jest": "^26.2.2", | ||
"jest": "^26.2.2", | ||
"@semantic-release/changelog": "^5.0.1", | ||
"@semantic-release/git": "^9.0.0", | ||
"@types/jest": "^26.0.9", | ||
"@types/ws": "^7.2.6", | ||
"@typescript-eslint/eslint-plugin": "^3.9.0", | ||
"@typescript-eslint/parser": "^3.9.0", | ||
"babel-jest": "^26.3.0", | ||
"eslint": "^7.6.0", | ||
"eslint-config-prettier": "^6.11.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"graphql": "^15.3.0", | ||
"graphql-subscriptions": "^1.1.0", | ||
"jest": "^26.3.0", | ||
"prettier": "^2.0.5", | ||
"semantic-release": "^17.1.1", | ||
"typedoc": "^0.18.0", | ||
"typedoc-plugin-markdown": "^2.4.1", | ||
"typescript": "^3.9.7" | ||
} | ||
} |
# graphql-transport-ws | ||
**Work in progress!** | ||
> A coherent, zero-dependency, lazy, simple and easy to use server and client implementation of the [GraphQL over WebSocket Protocol](PROTODCOL.md). | ||
A client for GraphQL subscriptions over WebSocket. _Server implementation coming soon!_ | ||
## Getting started | ||
@@ -17,4 +15,6 @@ | ||
### Relay | ||
### Examples | ||
#### Client usage with [Relay](https://relay.dev) | ||
```ts | ||
@@ -63,9 +63,56 @@ import { createClient } from '@enisdenjo/graphql-transport-ws'; | ||
#### Client usage with [Apollo](https://www.apollographql.com) | ||
```typescript | ||
import { print } from 'graphql'; | ||
import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client'; | ||
import { createClient, Config, Client } from '@enisdenjo/graphql-transport-ws'; | ||
class WebSocketLink extends ApolloLink { | ||
private client: Client; | ||
constructor(config: Config) { | ||
super(); | ||
this.client = createClient(config); | ||
} | ||
public request({ | ||
operationName, | ||
query, | ||
variables, | ||
}: Operation): Observable<FetchResult> { | ||
return new Observable((sink) => { | ||
return this.client.subscribe<FetchResult>( | ||
{ operationName, query: print(query), variables }, | ||
sink, | ||
); | ||
}); | ||
} | ||
} | ||
const link = new WebSocketLink({ | ||
url: 'wss://some.url/graphql', | ||
connectionParams: () => { | ||
const session = getSession(); | ||
if (!session) { | ||
return null; | ||
} | ||
return { | ||
Authorization: `Bearer ${session.token}`, | ||
}; | ||
}, | ||
}); | ||
``` | ||
## Documentation | ||
[TypeDoc](https://typedoc.org) generated documentation is located in the [docs folder](docs/). | ||
## Protocol | ||
Read more about it in the [PROTOCOL.md](PROTOCOL.md) | ||
Read about the exact transport protocol used by the library in the [PROTOCOL.md](PROTOCOL.md) document. | ||
## Want to help? | ||
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our | ||
guidelines for [contributing](CONTRIBUTING.md). | ||
File a bug, contribute with code, or improve documentation? Welcome 👋! | ||
Read up on our guidelines for [contributing](CONTRIBUTING.md). |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
56855
17
920
0
117
2
25
1
+ Addedws@^7.3.1
+ Addedgraphql@16.10.0(transitive)
+ Addedws@7.5.10(transitive)
- Removedwebsocket-as-promised@^1.0.1
- Removedarray-buffer-byte-length@1.0.2(transitive)
- Removedarraybuffer.prototype.slice@1.0.4(transitive)
- Removedasync-function@1.0.0(transitive)
- Removedavailable-typed-arrays@1.0.7(transitive)
- Removedcall-bind@1.0.8(transitive)
- Removedcall-bind-apply-helpers@1.0.1(transitive)
- Removedcall-bound@1.0.3(transitive)
- Removedchnl@1.2.0(transitive)
- Removeddata-view-buffer@1.0.2(transitive)
- Removeddata-view-byte-length@1.0.2(transitive)
- Removeddata-view-byte-offset@1.0.1(transitive)
- Removeddefine-data-property@1.1.4(transitive)
- Removeddefine-properties@1.2.1(transitive)
- Removeddunder-proto@1.0.1(transitive)
- Removedes-abstract@1.23.9(transitive)
- Removedes-define-property@1.0.1(transitive)
- Removedes-errors@1.3.0(transitive)
- Removedes-object-atoms@1.1.1(transitive)
- Removedes-set-tostringtag@2.1.0(transitive)
- Removedes-to-primitive@1.3.0(transitive)
- Removedfor-each@0.3.4(transitive)
- Removedfunction-bind@1.1.2(transitive)
- Removedfunction.prototype.name@1.1.8(transitive)
- Removedfunctions-have-names@1.2.3(transitive)
- Removedget-intrinsic@1.2.7(transitive)
- Removedget-proto@1.0.1(transitive)
- Removedget-symbol-description@1.1.0(transitive)
- Removedglobalthis@1.0.4(transitive)
- Removedgopd@1.2.0(transitive)
- Removedhas-bigints@1.1.0(transitive)
- Removedhas-property-descriptors@1.0.2(transitive)
- Removedhas-proto@1.2.0(transitive)
- Removedhas-symbols@1.1.0(transitive)
- Removedhas-tostringtag@1.0.2(transitive)
- Removedhasown@2.0.2(transitive)
- Removedinternal-slot@1.1.0(transitive)
- Removedis-array-buffer@3.0.5(transitive)
- Removedis-async-function@2.1.1(transitive)
- Removedis-bigint@1.1.0(transitive)
- Removedis-boolean-object@1.2.1(transitive)
- Removedis-callable@1.2.7(transitive)
- Removedis-data-view@1.0.2(transitive)
- Removedis-date-object@1.1.0(transitive)
- Removedis-finalizationregistry@1.1.1(transitive)
- Removedis-generator-function@1.1.0(transitive)
- Removedis-map@2.0.3(transitive)
- Removedis-number-object@1.1.1(transitive)
- Removedis-regex@1.2.1(transitive)
- Removedis-set@2.0.3(transitive)
- Removedis-shared-array-buffer@1.0.4(transitive)
- Removedis-string@1.1.1(transitive)
- Removedis-symbol@1.1.1(transitive)
- Removedis-typed-array@1.1.15(transitive)
- Removedis-weakmap@2.0.2(transitive)
- Removedis-weakref@1.1.0(transitive)
- Removedis-weakset@2.0.4(transitive)
- Removedisarray@2.0.5(transitive)
- Removedmath-intrinsics@1.1.0(transitive)
- Removedobject-inspect@1.13.3(transitive)
- Removedobject-keys@1.1.1(transitive)
- Removedobject.assign@4.1.7(transitive)
- Removedown-keys@1.0.1(transitive)
- Removedpossible-typed-array-names@1.0.0(transitive)
- Removedpromise-controller@1.0.0(transitive)
- Removedpromise.prototype.finally@3.1.8(transitive)
- Removedreflect.getprototypeof@1.0.10(transitive)
- Removedregexp.prototype.flags@1.5.4(transitive)
- Removedsafe-array-concat@1.1.3(transitive)
- Removedsafe-push-apply@1.0.0(transitive)
- Removedsafe-regex-test@1.1.0(transitive)
- Removedset-function-length@1.2.2(transitive)
- Removedset-function-name@2.0.2(transitive)
- Removedset-proto@1.0.0(transitive)
- Removedside-channel@1.1.0(transitive)
- Removedside-channel-list@1.0.0(transitive)
- Removedside-channel-map@1.0.1(transitive)
- Removedside-channel-weakmap@1.0.2(transitive)
- Removedstring.prototype.trim@1.2.10(transitive)
- Removedstring.prototype.trimend@1.0.9(transitive)
- Removedstring.prototype.trimstart@1.0.8(transitive)
- Removedtyped-array-buffer@1.0.3(transitive)
- Removedtyped-array-byte-length@1.0.3(transitive)
- Removedtyped-array-byte-offset@1.0.4(transitive)
- Removedtyped-array-length@1.0.7(transitive)
- Removedunbox-primitive@1.1.0(transitive)
- Removedwebsocket-as-promised@1.1.0(transitive)
- Removedwhich-boxed-primitive@1.1.1(transitive)
- Removedwhich-builtin-type@1.2.1(transitive)
- Removedwhich-collection@1.0.2(transitive)
- Removedwhich-typed-array@1.1.18(transitive)