-
Notifications
You must be signed in to change notification settings - Fork 341
WIP: Graphql Transport WS Protocol #108
WIP: Graphql Transport WS Protocol #108
Conversation
…s both on client and server side using websockets as transport layer to be used on a new package called graphql-transport-ws. fix(NA): fixed path of new files.
@mistic: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/ |
@mistic great job! I love your PR and it helps us move a great step forward. The direction is exactly as discussed on the design doc and you can move forward. Here are some comments from reviewing the current state:
Great job! |
regarding backwards compatibility, we should expose the new names by the old names with deprecation warning. |
@DxCx I've changed the names both on server and on the client back for the old ones as we discussed. @Urigo Can u review my updates to see if they matches your expectations? This is an example of how to do hybrid mode (https + ws) or full ws mode with that implementation: import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
import ApolloClient, {createNetworkInterface} from 'apollo-client';
// Create WebSocket client
const wsClientOptions = {
reconnect: true,
connectionParams: {
// Pass any arguments you want for initialization
}
};
const wsNetworkInterface = new SubscriptionClient('ws://localhost:3000/graphql', wsClientOptions);
// Create regular NetworkInterface by using apollo-client's API:
const httpNetworkInterface = createNetworkInterface({
uri: '/graphql' // Your GraphQL endpoint
});
// Create Hybrid interface
const hybridInterface = addGraphQLSubbscriptions(httpNetworkInterface, wsNetworkInterface)
// Finally, create your ApolloClient instance with the modified network interface
// Full websocket: wsNetworkInterface
// Mixed Mode: hybridInterface
const apolloClient = new ApolloClient({
networkInterface: hybridInterface
}); PS: I pointed both Githunt-API and Githunt-react for this code and all the old behaviour with subscriptions over ws and queries and mutations over http is working :D At the moment I'm bot able to make a test with GitHunt with full ws transport because it requires some more work. |
10dafaf
to
0fedbd3
Compare
src/client.ts
Outdated
@@ -159,11 +161,11 @@ export class GraphQLTransportWSClient { | |||
} | |||
|
|||
if ( | |||
!isString(query) || | |||
( !isString(query) && !getOperationAST(query, operationName)) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
query instanceof DocumentNode
is more cleaner.. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It cannot be done that way as we talked :(
src/helpers.ts
Outdated
}, | ||
unsubscribe(id: number): void { | ||
wsClient.unsubscribe(id); | ||
}, | ||
}); | ||
} | ||
|
||
export function addGraphQLWS(networkInterface: any, wsClientOptions: ClientOptions): any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, i might be confused, but why do we need that?
src/helpers.ts
Outdated
}, | ||
unsubscribe(id: number): void { | ||
wsClient.unsubscribe(id); | ||
}, | ||
}); | ||
} | ||
|
||
export function addGraphQLWS(networkInterface: any, wsClientOptions: ClientOptions): any { | ||
if (typeof networkInterface._uri !== 'string') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are checking if network interface has implementation related uri.. :\
24202de
to
4952d35
Compare
feat(NA): applied changes according pull request review.. feat(NA): added static method for instance creation. refact(NA): change the class names back to subscriptionserver and subscriptionclient. feat(NA): applied changes according pull request review. feat(NA): added static method for instance creation. fix(NA): fixed error thrown when we try to print and query its already a string. refact(NA): change the class names back to subscriptionserver and subscriptionclient. feat(NA): added new helper function to fully extend network interface with ws transport. fix(NA): built legacy messages on server side and other bad object accesses. feat(NA): enclose wsclient creation inside addGraphQLWS function. fix(NA): removed useless addGraphQLWS function. feat(NA): added legacy support to the old server protocol.
6ae8112
to
7b19459
Compare
src/server.ts
Outdated
}); | ||
private sendError(connectionContext: ConnectionContext, reqId: string, errorPayload: any, | ||
overrideDefaultErrorType?: string): void { | ||
if ([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BUG: overrideDefaultErrorType might be undefined.
src/client.ts
Outdated
const requestPayloadErrors = parsedMessage.payload.errors ? | ||
this.formatErrors(parsedMessage.payload.errors) : null; | ||
|
||
this.requests[reqId].handler(requestPayloadErrors, requestPayloadData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bug: Should be this.requests[reqId].handler(null, parsedMessage.payload);
src/client.ts
Outdated
} | ||
}, this.subscriptionTimeout); | ||
return subId; | ||
public subscribe(options: RequestOptions, handler: (error: Error[], result?: any) => void) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here we need to add a wrapper handler for converting payload to:
const requestPayloadData = parsedMessage.payload.data || null;
const requestPayloadErrors = parsedMessage.payload.errors ?
this.formatErrors(parsedMessage.payload.errors) : null;
to keep backward comptiable with subscription manager
feat(NA): added subscription function request. fix(NA): fixed unsubscribe function. feat(NA): added constructor verification for executor and schema. chore(NA): added correct comment inside on message function. feat(NA): added initial filtering for requests. feat(NA): added initial filtering for requests. fix(NA): changed way to store unsubscribe function. fix(NA): removed contextValue from serverOptions. fix(NA): correct warn comment when using legacy subscription manager, chore(NA): Implement Adapter pattern for executor refact(NA): refactor on ExecuteAdapters. feat(NA): added code to support formatError and formatResponse. fix(NA): fix way we invoke format functions. fix(NA): small changes on error creation and executor/subscriptionManager check on loadExecutor. fix(NA): fix bad usage of base params on execute. feat(NA): updated ws dependency to the last version. fix(NA): overrideDefaultErrorType might be undefined. fix(NA): support for legacy handler. fix(NA): fixed const and payload errors.
ff666f7
to
062d1d9
Compare
@Urigo I'll already merge and solve the conflicts between this branch and last changes on master :D I already finished the GitHunt example integration with the full ws transport. You can check it here: https://github.com/mistic/GitHunt-API/tree/full-transport-ws However in order to be able to make a PR to GitHunt I need to complete some minor details (like a toggle between hybrid and full ws transport on GitHunt). Let's discuss this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Urigo asked me to review 🙂
Great job so far @mistic! I had a lot of comments, but they are almost all minor (but still important to me, haha).
The biggest thing is probably that since this will go into a different package, and not subscriptions-transport-ws, I don't think we should keep any backwards compatibility. The backwards compatibility can be kept entirely in subscriptions-transport-ws, which will be a deprecated layer on graphql-transport-ws.
Once we merge this PR, we should keep a branch separate from master for subscriptions-transport-ws and rename this project to graphql-transport-ws (to keep the stars, links etc.).
I only had time to review the client, but I'll try to review the server before the weekend as well.
src/client.ts
Outdated
import isString = require('lodash.isstring'); | ||
import isObject = require('lodash.isobject'); | ||
import { ExecutionResult, print, getOperationAST } from 'graphql'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think importing like this will create a huge bundle. We should check the bundle size with a script like we do for react-apollo and apollo-client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/client.ts
Outdated
if (!query) { | ||
throw new Error('Must provide `query` to subscribe.'); | ||
} | ||
public query(options: RequestOptions): Promise<ExecutionResult> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this function purely for convenience? What if someone submits a live query to it? Or if the document is a subscription? I think the transport should be absolutely minimal and just have an "executeRequest" on it. Everything else can be handled by the caller, which may be a network interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well @helfer at the moment we will be compatible with current apollo,
however, it is built in a way that when you will go forward to use 1 method that returns an Observable, the changes here are minimal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think if @Urigo prefers it this way, it's fine 🙂 We can rewrite it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So lets keep this one as is
src/client.ts
Outdated
throw new Error('Incorrect option types to subscribe. `subscription` must be a string,' + | ||
'`operationName` must be a string, and `variables` must be an object.'); | ||
} | ||
public subscribe(options: RequestOptions, handler: (error: Error[], result?: any) => void) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here as for query
. I don't think we need legacy compatibility if we're building a completely new transport. We can provide that legacy compat by making the next update of the old packages wrap the new transport. Or am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to change it right now, it's ok to keep it for backcompat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's keep things related with backwards compatibility for now.
@@ -165,95 +149,151 @@ export class SubscriptionClient { | |||
} | |||
|
|||
public unsubscribe(id: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're supporting all kinds of operations, including live queries, then all the operations can be stopped. In that case unsubscribe isn't the right word, so we should use stop
or something (for the new transport, not in this package, of course).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to change it right now, it's ok to keep it for backcompat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's keep things related with backwards compatibility for now.
src/client.ts
Outdated
if (this.requests[id]) { | ||
delete this.requests[id]; | ||
} | ||
this.sendMessage(id, MessageTypes.GQL_END, undefined); | ||
} | ||
|
||
public unsubscribeAll() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe stopAll
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to change it right now, it's ok to keep it for backcompat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's keep things related with backwards compatibility for now.
src/client.ts
Outdated
|
||
// Complete every request that are not registered on the client but are being sent by the server and are not | ||
// in the allowed list | ||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit hard to read. I think if the code was easier to understand, the comment wouldn't be needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/helpers.ts
Outdated
export function addGraphQLSubscriptions(networkInterface: any, wsClient: SubscriptionClient): any { | ||
console.warn('This method becomes deprecated in the new package graphql-transport-ws. Start using the ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make sure this warning is not printed if NODE_ENV is production.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
src/message-types.ts
Outdated
public static GQL_DATA = 'data'; | ||
public static GQL_ERROR = 'error'; | ||
public static GQL_COMPLETE = 'complete'; | ||
public static GQL_END = 'end'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference between COMPLETE
and END
? If end is to stop an operation, we should call it STOP
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
COMPLETE => Means no data from server
END => Means client doesn't want anymore data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let's call that STOP. end is more commonly used as a noun. Stop is almost always a verb.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
src/message-types.ts
Outdated
public static GQL_ERROR = 'error'; | ||
public static GQL_COMPLETE = 'complete'; | ||
public static GQL_END = 'end'; | ||
// NOTE: Those message types are deprecated and will be removed soon. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the new package we shouldn't have these at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's exactly what this comment means.
but let's finish merging the compatible version first before moving forward
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's keep things related with backwards compatibility for now.
|
||
// Quick way to add the subscribe and unsubscribe functions to the network interface | ||
// We will move this into a new package in the future |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the new package we shouldn't have this, but we can keep it in the old one and just print the deprecation warning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed let's keep things related with backwards compatibility for now.
src/server.ts
Outdated
if ([ | ||
MessageTypes.GQL_CONNECTION_ERROR, | ||
MessageTypes.GQL_ERROR, | ||
].indexOf(overrideDefaultErrorType) === -1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you forgot to use sanitizedOverrideDefaultErrorType
in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah thank you :D
@mistic @DxCx @Urigo |
@dotansimha Have u tested with last ws version? Ins't the problem related with ws library itself? |
Guys ( @DxCx @helfer @NeoPhi @Urigo @dotansimha or anyone else interested in give some help ) will be really appreciated some help with those 2 tasks:
|
…ng on old tests to keep working on new code. fix(NA): error handling between client and server. refact(NA): applied some changes purposed on pull request. refact(NA): change executeRequest to executeOperation on client. fix(NA): apply console warn only outside production env. fix(NA): import one by one to reduce bundler size. fix(NA): return original errors format. test(NA): refact some tests to work with new version. refact(NA): removed console logs from tests. test(NA): update old tests. test(NA): update old tests. refact(NA): changed requests to operations. fix(NA): add return to discard invalid messages. test(NA): fixed bad timeout on test. test(NA): commented non sense unit test. test(NA): adapt legacy test to new server code logic with unsubscribe. test(NA): fixed some legacy tests. fix(errors): error handling between client & server chore(NA): added todo about validation error in the server.
…s-protocol-review
2c48656
to
1be31a1
Compare
@mistic wow, great work! I've been really busy myself, so I couldn't help or even review until now. If Uri can't review, I hope I can do it over the weekend. |
Do I understand correctly that this PR makes it possible to run the GraphQL and the subscriptions server off of the same server and thus the same port? (which is a requirement for deploying on now.sh) I've tried taking the code sample above and using the branch this PR lives in, but I can't seem to make it work. Are there any docs yet so I can try this out and give some feedback? |
407969e
to
23cc224
Compare
@mxstbr You'll be able to understand how to use this PR looking for those already changed GitHunt example to support this PR: https://github.com/mistic/GitHunt-API/tree/full-transport-ws |
Guys ( @DxCx @helfer @NeoPhi @Urigo @dotansimha ) I think we're reaching the end of the work on this PR. Today I merged into this PR the Uri changes to correctly support subscribe method from Work needed to be done to merge this PR:
|
docs(NA): changelog update. docs(NA): changelog update. docs(NA): changelog update. feat(NA): add subscribe from graphql. fix(NA): check for connection state inside sendMessage. fix(NA): merge executeFromSubscribe with executeFromExecute. feat(NA): changelog update. fix(NA): clear immediate action on unsubscribe. fix(NA): removed typo from condition. fix(NA): added esnext to tslint config.
e5db8a9
to
a854e33
Compare
@mistic amazing amazing work. During the next couple of days:
|
@mistic , thank you again for your great PR. This changes a bit our goals so I want to reflect on that for a sec, to create an updated path with slight adjustments. V0.7 goals:
Changes by @mistic :
On top of those changes, I’ve added the support for the new graphql.js subscriptions implementation:
The problems I have with the current state are those:
So the changes I'm proposing to do before releasing 0.7 are:
V0.8 goals:
V0.9 goals:
@DxCx @mistic @helfer @stubailo @NeoPhi I want to hear your opinions on that plan |
@Urigo the work on Websocket is done for ~0.5year now, we used observables back then and that's the reason it has observables, AsyncIterator was not in the picture until last month. (more then that, supporting Observables in GraphiQL was already in progress, which made sense why using it). if you want to change executeReactive signature to return AsyncIterator, I'm totally fine with that because graphql internally started to use, but we shouldn't remove executeReactive, if you have a better proposed solution, let me know. so, unless you know that plans for how facebook is going to support reactive directives such as about the versioning, generally i agree about the content but i think that V0.8 Should be V1.0 or something like that because it will have a lot of breaking changes. |
I agree with you about versioning. let's do a |
alright, @Urigo so as i suggested we can move implementation to async iterator instead of observable, and that will be the smallest change possible, i don't understand why do we need to completely remove also, it will let people with legacy subscription server upgrade to run mutation + queries with one or two lines of change, not needing to rebuild their whole schemas. one last thing is that if you can discuess lee about making executeReactive (or any other name you will choose) that accepts executes parameters and return AsyncIterator we can also make sure that the name will less likely to change.
|
@Urigo I agree with @DxCx in almost everything! 1 - the v0.8 should be the v1.0.0. It will have a lot of breaking changes and it's almost a completely rewrite. In my opinion is a major version. 2 - Regarding to the SubscriptionsServer, what's the problem with the executor pattern? In my opinion this isn't a deal breaker with anything coming from graphql-js. We just accept an executor in our Server that can be composed by 3 - If you prefer we can move observables to AsyncIterator. |
@mistic @DxCx @Urigo This means that Hagai can implement The transport can check if the return value is |
@dotansimha that works as well! |
@DxCx And change it to: export type ExecuteFunction = (
schema: GraphQLSchema,
document: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues?: {[key: string]: any},
operationName?: string,
) => Promise<ExecutionResult> | AsyncIterator<ExecutionResult>; This way, What do you think? |
@dotansimha i totally understood your meaning, as i said it could work. |
@DxCx For example, Hagai's |
@dotansimha as i mentioned above, i don't mind terminating the observable. |
I meant to expose |
@dotansimha i think there is a confusion in here, this is what i'm trying to achieve:
Do you agree? |
@DxCx Yeah that's sound great. Also, I think that since we can remove I think that the future version of What do you think? |
the idea behaind it was to allow people people that are using |
* chore(package): update graphql to version 0.7.0 https://greenkeeper.io/ * fix failing tests
This PR is still a work in progress (with the remain tasks needed to be done described below), that has been done close to @DxCx supervision, but it has already implemented the majority of needed changes to the new package graphql-trasnport-ws according our point 2 on the shared design document ( GraphQL WebSocket transport plan ). So in this PR we have implemented the new protocol in order to support queries, mutations and subscriptions both on server and on the client side (all the code tasks from point 2 of the document are done).
Please note this is an intermediate pull request to allow you guys start reviewing the most important changes. However a final pull request will be opened from a branch called graphql-transport-ws containing the current code of this branch (graphql-transport-ws-protocol) plus the changes you request in this review, unit tests and those following changes:
PS: @DxCx thank you so much for your help, support and continuous reviews during the all process!
TODO: