Skip to content

Commit

Permalink
Merge pull request #5012 from rmosolgo/relay-broadcastable
Browse files Browse the repository at this point in the history
Make it possible to broadcast nodes field
  • Loading branch information
rmosolgo authored Jul 12, 2024
2 parents a3aee8c + d1e08ce commit 219a924
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 5 deletions.
24 changes: 23 additions & 1 deletion guides/subscriptions/broadcast.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ desc: Delivering the same GraphQL result to multiple subscribers
index: 3
---

GraphQL-Ruby 1.11+ introduced a new algorithm for tracking subscriptions and delivering updates, _broadcasts_.
GraphQL subscription updates may _broadcast_ data to multiple subscribers.

A broadcast is a subscription update which is executed _once_, then delivered to _any number_ of subscribers. This reduces the time your server spends running GraphQL queries, since it doesn't have to re-run the query for every subscriber.

Expand Down Expand Up @@ -94,3 +94,25 @@ MySchema.subscriptions.broadcastable?(subscription_string)
```

Use this in your application's tests to make sure that broadcastable fields aren't accidentally made non-broadcastable.

## Connections and Edges

You can configure your generated `Connection` and `Edge` types to be broadcastable by setting `default_broadcastable(true)` in their definition:

```ruby
# app/types/base_connection.rb
class Types::BaseConnection < Types::BaseObject
include GraphQL::Types::Relay::ConnectionBehaviors
default_broadcastable(true)
end

# app/types/base_edge.rb
class Types::BaseEdge < Types::BaseObject
include GraphQL::Types::Relay::EdgeBehaviors
default_broadcastable(true)
end
```

(In your `BaseObject`, you should also have `connection_type_class(Types::BaseConnection)` and `edge_type_class(Types::BaseEdge)`.)

`PageInfo` is broadcastable by default.
14 changes: 10 additions & 4 deletions lib/graphql/subscriptions/broadcast_analyzer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,16 @@ def on_enter_field(node, parent, visitor)
end

current_field = visitor.field_definition
apply_broadcastable(current_field)

current_type = visitor.parent_type_definition
apply_broadcastable(current_type, current_field)
if current_type.kind.interface?
pt = @query.possible_types(current_type)
pt.each do |object_type|
ot_field = @query.get_field(object_type, current_field.graphql_name)
# Inherited fields would be exactly the same object;
# only check fields that are overrides of the inherited one
if ot_field && ot_field != current_field
apply_broadcastable(ot_field)
apply_broadcastable(object_type, ot_field)
end
end
end
Expand All @@ -55,17 +54,24 @@ def result
private

# Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
def apply_broadcastable(field_defn)
def apply_broadcastable(owner_type, field_defn)
current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?

if current_field_broadcastable.nil? && owner_type.respond_to?(:default_broadcastable?)
current_field_broadcastable = owner_type.default_broadcastable?
end

case current_field_broadcastable
when nil
query.logger.debug { "`broadcastable: nil` for field: #{field_defn.path}" }
# If the value wasn't set, mix in the default value:
# - If the default is false and the current value is true, make it false
# - If the default is true and the current value is true, it stays true
# - If the default is false and the current value is false, keep it false
# - If the default is true and the current value is false, keep it false
@subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
when false
query.logger.debug { "`broadcastable: false` for field: #{field_defn.path}" }
# One non-broadcastable field is enough to make the whole subscription non-broadcastable
@subscription_broadcastable = false
when true
Expand Down
10 changes: 10 additions & 0 deletions lib/graphql/types/relay/connection_behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def self.included(child_class)
self.node_type = nil
self.edge_class = nil
}
child_class.default_broadcastable(nil)
add_page_info_field(child_class)
end

Expand All @@ -31,12 +32,21 @@ def inherited(child_class)
child_class.edge_type = nil
child_class.node_type = nil
child_class.edge_class = nil
child_class.default_broadcastable(default_broadcastable?)
end

def default_relay?
true
end

def default_broadcastable?
@default_broadcastable
end

def default_broadcastable(new_value)
@default_broadcastable = new_value
end

# @return [Class]
attr_reader :node_type

Expand Down
10 changes: 10 additions & 0 deletions lib/graphql/types/relay/edge_behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def self.included(child_class)
child_class.extend(ClassMethods)
child_class.class_eval { self.node_type = nil }
child_class.node_nullable(true)
child_class.default_broadcastable(nil)
end

def node
Expand All @@ -24,12 +25,21 @@ def inherited(child_class)
super
child_class.node_type = nil
child_class.node_nullable = nil
child_class.default_broadcastable(default_broadcastable?)
end

def default_relay?
true
end

def default_broadcastable?
@default_broadcastable
end

def default_broadcastable(new_value)
@default_broadcastable = new_value
end

# Get or set the Object type that this edge wraps.
#
# @param node_type [Class] A `Schema::Object` subclass
Expand Down
4 changes: 4 additions & 0 deletions lib/graphql/types/relay/page_info_behaviors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ module ClassMethods
def default_relay?
true
end

def default_broadcastable?
true
end
end
end
end
Expand Down
47 changes: 47 additions & 0 deletions spec/graphql/subscriptions/broadcast_analyzer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,36 @@

describe GraphQL::Subscriptions::BroadcastAnalyzer do
class BroadcastTestSchema < GraphQL::Schema
LOG_OUTPUT = StringIO.new
LOGGER = Logger.new(LOG_OUTPUT)
LOGGER.formatter = ->(severity, time, progname, msg) { "#{severity}: #{msg}\n"}
module Throwable
include GraphQL::Schema::Interface
field :weight, Integer, null: false
field :too_heavy_for_viewer, Boolean, null: false, broadcastable: false
field :split_broadcastable_test, Boolean, null: false
end

class BroadcastableConnection < GraphQL::Types::Relay::BaseConnection
default_broadcastable(true)
end

class BroadcastableEdge < GraphQL::Types::Relay::BaseEdge
default_broadcastable(true)
end

class Javelin < GraphQL::Schema::Object
implements Throwable
edge_type_class(BroadcastableEdge)
connection_type_class(BroadcastableConnection)
field :split_broadcastable_test, Boolean, null: false, broadcastable: false
field :length, Integer, broadcastable: true
end

class Shot < GraphQL::Schema::Object
implements Throwable
field :viewer_can_put, Boolean, null: false, broadcastable: false
field :diameter, Integer, broadcastable: true
end

class Query < GraphQL::Schema::Object
Expand All @@ -41,13 +56,21 @@ class NewMaxThrowRecord < GraphQL::Schema::Subscription
end

field :new_max_throw_record, subscription: NewMaxThrowRecord, broadcastable: true

class NewJavelin < GraphQL::Schema::Subscription
field :javelins, Javelin.connection_type, broadcastable: true
field :shots, Shot.connection_type, broadcastable: true
end

field :new_javelin, subscription: NewJavelin, broadcastable: true
end

query(Query)
mutation(Mutation)
subscription(Subscription)
orphan_types(Shot, Javelin)
use GraphQL::Subscriptions, broadcast: true, default_broadcastable: true
default_logger(LOGGER)
end

# Inheritance doesn't quite work, because the query analyzer is carried over.
Expand All @@ -57,12 +80,18 @@ class BroadcastTestDefaultFalseSchema < GraphQL::Schema
subscription(BroadcastTestSchema::Subscription)
orphan_types(BroadcastTestSchema::Shot, BroadcastTestSchema::Javelin)
use GraphQL::Subscriptions, broadcast: true, default_broadcastable: false
default_logger(BroadcastTestSchema::LOGGER)
end

def broadcastable?(query_str, schema: BroadcastTestSchema)
schema.subscriptions.broadcastable?(query_str)
end

before do
BroadcastTestSchema::LOG_OUTPUT.rewind
BroadcastTestSchema::LOG_OUTPUT.string.clear
end

it "doesn't run for non-subscriptions" do
assert_nil broadcastable?("{ __typename }")
assert_nil broadcastable?("mutation { __typename }")
Expand Down Expand Up @@ -94,6 +123,24 @@ def broadcastable?(query_str, schema: BroadcastTestSchema)
end
end

describe "nodes field" do
it "can be broadcastable" do
query_str = "subscription { newJavelin { javelins { nodes { length } edges { node { length } } pageInfo { hasNextPage } } } }"
assert broadcastable?(query_str)
assert broadcastable?(query_str, schema: BroadcastTestDefaultFalseSchema)
end

it "follows the default schema setting" do
query_str = "subscription { newJavelin { shots { nodes { diameter } edges { node { diameter } } pageInfo { hasNextPage } } } }"
assert broadcastable?(query_str)
assert_equal BroadcastTestSchema.default_logger, BroadcastTestDefaultFalseSchema.default_logger
BroadcastTestSchema::LOG_OUTPUT.string.clear
BroadcastTestSchema::LOG_OUTPUT.rewind
refute broadcastable?(query_str, schema: BroadcastTestDefaultFalseSchema)
assert_equal "DEBUG: `broadcastable: nil` for field: ShotConnection.nodes\n", BroadcastTestSchema::LOG_OUTPUT.string
end
end

describe "abstract types" do
describe "when a field returns an interface" do
it "observes the interface-defined configuration" do
Expand Down

0 comments on commit 219a924

Please sign in to comment.