Skip to content

Commit 7a4abcf

Browse files
author
Jay Herron
committed
Subscription attempt with custom AsyncIterable
1 parent 377b321 commit 7a4abcf

2 files changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import NIO
2+
3+
class SimplePubSub<T> {
4+
let subscribers:[SimplePubSubSubscriber<T>] = []
5+
6+
public func emit(event:T) -> Bool {
7+
for subscriber in subscribers {
8+
subscriber.process(event: event)
9+
}
10+
return subscribers.count > 0
11+
}
12+
13+
public func getSubscriber() -> SimplePubSubSubscriber<T> {
14+
return SimplePubSubSubscriber()
15+
}
16+
}
17+
18+
class SimplePubSubSubscriber<T> {
19+
var pullQueue:[EventLoopFuture<String>] = []
20+
var pushQueue:[EventLoopFuture<String>] = []
21+
var listening = true
22+
23+
func process(event:T) {
24+
25+
}
26+
27+
28+
func emptyQueue() {
29+
listening = false
30+
// subscribers.delete(pushValue) // TODO How do we remove this subscriber from the list??
31+
// for future in pullQueue { // TODO How can we short-circuit the futures in pullQueue
32+
// future.["value":"undefined", "done":"true"])
33+
// }
34+
pullQueue.removeAll()
35+
pushQueue.removeAll()
36+
}
37+
}
38+
39+
class SimplePubSubAsyncIterable : AsyncIterable {
40+
let eventLoopGroup:EventLoopGroup
41+
42+
var pullQueue:[EventLoopFuture<String>] = []
43+
var pushQueue:[EventLoopFuture<String>] = []
44+
var listening = true
45+
46+
init() {
47+
self.eventLoopGroup = MultiThreadedEventLoopGroup.init(numberOfThreads: 1)
48+
}
49+
50+
func next() -> EventLoopFuture<[String:String]> {
51+
if !listening {
52+
return eventLoopGroup.next().submit {
53+
return ["value":"undefined", "done":"true"]
54+
}
55+
} else if pushQueue.count > 0 {
56+
return eventLoopGroup.next().submit {
57+
return ["value": try! self.pushQueue.removeFirst().wait(), "done":"false"]
58+
}
59+
}
60+
// else { // TODO Figure out why pushQueue is used as a value, but pullQueue is used as a map...
61+
// return pullQueue.last!
62+
// }
63+
return eventLoopGroup.next().submit { // TODO PLACEHOLDER
64+
return ["value":"PLACEHOLDER", "done":"false"]
65+
}
66+
}
67+
}
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import Dispatch
2+
import Runtime
3+
import RxSwift
4+
import NIO
5+
6+
/**
7+
* Implements the "Subscribe" algorithm described in the GraphQL specification.
8+
*
9+
* Returns a Promise which resolves to either an AsyncIterator (if successful)
10+
* or an ExecutionResult (error). The promise will be rejected if the schema or
11+
* other arguments to this function are invalid, or if the resolved event stream
12+
* is not an async iterable.
13+
*
14+
* If the client-provided arguments to this function do not result in a
15+
* compliant subscription, a GraphQL Response (ExecutionResult) with
16+
* descriptive errors and no data will be returned.
17+
*
18+
* If the source stream could not be created due to faulty subscription
19+
* resolver logic or underlying systems, the promise will resolve to a single
20+
* ExecutionResult containing `errors` and no `data`.
21+
*
22+
* If the operation succeeded, the promise resolves to an AsyncIterator, which
23+
* yields a stream of ExecutionResults representing the response stream.
24+
*
25+
* Accepts either an object with named arguments, or individual arguments.
26+
*/
27+
func subscribe(
28+
queryStrategy: QueryFieldExecutionStrategy,
29+
mutationStrategy: MutationFieldExecutionStrategy,
30+
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
31+
instrumentation: Instrumentation,
32+
schema: GraphQLSchema,
33+
documentAST: Document,
34+
rootValue: Any,
35+
context: Any,
36+
eventLoopGroup: EventLoopGroup,
37+
variableValues: [String: Map] = [:],
38+
operationName: String? = nil,
39+
fieldResolver: GraphQLFieldResolve,
40+
subscribeFieldResolver: GraphQLFieldResolve
41+
) -> EventLoopFuture<Any?> { // This is either an AsyncIterator or a GraphQLResult
42+
43+
44+
let sourceFuture = createSourceEventStream(
45+
queryStrategy: queryStrategy,
46+
mutationStrategy: mutationStrategy,
47+
subscriptionStrategy: subscriptionStrategy,
48+
instrumentation: instrumentation,
49+
schema: schema,
50+
documentAST: documentAST,
51+
rootValue: rootValue,
52+
context: context,
53+
eventLoopGroup: eventLoopGroup,
54+
variableValues: variableValues,
55+
operationName: operationName,
56+
subscribeFieldResolver: subscribeFieldResolver
57+
)
58+
59+
// For each payload yielded from a subscription, map it over the normal
60+
// GraphQL `execute` function, with `payload` as the rootValue.
61+
// This implements the "MapSourceToResponseEvent" algorithm described in
62+
// the GraphQL specification. The `execute` function provides the
63+
// "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
64+
// "ExecuteQuery" algorithm, for which `execute` is also used.
65+
func mapSourceToResponse(payload:GraphQLResult) -> EventLoopFuture<GraphQLResult> {
66+
return execute(
67+
queryStrategy: queryStrategy,
68+
mutationStrategy: mutationStrategy,
69+
subscriptionStrategy: subscriptionStrategy,
70+
instrumentation: instrumentation,
71+
schema: schema,
72+
documentAST: documentAST,
73+
rootValue: payload, // Make payload the root value
74+
context: context,
75+
eventLoopGroup: eventLoopGroup,
76+
variableValues: variableValues,
77+
operationName: operationName
78+
)
79+
}
80+
return sourceFuture.flatMap({ (resultOrStream) -> EventLoopFuture<Any?> in
81+
if resultOrStream is GraphQLResult { // Return the result directly
82+
return eventLoopGroup.next().makeSucceededFuture(resultOrStream)
83+
} else { // We can assume that it's an AsyncIterable
84+
let stream:AsyncIterable<GraphQLResult>! = resultOrStream
85+
return MappedAsyncIterator(from: stream) { payload -> EventLoopFuture<GraphQLResult> in
86+
mapSourceToResponse(payload: payload)
87+
}
88+
}
89+
})
90+
}
91+
92+
/**
93+
* Implements the "CreateSourceEventStream" algorithm described in the
94+
* GraphQL specification, resolving the subscription source event stream.
95+
*
96+
* Returns a Promise which resolves to either an AsyncIterable (if successful)
97+
* or an ExecutionResult (error). The promise will be rejected if the schema or
98+
* other arguments to this function are invalid, or if the resolved event stream
99+
* is not an async iterable.
100+
*
101+
* If the client-provided arguments to this function do not result in a
102+
* compliant subscription, a GraphQL Response (ExecutionResult) with
103+
* descriptive errors and no data will be returned.
104+
*
105+
* If the the source stream could not be created due to faulty subscription
106+
* resolver logic or underlying systems, the promise will resolve to a single
107+
* ExecutionResult containing `errors` and no `data`.
108+
*
109+
* If the operation succeeded, the promise resolves to the AsyncIterable for the
110+
* event stream returned by the resolver.
111+
*
112+
* A Source Event Stream represents a sequence of events, each of which triggers
113+
* a GraphQL execution for that event.
114+
*
115+
* This may be useful when hosting the stateful subscription service in a
116+
* different process or machine than the stateless GraphQL execution engine,
117+
* or otherwise separating these two steps. For more on this, see the
118+
* "Supporting Subscriptions at Scale" information in the GraphQL specification.
119+
*/
120+
func createSourceEventStream(
121+
queryStrategy: QueryFieldExecutionStrategy,
122+
mutationStrategy: MutationFieldExecutionStrategy,
123+
subscriptionStrategy: SubscriptionFieldExecutionStrategy,
124+
instrumentation: Instrumentation,
125+
schema: GraphQLSchema,
126+
documentAST: Document,
127+
rootValue: Any,
128+
context: Any,
129+
eventLoopGroup: EventLoopGroup,
130+
variableValues: [String: Map] = [:],
131+
operationName: String? = nil,
132+
subscribeFieldResolver: GraphQLFieldResolve
133+
) -> EventLoopFuture<Any?> { // This is either an AsyncIterator or a GraphQLResult
134+
135+
let executeStarted = instrumentation.now
136+
let exeContext: ExecutionContext
137+
138+
do {
139+
// If a valid context cannot be created due to incorrect arguments,
140+
// this will throw an error.
141+
exeContext = try buildExecutionContext(
142+
queryStrategy: queryStrategy,
143+
mutationStrategy: mutationStrategy,
144+
subscriptionStrategy: subscriptionStrategy,
145+
instrumentation: instrumentation,
146+
schema: schema,
147+
documentAST: documentAST,
148+
rootValue: rootValue,
149+
context: context,
150+
eventLoopGroup: eventLoopGroup,
151+
rawVariableValues: variableValues,
152+
operationName: operationName
153+
// TODO shouldn't we be including the subscribeFieldResolver??
154+
)
155+
} catch let error as GraphQLError {
156+
instrumentation.operationExecution(
157+
processId: processId(),
158+
threadId: threadId(),
159+
started: executeStarted,
160+
finished: instrumentation.now,
161+
schema: schema,
162+
document: documentAST,
163+
rootValue: rootValue,
164+
eventLoopGroup: eventLoopGroup,
165+
variableValues: variableValues,
166+
operation: nil,
167+
errors: [error],
168+
result: nil
169+
)
170+
171+
return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error]))
172+
} catch {
173+
return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)]))
174+
}
175+
176+
return try! executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup)
177+
}
178+
179+
func executeSubscription(
180+
context: ExecutionContext,
181+
eventLoopGroup: EventLoopGroup
182+
) throws -> EventLoopFuture<Any?> { // This is either an AsyncIterator or a GraphQLResult
183+
184+
// Get the first node
185+
let type = try getOperationRootType(schema: context.schema, operation: context.operation)
186+
var inputFields: [String:[Field]] = [:]
187+
var visitedFragmentNames: [String:Bool] = [:]
188+
let fields = try collectFields(
189+
exeContext: context,
190+
runtimeType: type,
191+
selectionSet: context.operation.selectionSet,
192+
fields: &inputFields,
193+
visitedFragmentNames: &visitedFragmentNames
194+
)
195+
let responseNames = fields.keys
196+
let responseName = responseNames.first! // TODO add error handling here
197+
let fieldNodes = fields[responseName]!
198+
let fieldNode = fieldNodes.first!
199+
200+
guard let fieldDef = getFieldDef(schema: context.schema, parentType: type, fieldAST: fieldNode) else {
201+
throw GraphQLError.init(
202+
message: "`The subscription field '\(fieldNode.name)' is not defined.`",
203+
nodes: fieldNodes
204+
)
205+
}
206+
207+
let path = IndexPath.init().appending(fieldNode.name.value)
208+
let info = buildResolveInfo(
209+
context: context,
210+
fieldDef: fieldDef,
211+
fieldASTs: fieldNodes,
212+
parentType: type,
213+
path: path
214+
)
215+
216+
// Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
217+
// It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
218+
219+
// Build a map of arguments from the field.arguments AST, using the
220+
// variables scope to fulfill any variable references.
221+
let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variableValues: context.variableValues)
222+
223+
// The resolve function's optional third argument is a context value that
224+
// is provided to every resolve function within an execution. It is commonly
225+
// used to represent an authenticated user, or request-specific caches.
226+
let contextValue = context.context
227+
228+
// Call the `subscribe()` resolver or the default resolver to produce an
229+
// AsyncIterable yielding raw payloads.
230+
let resolve = fieldDef.subscribe ?? fieldDef.resolve ?? defaultResolve
231+
232+
// Get the resolve func, regardless of if its result is normal
233+
// or abrupt (error).
234+
let result = resolveOrError(
235+
resolve: resolve,
236+
source: context.rootValue,
237+
args: args,
238+
context: contextValue,
239+
eventLoopGroup: eventLoopGroup,
240+
info: info
241+
)
242+
243+
return try completeValueCatchingError(
244+
exeContext: context,
245+
returnType: fieldDef.type,
246+
fieldASTs: fieldNodes,
247+
info: info,
248+
path: path,
249+
result: result
250+
).flatMap { value -> EventLoopFuture<Any?> in
251+
if let asyncIterable = value as? EventLoopFuture<AsyncIterable> {
252+
return asyncIterable
253+
} else {
254+
context.append(error: GraphQLError(message: "Subscription field must return AsyncIterable."))
255+
return context.eventLoopGroup.next().makeSucceededFuture(nil)
256+
}
257+
}
258+
}
259+
260+
protocol AsyncIterable {
261+
associatedtype Item
262+
func next() -> EventLoopFuture<Item>
263+
}
264+
265+
class MappedAsyncIterator<OrigType: AsyncIterable, MappedType> : AsyncIterable {
266+
let origIterable: OrigType
267+
let callback: (OrigType.Item) -> Future<MappedType>
268+
269+
init(from: OrigType, by: @escaping (OrigType.Item) -> Future<MappedType>) {
270+
origIterable = from
271+
callback = by
272+
}
273+
274+
func next() -> EventLoopFuture<MappedType> {
275+
origIterable.next().flatMap { origResult -> Future<MappedType> in
276+
self.callback(origResult)
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)