From 2c079a2eecba291d123e076e9e537a2512b3e415 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 1 Jan 2026 21:04:41 -0700 Subject: [PATCH 1/2] fix: Field resolvers are mutable This allows us to modify resolvers after creation, making conversions between AST schemas (that have no resolvers) and executable schemas easier. --- Sources/GraphQL/Type/Definition.swift | 44 +++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index ee198993..0d585aed 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -505,13 +505,32 @@ public struct GraphQLResolveInfo: Sendable { public typealias GraphQLFieldMap = OrderedDictionary -public struct GraphQLField: Sendable { +public final class GraphQLField: @unchecked Sendable { public let type: GraphQLOutputType public let args: GraphQLArgumentConfigMap public let deprecationReason: String? public let description: String? - public let resolve: GraphQLFieldResolve? - public let subscribe: GraphQLFieldResolve? + + private var _resolve: GraphQLFieldResolve? + public var resolve: GraphQLFieldResolve? { + get { + fieldPropertyQueue.sync { _resolve } + } + set { + fieldPropertyQueue.sync(flags: .barrier) { _resolve = newValue } + } + } + + private var _subscribe: GraphQLFieldResolve? + public var subscribe: GraphQLFieldResolve? { + get { + fieldPropertyQueue.sync { _subscribe } + } + set { + fieldPropertyQueue.sync(flags: .barrier) { _subscribe = newValue } + } + } + public let astNode: FieldDefinition? public init( @@ -526,8 +545,8 @@ public struct GraphQLField: Sendable { self.deprecationReason = deprecationReason self.description = description self.astNode = astNode - resolve = nil - subscribe = nil + _resolve = nil + _subscribe = nil } public init( @@ -544,8 +563,8 @@ public struct GraphQLField: Sendable { self.deprecationReason = deprecationReason self.description = description self.astNode = astNode - self.resolve = resolve - self.subscribe = subscribe + self._resolve = resolve + self._subscribe = subscribe } public init( @@ -562,11 +581,11 @@ public struct GraphQLField: Sendable { self.description = description self.astNode = astNode - self.resolve = { source, args, context, info in + self._resolve = { source, args, context, info in let result = try resolve(source, args, context, info) return result } - subscribe = nil + _subscribe = nil } } @@ -1422,3 +1441,10 @@ private let cacheQueue = DispatchQueue( label: "graphql.objecttype.cache", attributes: .concurrent ) + +/// Shared queue for field property access +/// Uses reader/writer pattern for read-heavy workload +private let fieldPropertyQueue = DispatchQueue( + label: "graphql.field.properties", + attributes: .concurrent +) From eb48dc8cb083621a5a28610b980ae95666fa4d5b Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 3 Jan 2026 15:03:43 -0700 Subject: [PATCH 2/2] refactor: Documentation and cleanup --- Sources/GraphQL/Type/Definition.swift | 32 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 0d585aed..da9203d5 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -276,8 +276,8 @@ public final class GraphQLObjectType: @unchecked Sendable { public let name: String public let description: String? - // While technically not sendable, fields and interfaces should not be mutated after schema - // creation. + /// The fields that the object defines. These may be mutated during setup, but should not be + /// modified once the schema is being used for execution. public var fields: () throws -> GraphQLFieldMap { get { fieldFunc @@ -294,6 +294,8 @@ public final class GraphQLObjectType: @unchecked Sendable { private var fieldFunc: () throws -> GraphQLFieldMap private var fieldCache: GraphQLFieldDefinitionMap? + /// The interfaces that the object conforms to. These may be mutated during setup, but should + /// not be modified once the schema is being used for execution. public var interfaces: () throws -> [GraphQLInterfaceType] { get { interfaceFunc @@ -511,7 +513,6 @@ public final class GraphQLField: @unchecked Sendable { public let deprecationReason: String? public let description: String? - private var _resolve: GraphQLFieldResolve? public var resolve: GraphQLFieldResolve? { get { fieldPropertyQueue.sync { _resolve } @@ -521,7 +522,8 @@ public final class GraphQLField: @unchecked Sendable { } } - private var _subscribe: GraphQLFieldResolve? + private var _resolve: GraphQLFieldResolve? + public var subscribe: GraphQLFieldResolve? { get { fieldPropertyQueue.sync { _subscribe } @@ -531,6 +533,8 @@ public final class GraphQLField: @unchecked Sendable { } } + private var _subscribe: GraphQLFieldResolve? + public let astNode: FieldDefinition? public init( @@ -563,8 +567,8 @@ public final class GraphQLField: @unchecked Sendable { self.deprecationReason = deprecationReason self.description = description self.astNode = astNode - self._resolve = resolve - self._subscribe = subscribe + _resolve = resolve + _subscribe = subscribe } public init( @@ -581,7 +585,7 @@ public final class GraphQLField: @unchecked Sendable { self.description = description self.astNode = astNode - self._resolve = { source, args, context, info in + _resolve = { source, args, context, info in let result = try resolve(source, args, context, info) return result } @@ -730,8 +734,8 @@ public final class GraphQLInterfaceType: @unchecked Sendable { public let description: String? public let resolveType: GraphQLTypeResolve? - // While technically not sendable, fields and interfaces should not be mutated after schema - // creation. + /// The fields that the interface defines. These may be mutated during setup, but should not be + /// modified once the schema is being used for execution. public var fields: () throws -> GraphQLFieldMap { get { fieldFunc @@ -748,6 +752,8 @@ public final class GraphQLInterfaceType: @unchecked Sendable { private var fieldFunc: () throws -> GraphQLFieldMap private var fieldCache: GraphQLFieldDefinitionMap? + /// The interfaces that the interface conforms to. This may be mutated during setup, but should + /// not be modified once the schema is being used for execution. public var interfaces: () throws -> [GraphQLInterfaceType] { get { interfaceFunc @@ -888,7 +894,9 @@ public final class GraphQLUnionType: @unchecked Sendable { public let name: String public let description: String? public let resolveType: GraphQLTypeResolve? - // While technically not sendable, types should not be mutated after schema creation. + + /// The types that belong to the union. This may be mutated during setup, but must not be + /// modified once the schema is being used for execution. public internal(set) var types: () throws -> [GraphQLObjectType] public let possibleTypeNames: [String: Bool] let extensions: [GraphQLUnionTypeExtensions] @@ -1183,7 +1191,9 @@ public struct GraphQLEnumValueDefinition: Sendable { public final class GraphQLInputObjectType: @unchecked Sendable { public let name: String public let description: String? - // While technically not sendable, this should not be mutated after schema creation. + + /// The fields that the input has. This may be mutated during setup, but must not be modified + /// once the schema is being used for execution. public var fields: () throws -> InputObjectFieldMap public let astNode: InputObjectTypeDefinition? public let extensionASTNodes: [InputObjectExtensionDefinition]