Skip to content

Commit

Permalink
Room::ensureEvent()
Browse files Browse the repository at this point in the history
Pulls out the logic recently introduced in Quotest, to the library so
that everybody could benefit from it. The part of the logic triggered by
the historical batch arrival is now executed directly from
getPreviousContent() instead of connecting to a signal.
  • Loading branch information
KitsuneRal committed Dec 16, 2024
1 parent 58e13d2 commit 0cb2171
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 21 deletions.
7 changes: 5 additions & 2 deletions Quotient/eventitem.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ namespace EventStatus {
Q_ENUM_NS(Code)
} // namespace EventStatus

using EventPromise = QPromise<std::reference_wrapper<const RoomEvent>>;
using EventFuture = QFuture<std::reference_wrapper<const RoomEvent>>;

class QUOTIENT_API EventItemBase {
public:
using value_type = RoomEvent;
Expand Down Expand Up @@ -98,7 +101,7 @@ class QUOTIENT_API TimelineItem : public EventItemBase {

class QUOTIENT_API PendingEventItem : public EventItemBase {
public:
using future_type = QFuture<std::reference_wrapper<const RoomEvent>>;
using future_type = EventFuture;

explicit PendingEventItem(RoomEventPtr&& e) : EventItemBase(std::move(e))
{
Expand Down Expand Up @@ -141,7 +144,7 @@ class QUOTIENT_API PendingEventItem : public EventItemBase {
EventStatus::Code _status = EventStatus::Submitted;
QDateTime _lastUpdated = QDateTime::currentDateTimeUtc();
QString _annotation;
QPromise<std::reference_wrapper<const RoomEvent>> _promise;
EventPromise _promise;

void setStatus(EventStatus::Code status)
{
Expand Down
90 changes: 87 additions & 3 deletions Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,16 @@
using namespace Quotient;
using std::llround;

namespace {
enum EventsPlacement : int { Older = -1, Newer = 1 };

struct HistoryRequest {
EventId upToEventId;
QDeadlineTimer deadline;
EventPromise promise{};
};
}

class Q_DECL_HIDDEN Room::Private {
public:
Private(Connection* c, QString id_, JoinState initialJoinState)
Expand Down Expand Up @@ -149,6 +157,7 @@ class Q_DECL_HIDDEN Room::Private {
std::optional<QString> prevBatch = QString();
int lastRequestedHistorySize = 0;
JobHandle<GetRoomEventsJob> eventsHistoryJob;
std::vector<HistoryRequest> historyRequests;
JobHandle<GetMembersByRoomJob> allMembersJob;
//! Map from megolm sessionId to set of eventIds
std::unordered_map<QString, QSet<QString>> undecryptedEvents;
Expand Down Expand Up @@ -223,6 +232,7 @@ class Q_DECL_HIDDEN Room::Private {
Timeline::const_iterator syncEdge() const { return timeline.cend(); }

JobHandle<GetRoomEventsJob> getPreviousContent(int limit = 10, const QString &filter = {});
void checkForRequestedEvents(const rev_iter_t& from, bool allHistoryLoaded);

Changes updateStateFrom(StateEvents&& events)
{
Expand Down Expand Up @@ -2372,6 +2382,80 @@ JobHandle<GetRoomEventsJob> Room::getPreviousContent(int limit, const QString& f
return d->getPreviousContent(limit, filter);
}

EventFuture Room::ensureEvent(const QString& eventId, quint16 maxWaitSeconds)
{
if (auto eventIt = findInTimeline(eventId); eventIt != historyEdge())
return makeReadyValueFuture(std::cref(**eventIt));

if (allHistoryLoaded())
return {};
// Request a small number of events (or whatever the ongoing request says, if there's any),
// to make sure checkForRequestedEvents() gets executed
getPreviousContent();
HistoryRequest r{ eventId,
QDeadlineTimer{ std::chrono::seconds(maxWaitSeconds), Qt::VeryCoarseTimer } };
auto future = r.promise.future();
d->historyRequests.push_back(std::move(r));
return future;
}

namespace {
template <typename RangeT>
inline auto dumpJoined(RangeT&& range, const QString& separator = u","_s)
requires(std::convertible_to<std::ranges::range_reference_t<RangeT>, QString>)
{
return [r = std::forward<RangeT>(range), &separator](QDebug dbg) mutable {
QDebugStateSaver _(dbg);
dbg.noquote();
#if defined(__cpp_lib_ranges_join_with) && defined(__cpp_lib_ranges_to_container)
dbg << std::ranges::to<QString>(std::views::join_with(r, separator));
#else
dbg << QStringList(begin(r), end(r)).join(separator);
#endif
return dbg;
};
}
}

void Room::Private::checkForRequestedEvents(const rev_iter_t& from, bool allHistoryLoaded)
{
using namespace std::ranges;
std::erase_if(historyRequests, [this, from](HistoryRequest& request) {
auto& [upToEventId, deadline, promise] = request;
if (promise.isCanceled()) {
qCInfo(MESSAGES) << "The request to ensure event" << upToEventId << "has been cancelled";
return true;
}
if (auto it = find(from, historyEdge(), upToEventId, &RoomEvent::id); it != historyEdge()) {
promise.addResult(std::cref(**it));
promise.finish();
return true;
}
if (deadline.hasExpired()) {
qCWarning(MESSAGES) << "Timeout - giving up on obtaining event" << upToEventId;
promise.future().cancel();
return true;
}
return false;
});
if (!historyRequests.empty()) {
auto requestedIds =
dumpJoined(std::views::transform(historyRequests, &HistoryRequest::upToEventId));
if (allHistoryLoaded) {
qCDebug(MESSAGES) << "Could not find in the whole room history:" << requestedIds;
for_each(historyRequests, [](auto& r) { r.promise.future().cancel(); });
historyRequests.clear();
}
static constexpr auto EventsProgression = std::array{ 50, 100, 200, 500, 1000 };
static_assert(is_sorted(EventsProgression));
const auto thisMany = lastRequestedHistorySize >= EventsProgression.back()
? EventsProgression.back()
: *upper_bound(EventsProgression, lastRequestedHistorySize);
qCDebug(MESSAGES) << "Requesting" << thisMany << "events, looking for" << requestedIds;
getPreviousContent(thisMany);
}
}

JobHandle<GetRoomEventsJob> Room::Private::getPreviousContent(int limit, const QString& filter)
{
if (!prevBatch)
Expand All @@ -2384,7 +2468,7 @@ JobHandle<GetRoomEventsJob> Room::Private::getPreviousContent(int limit, const Q
eventsHistoryJob =
connection->callApi<GetRoomEventsJob>(id, "b"_L1, *prevBatch, QString(), limit, filter);
emit q->eventsHistoryJobChanged();
connect(eventsHistoryJob, &BaseJob::success, q, [this] {
eventsHistoryJob.then([this] {
if (const auto newPrevBatch = eventsHistoryJob->end();
!newPrevBatch.isEmpty() && *prevBatch != newPrevBatch) //
{
Expand All @@ -2403,9 +2487,9 @@ JobHandle<GetRoomEventsJob> Room::Private::getPreviousContent(int limit, const Q
changes |= updateStats(from, historyEdge());
if (changes > 0)
postprocessChanges(changes);
checkForRequestedEvents(from, !prevBatch);
});
connect(eventsHistoryJob, &QObject::destroyed, q,
&Room::eventsHistoryJobChanged);
connect(eventsHistoryJob, &QObject::destroyed, q, &Room::eventsHistoryJobChanged);
return eventsHistoryJob;
}

Expand Down
16 changes: 16 additions & 0 deletions Quotient/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,22 @@ class QUOTIENT_API Room : public QObject {

QJsonArray exportMegolmSessions();

//! \brief Loads the message history until the specified event id is found
//!
//! This is potentially heavy; clients should use this sparingly. One intended use case is
//! loading the timeline until the last read event, assuming that the last read event is
//! not too far back and that the user will read or at least scroll through the just loaded
//! events anyway. This will not be necessary once we move to sliding sync but sliding sync
//! support is still a bit away in the future.
//!
//! Because the process is heavy (particularly on the homeserver), ensureEvent() will cancel
//! after \p maxWaitSeconds. Clients may opt to reduce this number; it is not recommended
//! to increase it, as most users will give up waiting much earlier than even the default value.
//! \return the future that resolves to the event with \p eventId, or self-cancels if the event
//! is not found
Q_INVOKABLE Quotient::EventFuture ensureEvent(const QString& eventId,
quint16 maxWaitSeconds = 20);

public Q_SLOTS:
/** Check whether the room should be upgraded */
void checkVersion();
Expand Down
31 changes: 15 additions & 16 deletions quotest/quotest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -587,18 +587,6 @@ TEST_IMPL(setTopic)
return false;
}

// TODO: maybe move it to Room?..
QFuture<void> ensureEvent(Room* room, const QString& evtId, QPromise<void>&& p = QPromise<void>{})
{
auto future = p.future();
if (room->findInTimeline(evtId) == room->historyEdge()) {
clog << "Loading a page of history, " << room->timelineSize() << " events so far\n";
room->getPreviousContent().then(std::bind_front(ensureEvent, room, evtId, std::move(p)));
} else
p.finish();
return future;
}

TEST_IMPL(redactEvent)
{
using TargetEventType = RoomMemberEvent;
Expand All @@ -613,9 +601,18 @@ TEST_IMPL(redactEvent)
Q_ASSERT(memberEventToRedact); // ...or the room state is totally screwed
const auto& evtId = memberEventToRedact->id();

// Make sure the event is loaded in the timeline before proceeding with the test, to make sure
// the replacement tracked below actually occurs
ensureEvent(targetRoom, evtId).then([this, thisTest, evtId] {
// Make sure the event is loaded in the timeline before proceeding with the test, so that
// Room::replacedEvent is actually emitted
targetRoom->ensureEvent(evtId).then([this, thisTest, evtId](const RoomEvent& checkEvt) {
auto it = targetRoom->findInTimeline(evtId);
if (it == targetRoom->historyEdge()) {
clog << "Room::ensureEvent() failed to actually ensure the event in the timeline\n";
FAIL_TEST();
}
if (it->event() != &checkEvt) {
clog << "Room::ensureEvent() resolved to a different event than expected\n";
FAIL_TEST();
}
clog << "Redacting the latest member event" << endl;
targetRoom->redactEvent(evtId, origin);
connectUntil(targetRoom, &Room::replacedEvent, this,
Expand All @@ -625,11 +622,13 @@ TEST_IMPL(redactEvent)
if (evt->id() != evtId)
return false;
FINISH_TEST(evt->switchOnType([this](const TargetEventType& e) {
return e.redactionReason() == origin && e.membership() == Membership::Join;
return e.redactionReason() == origin
&& e.membership() == Membership::Join;
// The second condition above tests MSC2176 - if it's violated (pre 0.8
// beta), membership() ends up being Membership::Undefined
}));
});
return false;
});

return false;
Expand Down

0 comments on commit 0cb2171

Please sign in to comment.