enisdenjo / graphql-ws Goto Github PK
View Code? Open in Web Editor NEWCoherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client.
Home Page: https://the-guild.dev/graphql/ws
License: MIT License
Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client.
Home Page: https://the-guild.dev/graphql/ws
License: MIT License
Closes #16
Sometimes you want to use the client in a non-browser environment or with an outdated browser. In order to support both worlds, the client supports passing in a custom WebSocket implementation during the setup.
per https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md#streaming-operation, it looks like ws client can unsubscribe from server, so the stream can be stopped.
also ubscriptions-transport-ws has a dedicated interface to do so by sending messageType.STOP to server: https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/message-types.ts#L14
Screenshot
First websocket and its related subscriptions
Next valid connection with its related subscriptions:
As you can see, the second screenshot has much less subscriptions sent to the server.
Expected Behaviour
No subscriptions are lost while reconnecting to a server
Actual Behaviour
It managed to reconnect to the server properly but didn't send the whole set of subscriptions
Debug Information
This seems to happen only when the websockets aren't able to reconnect right away to a server.
In my case: I restarted my server locally and because it's using typescript and transpiling on the fly with babel, it takes a little while to restart properly. So to reproduce I just launched my client, my server and then restarted my server and waited.
I am joining a har
file below that has content as well, you can open this in any Chromium browser (sadly Firefox doesn't handle HAR with content yet) by dragging the file into your dev tools network
panel.
It should load the whole story and you would be able to see my network at the moment of the bug happening.
websockets-operations-after-reconnection.har.zip
Further Information
As with the previous issue, please let me know if you need more information and/or help 🙏
Thank you again for your great work on this new library!
Thanks for making this, it looks awesome
I think it would make it easier and more appealing to have some github repo where you can clone and run to play with this implementation
we could have a monorepo with this matrix of implementations:
like #85 i have same behavior
i see this happend when websocket stay in CONNECTING state more than 2750ms
Connect function when websocket state is CONNECTING call itself
This cycle is limited to 10 times to prevent many recursive calls and at every call wait for 50*callDepth ms
So if websocket state is CONNECTING for more than 2750ms connect thow this error
Subscription promise catch this error so this mean that subscription message never send when websocket state switch to CONNECTED
I also note that if sync object is wrapped with observable library like rxjs error throw here will call unsubscribe that definetly stop subscription life
import { Observable } from 'rxjs'
import { Client, SubscribePayload } from 'graphql-ws'
function subscribe(client: Client, gql: SubscribePayload) {
return new Observable(subscriber => {
const unsubscribe = client.subscribe(gql, {
next: subscriber.next.bind(subscriber),
error: subscriber.error.bind(subscriber),
complete: subscriber.complete.bind(subscriber),
})
return () => unsubscribe()
})
}
As a server developer I want to pass some connection context to client (for example - current session registration step).
So that client can get it without additional requests.
ConnectionAck
message (return object in onConnect instead of true?).In subscription-transport-ws it's mentioned in docs, but not implemented in code.
Currently, if the socket gets closed for whatever reason, all sinks will immediately complete or error out (depending on the close event).
A much better UX is to silently retry connecting for X amount of times and complete/error out the sink only if the allowed retry count has been exceeded.
Haven't properly evaluated this library yet, but was wondering if you have taken a look at https://github.com/uNetworking/uWebSockets.js ?
I just checked the test cases and noted that the server sends information to the client that the schema is unavailable.
I have concerns that this information should not be leaked to clients in a non-development environment. The client should rather get an unexpected error occurred type of message.
Hello,
I'm trying to figure it out how to make this work with Relay. According to Relay docs a subscribe function has to return an observable.
When I write this function:
const subscribe = (request, variables) => {
const subscribeObservable = subscribeClient.subscribe({
query: request.text,
operationName: request.name,
variables,
})
// Important: Convert subscriptions-transport-ws observable type to Relay's
return Observable.from(subscribeObservable);
}
It just doesn't works, because RelayNetwork returns error: No data returned for operation.
On other hand, when I use subscription-transport-ws it just works.
Screenshot
(node:48769) UnhandledPromiseRejectionWarning: RangeError: The message must not be greater than 123 bytes
at Sender.close (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/node_modules/ws/lib/sender.js:120:15)
at WebSocket.close (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/node_modules/ws/lib/websocket.js:233:18)
at WebSocket.onMessage (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/cjs/server.js:326:28)
at processTicksAndRejections (internal/process/task_queues.js:89:5)
Expected Behaviour
If an error is thrown, I expect some message about what I did wrong
Actual Behaviour
In this case, graphql-ws tries to send my client the error message from the server, but there is a limit of 123 bytes for the message of a socket close payload. So my client gets this RangeError instead.
Debug Information
This error gets thrown right here:
Line 710 in ec1b461
Further Information
I guess just truncate the error message? Should probably log the full error plus stacktrace on the server side?
I ran into this error because I returned a rather large object inside my onConnect()
handler. This object was not JSON serializable because it contained a cycle. The error warning about the cycle was longer than 123 bytes. Should I create a separate issue for how to handle passing in non-serializable payloads?
Expected Behaviour
flawless loop query from this most excellent gql websocket library.
Actual Behaviour
Using v4.1.2 iteration 8 of a loop query cause
(node:20284) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [WebSocket]. Use emitter.setMaxListeners() to increase limit
in node v12.19
Debug Information
It seems change https://github.com/enisdenjo/graphql-ws/blame/c4a3c2326192e6a01db08fba49dc5abe6b119c55/src/client.ts#L372 removed the socket.removeEventListener('close',..)
previously there, which (maybe?) cause MaxListenersExceededWarning() to emit after using the Client usage with Promise
Recipe 8+ times. The MaxListenersExceededWarning
is not emitted in graphql-ws v4.1.1.
Further Information
I wrote a simple loop query using the "Client usage with Promise" recipe doing a
for (const x of Array(15).keys()) {
const UserLoop = await execute(
{
operationName: "user",
query: '{ user { id } }',
variables: {},
});
console.log("UserLoop",x,"id is", UserLoop?.data?.user?.id)
}
Using graphql-ws v4.1.1 everything runs smoothly.
Using graphql-ws v4.1.2 MaxListenersExceededWarning: is emitted after 8th' loop.
Perhaps I am doing something wrong?
Thanks @enisdenjo for this awesome package!
A question, does this work with express-graphql
or replace it?
Before I had something like
import { graphqlHTTP } from 'express-graphql';
const graphQLMiddleware = graphqlHTTP({
context: {
dataLoaders: {
user: userDataLoader,
},
},
schema,
});
However, when I write code like this, it does not supports context
:
import { createServer } from 'graphql-transport-ws';
createServer(
{
context: { // <- not support here
dataLoaders: {
user: userDataLoader,
},
},
schema,
},
{
server,
path: '/graphql',
}
);
Currently the connectionParams option must be either a Record<string, unknown> or a synchronous function that returns Record<string, unknown>.
/**
* Optional parameters, passed through the `payload` field with the `ConnectionInit` message,
* that the client specifies when establishing a connection with the server. You can use this
* for securely passing arguments for authentication.
*/
connectionParams?: Record<string, unknown> | (() => Record<string, unknown>);
This is an issue for applications that need to asynchronously retrieve the data for the ConnectionInit message. Like for instance retrieving a security token required for the WS authentication. This is particularly a problem in case lazy
option is set to false and the client tries to open the WS immediately.
This is enhancement request to allow connectionParams to be either a Promise or async function, so the application can asynchronously retrieve the information required for the ConnectionInit
message.
When a websocket connection is broken (for example, when server restarts), what is the recommended way for the client to reestablish the websocket connection to the server?
I'm using apollo server, and I get the following error on the client side, whenever the server breaks the ws connection:
Object {
"error": Event {
"code": 1001,
"isTrusted": false,
"reason": "Stream end encountered",
},
}
In the client websocket link, I can see the error:
class WebSocketLink extends ApolloLink {
private client: Client;
constructor(options: ClientOptions) {
super();
this.client = createClient(options);
}
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
next: sink.next.bind(sink),
complete: sink.complete.bind(sink),
error: (err) => {
switch (err?.code) {
case 1001:
// error caught here, but how to reconnect?
break;
}
sink.error(err);
},
}
);
});
}
}
Currently, I workaround the problem by re-initializing the Apollo Client instance, but I would like to know if there is an official API to restart the socket connection?
Any chance on a browser specific bundle being added to the build process so that it's easy to use with services like unpkg?
I want to be able to only send an id
instead of a full GraphQL document to the server and then read the actual GraphQL document from a hash table (maybe even asynchronous).
Edit: #36 showed that this is already possible. It just needs to be documented.
In my Socket.io GraphQL layer I choose to omit passing a context, schema, execute, subscribe etc parameters on a server factory level, but instead choose that a getParameter
function must be passed to the server factory. That function is invoked for each GraphQL operation executed with the socket and the sent payload. The user can then decide to map the provided GraphQL Payload to anything else and providing a different context/schema/execute/subscribe based on that.
It has the following type signature
export type GetParameterFunctionParameter = {
socket: SocketIO.Socket;
graphQLPayload: {
source: string;
variableValues: { [key: string]: any } | null;
operationName: string | null;
};
};
export type GetParameterFunction = (
parameter: GetParameterFunctionParameter
) => PromiseOrPlain<{
graphQLExecutionParameter: {
schema: GraphQLSchema;
contextValue?: unknown;
rootValue?: unknown;
// These will overwrite the initials if provided; Useful for persisted queries etc.
operationName?: string;
source?: string;
variableValues?: { [key: string]: any } | null;
};
execute?: typeof execute;
subscribe?: typeof subscribe;
}>;
Which then allows doing stuff like this:
import socketIO from "socket.io";
const persistedOperations = {
"1": "query { ping }"
"2": "mutation { ping }"
}
const socketServer = socketIO();
const graphqlServer = registerSocketIOGraphQLServer({
socketServer,
getParameter: ({ socket, graphQLPayload }) => ({
/* The paramaters used for the operation execution. */
graphQLExecutionParameter: {
schema,
rootValue:,
contextValue: {
socket,
},
// client source is just the id instead of a full document.
// we map the id to the actual document.
source: persistedOperations[graphQLPayload.source]
},
}),
});
(This design could also address #36 as the getParameter function could throw an error or return a rejected Promise etc).
graphql-transport-ws
might get some inspiration from this.
Expected Behaviour
Recipe Client usage with Observable
working.
Actual Behaviour
at subscription.unsubscribe()
I get (node:14584) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'isStopped' of undefined
.
Using graphql-ws v4.1.2, rxjs 6.6.3 and node 12.19
Debug Information
Try out Recipe Client usage with Observable
Further Information
Modifying the examples toObservable(..)
to
function toObservable(operation) {
return new Observable((observer) => client.subscribe(operation, {
next: observer.next.bind(observer),
complete: observer.complete.bind(observer),
error: (err) => observer.error(err)
}
));
}
solves the problem and makes a working rxjs Observable for subscription.
Hi!
First of all thanks a lot for taking over this and moving GraphQL over web sockets forward, I believe it's quite important to get some consistency here 🎉 !
I was playing around with this a bit and built an alternate server implementation of the protocol, with the goal of supporting @apollo/client out of the box, with no changes on the client side. I found that there's only a few changes that would allow the latest @apollo/client:
graphql-transport-ws
and graphql-ws
sub-protocols (the latter is used with @apollo/client)type: "start"
in addition to type: "subscribe"
for Subscribe
messagestype: "stop"
in addition to type: "complete"
for client-to-server Complete
messagestype: "data"
instead of type: "next"
for Next
messages if socket.protocol === "graphql-ws"
Subscribe
messages before receiving ConnectionAck
. This was the "hardest" one, as the server needs to defer handling the received Subscribe
messages until it actually acknowledges the connection. I resolved this through introducing a promise for the acknowledgment, which the Subscribe
handlers await before proceeding.I believe some degree of legacy protocol handling could drive adoption a lot. If we had a server-side GraphQL-over-WS implementation that caters to all popular clients right now, that would be really cool (and graphql-ws could be just that!).
This does not necessarily have to be reflected in the spec and may just go to the implementation. Still, I could imagine adding something like:
Subscribe
messages before a client received the ConnectionAck
message in order to support legacy clients.Subscribe
messages before sending the ConnectionAck
message.I did write an alternate implementation from scratch (and this was quite easy thanks to your work on the protocol) but I could easily provide a PR for the above if you're interested. Let know what you think :)
All the best
Morris
im following recipes code ( promise ) from https://www.npmjs.com/package/graphql-ws
Expected Behaviour
successful handshake with graphql subscription with wss:// protocol
Actual Behaviour
got error "Expected HTTP 101 response but was '400 Bad Request'" and "isTrusted:false"
Debug Information
already tested using graphiql and it works fine. Got error when implemented in react native
Further Information
just copy paste the example code. nothing change.
below is my actual code in react native. straight to err ( line-209)
sorry im a beginner in graphql subscription.
We have already talked about this once in graphql/graphql-over-http#140 (comment), but I have one confusion.
Let's consider a scenerio of a faulty client (which is usually the culprit for conflict id cases anyway) sending both a mutation and a subscription using the same id.
The client first requests a subscription with id="a". Client does not want to end this subscription yet.
Then the client then sends a mutation with id="a". This is obvious not a problem for the server. However, when the server returns the result of that mutation via type=next,id=a
, the client cannot tell if the incoming result is for Subscription with id="a" or Mutation with id="a".
Then the server proceeds to send type=Complete,id=a
. However, in this case, it also accidentally makes the client thinks that it is unsubscribed from the the initial subscription query.
I recently got a windows machine and the tests are failing when executed.
FAIL src/tests/server.ts
● Connect › should not close the socket after the `connectionInitWaitTimeout` has passed but the callback is still resolving
expect.assertions(2)
Expected two assertions to be called but received one assertion call.
268 |
269 | it('should not close the socket after the `connectionInitWaitTimeout` has passed but the callback is still resolving', async () => {
> 270 | expect.assertions(2);
| ^
271 |
272 | await makeServer({
273 | connectionInitWaitTimeout: 10,
at Object.<anonymous> (src/tests/server.ts:270:12)
● Connect › should close the socket if an additional `ConnectionInit` message is received while one is pending
expect.assertions(3)
Expected three assertions to be called but received four assertion calls.
298 |
299 | it('should close the socket if an additional `ConnectionInit` message is received while one is pending', async () => {
> 300 | expect.assertions(3);
| ^
301 |
302 | await makeServer({
303 | connectionInitWaitTimeout: 10,
at Object.<anonymous> (src/tests/server.ts:300:12)
FAIL src/tests/client.ts
● "concurrency" › should dispatch and receive messages even if one subscriber disposes while another one subscribes
expect(jest.fn()).toBeCalled()
Expected number of calls: >= 1
Received number of calls: 0
399 | expect(nextFnForHappy).not.toBeCalled();
400 | expect(completeFnForHappy).toBeCalled();
> 401 | expect(nextFnForBananas).toBeCalled();
| ^
402 | });
403 | });
404 |
at Object.<anonymous> (src/tests/client.ts:401:30)
● lazy › should connect immediately when mode is disabled
expect(received).toBe(expected) // Object.is equality
Expected: 1
Received: 2
411 | await wait(10);
412 |
> 413 | expect(server.webSocketServer.clients.size).toBe(1);
| ^
414 | server.webSocketServer.clients.forEach((client) => {
415 | expect(client.readyState).toBe(WebSocket.OPEN);
416 | });
at Object.<anonymous> (src/tests/client.ts:413:49)
Test Suites: 2 failed, 2 total
Tests: 4 failed, 1 todo, 37 passed, 42 total
Snapshots: 0 total
Time: 4.138 s
Ran all test suites.
We would like to send a bearer token with all of our websocket messages. I know we can add this underneath payload, but this seems more like metadata to me and might fit nice at the top level (next to type and payload).
What do people think of adding an optional authorization
key at the top level (or perhaps a way for users to add in custom top level keys?).
I'm thinking something like:
{
"type":"connection_init",
"authorization":"BEARER_TOKEN_HERE"
}
{
"id":"4a4ef089-b43c-447f-9746-0de97f4753c9",
"type":"subscribe",
"authorization":"BEARER_TOKEN_HERE",
"payload":{
"query":"subscription ...",
"variables":{
"..."
},
"operationName":"..."
}
}
As a client
I want to use exp back-off and randomisation on retries
So that I prevent the "thundering herd" problem and avoid DDOSing the server when it restarts
apollographql/subscriptions-transport-ws
The following issue collection is only up until December 2018
onConnect
server handler can be used as a form of gatekeeping if you use it to throw or returns false. This can be bypassed by simply not sending the INIT
message, which means the initPromise
will simply be true
. Then, the client can send a GQL_START
message, even if the onConnect
was designed to block it.
GQL_STOP
message is being sent from the client, after receiving the GQL_COMPLETE
message back from the server.GQL_STOP
can still be sent - causing the client to break.GQL_CONNECTION_ACK
from the server.GQL_START
is not completed before a GQL_STOP
call the client keeps receiving subscription events.GQL_CONNECTION_ERROR
is defined as a Server -> Client message. However, if an exception occurs while setting up the connection params or sending the GQL_CONNECTION_INIT
- this message is sent from Client -> Server.Hello,
we have been looking for an alternative protocol to subscriptions-transport-ws and found this repository which looks very promising and far more mature.
The only problem is the query type in the subscribe
message that seems to be javascript specific as the DocumentNode
is part of internal classes of graphql-js, and our server is written in Java. Do you plan to make the protocol more open for other languages that use only the string
query representation of GQL query in GQL library public API?
Use the "Ping and Pongs: The Heartbeat of WebSockets" as a keep-alive implementation.
Because of various client and network needs - allow the user to configure the keep-alive timeout on the server.
Expected Behaviour
Using the client in Node environments works.
Actual Behaviour
The client breaks when invoking the subscribe
method.
ReferenceError: window is not defined
at generateUUID (node_modules/graphql-transport-ws/lib/client.js:367:5)
at Object.subscribe (node_modules/graphql-transport-ws/lib/client.js:266:26)
Debug Information
Client depends on the browser specific window
global object to generate IDs.
Further Information
In order to support the Node environment, the client needs to find the Crypto implementation in the relevant global scope (or maybe even allow passing in a custom generateUUID
function?).
UMD file has missing exports that is declared in *d.ts file
When using module bundler for browser like webpack with web target that honor
Line 25 in 3f5c42e
For example using enum MessageType in application code, the bundle resulting buggy in runtime due to missing exported MessageType in graphql-ws umd
To fix just re-export in client.ts or change entry point for rollup from ./src/client.ts
to ./src/index.ts
As a server I want to track the amount of open websocket connections and update the user's online status when a user disconnects so that I can monitor and analyze the health of my server and update a user's online status immediately when the browser is closed.
import client from 'prom-client'
const webSocketTotalConnectionsCounter = new client.Counter({
name: 'websocket_connections_total',
help: 'The total number of Apollo WebSocket connections handled so far.',
})
const webSocketOpenConnectionsGauge = new client.Gauge({
name: 'websocket_connections_open',
help: 'The number of open Apollo WebSocket connections.',
})
useServer(
{
schema,
execute,
subscribe,
async onConnect(ctx) {
webSocketTotalConnectionsCounter.inc()
webSocketOpenConnectionsGauge.inc()
await setOnline(ctx)
},
async onDisconnect(ctx) {
webSocketOpenConnectionsGauge.dec()
await setOffline(ctx)
},
},
wsServer,
)
I'm want to be able to send user authorization token that can modify the GraphQL context
When using subscriptions-transport-ws
I could generate the GraphQL Context using the onConnect
helper
https://github.com/sibelius/relay-workshop/blob/master/packages/server/src/index.ts#L30
import { SubscriptionServer } from 'subscriptions-transport-ws';
SubscriptionServer.create(
{
onConnect: async (connectionParams: ConnectionParams) => {
const { user } = await getUser(connectionParams?.authorization);
return getContext({ user });
},
// eslint-disable-next-line
onDisconnect: () => console.log('Client subscription disconnected!'),
execute,
subscribe,
schema,
},
{
server,
path: '/subscriptions',
},
);
When using this package, we need to pass context
on server creating that we won't be able to modify context based on use authorization token
As a developer I want a message type so that I can inform clients of the reason for the denial of their connection request.
As a client I want to know why my ConnectionInit request was not accepted so that I can fix the issue (e.g. prompt user for auth data) rather than have the connection suddenly close.
Expected Behaviour
I expected the connection was created successful.
Actual Behaviour
On localhost, all browsers succeed.
In production,
The errors are like these in Firefox:
Firefox can’t establish a connection to the server at wss://www.hongbomiao.com/graphql. 2.2c591de4.chunk.js:2:134796
The connection to wss://www.hongbomiao.com/graphql was interrupted while the page was loading.
Firefox can’t establish a connection to the server at wss://www.hongbomiao.com/graphql.
or like this in Chrome:
WebSocket connection to 'wss://www.hongbomiao.com/graphql' failed: WebSocket is closed before the connection is established.
Debug Information
Help us debug the bug? Sure.
I added a ping pong subscription demo by hongbo-miao/hongbomiao.com#965
If you open the https://www.hongbomiao.com in Firefox, you will most likely to see the error.
Further Information
This might be helpful
https://stackoverflow.com/questions/14140414/websocket-was-interrupted-while-page-is-loading-on-firefox-for-socket-io
I'm using graphql-ws together with Apollo client.
I noticed that for queries and mutations the cancellerRef.current is being invoked twice. Once when the Complete message is received and again from the cleanup function.
As a result state.locks gets decremented twice for the same operation, which causes the socket to be closed even though there are pending operations.
You want to use an existing contextValue
and pass it as a server option during creation like this:
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
// some context which has to be passed in to all execution/subscription operations
const context = { ... };
createServer(
{
schema,
context, // passed in to the operations below
execute,
subscribe,
},
{ ... }
);
This is possible right now, but you have to inject it yourself. Like this:
import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';
// some context which has to be passed in to all execution/subscription operations
const contextValue = { ... };
createServer(
{
schema,
execute: (args) =>
execute({
...args,
contextValue, // inject
}),
subscribe: (args) =>
subscribe({
...args,
contextValue, // inject
}),
},
{ ... },
);
This is my client:
const client = createClient({
url: 'wss://localhost:5000/graphql',
});
client.subscribe(
{
query: 'subscription { greeting }',
},
{
next: (data) => {
console.log('data', data);
},
error: (error) => {
console.error('error', error);
},
complete: () => {
console.log('no more greetings');
},
}
);
I succeed by GraphQL Schema Definition Language way just like the demo in readme (I only changed greetings
to greeting
). Got 101 Switching Protocols
. And the client will print out the greeting
five times in the browser console.
Server (Succeed version)
const schema = buildSchema(`
type Subscription {
greeting: String
}
`);
// The roots provide resolvers for each GraphQL operation
const roots = {
subscription: {
greeting: async function* sayHiIn5Languages() {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greeting: hi };
}
},
},
};
createServer(
{
schema,
execute,
subscribe,
roots,
},
{
server,
path: '/graphql',
}
);
However, I failed on GraphQL.js GraphQLSchema object way. Got 101 Switching Protocols
. But the client never prints out the greeting
in the browser console.
Server (Failed version)
import { execute, subscribe, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
import { createServer } from 'graphql-transport-ws';
const GreetingGraphQLType = new GraphQLObjectType({
name: 'Greeting',
fields: {
greeting: { type: GraphQLString },
},
});
const subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
greeting: {
type: GreetingGraphQLType,
resolve: async function* sayHiIn5Languages() {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greeting: hi };
}
},
},
},
});
const schema = new GraphQLSchema({
subscription,
});
createServer(
{
schema,
execute,
subscribe,
},
{
server,
path: '/graphql',
}
);
Any idea? Thanks
I've tried running the "Client usage with Apollo" example from the README.md
under Node.
Expected Behaviour
Actual Behaviour
ReferenceError: WebSocket is not defined
at connect (.../node_modules/graphql-transport-ws/lib/client.js:126:24)
at .../node_modules/graphql-transport-ws/lib/client.js:220:67
at Object.createClient (.../node_modules/graphql-transport-ws/lib/client.js:244:11)
...
Further Information
subscriptions-transport-ws
offers an optional webSocketImpl
param on the client constructor (https://github.com/apollographql/subscriptions-transport-ws#constructorurl-options-websocketimpl) - any plans to do something similar?
Hi I'm having an issue getting subscriptions to function with Urql, I am using a class created using examples in docs
class WebSocketLink {
private client: Client;
constructor(config: ClientOptions) {
this.client = createClient(config);
}
public request(op: SubscriptionOperation): Observable<ExecutionResult> {
return new Observable((sink) => {
return this.client.subscribe<ExecutionResult>(
{ query: op.query, variables: op.variables as Record<string, unknown> | null | undefined },
{
...sink,
error: (err) => {
console.log('Error: ', err);
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(new Error(`Socket closed with even ${err.code}` + err.reason ? `: ${err.reason}` : ''));
} else {
sink.error(new Error(err.map(({ message }) => message).join(',')));
}
},
},
);
});
}
}
Below is what Urql passes to client.subscribe of which I extract query and variables and pass them on.
export interface SubscriptionOperation {
query: string;
variables?: object;
key: string;
context: OperationContext;
}
When calling a subscription via Urql I get an error with code: 1002 which appears to be a low level Websocket protocol violation from what I can find. Comparing what is sent to the server from subscriptions-transport-ws and graphql-transport-ws there doesn't appear to be a difference. I am authenticating connections via connectionParams.
I use Express/Postgraphile on the server which uses subscriptions-transport-ws, I am aware you have a pull request to replace subscriptions-transport-ws with your implementation, so unsure if this is a compatibility issue, an error on my part or something else.
Many thanks.
I'd like to add some tests in our GraphQL subscriptions server to make sure subscriptions are working well, my current problem is that subscribe
happens after our pubsub.publish
call.
example:
client.subscribe()
pubsub.publish() // this happens before `subscribe` completes
similar to the current test that uses wait(10)
https://github.com/enisdenjo/graphql-transport-ws/blob/f76ac73e9d21c80abe0118007e168e4f5d525036/src/tests/client.ts#L113
does it make sense to emit an event
when the subscribe
is "ready"
A stream payload published by an AsyncIterable returned by execute
(from [email protected]
) cannot be stringified
{
id: '7aef51de-707e-49e5-8cbf-df1bff572e5f',
type: 'next',
payload: { data: 'Friend', path: [ 'streamTest', 2 ], hasNext: true }
}
It fails silently with the following message, I had to dig into node_modules and add a bunch of console.logs before I had access to this message.
Error: Cannot stringify invalid message
at Object.stringifyMessage (/Users/laurin/projects/stream-defer-demo/node_modules/graphql-ws/cjs/message.js:87:15)
It would be nice if such an error would not be swallowed by default but instead forwarded to the console. The user could then overwrite the default behavior.
Aside from that, this message object (returned from execute) should be accepted as a valid message. I don't understand why exactly the message sent by the server must be validated - as the server constructs it should be fine? Also, the current validation seems to be rather inflexible and must be updated every time graphql supports a new execution result variation. The responsibility for returning a correct result should be within execute.
Reference code:
type Query {
greetings: [String]
}
query StreamTestQuery {
streamTest @stream(initialCount: 1)
}
const Query = new GraphQLObjectType({
name: "Query",
fields: {
streamTest: {
type: GraphQLList(GraphQLString),
resolve: async function* () {
for (const item of ["Hi", "My", "Friend"]) {
yield item;
await sleep(1000);
}
},
},
})
Hi! That's a pretty cool and simple implementation, I'm wondering if I could use just this lib as graphql client without any other wrapper (apollo client or relay).
I would like to perform queries, mutations and subscriptions using websockets.
Actual Behaviour
Hello,
I have the error :
WebSocket connection to 'the url' failed: Error during WebSocket handshake: 'Sec-WebSocket-Protocol' header value 'graphql-ws' in response does not match any of sent values
Debug Information
I work with Hasura on Heroku.
I just interface graphql-ws with Apollo with your code.
Further Information
Everything was working with ws client from Apollo.
But I had to change because of security issues that your kind work strive to resolve.
When a subscription comes in, I'd like to set the context
that the resolver receives.
This can be done using the context
option of createServer.
Right now, whatever is returned by this function is passed as the context to all resolvers.
I'd like the option to return a Promise, and have the returned value be awaited upon, so that all my resolvers don't need to await the context.
In order support the adoption of graphql-ws in express-graphql, per our discussion here: graphql/express-graphql#687, I would like to see if graphql-ws client can adopt interface to support the fetcher interface of GraphiQL as defined in here: https://github.com/apollographql/GraphiQL-Subscriptions-Fetcher/blob/master/src/fetcher.ts
As a server developer I want to reject connection with custom reason, based on connection params. e.g. user sends expected api version or app token, and I want to reject with "UpgradeNeeded" or "InvalidToken" reason.
onConnect
?.. Not sure about code, maybe also it worth return [number, string] for custom code).closed
callback).Currently, if the ClientOption lazy is true, the websocket is closed as soon as there are no pending operations.
This adds a lot of overhead in case you have an application that is often sending queries or mutations since the client needs to re-establish the socket for every query/mutation.
It would be great if we could add a new keepalive
parameter to ClientOptions that would control how long the socket should remain open after the last subscription has completed.
For instance:
export interface ClientOptions {
...
/**
* How long the client should wait, after the last query/mutation/subscription is completed, before closing the socket.
*
* @default 0 * 1000 (0 seconds)
*/
keepalive?: number;
...
}
In readme file, section "Client usage with Apollo".
The spread operator on sink
object does not work for me. The client throws an error that the next
function is undefined. Assigning the properties explicitly works (see the code snippet).
I wonder why is this happening, probably some prototype-related magic that is not clear to me.
public request(operation: Operation): Observable<FetchResult> {
return new Observable((sink) => {
return this.client.subscribe<FetchResult>(
{ ...operation, query: print(operation.query) },
{
// ...sink,
next: sink.next,
complete: sink.complete,
error: (err) => {
if (err instanceof Error) {
sink.error(err);
} else if (err instanceof CloseEvent) {
sink.error(
new Error(
`Socket closed with event ${err.code}` + err.reason
? `: ${err.reason}` // reason will be available on clean closes
: '',
),
);
} else {
// GraphQLError[]
sink.error(
new Error(err.map(({ message }) => message).join(', ')),
);
}
},
},
);
});
}
As a user I'm sometimes opening subscriptions to servers that may need to authenticate some form of credentials which could be dependant on other services
I want to be able to specify a longer timeout before the connection is closed while waiting for the ConnectionAck method
So that I don't fail to connect when authentication to the server takes longer.
Expected Behaviour
I should be able to create a subscription on the client (playground). I'm using Apollo Server.
Actual Behaviour
Graphql queries are working but subscription is not.
Debug Information
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import ws from 'ws';
const mockChatServer = new ApolloServer({
schema: mockSchema,
});
const app = express();
app.use(bodyParser.json());
mockChatServer.applyMiddleware({ app });
const server = createServer(app);
const wsServer = new ws.Server({
server,
path: '/graphql',
});
server.listen(3002, () => {
useServer(
{
execute,
subscribe,
roots: chatResolvers,
schema: mockSchema,
onConnect: ctx => {
console.log('onconnect');
return true;
},
context: {
name: 'cool',
},
onSubscribe: () => {
console.log('onsubscribe');
},
},
wsServer,
);
});
Further Information
None of the console logs about are being called. This is my first time trying to use graphql subscription so I might be making a silly mistake. The subscription endpoint (/graphql) is same as graphql query endpoint but that should not be a problem.
Thank you so much for writing this library. It'd be great if there was a working example.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.