Class: GraphQL::Schema::Subscription

Inherits:
Resolver
  • Object
show all
Extended by:
Member::HasFields, Resolver::HasPayloadType
Defined in:
lib/graphql/schema/subscription.rb

Overview

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.

Defined Under Namespace

Classes: EarlyUnsubscribe

Constant Summary collapse

NO_UPDATE =
:no_update

Constants included from Resolver::HasPayloadType

Resolver::HasPayloadType::NO_INTERFACES

Constants included from Member::HasFields

Member::HasFields::CONFLICT_FIELD_NAMES, Member::HasFields::GRAPHQL_RUBY_KEYWORDS, Member::HasFields::RUBY_KEYWORDS

Constants included from EmptyObjects

EmptyObjects::EMPTY_ARRAY, EmptyObjects::EMPTY_HASH

Constants included from Member::HasArguments

Member::HasArguments::NO_ARGUMENTS

Constants included from Member::GraphQLTypeNames

Member::GraphQLTypeNames::Boolean, Member::GraphQLTypeNames::ID, Member::GraphQLTypeNames::Int

Instance Attribute Summary

Attributes inherited from Resolver

#context, #exec_index, #exec_result, #field, #field_resolve_step, #object, #prepared_arguments, #raw_arguments

Attributes included from Member::BaseDSLMethods

#default_graphql_name, #graphql_name

Attributes included from Member::HasDeprecationReason

#deprecation_reason

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Resolver::HasPayloadType

field, field_class, object_class, payload_type, type

Methods included from Member::HasFields

add_field, all_field_definitions, field, field_class, global_id_field, has_no_fields, has_no_fields?, own_fields

Methods inherited from Resolver

all_field_argument_definitions, any_field_arguments?, argument, #arguments, #authorized?, authorizes?, broadcastable, broadcastable?, #call, complexity, default_page_size, extension, extensions, extras, field_arguments, get_field_argument, has_default_page_size?, has_max_page_size?, max_page_size, null, #ready?, resolve_method, resolver_method, type, type_expr, #unauthorized_object

Methods included from Member::BaseDSLMethods

#authorized?, #comment, #default_relay?, #description, #introspection, #introspection?, #mutation, #visible?

Methods included from Member::HasArguments

#add_argument, #all_argument_definitions, #any_arguments?, #argument, #argument_class, #arguments, #arguments_statically_coercible?, #coerce_arguments, #get_argument, #own_arguments, #remove_argument, #validate_directive_argument

Methods included from Member::HasAuthorization

#authorized?

Methods included from Member::HasValidators

#validates, #validators

Methods included from Member::HasPath

#path

Methods included from Member::HasDirectives

add_directive, #directive, #directives, get_directives, #inherited, #remove_directive, remove_directive

Methods included from Member::HasDataloader

#dataload, #dataload_all, #dataload_all_associations, #dataload_all_records, #dataload_association, #dataload_record, #dataloader

Constructor Details

#initialize(object:, context:, field:) ⇒ Subscription

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of Subscription.



21
22
23
24
25
26
27
28
29
30
31
# File 'lib/graphql/schema/subscription.rb', line 21

def initialize(object:, context:, field:)
  super
  # Figure out whether this is an update or an initial subscription
  @mode = context.query.subscription_update? ? :update : :subscribe
  @subscription_written = false
  @original_arguments = nil
  if (subs_ns = context.namespace(:subscriptions)) &&
    (sub_insts = subs_ns[:subscriptions])
    sub_insts[context.current_path] = self
  end
end

Class Method Details

.subscription_scope(new_scope = NOT_CONFIGURED, optional: false) ⇒ Symbol

Call this method to provide a new subscription_scope; OR call it without an argument to get the subscription_scope

Parameters:

  • new_scope (Symbol) (defaults to: NOT_CONFIGURED)
  • optional (Boolean) (defaults to: false)

    If true, then don't require scope: to be provided to updates to this subscription.

Returns:

  • (Symbol)


164
165
166
167
168
169
170
171
172
173
# File 'lib/graphql/schema/subscription.rb', line 164

def self.subscription_scope(new_scope = NOT_CONFIGURED, optional: false)
  if new_scope != NOT_CONFIGURED
    @subscription_scope = new_scope
    @subscription_scope_optional = optional
  elsif defined?(@subscription_scope)
    @subscription_scope
  else
    find_inherited_value(:subscription_scope)
  end
end

.subscription_scope_optional?Boolean

Returns:



175
176
177
178
179
180
181
# File 'lib/graphql/schema/subscription.rb', line 175

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

.topic_for(arguments:, field:, scope:) ⇒ String

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.

Parameters:

  • arguments (Hash<String => Object>)

    The arguments for this topic, in GraphQL-style (camelized strings)

  • field (GraphQL::Schema::Field)
  • scope (Object, nil)

    A value corresponding to .trigger(... scope:) (for updates) or the subscription_scope found in context (for initial subscriptions).

Returns:

  • (String)

    An identifier corresponding to a stream of updates

See Also:

  • for how to skip updates when an event comes with a matching topic.


199
200
201
# File 'lib/graphql/schema/subscription.rb', line 199

def self.topic_for(arguments:, field:, scope:)
  Subscriptions::Serialize.dump_recursive([scope, field.graphql_name, arguments])
end

Instance Method Details

#call_resolve(args_hash) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



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
# File 'lib/graphql/schema/subscription.rb', line 34

def call_resolve(args_hash)
  if @field_resolve_step.nil?
    super
  else
    context.namespace(:subscriptions)[:update_event] = event
    result = nil
    unsubscribed = true
    unsubscribed_result = nil
    begin
      result = super
      unsubscribed = false
    rescue EarlyUnsubscribe => err
      unsubscribed_result = err.unsubscribed_result
    end


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

#eventSubscriptions::Event

Returns This object is used as a representation of this subscription for the backend.

Returns:

  • (Subscriptions::Event)

    This object is used as a representation of this subscription for the backend



228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/graphql/schema/subscription.rb', line 228

def event
  @event ||= begin
    if @original_arguments.nil? && @field_resolve_step
      @original_arguments, _errors = @field_resolve_step.arguments_without_loads
    end

    Subscriptions::Event.new(
      name: field.name,
      arguments: @original_arguments,
      context: context,
      field: field,
    )
  end
end

#load_application_object_failed(err) ⇒ Object

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).



138
139
140
141
142
143
# File 'lib/graphql/schema/subscription.rb', line 138

def load_application_object_failed(err)
  if @mode == :update
    unsubscribe
  end
  super
end

#resolve(**args) ⇒ Object

Implement the Resolve API. You can implement this if you want code to run for both the initial subscription and for later updates. Or, implement #subscribe and #update



92
93
94
95
96
# File 'lib/graphql/schema/subscription.rb', line 92

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

#resolve_subscribe(**args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wrap the user-defined #subscribe hook



100
101
102
103
104
105
106
107
# File 'lib/graphql/schema/subscription.rb', line 100

def resolve_subscribe(**args)
  ret_val = !args.empty? ? subscribe(**args) : subscribe
  if ret_val == :no_response
    context.skip
  else
    ret_val
  end
end

#resolve_update(**args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Wrap the user-provided #update hook



118
119
120
121
122
123
124
125
126
# File 'lib/graphql/schema/subscription.rb', line 118

def resolve_update(**args)
  ret_val = !args.empty? ? update(**args) : update
  if ret_val == NO_UPDATE
    context.namespace(:subscriptions)[:no_update] = true
    context.skip
  else
    ret_val
  end
end

#resolve_with_support(**args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/graphql/schema/subscription.rb', line 64

def resolve_with_support(**args)
  @original_arguments = args # before `loads:` have been run
  result = nil
  unsubscribed = true
  unsubscribed_result = nil
  begin
    result = super
    unsubscribed = false
  rescue EarlyUnsubscribe => err
    unsubscribed_result = err.unsubscribed_result
  end


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

#subscribe(args = {}) ⇒ Object

The default implementation returns nothing on subscribe. Override it to return an object or :no_response to (explicitly) return nothing.



112
113
114
# File 'lib/graphql/schema/subscription.rb', line 112

def subscribe(args = {})
  :no_response
end

#subscription_written?Boolean

Returns true if #write_subscription was called already.

Returns:



223
224
225
# File 'lib/graphql/schema/subscription.rb', line 223

def subscription_written?
  @subscription_written
end

#unsubscribe(update_value = nil) ⇒ void

This method returns an undefined value.

Call this to halt execution and remove this subscription from the system

Parameters:

  • update_value (Object) (defaults to: nil)

    if present, deliver this update before unsubscribing



148
149
150
151
152
153
# File 'lib/graphql/schema/subscription.rb', line 148

def unsubscribe(update_value = nil)
  context.namespace(:subscriptions)[:unsubscribed] = true
  err = EarlyUnsubscribe.new
  err.unsubscribed_result = update_value
  raise err
end

#update(args = {}) ⇒ Object

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.



131
132
133
# File 'lib/graphql/schema/subscription.rb', line 131

def update(args = {})
  object
end

#write_subscriptionvoid

This method returns an undefined value.

Calls through to schema.subscriptions to register this subscription with the backend. This is automatically called by GraphQL-Ruby after a query finishes successfully, but if you need to commit the subscription during #subscribe, you can call it there. (This method also sets a flag showing that this subscription was already written.)

If you call this method yourself, you may also need to #unsubscribe or call subscriptions.delete_subscription to clean up the database if the query crashes with an error later in execution.



212
213
214
215
216
217
218
219
220
# File 'lib/graphql/schema/subscription.rb', line 212

def write_subscription
  if subscription_written?
    raise GraphQL::Error, "`write_subscription` was called but `#{self.class}#subscription_written?` is already true. Remove a call to `write subscription`."
  else
    @subscription_written = true
    context.schema.subscriptions.write_subscription(context.query, [event])
  end
  nil
end