diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index ee198993..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 @@ -505,13 +507,34 @@ 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? + + public var resolve: GraphQLFieldResolve? { + get { + fieldPropertyQueue.sync { _resolve } + } + set { + fieldPropertyQueue.sync(flags: .barrier) { _resolve = newValue } + } + } + + private var _resolve: GraphQLFieldResolve? + + public var subscribe: GraphQLFieldResolve? { + get { + fieldPropertyQueue.sync { _subscribe } + } + set { + fieldPropertyQueue.sync(flags: .barrier) { _subscribe = newValue } + } + } + + private var _subscribe: GraphQLFieldResolve? + public let astNode: FieldDefinition? public init( @@ -526,8 +549,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 +567,8 @@ public struct GraphQLField: Sendable { self.deprecationReason = deprecationReason self.description = description self.astNode = astNode - self.resolve = resolve - self.subscribe = subscribe + _resolve = resolve + _subscribe = subscribe } public init( @@ -562,11 +585,11 @@ public struct GraphQLField: 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 } - subscribe = nil + _subscribe = nil } } @@ -711,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 @@ -729,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 @@ -869,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] @@ -1164,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] @@ -1422,3 +1451,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 +)