Skip to content

Commit db77ae5

Browse files
feat!: Switches SubscriptionResult with Result
1 parent d25254b commit db77ae5

File tree

5 files changed

+49
-82
lines changed

5 files changed

+49
-82
lines changed

MIGRATION.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters.
2020

2121
The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream<GraphQLResult, Error>` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary.
2222

23+
### SubscriptionResult removal
24+
25+
The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns a true Swift `Result` type.
26+
2327
### Instrumentation removal
2428

2529
The `Instrumentation` type has been removed, with anticipated support for tracing using [`swift-distributed-tracing`](https://github.com/apple/swift-distributed-tracing). `instrumentation` arguments must be removed from `graphql` and `graphqlSubscribe` calls.

Sources/GraphQL/GraphQL.swift

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,11 @@ public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertib
4242
}
4343
}
4444

45-
/// SubscriptionResult wraps the observable and error data returned by the subscribe request.
46-
public struct SubscriptionResult {
47-
public let stream: AsyncThrowingStream<GraphQLResult, Error>?
45+
/// A collection of GraphQL errors. Enables returning multiple errors from Result types.
46+
public struct GraphQLErrors: Error, Sendable {
4847
public let errors: [GraphQLError]
4948

50-
public init(
51-
stream: AsyncThrowingStream<GraphQLResult, Error>? = nil,
52-
errors: [GraphQLError] = []
53-
) {
54-
self.stream = stream
49+
public init(_ errors: [GraphQLError]) {
5550
self.errors = errors
5651
}
5752
}
@@ -228,7 +223,7 @@ public func graphqlSubscribe(
228223
context: Any = (),
229224
variableValues: [String: Map] = [:],
230225
operationName: String? = nil
231-
) async throws -> SubscriptionResult {
226+
) async throws -> Result<AsyncThrowingStream<GraphQLResult, Error>, GraphQLErrors> {
232227
let source = Source(body: request, name: "GraphQL Subscription request")
233228
let documentAST = try parse(source: source)
234229
let validationErrors = validate(
@@ -238,7 +233,7 @@ public func graphqlSubscribe(
238233
)
239234

240235
guard validationErrors.isEmpty else {
241-
return SubscriptionResult(errors: validationErrors)
236+
return .failure(.init(validationErrors))
242237
}
243238

244239
return try await subscribe(

Sources/GraphQL/Subscription/Subscribe.swift

Lines changed: 38 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,16 @@ import OrderedCollections
33
/**
44
* Implements the "Subscribe" algorithm described in the GraphQL specification.
55
*
6-
* Returns a future which resolves to a SubscriptionResult containing either
7-
* a SubscriptionObservable (if successful), or GraphQLErrors (error).
6+
* Returns a `Result` that either succeeds with an `AsyncThrowingStream`, or fails with `GraphQLErrors`.
87
*
98
* If the client-provided arguments to this function do not result in a
10-
* compliant subscription, the future will resolve to a
11-
* SubscriptionResult containing `errors` and no `observable`.
9+
* compliant subscription, the `Result` will fails with descriptive errors.
1210
*
1311
* If the source stream could not be created due to faulty subscription
14-
* resolver logic or underlying systems, the future will resolve to a
15-
* SubscriptionResult containing `errors` and no `observable`.
12+
* resolver logic or underlying systems, the `Result` will fail with errors.
1613
*
17-
* If the operation succeeded, the future will resolve to a SubscriptionResult,
18-
* containing an `observable` which yields a stream of GraphQLResults
14+
* If the operation succeeded, the `Result` will succeed with an `AsyncThrowingStream` of `GraphQLResult`s
1915
* representing the response stream.
20-
*
21-
* Accepts either an object with named arguments, or individual arguments.
2216
*/
2317
func subscribe(
2418
queryStrategy: QueryFieldExecutionStrategy,
@@ -30,7 +24,7 @@ func subscribe(
3024
context: Any,
3125
variableValues: [String: Map] = [:],
3226
operationName: String? = nil
33-
) async throws -> SubscriptionResult {
27+
) async throws -> Result<AsyncThrowingStream<GraphQLResult, Error>, GraphQLErrors> {
3428
let sourceResult = try await createSourceEventStream(
3529
queryStrategy: queryStrategy,
3630
mutationStrategy: mutationStrategy,
@@ -43,7 +37,7 @@ func subscribe(
4337
operationName: operationName
4438
)
4539

46-
if let sourceStream = sourceResult.stream {
40+
return sourceResult.map { sourceStream in
4741
// We must create a new AsyncSequence because AsyncSequence.map requires a concrete type
4842
// (which we cannot know),
4943
// and we need the result to be a concrete type.
@@ -80,30 +74,24 @@ func subscribe(
8074
task.cancel()
8175
}
8276
}
83-
return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors)
84-
} else {
85-
return SubscriptionResult(errors: sourceResult.errors)
77+
return subscriptionStream
8678
}
8779
}
8880

8981
/**
9082
* Implements the "CreateSourceEventStream" algorithm described in the
9183
* GraphQL specification, resolving the subscription source event stream.
9284
*
93-
* Returns a Future which resolves to a SourceEventStreamResult, containing
94-
* either an Observable (if successful) or GraphQLErrors (error).
85+
* Returns a Result that either succeeds with an `AsyncSequence` or fails with `GraphQLErrors`.
9586
*
9687
* If the client-provided arguments to this function do not result in a
97-
* compliant subscription, the future will resolve to a
98-
* SourceEventStreamResult containing `errors` and no `observable`.
88+
* compliant subscription, the `Result` will fail with descriptive errors.
9989
*
10090
* If the source stream could not be created due to faulty subscription
101-
* resolver logic or underlying systems, the future will resolve to a
102-
* SourceEventStreamResult containing `errors` and no `observable`.
91+
* resolver logic or underlying systems, the `Result` will fail with errors.
10392
*
104-
* If the operation succeeded, the future will resolve to a SubscriptionResult,
105-
* containing an `observable` which yields a stream of event objects
106-
* returned by the subscription resolver.
93+
* If the operation succeeded, the `Result` will succeed with an AsyncSequence for the
94+
* event stream returned by the resolver.
10795
*
10896
* A Source Event Stream represents a sequence of events, each of which triggers
10997
* a GraphQL execution for that event.
@@ -123,32 +111,34 @@ func createSourceEventStream(
123111
context: Any,
124112
variableValues: [String: Map] = [:],
125113
operationName: String? = nil
126-
) async throws -> SourceEventStreamResult {
114+
) async throws -> Result<any AsyncSequence, GraphQLErrors> {
115+
// If a valid context cannot be created due to incorrect arguments,
116+
// this will throw an error.
117+
let exeContext = try buildExecutionContext(
118+
queryStrategy: queryStrategy,
119+
mutationStrategy: mutationStrategy,
120+
subscriptionStrategy: subscriptionStrategy,
121+
schema: schema,
122+
documentAST: documentAST,
123+
rootValue: rootValue,
124+
context: context,
125+
rawVariableValues: variableValues,
126+
operationName: operationName
127+
)
127128
do {
128-
// If a valid context cannot be created due to incorrect arguments,
129-
// this will throw an error.
130-
let exeContext = try buildExecutionContext(
131-
queryStrategy: queryStrategy,
132-
mutationStrategy: mutationStrategy,
133-
subscriptionStrategy: subscriptionStrategy,
134-
schema: schema,
135-
documentAST: documentAST,
136-
rootValue: rootValue,
137-
context: context,
138-
rawVariableValues: variableValues,
139-
operationName: operationName
140-
)
141129
return try await executeSubscription(context: exeContext)
142130
} catch let error as GraphQLError {
143-
return SourceEventStreamResult(errors: [error])
131+
// If it is a GraphQLError, report it as a failure.
132+
return .failure(.init([error]))
144133
} catch {
145-
return SourceEventStreamResult(errors: [GraphQLError(error)])
134+
// Otherwise treat the error as a system-class error and re-throw it.
135+
throw error
146136
}
147137
}
148138

149139
func executeSubscription(
150140
context: ExecutionContext
151-
) async throws -> SourceEventStreamResult {
141+
) async throws -> Result<any AsyncSequence, GraphQLErrors> {
152142
// Get the first node
153143
let type = try getOperationRootType(schema: context.schema, operation: context.operation)
154144
var inputFields: OrderedDictionary<String, [Field]> = [:]
@@ -238,35 +228,21 @@ func executeSubscription(
238228
resolved = success
239229
}
240230
if !context.errors.isEmpty {
241-
return SourceEventStreamResult(errors: context.errors)
231+
return .failure(.init(context.errors))
242232
} else if let error = resolved as? GraphQLError {
243-
return SourceEventStreamResult(errors: [error])
233+
return .failure(.init([error]))
244234
} else if let stream = resolved as? any AsyncSequence {
245-
return SourceEventStreamResult(stream: stream)
235+
return .success(stream)
246236
} else if resolved == nil {
247-
return SourceEventStreamResult(errors: [
237+
return .failure(.init([
248238
GraphQLError(message: "Resolved subscription was nil"),
249-
])
239+
]))
250240
} else {
251241
let resolvedObj = resolved as AnyObject
252-
return SourceEventStreamResult(errors: [
242+
return .failure(.init([
253243
GraphQLError(
254244
message: "Subscription field resolver must return an AsyncSequence. Received: '\(resolvedObj)'"
255245
),
256-
])
257-
}
258-
}
259-
260-
// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having
261-
// covariant generic support for type
262-
// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers
263-
// for query fields.
264-
struct SourceEventStreamResult {
265-
public let stream: (any AsyncSequence)?
266-
public let errors: [GraphQLError]
267-
268-
public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) {
269-
self.stream = stream
270-
self.errors = errors
246+
]))
271247
}
272248
}

Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,5 @@ func createSubscription(
197197
variableValues: variableValues,
198198
operationName: nil
199199
)
200-
201-
if let stream = result.stream {
202-
return stream
203-
} else {
204-
throw result.errors.first! // We may have more than one...
205-
}
200+
return try result.get()
206201
}

Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@ class SubscriptionTests: XCTestCase {
3030
schema: schema,
3131
request: query
3232
)
33-
guard let stream = subscriptionResult.stream else {
34-
XCTFail(subscriptionResult.errors.description)
35-
return
36-
}
33+
let stream = try subscriptionResult.get()
3734
var iterator = stream.makeAsyncIterator()
3835

3936
db.trigger(email: Email(

0 commit comments

Comments
 (0)