Skip to content

Commit d62207e

Browse files
committed
Update documentation for service::FieldParams
1 parent af152e2 commit d62207e

File tree

1 file changed

+124
-42
lines changed

1 file changed

+124
-42
lines changed

doc/fieldparams.md

Lines changed: 124 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,18 @@ shared state or `directives` from the `query`, so the `resolveField` method
77
also packs that information into a `graphql::service::FieldParams` struct and
88
passes 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

1220
The `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
3724
struct 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.
6249
struct 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

7663
The `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

8190
The `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.
8998
struct RequestState : std::enable_shared_from_this<RequestState>
9099
{
91100
};
@@ -96,16 +105,27 @@ struct RequestState : std::enable_shared_from_this<RequestState>
96105
Each of the `directives` members contains the values of the `directives` and
97106
any of their arguments which were in effect at that scope of the `query`.
98107
Implementers 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

101120
As 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

110130
The implementer does not need to capture the values of `operationDirectives`
111131
or `fragmentDefinitionDirectives` because those are kept alive until the
@@ -132,6 +152,68 @@ this member in their `FieldParams` argument, and they may change their own
132152
behavior 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
137219
1. The `getField` methods are discussed in more detail in [resolvers.md](./resolvers.md).

0 commit comments

Comments
 (0)