@@ -7,31 +7,18 @@ shared state or `directives` from the `query`, so the `resolveField` method
77also packs that information into a ` graphql::service::FieldParams ` struct and
88passes it to every ` getField ` method as the first parameter.
99
10+ This parameter is optional. The type-erased implementation of ` graphql::service::Object `
11+ for each ` Object ` type in the schema declares a pair of ` methods::ObjectHas::getFieldWithParams `
12+ and ` methods::ObjectHas::getField ` concepts for each field getter. If the implementation
13+ type supports passing the ` graphql::service::FieldParams ` struct as the first parameter,
14+ the type-erased ` object::Object ` invokes the ` getFieldWithParams ` version, otherwise it
15+ drops the parameter and calls the ` getField ` version with whatever other field
16+ arguments the schema specified.
17+
1018## Details of Field Parameters
1119
1220The ` graphql::service::FieldParams ` struct is declared in [ GraphQLService.h] ( ../include/graphqlservice/GraphQLService.h ) :
1321``` cpp
14- // Resolvers may be called in multiple different Operation contexts.
15- enum class ResolverContext
16- {
17- // Resolving a Query operation.
18- Query,
19-
20- // Resolving a Mutation operation.
21- Mutation,
22-
23- // Adding a Subscription. If you need to prepare to send events for this Subsciption
24- // (e.g. registering an event sink of your own), this is a chance to do that.
25- NotifySubscribe,
26-
27- // Resolving a Subscription event.
28- Subscription,
29-
30- // Removing a Subscription. If there are no more Subscriptions registered this is an
31- // opportunity to release resources which are no longer needed.
32- NotifyUnsubscribe,
33- };
34-
3522// Pass a common bundle of parameters to all of the generated Object::getField accessors in a
3623// SelectionSet
3724struct SelectionSetParams
@@ -42,50 +29,72 @@ struct SelectionSetParams
4229 // The lifetime of each of these borrowed references is guaranteed until the future returned
4330 // by the accessor is resolved or destroyed. They are owned by the OperationData shared pointer.
4431 const std::shared_ptr<RequestState>& state;
45- const response::Value & operationDirectives;
46- const response::Value& fragmentDefinitionDirectives;
32+ const Directives & operationDirectives;
33+ const std::shared_ptr<FragmentDefinitionDirectiveStack> fragmentDefinitionDirectives;
4734
4835 // Fragment directives are shared for all fields in that fragment, but they aren't kept alive
4936 // after the call to the last accessor in the fragment. If you need to keep them alive longer,
50- // you'll need to explicitly copy them into other instances of response::Value .
51- const response::Value& fragmentSpreadDirectives;
52- const response::Value& inlineFragmentDirectives;
37+ // you'll need to explicitly copy them into other instances of Directives .
38+ const std::shared_ptr<FragmentSpreadDirectiveStack> fragmentSpreadDirectives;
39+ const std::shared_ptr<FragmentSpreadDirectiveStack> inlineFragmentDirectives;
5340
5441 // Field error path to this selection set.
5542 std::optional<field_path> errorPath;
5643
5744 // Async launch policy for sub-field resolvers.
58- const std::launch launch = std::launch::deferred ;
45+ const await_async launch {} ;
5946};
6047
6148// Pass a common bundle of parameters to all of the generated Object::getField accessors.
6249struct FieldParams : SelectionSetParams
6350{
6451 GRAPHQLSERVICE_EXPORT explicit FieldParams(
65- SelectionSetParams&& selectionSetParams, response::Value&& directives);
52+ SelectionSetParams&& selectionSetParams, Directives directives);
6653
6754 // Each field owns its own field-specific directives. Once the accessor returns it will be
6855 // destroyed, but you can move it into another instance of response::Value to keep it alive
6956 // longer.
70- response::Value fieldDirectives;
57+ Directives fieldDirectives;
7158};
7259```
7360
7461### Resolver Context
7562
7663The ` SelectionSetParams::resolverContext ` enum member informs the ` getField `
77- accessors about what type of operation is being resolved.
64+ accessors about what type of operation is being resolved:
65+ ``` cpp
66+ // Resolvers may be called in multiple different Operation contexts.
67+ enum class ResolverContext
68+ {
69+ // Resolving a Query operation.
70+ Query,
71+
72+ // Resolving a Mutation operation.
73+ Mutation,
74+
75+ // Adding a Subscription. If you need to prepare to send events for this Subsciption
76+ // (e.g. registering an event sink of your own), this is a chance to do that.
77+ NotifySubscribe,
78+
79+ // Resolving a Subscription event.
80+ Subscription,
81+
82+ // Removing a Subscription. If there are no more Subscriptions registered this is an
83+ // opportunity to release resources which are no longer needed.
84+ NotifyUnsubscribe,
85+ };
86+ ```
7887
7988### Request State
8089
8190The ` SelectionSetParams::state ` member is a reference to the
8291` std::shared_ptr<graphql::service::RequestState> ` parameter passed to
8392` Request::resolve ` (see [ resolvers.md] ( ./resolvers.md ) for more info):
8493``` cpp
85- // The RequestState is nullable, but if you have multiple threads processing requests and there's any
86- // per-request state that you want to maintain throughout the request (e.g. optimizing or batching
87- // backend requests), you can inherit from RequestState and pass it to Request::resolve to correlate the
88- // asynchronous/recursive callbacks and accumulate state in it.
94+ // The RequestState is nullable, but if you have multiple threads processing requests and there's
95+ // any per-request state that you want to maintain throughout the request (e.g. optimizing or
96+ // batching backend requests), you can inherit from RequestState and pass it to Request::resolve to
97+ // correlate the asynchronous/recursive callbacks and accumulate state in it.
8998struct RequestState : std::enable_shared_from_this<RequestState >
9099{
91100};
@@ -96,16 +105,27 @@ struct RequestState : std::enable_shared_from_this<RequestState>
96105Each of the ` directives ` members contains the values of the ` directives ` and
97106any of their arguments which were in effect at that scope of the ` query ` .
98107Implementers may inspect those values in the call to ` getField ` and alter their
99- behavior based on those custom ` directives ` .
108+ behavior based on those custom ` directives ` :
109+ ``` cpp
110+ // Directive order matters, and some of them are repeatable. So rather than passing them in a
111+ // response::Value, pass directives in something like the underlying response::MapType which
112+ // preserves the order of the elements without complete uniqueness.
113+ using Directives = std::vector<std::pair<std::string_view, response::Value>>;
114+
115+ // Traversing a fragment spread adds a new set of directives.
116+ using FragmentDefinitionDirectiveStack = std::list<std::reference_wrapper<const Directives>>;
117+ using FragmentSpreadDirectiveStack = std::list<Directives>;
118+ ```
100119
101120As noted in the comments, the ` fragmentSpreadDirectives ` and
102- ` inlineFragmentDirectives ` are borrowed ` const ` references, shared accross
103- calls to multiple ` getField ` methods, but they will not be kept alive after
104- the relevant ` SelectionSet ` has been resolved. The ` fieldDirectives ` member is
105- passed by value and is not shared with other ` getField ` method calls, but it
106- will not be kept alive after that call returns. It's up to the implementer to
107- capture the values in these ` directives ` which they might need for asynchronous
108- evaulation after the call to the current ` getField ` method has returned.
121+ ` inlineFragmentDirectives ` are stacks of directives passed down through nested
122+ inline fragments and fragment spreads. The ` Directives ` object for each frame of
123+ the stack is shared accross calls to multiple ` getField ` methods in a single fragment,
124+ but they will be popped from the stack when the last field has been visited. The
125+ ` fieldDirectives ` member is passed by value and is not shared with other ` getField `
126+ method calls. It's up to the implementer to capture the values in these ` directives `
127+ which they might need for asynchronous evaulation after the call to the current
128+ ` getField ` method has returned.
109129
110130The implementer does not need to capture the values of ` operationDirectives `
111131or ` fragmentDefinitionDirectives ` because those are kept alive until the
@@ -132,6 +152,68 @@ this member in their `FieldParams` argument, and they may change their own
132152behavior based on that, but they cannot alter the launch policy which
133153` graphqlservice ` uses for the resolvers themselves.
134154
155+ In previous versions, this was a ` std::launch ` enum value used with the
156+ ` std::async ` standard library function. In the latest version, this is a C++20
157+ ` Awaitable ` , specifically a type-erased type defined in ` graphql::service::await_async ` :
158+ ``` cpp
159+ // Type-erased awaitable.
160+ class await_async : public coro :: suspend_always
161+ {
162+ private:
163+ struct Concept
164+ {
165+ virtual ~ Concept() = default;
166+
167+ virtual bool await_ready() const = 0;
168+ virtual void await_suspend(coro::coroutine_handle<> h) const = 0;
169+ virtual void await_resume() const = 0;
170+ };
171+ ...
172+
173+ public:
174+ // Type-erased explicit constructor for a custom awaitable.
175+ template <class T >
176+ explicit await_async(std::shared_ptr<T > pimpl)
177+ : _ pimpl { std::make_shared<Model<T >>(std::move(pimpl)) }
178+ {
179+ }
180+
181+ // Default to immediate synchronous execution.
182+ await_async ()
183+ : _pimpl { std::static_pointer_cast<Concept>(
184+ std::make_shared<Model<coro::suspend_never>>(std::make_shared<coro::suspend_never>())) }
185+ {
186+ }
187+
188+ // Implicitly convert a std::launch parameter used with std::async to an awaitable.
189+ await_async (std::launch launch)
190+ : _ pimpl { ((launch & std::launch::async) == std::launch::async)
191+ ? std::static_pointer_cast<Concept >(std::make_shared<Model<await_worker_thread>>(
192+ std::make_shared<await_worker_thread>()))
193+ : std::static_pointer_cast<Concept >(std::make_shared<Model< coro::suspend_never > >(
194+ std::make_shared< coro::suspend_never > ())) }
195+ {
196+ }
197+ ...
198+ };
199+ ```
200+ For convenience, it will use `graphql::service::await_worker_thread` if you specify `std::launch::async`,
201+ which should have the same behavior as calling `std::async(std::launch::async, ...)` did before.
202+
203+ If you specify any other flags for `std::launch`, it does not honor them. It will use `coro::suspend_never`
204+ (an alias for `std::suspend_never` or `std::experimental::suspend_never`), which as the name suggests,
205+ continues executing the coroutine without suspending. In other words, `std::launch::deferred` will no
206+ longer defer execution as in previous versions, it will execute immediately.
207+
208+ There is also a default constructor which also uses `coro::suspend_never`, so that is the default
209+ behavior anywhere that `await_async` is default-initialized with `{}`.
210+
211+ Other than simplification, the big advantage this brings is in the type-erased template constructor.
212+ If you are using another C++20 library or thread/task pool with coroutine support, you can implement
213+ your own `Awaitable` for it and wrap that in `graphql::service::await_async`. It should automatically
214+ start parallelizing all of its resolvers using your custom scheduler, which can pause and resume the
215+ coroutine when and where it likes.
216+
135217## Related Documents
136218
1372191. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md).
0 commit comments