File: subscription.rb

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (155 lines) | stat: -rw-r--r-- 5,913 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# frozen_string_literal: true

module GraphQL
  class Schema
    # This class can be extended to create fields on your subscription root.
    #
    # It provides hooks for the different parts of the subscription lifecycle:
    #
    # - `#authorized?`: called before initial subscription and subsequent updates
    # - `#subscribe`: called for the initial subscription
    # - `#update`: called for subsequent update
    #
    # Also, `#unsubscribe` terminates the subscription.
    class Subscription < GraphQL::Schema::Resolver
      extend GraphQL::Schema::Resolver::HasPayloadType
      extend GraphQL::Schema::Member::HasFields
      NO_UPDATE = :no_update
      # The generated payload type is required; If there's no payload,
      # propagate null.
      null false

      def initialize(object:, context:, field:)
        super
        # Figure out whether this is an update or an initial subscription
        @mode = context.query.subscription_update? ? :update : :subscribe
      end

      def resolve_with_support(**args)
        result = nil
        unsubscribed = true
        unsubscribed_result = catch :graphql_subscription_unsubscribed do
          result = super
          unsubscribed = false
        end


        if unsubscribed
          if unsubscribed_result
            context.namespace(:subscriptions)[:final_update] = true
            unsubscribed_result
          else
            context.skip
          end
        else
          result
        end
      end

      # Implement the {Resolve} API
      def resolve(**args)
        # Dispatch based on `@mode`, which will raise a `NoMethodError` if we ever
        # have an unexpected `@mode`
        public_send("resolve_#{@mode}", **args)
      end

      # Wrap the user-defined `#subscribe` hook
      def resolve_subscribe(**args)
        ret_val = args.any? ? subscribe(**args) : subscribe
        if ret_val == :no_response
          context.skip
        else
          ret_val
        end
      end

      # The default implementation returns nothing on subscribe.
      # Override it to return an object or
      # `:no_response` to (explicitly) return nothing.
      def subscribe(args = {})
        :no_response
      end

      # Wrap the user-provided `#update` hook
      def resolve_update(**args)
        ret_val = args.any? ? update(**args) : update
        if ret_val == NO_UPDATE
          context.namespace(:subscriptions)[:no_update] = true
          context.skip
        else
          ret_val
        end
      end

      # The default implementation returns the root object.
      # Override it to return {NO_UPDATE} if you want to
      # skip updates sometimes. Or override it to return a different object.
      def update(args = {})
        object
      end

      # If an argument is flagged with `loads:` and no object is found for it,
      # remove this subscription (assuming that the object was deleted in the meantime,
      # or that it became inaccessible).
      def load_application_object_failed(err)
        if @mode == :update
          unsubscribe
        end
        super
      end

      # Call this to halt execution and remove this subscription from the system
      # @param update_value [Object] if present, deliver this update before unsubscribing
      # @return [void]
      def unsubscribe(update_value = nil)
        context.namespace(:subscriptions)[:unsubscribed] = true
        throw :graphql_subscription_unsubscribed, update_value
      end

      READING_SCOPE = ::Object.new
      # Call this method to provide a new subscription_scope; OR
      # call it without an argument to get the subscription_scope
      # @param new_scope [Symbol]
      # @param optional [Boolean] If true, then don't require `scope:` to be provided to updates to this subscription.
      # @return [Symbol]
      def self.subscription_scope(new_scope = READING_SCOPE, optional: false)
        if new_scope != READING_SCOPE
          @subscription_scope = new_scope
          @subscription_scope_optional = optional
        elsif defined?(@subscription_scope)
          @subscription_scope
        else
          find_inherited_value(:subscription_scope)
        end
      end

      def self.subscription_scope_optional?
        if defined?(@subscription_scope_optional)
          @subscription_scope_optional
        else
          find_inherited_value(:subscription_scope_optional, false)
        end
      end

      # 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
  end
end