Unsubscribed callback method on Subscription
Hi,
I'd like to know if there's a way to have a callback method like def subscribe for subscriptions but for unsubscribed.
We have a lot of context information in each subscription that we need to use when the subscription is unsubscribed and we only have it in the subscription not when the channel has been unsubscribed.
Is it possible to have a workaround this right now or possibly add this functionality?
Thank you
Yes, it's definitely possible. There are currently delete_subscription(sub_id) hooks which are used for cleaning up whatever persistence backend the subscription system uses:
https://github.com/rmosolgo/graphql-ruby/blob/a242ffabf395369700b9a26c4b79f10a4234b4f4/lib/graphql/subscriptions/action_cable_subscriptions.rb#L215
We could leverage that same hook to call some kind of hook for the subscription. I'll take a look and follow up here when I have a PR ready.
@rmosolgo cool, I tried yesterday and failed. We are using graphql-anycable, this is the data I was able to get from it but I couldn't figured out how to get the right variables (not arguments) and scope to trigger something before unsubscribing. Maybe it's not even this way.
aaa = RocketReferralsSchema.subscriptions.instance_eval {s::SUBSCRIPTION_PREFIX}#{subscription_id}",
redis.smembers(GraphQL::Subscriptions::AnyCableSubscriptions::CHANNEL_PREFIX + channel_id).map do |subscription_id|
redis.mapped_hmget(
"#{GraphQL::Subscriptions::AnyCableSubscriptions::SUBSCRIPTION_PREFIX}#{subscription_id}",
:query_string, :variables, :context, :operation_name
)
end
}
=> [
[0] {
:query_string => "subscription ConversationTyping($conversationId: ID!) {\n conversationTyping(conversationId: $conversationId) {\n ...
=> [
[0] {
:query_string => "subscription Conversation...",
:variables => "{\"conversationId\":\"MBLNyshSXDUkAV0/zRnXB8o3wzipTrBeDsPn6Q5K1/gyag\"}",
:context => "{\"current_account_id\":53,\"current_user_id\":16896,\"__sym_keys__\":[\"current_account_id\",\"current_user_id\"]}",
:operation_name => "ConversationTyping"
}
]
trigger something
Just curious, do you mean that you want to send more data to the client from this point? (It could be tricky, but it might be possible...)
no, my bad, I was just trying to find a way to add an unsubscribed method to the subscription.
Hi @rmosolgo, did you have any more thoughts on implementing a callback within delete_subscription? I'd like to do some cleanup when someone closes a subscription.
Hey, sorry for the silence on this. I did look into it a while ago, but I found that there's no easy way to work it into the existing implementation. Instead, it really makes most sense to add it on a per-implementation basis. (That is, ActionCableSubscriptions, PusherSubscriptions, etc, should each call .delete_subscription in their own code.) For example, these could be extended to read subscription data, call delete_subscription, then clean up their storage:
https://github.com/rmosolgo/graphql-ruby/blob/a242ffabf395369700b9a26c4b79f10a4234b4f4/lib/graphql/subscriptions/action_cable_subscriptions.rb#L214-L216
And probably here, if I had to guess: https://github.com/anycable/graphql-anycable/blob/495e5505830fdf1ef5566a270ab9ee1759be21ab/lib/graphql/subscriptions/anycable_subscriptions.rb#L190
(For the record I also received an email requesting a feature like this for Ably subscriptions.)
It's really important for us to find a way. We need to clean up data after the subscription is unsubscribed. We are relying in background jobs, pings, and workarounds just because we don't have access to an unsubscribe method / callback. It's really a blocker for a perfect UX and use of good practice on the ruby side. We use this in conjunction with anycable/graphql-anycable
We're running into this issue as well with actioncable and would appreciate some help with this.
We have a similar delete_subscription(sid) call
def unsubscribed
@subscription_ids.each { |sid|
MySchema.subscriptions.delete_subscription(sid)
}
end
but I'm unclear how to get back to the instantiated GraphQL::Schema::Subscription class that subscribe() gets called on when subscribing. read_subscription(sid) seems to be a different object and I'm not seeing a reference to the other class anywhere from there.
Hey y'all!
Had a similar issue when setting up subscriptions in an application using the gem over ActionCable. Ended up solving it with an in-house implementation to register and execute the callbacks
In my case, the solution involves changing the GraphQL channel (from ActionCable) to include the following:
def receive(data)
# ...
# Note we need to send the `channel` key here
Schema.execute(query, variables:, context: { channel: self })
# ...
end
# Accessor for the registered callbacks
def on_unsubscribe
@on_unsubscribe ||= []
end
# Setter for the registered callbacks
def on_unsubscribe=(callback)
on_unsubscribe << callback
end
def unsubscribed
# Delete all of the consumer's subscriptions from the GraphQL Schema
@subscription_ids.each do |sid|
Schema.subscriptions.delete_subscription(sid)
end
# GraphQL::Ruby does not support a callback per subscription, so we have to
# manually call the callback for each subscription the consumer was listening for,
# if they are defined
on_unsubscribe.each(&:call)
end
And then, on the actual subscription file, we can append a callback to be executed when the subscription is closed:
def subscribe(id:)
channel.on_unsubscribe = -> { unsubscribe(id:) }
end
def unsubscribe(id:)
# Add the unsubscribe logic
end
Is probably not the most efficient or even simple solution to maintain, but should do the trick for most cases. Hope yet helps!