rmosolgo/graphql-ruby

Scoping _only_ by `subscription_scope`

jscheid opened this issue · 7 comments

Is your feature request related to a problem? Please describe.

We have several subscription endpoints, each with different names (obviously) and different argument types, but all returning the same payload type. Each subscription is scoped (using subscription_scope) to an ID unique per subscription (not only unique per subscription endpoint.)

We have helper code to generate events for these subscriptions, shared between the different subscription endpoints. Unfortunately, this helper code needs to be aware of the subscription field name and arguments in order to be able to trigger events - knowledge of the unique ID scope isn't sufficient when it could be, since the unique ID alone is enough to disambiguate between different concurrent subscriptions. This complicates our code needlessly.

Is there a mechanism to trigger events using only the scope, ignoring field name and arguments? If not, could one be added?

Describe the solution you'd like

One solution might be a configuration on the subscription along the lines of subscription_scope_is_unique, that would then ignore field and arguments for trigger matching. Another solution would be a way to override the field name and argument hash per subscription endpoint, which would let us set both to common (dummy) values.

Describe alternatives you've considered

Nothing comes to mind, sorry if I'm missing something obvious.

Additional context

N/A

Hey, great question. I think this should be possible by implementing def self.topic_for in your subscription classes (or in the inheritance chain somewhere). That method is called with a bunch of runtime info and returns the string which is used for "routing" updates.

Here's the source for that method:

# This is called during initial subscription to get a "name" for this subscription.
# Later, when `.trigger` is called, this will be called again to build another "name".
# Any subscribers with matching topic will begin the update flow.
#
# The default implementation creates a string using the field name, subscription scope, and argument keys and values.
# In that implementation, only `.trigger` calls with _exact matches_ result in updates to subscribers.
#
# To implement a filtered stream-type subscription flow, override this method to return a string with field name and subscription scope.
# Then, implement {#update} to compare its arguments to the current `object` and return {NO_UPDATE} when an
# update should be filtered out.
#
# @see {#update} for how to skip updates when an event comes with a matching topic.
# @param arguments [Hash<String => Object>] The arguments for this topic, in GraphQL-style (camelized strings)
# @param field [GraphQL::Schema::Field]
# @param scope [Object, nil] A value corresponding to `.trigger(... scope:)` (for updates) or the `subscription_scope` found in `context` (for initial subscriptions).
# @return [String] An identifier corresponding to a stream of updates
def self.topic_for(arguments:, field:, scope:)
Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments])
end
end

I think if you implemented it to only return scope.to_s, you'd have the desired setup. Want to give that a try?

Hey Robert, yes that's definitely an improvement because we no longer have to match the arguments - thank you for reminding me of this feature. Unfortunately it doesn't work for the field name, if I change that to some bogus value (in the trigger) I'm getting:

GraphQL::Subscriptions::InvalidTriggerError: No subscription matching trigger: foo (looked for Subscription.foo)

Any other ideas?

Hmm, too bad -- it's definitely supposed to support this case! Could you share the full backtrace of that error?

It gets raised here - makes sense in a way because there's no such field on the subscriptions object.

field = dummy_query.types.field(@schema.subscription, normalized_event_name) # rubocop:disable Development/ContextIsPassedCop
if field.nil?
raise InvalidTriggerError, "No subscription matching trigger: #{event_name} (looked for #{@schema.subscription.graphql_name}.#{normalized_event_name})"
end

(We're calling this directly from our app code so the rest of the backtrace isn't relevant.)

If I set it to some existing (unrelated) subscription field's name, no error is raised but I'm also not seeing any subscription updates anymore.

Thanks for those details. How about calling .trigger with one of the field names that uses the shared scope: value? That way, GraphQL-Ruby can find an example Subscription class to call .topic_for on. I added a test to GraphQL-Ruby for what I think you're trying to do here: 879096d and that approach worked for me. Let me know what you find!

Derp - right, without knowing the Subscription class it couldn't know how to encode the topic. I've done that, by sifting through Subscriptions fields to find one whose class includes our shared module, and seems to work fine! I'll close this for now, thanks for your help!