From 8c3772b1332c99f6d07fa7f841da678881b90106 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Oct 2025 21:29:35 +0200 Subject: [PATCH 1/6] JobHandle: enable using member functions for handlers ...namely, member functions of the respective job class, or of the result object. --- Quotient/jobs/jobhandle.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Quotient/jobs/jobhandle.h b/Quotient/jobs/jobhandle.h index 5cd0da5e8..c612a5c10 100644 --- a/Quotient/jobs/jobhandle.h +++ b/Quotient/jobs/jobhandle.h @@ -222,17 +222,17 @@ class QUOTIENT_API JobHandle : public QPointer, public QFuture { auto callFn(future_value_type job) { if constexpr (std::invocable) { - return std::forward(fn)(); + return std::invoke(std::forward(fn)); } else { static_assert(AllowJobArg, "onCanceled continuations should not accept arguments"); - if constexpr (requires { fn(job); }) - return fn(job); + if constexpr (std::invocable) + return std::invoke(std::forward(fn), job); else if constexpr (requires { collectResponse(job); }) { static_assert( - requires { fn(collectResponse(job)); }, + std::invocable, "The continuation function must accept either of: 1) no arguments; " "2) the job pointer itself; 3) the value returned by collectResponse(job)"); - return fn(collectResponse(job)); + return std::invoke(std::forward(fn), collectResponse(job)); } } } From 8d991bd44dddc1f196fc9e97fb1100b8feb98eed Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 12 Aug 2025 11:09:55 +0200 Subject: [PATCH 2/6] JobHandle: responseFuture() -> toFuture(); add toFutureExpected() toFutureExpected() doesn't have any users yet. --- Quotient/avatar.cpp | 4 ++-- Quotient/connection.cpp | 2 +- Quotient/jobs/jobhandle.h | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Quotient/avatar.cpp b/Quotient/avatar.cpp index d481f56d2..d86e30230 100644 --- a/Quotient/avatar.cpp +++ b/Quotient/avatar.cpp @@ -63,13 +63,13 @@ QImage Avatar::get(int width, int height, get_callback_t callback) const QFuture Avatar::upload(const QString& fileName) const { d->uploadRequest = d->connection->uploadFile(fileName); - return d->uploadRequest.responseFuture(); + return d->uploadRequest.toFuture(); } QFuture Avatar::upload(QIODevice* source) const { d->uploadRequest = d->connection->uploadContent(source); - return d->uploadRequest.responseFuture(); + return d->uploadRequest.toFuture(); } bool Avatar::isEmpty() const { return d->_url.isEmpty(); } diff --git a/Quotient/connection.cpp b/Quotient/connection.cpp index f9a3b7043..04e67ffed 100644 --- a/Quotient/connection.cpp +++ b/Quotient/connection.cpp @@ -1463,7 +1463,7 @@ QFuture> Connection::setHomeserver(const QUrl& baseUrl) d->loginFlows.clear(); emit loginFlowsChanged(); }); - return d->loginFlowsJob.responseFuture(); + return d->loginFlowsJob.toFuture(); } void Connection::saveRoomState(Room* r) const diff --git a/Quotient/jobs/jobhandle.h b/Quotient/jobs/jobhandle.h index c612a5c10..71641d326 100644 --- a/Quotient/jobs/jobhandle.h +++ b/Quotient/jobs/jobhandle.h @@ -5,6 +5,8 @@ #include #include +#include + namespace Quotient { template @@ -191,9 +193,14 @@ class QUOTIENT_API JobHandle : public QPointer, public QFuture { } //! Get a QFuture for the value returned by `collectResponse()` called on the underlying job - auto responseFuture() + auto toFuture() { - return future_type::then([](auto* j) { return collectResponse(j); }); + return future_type::then([](future_type ft) mutable { + auto *const job = ft.result(); + if (!job->status().good()) + ft.cancel(); + return collectResponse(job); + }); } //! \brief Abandon the underlying job, if there's one pending From 1677d5a82c04544eda6826ebd20c2d104ae0d13b Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Mon, 6 Oct 2025 20:12:03 +0200 Subject: [PATCH 3/6] JobHandle: Allow different return types in then() handlers If the return types are different, then() will now return a future wrapping a std::expected object, instead of failing the build. --- Quotient/jobs/jobhandle.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Quotient/jobs/jobhandle.h b/Quotient/jobs/jobhandle.h index 71641d326..966650af7 100644 --- a/Quotient/jobs/jobhandle.h +++ b/Quotient/jobs/jobhandle.h @@ -298,8 +298,12 @@ class QUOTIENT_API JobHandle : public QPointer, public QFuture { else if constexpr (std::is_same_v) { // Still call fFn to suppress unused lambda warning return job->status().good() ? sFn(job) : (fFn(job), sType{}); - } else + } else if constexpr (std::is_same_v) return job->status().good() ? sFn(job) : fFn(job); + else { + using result_t = std::expected; + return job->status().good() ? result_t(sFn(job)) : std::unexpected(fFn(job)); + } }; } From e77cd547f7969fa5fecccc28fd8a018ee3cd9609 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Oct 2025 19:42:10 +0200 Subject: [PATCH 4/6] JobHandle::toFutureExpected() With 2-arg then() combining outcomes into a std::expected its implementation is primitive as can be. It doesn't have its users yet though, so tread carefully. --- Quotient/jobs/jobhandle.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Quotient/jobs/jobhandle.h b/Quotient/jobs/jobhandle.h index 966650af7..372b444df 100644 --- a/Quotient/jobs/jobhandle.h +++ b/Quotient/jobs/jobhandle.h @@ -203,6 +203,11 @@ class QUOTIENT_API JobHandle : public QPointer, public QFuture { }); } + auto toFutureExpected() + { + return then([] (JobT* j) { return collectResponse(j); }, &BaseJob::status); + } + //! \brief Abandon the underlying job, if there's one pending //! //! Unlike cancel() that only applies to the current future object but not the upstream chain, From 88aa83b2acc9d6338563e0566662d3d213a5d302 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Oct 2025 07:38:18 +0200 Subject: [PATCH 5/6] Expected_Class and JobResult<> The two facilities to make working with std::expected less verbose. E.g. you can now use `const Expected_Class auto&` or even `JobResult` instead of `std::expected` for the accepted type. --- Quotient/jobs/jobhandle.h | 3 +++ Quotient/room.cpp | 5 ++--- Quotient/room.h | 7 ++++--- Quotient/util.h | 8 ++++++++ 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Quotient/jobs/jobhandle.h b/Quotient/jobs/jobhandle.h index 372b444df..a39cd2b66 100644 --- a/Quotient/jobs/jobhandle.h +++ b/Quotient/jobs/jobhandle.h @@ -355,6 +355,9 @@ class QUOTIENT_API JobHandle : public QPointer, public QFuture { template JobT> JobHandle(JobT*) -> JobHandle; +template +using JobResult = std::expected; + } // namespace Quotient Q_DECLARE_SMART_POINTER_METATYPE(Quotient::JobHandle) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index ed8340f72..c7d6d1509 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -1325,14 +1325,13 @@ inline namespace v16 { } } -QFuture> Room::upgrade(QString newVersion, - const QStringList &additionalCreators) +QFuture> Room::upgrade(QString newVersion, const QStringList &additionalCreators) { if (!successorId().isEmpty()) { Q_ASSERT(!successorId().isEmpty()); emit upgradeFailed(tr("The room is already upgraded")); } - using future_t = std::expected; + using future_t = JobResult; return connection() ->callApi(id(), newVersion, additionalCreators) .then( diff --git a/Quotient/room.h b/Quotient/room.h index 01e38ef4d..fd3b3068e 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -820,9 +820,10 @@ class QUOTIENT_API Room : public QObject { //! a new room of the specified version. It is possible to specify \p additionalCreators for //! room versions that support those (unfortunately it is only possible to find out whether //! a given room version supports additional creators by attempting to upgrade a room). - //! \return a future eventually holding a new room once it arrives via sync - QFuture> upgrade( - QString newVersion, const QStringList &additionalCreators = {}); + //! \return a future eventually holding either a new room once it arrives via sync, + //! or the failed upgrade job status if the upgrade wasn't successful + QFuture> upgrade(QString newVersion, + const QStringList &additionalCreators = {}); public Q_SLOTS: /** Check whether the room should be upgraded */ diff --git a/Quotient/util.h b/Quotient/util.h index cd85d8c89..b0e7f3778 100644 --- a/Quotient/util.h +++ b/Quotient/util.h @@ -384,4 +384,12 @@ struct QUOTIENT_API HomeserverData { bool checkMatrixSpecVersion(QStringView targetVersion) const; }; + +//! Basic concept for all specialisations of std::expected +template +concept Expected_Class = requires(T exp) { + exp.value(); + exp.error(); +}; + } // namespace Quotient From 85ddf6123a0a01cc915f1cca92e92ebefdadb3c1 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 7 Oct 2025 19:40:42 +0200 Subject: [PATCH 6/6] Connection: std::expect'ify a couple of API calls getDirectChat() has now got the more robust tryGetDirectChat() counterpart; and joinAndGetRoom(), being effectively unused outside of Quotient, changed its signature on the spot. --- Quotient/connection.cpp | 25 ++++++++++++++++++------- Quotient/connection.h | 9 ++++++--- quotest/quotest.cpp | 11 +++++++---- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Quotient/connection.cpp b/Quotient/connection.cpp index 04e67ffed..7a68e478b 100644 --- a/Quotient/connection.cpp +++ b/Quotient/connection.cpp @@ -672,10 +672,11 @@ JobHandle Connection::joinRoom(const QString& roomAlias, const QStr .then([this](const QString& roomId) { provideRoom(roomId); }); } -QFuture Connection::joinAndGetRoom(const QString& roomAlias, const QStringList& serverNames) +QFuture> Connection::joinAndGetRoom(const QString &roomAlias, + const QStringList &serverNames) { return callApi(roomAlias, serverNames, serverNames) - .then([this](const QString& roomId) { return provideRoom(roomId); }); + .then([this](const QString &roomId) { return provideRoom(roomId); }, &BaseJob::status); } QFuture Connection::waitForNewRoom(const QString &roomId) @@ -839,10 +840,20 @@ JobHandle Connection::createRoom( void Connection::requestDirectChat(const QString& userId) { - getDirectChat(userId).then([this](Room* r) { emit directChatAvailable(r); }); + tryGetDirectChat(userId).then([this](const JobResult &expRoom) { + if (expRoom) + emit directChatAvailable(*expRoom); + }); +} + +QFuture Connection::getDirectChat(const QString &otherUserId) +{ + return tryGetDirectChat(otherUserId).then([](JobResult expRoom) { + return expRoom.value_or(nullptr); + }); } -QFuture Connection::getDirectChat(const QString& otherUserId) +QFuture> Connection::tryGetDirectChat(const QString& otherUserId) { auto* u = user(otherUserId); if (QUO_ALARM_X(!u, u"Couldn't get a user object for" % otherUserId)) @@ -861,7 +872,7 @@ QFuture Connection::getDirectChat(const QString& otherUserId) continue; qCDebug(MAIN) << "Requested direct chat with" << otherUserId << "is already available as" << r->id(); - return QtFuture::makeReadyValueFuture(r); + return QtFuture::makeReadyValueFuture>(r); } if (auto ir = invitation(roomId)) { Q_ASSERT(ir->id() == roomId); @@ -889,9 +900,9 @@ QFuture Connection::getDirectChat(const QString& otherUserId) emit directChatsListChanged({}, removals); } - return createDirectChat(otherUserId).then([this](const QString& roomId) { + return createDirectChat(otherUserId).then([this](const QString &roomId) { return room(roomId, JoinState::Join); - }); + }, &BaseJob::status); } JobHandle Connection::createDirectChat(const QString& userId, const QString& topic, diff --git a/Quotient/connection.h b/Quotient/connection.h index 56d7e7cf8..5e3542355 100644 --- a/Quotient/connection.h +++ b/Quotient/connection.h @@ -615,9 +615,12 @@ class QUOTIENT_API Connection : public QObject { //! \sa LoginFlowsJob, loginFlows, loginFlowsChanged, homeserverChanged Q_INVOKABLE QFuture > setHomeserver(const QUrl& baseUrl); - //! \brief Get a future to a direct chat with the user + [[deprecated("Use tryGetDirectChat() instead")]] Q_INVOKABLE QFuture getDirectChat(const QString& otherUserId); + //! \brief Get a future to a direct chat with the user + Q_INVOKABLE QFuture> tryGetDirectChat(const QString &otherUserId); + //! Create a direct chat with a single user, optional name and topic //! //! A room will always be created, unlike in requestDirectChat. @@ -631,8 +634,8 @@ class QUOTIENT_API Connection : public QObject { Q_INVOKABLE JobHandle joinRoom(const QString& roomAlias, const QStringList& serverNames = {}); - Q_INVOKABLE QFuture joinAndGetRoom(const QString& roomAlias, - const QStringList& serverNames = {}); + Q_INVOKABLE QFuture> joinAndGetRoom(const QString &roomAlias, + const QStringList &serverNames = {}); Q_INVOKABLE QFuture waitForNewRoom(const QString &roomId); diff --git a/quotest/quotest.cpp b/quotest/quotest.cpp index f5e8f8982..4fa1b195b 100644 --- a/quotest/quotest.cpp +++ b/quotest/quotest.cpp @@ -313,15 +313,18 @@ void TestManager::setupAndRun(const QString& targetRoomAlias) c->setLazyLoading(true); qInfo() << "Joining" << targetRoomAlias; - c->joinAndGetRoom(targetRoomAlias).then(this, [this](Room* room) { - if (!room) { - qCritical() << "Failed to join the test room"; + c->joinAndGetRoom(targetRoomAlias) + .then(this, [this](const JobResult &expectedRoom) { + if (!expectedRoom) { + auto logLine = qCritical(); + logLine << "Failed to join the test room: "; + expectedRoom.error().dumpToLog(logLine); finalize(); return; } // Ensure that the room has been joined and filled with some events // so that other tests could use that - testSuite = new TestSuite(room, origin, this); + testSuite = new TestSuite(*expectedRoom, origin, this); // Only start the sync after joining, to make sure the room just // joined is in it c->syncLoop();