Closed mattkrick closed 6 years ago
closing because there is no straightforward path to doing this.
the problem is we need to create and clear the cache from within a graphql resolve call.
we can create it by seeing if it exists, and then creating it if it doesn't.
however, if a mutation & a subscription call have payloads that share the same GraphQLObject, then we're not guaranteed we'll get everything we need in the source. a good example if the JoinIntegrationPayload. we want to fulfill teamMember
, and we can do that by supplying teamMemberId
to the source. otherwise, we'll have to look up the teammember based on the globalId, which means 1 more lookup. not the end of the world, since a dataloader could save us more than 1 load in the long run.
however, when to destroy is a tricky one.
we'd like to destroy after all the subscriptions have run.
so, we could set a counter == subs.length & then after each resolve gets called, we subsLen--.
however, that's difficult because it's all async, so a mutation might slip in the middle & then we'd clear the cache too soon.
we could tweak the pubsub to add a onLastSub
callback that runs after before the last sub gets called.
that callback would have to mutate the context saying something like channelName: 'lastCall'
. then, that resolve function would need to know the channel name it's working for, if any.
to do that, we'd have to pass in channelName
in the payload so it shows up in source of the resolve function.
then, if lastCall is true, we know we can clear it.
another issue: ideally, the mutation could share the cached value (or at least set it, since it'll be guaranteed to run first). however, there is no easy way to determine that subscriptions will come after the mutation. so, we can't just store it in the same place like we would a subscription, because if a sub never comes, then it'll just live there forever. the only decent way around this is to build a TTL into it.
We finally have some cases where this could be beneficial (get the teamMember name for every archived project). for epic 7 it'll be comically inefficient, which will motivate us to do it right for epic 8
well my brain hurts, but i figured out how to do this w/o any changes to graphql:
we could approach this 2 ways:
fetch the subsequent data in the mutation
completeFields...
from within the mutation. GraphQL doesn't give us everything we need (like the fieldDefs, resolveFn, etc) it'd require a PR to GraphQL that may not get approved.fetch the subsequent data in the subscription
I opted for the 2nd solution. Here's how it's implemented:
context.subscriptionDataLoaders =
. This mutates the object with a random key called mutationId
and a value of a new DataLoader.mutationId
to Redis so the subscription can look it up latersubscriberId
. That lives as long as the subscription is alivemutationId
. if it does, then we add a row to the dataloader so we can easily get the mutationId
from the subscriberId
(since mutationId
will not be present in the context)subscriberId
so we know whether this is a subscription or not. if it is, we use that subscriberId to get the mutationId, and with that mutaitonId, we get the dataloader for that mutation.subscriberId
.Considerations:
123
publishes a partial doc to a pub/sub. the subscriber with id 888
gets it & completes the doc with the 123
dataloader124
publishes a fresh doc and changes the reference for 888
so it points to 124
now before the message for 123
completes. 123
and then partially looked up on 124
.secretToken
that is a query that returns the secret token of the caller, (in this case the subscriber is using the cached value from the mutation). this would almost be impossible since subscribers get called first. i suppose a mutation could more easily request something secret from a subscriber, but again timing would be tough. we can completely avoid this if we separate dataloaders for folks who could grab sensitive info. solved, implemented in projets-to-relay
dataloader is usually used on a per-execution basis (ie 1 call to a graphql execute). that's pretty useless on subscriptions. instead, subscriptions should span across users on a per-channel basis.
so, the only way i think we can do this is by mutating the context passed into a mutation.
context.loader[channel] = context.loader[channel] || new Dataloader(...)
teamMemberId
, but the subscription needs theteamMember
object. getting it on the mutation side ruins the nice colocation. Also, something like addGitHubRepo has to fetch newRepo.users, which should also be colocated in the subscription, because there might come a time that the sub receives a call from a mutation & we don't need to listen to the users, so it'll be a wasted fetch.