libQuotient
A Qt library for building matrix clients
Loading...
Searching...
No Matches
jobhandle.h
Go to the documentation of this file.
1
#
pragma
once
2
3
#
include
"basejob.h"
4
5
#
include
<
QtCore
/
QFuture
>
6
7
namespace
Quotient
{
8
9
template
<
typename
FnT,
typename
JobT>
10
concept BoundResultHandler =
std
::
invocable
<
FnT
,
JobT
*> ||
std
::
invocable
<
FnT
>
11
||
requires
(
FnT
f
,
JobT
j
) {
f
(
collectResponse
(&
j
)); };
12
13
template
<
typename
FnT,
typename
JobT>
14
concept ResultHandler =
BoundResultHandler
<
FnT
,
JobT
> ||
std
::
is_member_function_pointer_v
<
FnT
>;
15
16
//! \brief A job pointer and a QFuture in a single package
17
//!
18
//! This class wraps a pointer to any job the same way QPointer does: it turns to nullptr when
19
//! the job is destroyed. On top of that though, it provides you with an interface of QFuture as-if
20
//! obtained by calling `QtFuture::connect(job, &BaseJob::result).then([job] { return job; });`
21
//! before any other slot is connected to it. In the end, you (still) get the interface of \p JobT
22
//! at `handle->`, and `handle.` gives you the interface (very close to that, read below for
23
//! differences) of `QFuture<JobT*>`.
24
//!
25
//! You can mix usage of the two interfaces, bearing in mind that any continuation attached via
26
//! the future interface will overtake anything connected to `BaseJob::result` but come behind
27
//! anything connected to `BaseJob::finished` (that applies to `onCanceled()`, too).
28
//!
29
//! QFuture is somewhat rigid in terms of what it accepts for (normal, i.e. not cancelled)
30
//! continuations: the continuation function must accept a single argument of the type carried by
31
//! the future, or of the future type itself. JobHandle allows normal continuation functions (i.e.
32
//! those passed to `then()`, `onResult()` and `onFailure()`) to accept:
33
//! - no parameters;
34
//! - `JobT*` or any pointer it is convertible to (`const JobT*`, `BaseJob*` etc.);
35
//! - the value returned by calling `collectResponse()` with the above pointer as the parameter.
36
//!
37
//! Aside from free functions and function objects (including lambdas), you can also pass member
38
//! functions of QObject-derived classes (`connect()` slot style) to all continuations, including
39
//! onCanceled().
40
//!
41
//! JobHandle doesn't support passing its full type to continuation functions like QFuture does,
42
//! as there's no case for that (we don't need to deal with exceptions).
43
//!
44
//! This extended interface helps with migration of the current code that `connect()`s to the job
45
//! completion signals. The existing code will (mostly) run fine without changes; the only thing
46
//! that will stop working is using `auto*` for a variable initialised from `Connection::callApi`
47
//! (plain `auto` still works). If you want to migrate the existing code to the future-like
48
//! interface, just replace:
49
//! \code
50
//! auto j = callApi<Job>(jobParams...);
51
//! connect(j, &BaseJob::result, object, slot);
52
//! \endcode
53
//! with `callApi<Job>(jobParams...).onResult(object, slot);` - that's all. If you have a connection
54
//! to `BaseJob::success`, use `then()` instead of `onResult()`, and if you only connect to
55
//! `BaseJob::failure`, `onFailure()` is at your service. And you can also combine the two using
56
//! `then()`, e.g.:
57
//! \code
58
//! callApi<Job>(jobParams...).then([this] { /* on success... */ },
59
//! [this] { /* on failure... */ });
60
//! \endcode
61
//!
62
//! One more extension to QFuture is the way the returned value is treated:
63
//! - if your function returns `void` the continuation will have type `JobHandler<JobT>` and carry
64
//! the same pointer as before;
65
//! - if your function returns a `JobHandle` (e.g. from another call to `Connection::callApi`),
66
//! it will be automatically rewrapped into a `QFuture`, because `QFuture<JobHandle<AnotherJobT>>`
67
//! is rather unwieldy for any intents and purposes, and `JobHandle<AnotherJobT>` would have
68
//! a very weird QPointer interface as that new job doesn't even exist when continuation is
69
//! constructed;
70
//! - otherwise, the return value is wrapped in a "normal" QFuture, JobHandle waves you good-bye and
71
//! further continuations will follow pristine QFuture rules.
72
template
<
class
JobT
>
73
class
QUOTIENT_API
JobHandle
:
public
QPointer
<
JobT
>,
public
QFuture
<
JobT
*> {
74
public
:
75
using
pointer_type
=
QPointer
<
JobT
>;
76
using
future_value_type
=
JobT
*;
77
using
future_type
=
QFuture
<
future_value_type
>;
78
79
private
:
80
//! A placeholder structure with a private type, co-sitting as a no-op function object
81
struct
Skip
:
public
decltype
([](
future_value_type
) {}) {};
82
83
JobHandle
(
JobT
*
job
,
future_type
&&
futureToWrap
)
84
:
pointer_type
(
job
),
future_type
(
std
::
move
(
futureToWrap
))
85
{}
86
87
static
future_type
setupFuture
(
JobT
*
job
)
88
{
89
return
job
?
job
->
future
().
then
([
job
] {
return
future_value_type
{
job
}; }) :
future_type
{};
90
}
91
92
public
:
93
Q_IMPLICIT
JobHandle
(
JobT
*
job
=
nullptr
) :
JobHandle
(
job
,
setupFuture
(
job
)) {}
94
95
//! \brief Attach a continuation to a successful or unsuccessful completion of the future
96
//!
97
//! The continuation passed via \p fn should be an invokable that accepts one of the following:
98
//! 1) no arguments - this is meant to simplify transition from job completion handlers
99
//! connect()ed to BaseJob::result, BaseJob::success or BaseJob::failure.
100
//! 2) a pointer to a (const, if you want) job object - this can be either `BaseJob*`,
101
//! `JobT*` (recommended), or anything in between. Unlike slot functions connected
102
//! to BaseJob signals, this option allows you to access the specific job type so you don't
103
//! need to carry the original job pointer in a lambda - JobHandle does it for you. This is
104
//! meant to be a transitional form on the way to (3); eventually we should migrate to
105
//! (1)+(3) entirely.
106
//! 3) the type returned by `collectResponse()` if it's well-formed (it is for all generated
107
//! jobs, needs overloading for manual jobs).
108
//!
109
//! \note The continuation returned from onResult() will not be triggered if/when the future is
110
//! cancelled or the underlying job is abandoned; use onCanceled() to catch cancellations.
111
//! You can also connect to BaseJob::finished using the QPointer interface of the handle
112
//! if you need to do something before _any_ continuation attached to his job kicks in.
113
//!
114
//! \param config passed directly to QFuture::then() as the first argument (see
115
//! the documentation on QFuture::then() for accepted types) and can also be used
116
//! as the object for a slot-like member function in QObject::connect() fashion
117
//! \param fn the continuation function to attach to the future; can be a member function
118
//! if \p config is a pointer to an QObject-derived class
119
//! \return if \p fn returns `void`, a new JobHandle for the same job, with the continuation
120
//! attached; otherwise, the return value of \p fn wrapped in a plain QFuture
121
template
<
typename
ConfigT
,
ResultHandler
<
JobT
>
FnT
>
122
auto
onResult
(
ConfigT
config
,
FnT
&&
fn
)
123
{
124
return
rewrap
(
future_type
::
then
(
config
,
continuation
(
std
::
forward
<
FnT
>(
fn
),
config
)));
125
}
126
127
//! The overload for onResult matching 1-arg QFuture::then
128
template
<
BoundResultHandler
<
JobT
>
FnT
>
129
auto
onResult
(
FnT
&&
fn
)
130
{
131
return
rewrap
(
future_type
::
then
(
continuation
(
std
::
forward
<
FnT
>(
fn
))));
132
}
133
134
//! \brief Attach continuations depending on the job success or failure
135
//!
136
//! This is inspired by `then()` in JavaScript; beyond the first argument passed through to
137
//! `QFuture::then`, it accepts two more arguments (\p onFailure is optional), combining them
138
//! in a single continuation: if the job ends with success, \p onSuccess is called; if the job
139
//! fails, \p onFailure is called. The requirements to both functions are the same as to the
140
//! single function passed to onResult().
141
template
<
typename
ConfigT
,
ResultHandler
<
JobT
>
SuccessFnT
,
ResultHandler
<
JobT
>
FailureFnT
=
Skip
>
142
auto
then
(
ConfigT
config
,
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
= {})
143
requires
requires
(
future_type
f
) {
f
.
then
(
config
,
Skip
{}); }
144
{
145
return
rewrap
(
future_type
::
then
(
146
config
,
combineContinuations
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
147
std
::
forward
<
FailureFnT
>(
onFailure
),
config
)));
148
}
149
150
//! The overload making the combined continuation as if with 1-arg QFuture::then
151
template
<
BoundResultHandler
<
JobT
>
SuccessFnT
,
BoundResultHandler
<
JobT
>
FailureFnT
=
Skip
>
152
auto
then
(
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
= {})
153
{
154
return
rewrap
(
future_type
::
then
(
combineContinuations
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
155
std
::
forward
<
FailureFnT
>(
onFailure
))));
156
}
157
158
//! Same as then(config, [] {}, fn)
159
template
<
typename
FnT
>
160
auto
onFailure
(
auto
config
,
FnT
&&
fn
)
161
{
162
return
then
(
config
,
Skip
{},
std
::
forward
<
FnT
>(
fn
));
163
}
164
165
//! Same as then([] {}, fn)
166
template
<
typename
FnT
>
167
auto
onFailure
(
FnT
&&
fn
)
168
{
169
return
then
(
Skip
{},
std
::
forward
<
FnT
>(
fn
));
170
}
171
172
//! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
173
//! returned values
174
template
<
typename
FnT
>
175
auto
onCanceled
(
QObject
*
context
,
FnT
&&
fn
)
176
{
177
return
rewrap
(
178
future_type
::
onCanceled
(
context
,
bindToContext
(
std
::
forward
<
FnT
>(
fn
),
context
)));
179
}
180
181
//! Same as QFuture::onCanceled but accepts QObject-derived member functions and rewraps
182
//! returned values
183
template
<
typename
FnT
>
184
auto
onCanceled
(
FnT
&&
fn
)
185
{
186
return
rewrap
(
future_type
::
onCanceled
(
BoundFn
{
std
::
forward
<
FnT
>(
fn
) }));
187
}
188
189
//! Get a QFuture for the value returned by `collectResponse()` called on the underlying job
190
auto
responseFuture
()
191
{
192
return
future_type
::
then
([](
auto
*
j
) {
return
collectResponse
(
j
); });
193
}
194
195
//! \brief Abandon the underlying job, if there's one pending
196
//!
197
//! Unlike cancel() that only applies to the current future object but not the upstream chain,
198
//! this actually goes up to the job and calls abandon() on it, thereby cancelling the entire
199
//! chain of futures attached to it.
200
//! \sa BaseJob::abandon
201
void
abandon
()
202
{
203
if
(
auto
pJob
=
pointer_type
::
get
();
isJobPending
(
pJob
)) {
204
Q_ASSERT
(
QThread
::
currentThread
() ==
pJob
->
thread
());
205
pJob
->
abandon
();
// Triggers cancellation of the future
206
}
207
}
208
209
private
:
210
//! A function object that can be passed to QFuture::then and QFuture::onCanceled
211
template
<
typename
FnT
>
212
struct
BoundFn
{
213
auto
operator
()() {
return
callFn
<
false
>(
nullptr
); }
// For QFuture::onCanceled
214
auto
operator
()(
future_value_type
job
) {
return
callFn
(
job
); }
// For QFuture::then
215
216
template
<
bool
AllowJobArg
=
true
>
217
auto
callFn
(
future_value_type
job
)
218
{
219
if
constexpr
(
std
::
invocable
<
FnT
>) {
220
return
std
::
forward
<
FnT
>(
fn
)();
221
}
else
{
222
static_assert
(
AllowJobArg
,
"onCanceled continuations should not accept arguments"
);
223
if
constexpr
(
requires
{
fn
(
job
); })
224
return
fn
(
job
);
225
else
if
constexpr
(
requires
{
collectResponse
(
job
); }) {
226
static_assert
(
227
requires
{
fn
(
collectResponse
(
job
)); },
228
"The continuation function must accept either of: 1) no arguments; "
229
"2) the job pointer itself; 3) the value returned by collectResponse(job)"
);
230
return
fn
(
collectResponse
(
job
));
231
}
232
}
233
}
234
235
// See https://www.cppstories.com/2021/no-unique-address/
236
#
ifndef
Q_CC_CLANG
237
// Apple Clang crashes with ICE and vanilla Clang 17 generates faulty code if fn has no
238
// unique address. https://github.com/llvm/llvm-project/issues/59831 might be related.
239
[[
no_unique_address
]]
240
#
endif
241
FnT
fn
;
242
};
243
244
template
<
typename
FnT
>
245
BoundFn
(
FnT
&&) ->
BoundFn
<
FnT
>;
246
247
template
<
typename
FnT
,
typename
ConfigT
=
Skip
>
248
static
auto
bindToContext
(
FnT
&&
fn
,
ConfigT
config
= {})
249
{
250
// Even though QFuture::then() and QFuture::onCanceled() can use context QObjects
251
// to determine the execution thread, they cannot bind slots to these context objects,
252
// the way QObject::connect() does; so we do it here.
253
if
constexpr
(
std
::
derived_from
<
std
::
remove_pointer_t
<
ConfigT
>,
QObject
>
254
&&
std
::
is_member_function_pointer_v
<
FnT
>) {
255
return
BoundFn
{
std
::
bind_front
(
std
::
forward
<
FnT
>(
fn
),
config
) };
256
}
else
257
return
BoundFn
{
std
::
forward
<
FnT
>(
fn
) };
258
}
259
260
template
<
ResultHandler
<
JobT
>
FnT
,
typename
ConfigT
=
Skip
>
261
static
auto
continuation
(
FnT
&&
fn
,
ConfigT
config
= {})
262
{
263
return
[
f
=
bindToContext
(
std
::
forward
<
FnT
>(
fn
),
config
)](
future_value_type
arg
)
mutable
{
264
if
constexpr
(
std
::
is_void_v
<
decltype
(
f
(
arg
))>) {
265
f
(
arg
);
266
return
arg
;
267
}
else
268
return
f
(
arg
);
269
};
270
}
271
272
template
<
ResultHandler
<
JobT
>
SuccessFnT
,
ResultHandler
<
JobT
>
FailureFnT
,
typename
ConfigT
=
Skip
>
273
static
auto
combineContinuations
(
SuccessFnT
&&
onSuccess
,
FailureFnT
&&
onFailure
,
274
ConfigT
config
= {})
275
{
276
return
[
sFn
=
bindToContext
(
std
::
forward
<
SuccessFnT
>(
onSuccess
),
config
),
277
fFn
=
bindToContext
(
std
::
forward
<
FailureFnT
>(
onFailure
),
config
)](
278
future_value_type
job
)
mutable
{
279
using
sType
=
decltype
(
sFn
(
job
));
280
using
fType
=
decltype
(
fFn
(
job
));
281
if
constexpr
(
std
::
is_void_v
<
sType
> &&
std
::
is_void_v
<
fType
>)
282
return
(
job
->
status
().
good
() ?
sFn
(
job
) :
fFn
(
job
),
job
);
283
else
if
constexpr
(
std
::
is_same_v
<
FailureFnT
,
Skip
>) {
284
// Still call fFn to suppress unused lambda warning
285
return
job
->
status
().
good
() ?
sFn
(
job
) : (
fFn
(
job
),
sType
{});
286
}
else
287
return
job
->
status
().
good
() ?
sFn
(
job
) :
fFn
(
job
);
288
};
289
}
290
291
auto
rewrap
(
future_type
&&
ft
)
const
292
{
293
return
JobHandle
(
pointer_type
::
get
(),
std
::
move
(
ft
));
294
}
295
296
template
<
typename
NewJobT
>
297
auto
rewrap
(
QFuture
<
JobHandle
<
NewJobT
>>
ft
)
298
->
QFuture
<
typename
JobHandle
<
NewJobT
>::
future_value_type
>
299
{
300
// When a continuation function returns a job handle (e.g. by invoking callApi() inside of
301
// it) that handle ends up being wrapped in a QFuture by QFuture::then() or
302
// QFuture::onCanceled() called internally from their JobHandle counterparts. In a pure
303
// QFuture world, there's QFuture::unwrap() to flatten the nested futures; unfortunately,
304
// QFuture::unwrap() requires the nested object to be exactly a QFuture, not anything
305
// derived from it. This method basically does what QFuture::unwrap() does, but is much
306
// simpler because we don't need to deal with exceptions and already know the nested type.
307
// It still returns QFuture and not JobHandle. Were it a JobHandle, its QPointer interface
308
// would be rather confusing: initially it would be nullptr because the job doesn't even
309
// exist when the continuation is constructed, and only later it would change its value
310
// to something useful. Unless the client code stored the original JobHandle, it would
311
// lose that change and only store nullptr; and if it stores a JobHandle then it can just
312
// use the QFuture interface instead. Therefore a pure QFuture is returned instead, that
313
// settles when the underlying job finishes or gets cancelled.
314
QFutureInterface
<
typename
JobHandle
<
NewJobT
>::
future_value_type
>
newPromise
(
315
QFutureInterfaceBase
::
State
::
Pending
);
316
ft
.
then
([
newPromise
](
JobHandle
<
NewJobT
>
nestedHandle
)
mutable
{
317
Q_ASSERT
(
nestedHandle
.
isStarted
());
318
newPromise
.
reportStarted
();
319
nestedHandle
.
then
([
newPromise
]()
mutable
{
newPromise
.
reportFinished
(); })
320
.
onCanceled
([
newPromise
]()
mutable
{
newPromise
.
cancelAndFinish
(); });
321
}).
onCanceled
([
newPromise
]()
mutable
{
322
newPromise
.
reportStarted
();
323
newPromise
.
cancelAndFinish
();
324
});
325
return
newPromise
.
future
();
326
}
327
328
static
auto
rewrap
(
auto
someOtherFuture
) {
return
someOtherFuture
; }
329
};
330
331
template
<
std
::
derived_from
<
BaseJob
>
JobT
>
332
JobHandle
(
JobT
*) ->
JobHandle
<
JobT
>;
333
334
}
// namespace Quotient
335
336
Q_DECLARE_SMART_POINTER_METATYPE
(
Quotient
::
JobHandle
)
Quotient::JobHandle
A job pointer and a QFuture in a single package.
Definition
jobhandle.h:73
Quotient
Definition
accountregistry.h:13
QUOTIENT_API
#define QUOTIENT_API
Definition
quotient_export.h:22
Quotient
jobs
jobhandle.h
Generated by
1.9.8