-
Notifications
You must be signed in to change notification settings - Fork 343
onSubscribe and use middleware to modify runtime SubscriptionOptions #78
Conversation
This is related to #75 -- while it doesn't create a default context, it instead allows you to override the It's a very basic solution to the broad challenge I'm facing -- having some macro controls over subscription creation with |
I feel this would be better handled in the glue layer between the |
so something like? import { SubscriptionClient } from './client';
import { print } from 'graphql-tag/printer';
// Quick way to add the subscribe and unsubscribe functions to the network interface
export function addGraphQLSubscriptions(networkInterface: any, wsClient: SubscriptionClient): any {
return Object.assign(networkInterface, {
subscribe(request: any, handler: any): number {
if(networkInterface.onSubscribe) {
request = networkInterface.onSubscribe(request);
}
return wsClient.subscribe({
query: print(request.query),
variables: request.variables,
context: request.context,
}, handler);
},
unsubscribe(id: number): void {
wsClient.unsubscribe(id);
},
});
} At the very least I think the subscribe function should be able to pass context. Just to be clear about the use case I'm running into, which I don't think is that uncommon: Even if we exposed context in |
@srtucker22 looks really good and I agree that this is a common use case that would be beneficial to support. If we choose to support it then it will be easier then @NeoPhi solution overriding the subscribe method. Maybe a better way of supporting it would be to add another client life-cycle hook called That means that the hooks can return only the part of the In the client there is already lifecycle hook event emitter we can use for that, here is an example - https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/client.ts#L147-L157 Would love to hear both of your thoughts.. |
also, maybe the best thing will be to make it as similar as possible to that existing API - http://dev.apollodata.com/core/network.html#networkInterfaceMiddleware |
Hey @Urigo, good to hear from you! I'm all for SubscriptionClient.use([{
applyMiddleware(options, next) {
// modify options for SubscriptionClient.subscribe here
next();
}
}]); My only concern with adding middlware this way (as it's written in |
ok so this latest push will use either i can forsee use cases where someone would like to run an async function to modify options before subscribing, and this doesn't address that. but anything with async middleware before subscribe will essentially demand breaking changes whereas these changes don't. happy to modify more if we want to go down the breaking changes path :) |
Thank you for your recent changes. Thanks @srtucker22 |
so i guess if you do it this way (latest commit) the changes aren't breaking per se -- before this change you already had to catch errors (1) when subscribing and (2) in the handler, but now some of those errors (like having a query) will also be caught in the handler where they weren't before, like if your middleware accidentally deleted the query. however, this code has an issue --> if you call apologies for being touch and go. i've been away in Bali these past few weeks and will be traveling for another couple months or so. just want to give you a heads up i might not respond for a few days in between coms |
ok, this version tracks unsubs and waits to sendMessage for unsubscribe if it comes before sendMessage for init for a given sub. doesn't feel elegant, but gets the job done |
a little cleaner now. lemme know what you think in tokyo now so better internet sitch |
src/client.ts
Outdated
const { query, variables, operationName, context } = options; | ||
public subscribe(opts: SubscriptionOptions, handler: (error: Error[], result?: any) => void) { | ||
// this.eventEmitter.emit('subscribe', options); | ||
this.checkSubscriptionParams(opts, handler); |
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 would just rely on the this. applyMiddlewares
handling instead of checking it twice. This also avoids the case that sometimes the handler is called with the error and not.
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 if we change this, it will break the following tests as they currently stand:
- should throw an exception when query is not provided
- should throw an exception when query is not valid
because those errors currently throw outside the handler
i'm all for deferring all error handling to the handler (aside from throwing if there isn't a handler), but was trying to make this feature without making breaking changes. but i agree it's cleaner -- so if you confirm this is the route to go, i'm happy to make the change
src/client.ts
Outdated
this.applyMiddlewares(opts).then(options => { | ||
try { | ||
this.checkSubscriptionParams(options, handler); | ||
} catch (e) { |
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.
Feels like this should just be a catch
on the promise to catch any issues.
src/client.ts
Outdated
this.checkSubscriptionParams(options, handler); | ||
} catch (e) { | ||
handler([e]); | ||
this.unsubscribe(subId); |
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.
We should unsubscribe before calling the handler.
src/client.ts
Outdated
setTimeout( () => { | ||
if (this.waitingSubscriptions[subId]) { | ||
handler([new Error('Subscription timed out - no response from server')]); | ||
this.unsubscribe(subId); |
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.
We should unsubscribe before calling the handler.
src/client.ts
Outdated
@@ -165,9 +172,14 @@ export class SubscriptionClient { | |||
} | |||
|
|||
public unsubscribe(id: number) { | |||
if (!this.subscriptions[id] && !this.waitingSubscriptions[id]) { | |||
this.waitingUnsubscribes[id] = true; |
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'm not sure what waitingUnsubscribes
is giving us. Each subscribe
call is going to generate a new id and I don't see this being used elsewhere to set a timeout to ensure that it was cleaned up.
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.
the issue waitingUnsubscribes
tries to resolve is if you unsubscribe before the middleware promise resolves. you're supposed to be able to call client.unsubscribe
synchronously (as it stands currently -- and i'd like to keep it this way), but if we remove waitingUnsubscribes
, the following would fail:
- should trigger onUnsubscribe when client unsubscribes
SUBSCRIPTION_END
gets sent before SUBSCRIPTION_START
and so we don't actually properly unsubscribe. we could check for an unsubscribe call before we send SUBSCRIPTION_START
which would prevent a needless START immediately followed by END, but I didn't want to break the existing tests (again was trying to keep this a non-breaking feature addition). let me know if this makes sense and which direction you'd like me to go forward with
@Urigo could you take another look at this? Thanks! 🙂 |
hi, I'm sorry for the delay but we have a huge important change here that should land soon and after that I will address the rest of the PRs. |
thanks for the heads up -- looks cool. also looks like i'm gonna have to rewrite my subscription tutorial :) |
src/client.ts
Outdated
public applyMiddlewares(options: SubscriptionOptions): Promise<SubscriptionOptions> { | ||
return new Promise((resolve, reject) => { | ||
const queue = (funcs: MiddlewareInterface[], scope: any) => { | ||
const next = () => { |
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.
Can be reject with next(error)
?
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.
Following lines should be change to:
const queue = (funcs: MiddlewareInterface[], scope: any) => {
const next = (error) => {
if (error) {
reject(error);
} else {
if (funcs.length > 0) {
const f = funcs.shift();
if (f) {
f.applyMiddleware.apply(scope, [options, next]);
}
} else {
resolve(options);
}
}
};
next();
};
And this line update to:
}).catch(e => handler([e]));
src/client.ts
Outdated
public applyMiddlewares(options: SubscriptionOptions): Promise<SubscriptionOptions> { | ||
return new Promise((resolve, reject) => { | ||
const queue = (funcs: MiddlewareInterface[], scope: any) => { | ||
const next = () => { |
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.
Following lines should be change to:
const queue = (funcs: MiddlewareInterface[], scope: any) => {
const next = (error) => {
if (error) {
reject(error);
} else {
if (funcs.length > 0) {
const f = funcs.shift();
if (f) {
f.applyMiddleware.apply(scope, [options, next]);
}
} else {
resolve(options);
}
}
};
next();
};
And this line update to:
}).catch(e => handler([e]));
Is this ready for merge, wait too long. :( |
SO, here is my proposal with the latest: Everything works, but I've changed up a few tests since we're in the spirit of major code overhaul:
Let me know what's good! |
@srtucker22 thank you so much for an amazing work and your patience keeping this PR updated. |
no worries! |
@@ -423,7 +424,7 @@ export class SubscriptionServer { | |||
query: parsedMessage.payload.query, | |||
variables: parsedMessage.payload.variables, | |||
operationName: parsedMessage.payload.operationName, | |||
context: Object.assign({}, isObject(initResult) ? initResult : {}), | |||
context: Object.assign({}, isObject(initResult) ? initResult : {}, parsedMessage.payload.context), |
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 still not working for me... I'm still getting "Insufficient Permissions" when I have permissions enabled... however, it works fine when permissions are disabled. I have even hard coded in the server auth token, and it still doesn't work. There seems to be some property naming inconsistency in this library:
So is the correct property name "Authorization" or "authToken"? Either way, I've tested both, and both fail. HERE ARE MY VERSIONS HERE IS MY ERROR ISSUES I'VE REVIEWED |
* log server events to logFunction * update changelog
TODO: