Page MenuHomePhorge

D241.1760161874.diff
No OneTemporary

Size
24 KB
Referenced Files
None
Subscribers
None

D241.1760161874.diff

diff --git a/src/base/kazvevents.hpp b/src/base/kazvevents.hpp
--- a/src/base/kazvevents.hpp
+++ b/src/base/kazvevents.hpp
@@ -324,6 +324,25 @@
std::string error;
};
+ /**
+ * Indicate that there are events to be saved.
+ */
+ struct SaveEventsRequested
+ {
+ /**
+ * The events to be saved in the timeline.
+ * The events saved in the timeline part of the storage can be considered continuous, i.e. they can be
+ * loaded to fully re-construct the timeline. There can be gaps,
+ * but all gaps are properly recorded in the client state so that
+ * it can be easily paginated.
+ */
+ immer::map<std::string /* roomId */, EventList> timelineEvents;
+ /**
+ * The events that should be saved, but not in the timeline.
+ */
+ immer::map<std::string /* roomId */, EventList> nonTimelineEvents;
+ };
+
struct UnrecognizedResponse
{
Response response;
@@ -382,6 +401,8 @@
UploadIdentityKeysSuccessful, UploadIdentityKeysFailed,
UploadOneTimeKeysSuccessful, UploadOneTimeKeysFailed,
ClaimKeysSuccessful, ClaimKeysFailed,
+ // storage
+ SaveEventsRequested,
// general
UnrecognizedResponse,
diff --git a/src/base/serialization/immer-set.hpp b/src/base/serialization/immer-set.hpp
new file mode 100644
--- /dev/null
+++ b/src/base/serialization/immer-set.hpp
@@ -0,0 +1,58 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2021-2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+
+#pragma once
+
+#include <libkazv-config.hpp>
+
+#include <boost/serialization/nvp.hpp>
+#include <boost/serialization/split_free.hpp>
+
+#include <immer/set.hpp>
+#include <immer/set_transient.hpp>
+
+namespace boost::serialization
+{
+
+ template <class Archive, class T, class H, class E, class MP>
+ void save(Archive &ar, const immer::set<T, H, E, MP> &s, const unsigned int /* version */)
+ {
+ auto size = s.size();
+ ar << BOOST_SERIALIZATION_NVP(size);
+ for (const auto &k : s) {
+ ar << k;
+ }
+ }
+
+ template <class Archive, class T, class H, class E, class MP>
+ void load(Archive &ar, immer::set<T, H, E, MP> &s, const unsigned int /* version */)
+ {
+ using TransientT = decltype(s.transient());
+ using SizeT = decltype(s.size());
+
+ TransientT t{};
+
+ SizeT size{};
+ ar >> BOOST_SERIALIZATION_NVP(size);
+
+ for (auto i = SizeT{}; i < size; ++i) {
+ T k;
+ ar >> k;
+ t.insert(std::move(k));
+ }
+
+ assert(size == t.size());
+ s = t.persistent();
+ }
+
+ template<class Archive, class T, class H, class E, class MP>
+ inline void serialize(Archive &ar, immer::set<T, H, E, MP> &s, const unsigned int version)
+ {
+ boost::serialization::split_free(ar, s, version);
+ }
+
+}
diff --git a/src/client/client-model.hpp b/src/client/client-model.hpp
--- a/src/client/client-model.hpp
+++ b/src/client/client-model.hpp
@@ -193,6 +193,8 @@
return triggers;
}
+ void maybeAddSaveEventsTrigger(const ClientModel &old);
+
using Action = ClientAction;
using Effect = ClientEffect;
using Result = ClientResult;
diff --git a/src/client/client-model.cpp b/src/client/client-model.cpp
--- a/src/client/client-model.cpp
+++ b/src/client/client-model.cpp
@@ -43,6 +43,8 @@
auto oldClient = m;
auto oldDeviceLists = m.deviceLists;
+ auto actionIsStorage = std::holds_alternative<LoadEventsFromStorageAction>(a) || std::holds_alternative<PurgeRoomTimelineAction>(a);
+
auto [newClient, effect] = lager::match(std::move(a))(
[&](RoomListAction a) -> Result {
m.roomList = RoomListModel::update(std::move(m.roomList), a);
@@ -120,6 +122,12 @@
newClient.maybeRotateSessions(oldClient);
+ // if it is an storage action, do not add it back because the things
+ // should be the same as the ones in storage
+ if (!actionIsStorage) {
+ newClient.maybeAddSaveEventsTrigger(oldClient);
+ }
+
return { std::move(newClient), std::move(effect) };
}
@@ -423,4 +431,64 @@
{
return crypto.value().get();
}
+
+ void ClientModel::maybeAddSaveEventsTrigger(const ClientModel &old)
+ {
+ SaveEventsRequested trigger;
+ using ELT = immer::flex_vector_transient<Event>;
+ auto addToTrigger = [&trigger](const std::string &roomId, ELT tl, ELT nonTl) {
+ if (!tl.empty()) {
+ trigger.timelineEvents = std::move(trigger.timelineEvents).set(roomId, std::move(tl).persistent());
+ }
+ if (!nonTl.empty()) {
+ trigger.nonTimelineEvents = std::move(trigger.nonTimelineEvents).set(roomId, std::move(nonTl).persistent());
+ }
+ };
+ immer::diff(old.roomList.rooms, roomList.rooms, immer::make_differ(
+ /* addedFn = */ [&addToTrigger](const auto &p) {
+ const auto &[roomId, room] = p;
+
+ ELT tl;
+ ELT nonTl;
+ for (const auto &[id, e] : room.messages) {
+ if (room.isInTimeline(id)) {
+ tl.push_back(e);
+ } else {
+ nonTl.push_back(e);
+ }
+ }
+
+ addToTrigger(roomId, std::move(tl), std::move(nonTl));
+ },
+ /* removedFn = */ [](auto &&) {},
+ /* changedFn = */ [&addToTrigger](const auto &p1, const auto &p2) {
+ const auto &roomId = p2.first;
+ const auto &newRoom = p2.second;
+ const auto &oldRoom = p1.second;
+
+ ELT tl;
+ ELT nonTl;
+ auto addEvent = [&tl, &nonTl, &newRoom](const auto &newPair) {
+ const auto &[eventId, event] = newPair;
+ if (newRoom.isInTimeline(eventId)) {
+ tl.push_back(event);
+ } else {
+ nonTl.push_back(event);
+ }
+ };
+ immer::diff(oldRoom.messages, newRoom.messages, immer::make_differ(
+ /* addedFn = */ addEvent,
+ /* removedFn = */ [](auto &&) {},
+ /* changedFn = */ [&addEvent](auto &&, auto &&newPair) {
+ addEvent(std::forward<decltype(newPair)>(newPair));
+ }
+ ));
+
+ addToTrigger(roomId, std::move(tl), std::move(nonTl));
+ }
+ ));
+ if (!trigger.timelineEvents.empty() || !trigger.nonTimelineEvents.empty()) {
+ nextTriggers = std::move(nextTriggers).push_back(trigger);
+ }
+ }
}
diff --git a/src/client/room/room-model.hpp b/src/client/room/room-model.hpp
--- a/src/client/room/room-model.hpp
+++ b/src/client/room/room-model.hpp
@@ -12,10 +12,12 @@
#include <variant>
#include <immer/flex_vector.hpp>
#include <immer/map.hpp>
+#include <immer/set.hpp>
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-map.hpp>
+#include <serialization/immer-set.hpp>
#include <serialization/immer-array.hpp>
#include <csapi/sync.hpp>
@@ -78,6 +80,8 @@
struct AddMessagesAction
{
EventList events;
+ /// @internal only to be used by AddToTimelineAction
+ bool alsoInTimeline{false};
};
struct AddToTimelineAction
@@ -304,6 +308,11 @@
immer::flex_vector<std::string /* eventId */>> undecryptedEvents;
immer::flex_vector<std::string> unreadNotificationEventIds;
+ /// The set of event ids that are in `messages` but not in `timeline`.
+ /// The rationale is that non-timeline events are sparse, so
+ /// instead of recording events that are in the timeline, we
+ /// record those not in the timeline.
+ immer::set<std::string> nonTimelineEvents;
immer::flex_vector<std::string> joinedMemberIds() const;
immer::flex_vector<std::string> invitedMemberIds() const;
@@ -357,6 +366,15 @@
*/
bool checkInvariants() const;
+ /**
+ * Check if the event is in the timeline.
+ *
+ * This function takes constant time.
+ * @param eventId The id of the event to check.
+ * @return true iff the event is in the timeline.
+ */
+ bool isInTimeline(const std::string &eventId) const;
+
using Action = std::variant<
AddStateEventsAction,
MaybeAddStateEventsAction,
@@ -416,6 +434,7 @@
&& a.eventReadUsers == b.eventReadUsers
&& a.undecryptedEvents == b.undecryptedEvents
&& a.unreadNotificationEventIds == b.unreadNotificationEventIds
+ && a.nonTimelineEvents == b.nonTimelineEvents
;
}
@@ -511,6 +530,9 @@
if (version >= 8) {
ar & r.unreadNotificationEventIds;
}
+ if (version >= 9) {
+ ar & r.nonTimelineEvents;
+ }
}
template<class Archive>
@@ -522,5 +544,5 @@
BOOST_CLASS_VERSION(Kazv::PendingRoomKeyEvent, 1)
BOOST_CLASS_VERSION(Kazv::ReadReceipt, 0)
-BOOST_CLASS_VERSION(Kazv::RoomModel, 8)
+BOOST_CLASS_VERSION(Kazv::RoomModel, 9)
BOOST_CLASS_VERSION(Kazv::RoomListModel, 0)
diff --git a/src/client/room/room-model.cpp b/src/client/room/room-model.cpp
--- a/src/client/room/room-model.cpp
+++ b/src/client/room/room-model.cpp
@@ -106,6 +106,11 @@
},
[&](AddMessagesAction a) {
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
+ if (!a.alsoInTimeline) {
+ for (const auto &e : a.events) {
+ r.nonTimelineEvents = std::move(r.nonTimelineEvents).insert(keyOfTimeline(e));
+ }
+ }
auto handleRedaction =
[&r](const auto &event) {
@@ -153,7 +158,7 @@
zug::map(keyOfTimeline), a.events);
auto oldMessages = r.messages;
- auto next = RoomModel::update(std::move(r), AddMessagesAction{a.events});
+ auto next = RoomModel::update(std::move(r), AddMessagesAction{a.events, /* alsoInTimeline = */ true});
r = std::move(next);
auto key =
[=](auto eventId) {
@@ -165,6 +170,10 @@
// Let `exists` function to always return false, so that it is checked for duplicates automatically.
r.timeline = sortedUniqueMerge(r.timeline, eventIds, [](auto &&) { return false; }, key);
+ for (const auto &e : eventIds) {
+ r.nonTimelineEvents = std::move(r.nonTimelineEvents).erase(e);
+ }
+
// We have 3 possibilities for the source of calling this action:
// pagination, sync, or load from storage.
//
@@ -767,4 +776,9 @@
});
});
}
+
+ bool RoomModel::isInTimeline(const std::string &eventId) const
+ {
+ return !nonTimelineEvents.count(eventId) && messages.count(eventId);
+ }
}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -91,6 +91,7 @@
client/get-versions-test.cpp
client/alias-test.cpp
client/encode-test.cpp
+ client/maybe-add-save-events-trigger-benchmark-test.cpp
EXTRA_LINK_LIBRARIES kazvclient kazveventemitter kazvjob client-test-lib kazvtestfixtures
EXTRA_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/client
)
diff --git a/src/tests/base/serialization-test.cpp b/src/tests/base/serialization-test.cpp
--- a/src/tests/base/serialization-test.cpp
+++ b/src/tests/base/serialization-test.cpp
@@ -14,6 +14,7 @@
#include <serialization/immer-flex-vector.hpp>
#include <serialization/immer-map.hpp>
+#include <serialization/immer-set.hpp>
#include <serialization/immer-box.hpp>
#include <serialization/immer-array.hpp>
#include <serialization/std-optional.hpp>
@@ -72,6 +73,14 @@
serializeTest(v, v2);
}
+TEST_CASE("Serialize immer::set", "[base][serialization]")
+{
+ auto v = immer::set<int>{1, 2, 3, 4, 5};
+ auto v2 = immer::set<int>{}.insert(10);
+
+ serializeTest(v, v2);
+}
+
TEST_CASE("Serialize immer::box", "[base][serialization]")
{
auto v = immer::box<int>{42};
diff --git a/src/tests/client/maybe-add-save-events-trigger-benchmark-test.cpp b/src/tests/client/maybe-add-save-events-trigger-benchmark-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/client/maybe-add-save-events-trigger-benchmark-test.cpp
@@ -0,0 +1,121 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <libkazv-config.hpp>
+#include "factory.hpp"
+#include <client-model.hpp>
+#include <zug/transducer/repeat.hpp>
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/benchmark/catch_benchmark.hpp>
+#include <iostream>
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+static auto generateChanges(std::size_t newCount, std::size_t changedCount, const EventList &events)
+{
+ return intoImmer(
+ EventList{},
+ zug::repeatn(newCount, 0)
+ | zug::map([](auto &&) {
+ return makeEvent();
+ })
+ ) + intoImmer(
+ EventList{},
+ zug::take(changedCount)
+ | zug::map([](auto &&e) {
+ return makeEvent(withEventId(e.id()) | withEventContent(json{{"mew", "mew"}}));
+ }),
+ events
+ );
+}
+
+// Assumption is that new/changed messages take only a small part in the whole room.
+static std::pair<ClientModel, ClientModel> data(
+ std::size_t roomCount,
+ std::size_t timelineMessagesPerRoom,
+ std::size_t nonTimelineMessagesPerRoom,
+ std::size_t newTimelineMessagesPerRoom,
+ std::size_t newNonTimelineMessagesPerRoom,
+ std::size_t changedTimelineMessagesPerRoom,
+ std::size_t changedNonTimelineMessagesPerRoom
+)
+{
+ std::cerr << "Generating data" << std::endl;
+ auto old = makeClient();
+ auto client = makeClient();
+ std::size_t generated = 0;
+
+ for (std::size_t r = 0; r < roomCount; ++r) {
+ auto room = makeRoom();
+ auto events = EventList{};
+ for (std::size_t m = 0; m < timelineMessagesPerRoom; ++m) {
+ auto event = makeEvent();
+ events = std::move(events).push_back(event);
+ ++generated;
+ if (generated % 10000 == 0) {
+ std::cerr << "Generated " << generated << " events" << std::endl;
+ }
+ }
+ withRoomTimeline(events)(room);
+
+ auto origNonTl = EventList{};
+ for (std::size_t m = 0; m < nonTimelineMessagesPerRoom; ++m) {
+ auto event = makeEvent();
+ events = std::move(events).push_back(event);
+ ++generated;
+ if (generated % 10000 == 0) {
+ std::cerr << "Generated " << generated << " events" << std::endl;
+ }
+ }
+
+ room = RoomModel::update(std::move(room), AddMessagesAction{origNonTl});
+ withRoom(room)(old);
+
+ auto tl = generateChanges(newTimelineMessagesPerRoom, changedTimelineMessagesPerRoom, events);
+
+ auto nonTl = generateChanges(newNonTimelineMessagesPerRoom, changedNonTimelineMessagesPerRoom, origNonTl);
+
+ withRoomTimeline(tl)(room);
+ room = RoomModel::update(std::move(room), AddMessagesAction{nonTl});
+ withRoom(room)(client);
+ }
+ return {old, client};
+}
+
+TEST_CASE("maybeAddSaveEventsTrigger() benchmark: small", "[client][storage-actions][!benchmark]")
+{
+ auto [old, client] = data(10, 200, 100, 5, 5, 2, 2);
+ BENCHMARK("maybeAddSaveEventsTrigger()") {
+ return client.maybeAddSaveEventsTrigger(old);
+ };
+}
+
+TEST_CASE("maybeAddSaveEventsTrigger() benchmark: medium", "[client][storage-actions][!benchmark]")
+{
+ auto [old, client] = data(50, 1000, 200, 10, 10, 5, 5);
+ BENCHMARK("maybeAddSaveEventsTrigger()") {
+ return client.maybeAddSaveEventsTrigger(old);
+ };
+}
+
+// unprobable case: all 100 rooms have 80 events added/changed during 1 sync
+TEST_CASE("maybeAddSaveEventsTrigger() benchmark: large", "[client][storage-actions][!benchmark]")
+{
+ auto [old, client] = data(100, 4000, 400, 20, 20, 20, 20);
+ BENCHMARK("maybeAddSaveEventsTrigger()") {
+ return client.maybeAddSaveEventsTrigger(old);
+ };
+}
+
+// unprobable case: all 500 rooms have 20 events added/changed during 1 sync
+TEST_CASE("maybeAddSaveEventsTrigger() benchmark: many rooms with few events", "[client][storage-actions][!benchmark]")
+{
+ auto [old, client] = data(500, 200, 100, 5, 5, 5, 5);
+ BENCHMARK("maybeAddSaveEventsTrigger()") {
+ return client.maybeAddSaveEventsTrigger(old);
+ };
+}
diff --git a/src/tests/client/room/purge-test.cpp b/src/tests/client/room/purge-test.cpp
--- a/src/tests/client/room/purge-test.cpp
+++ b/src/tests/client/room/purge-test.cpp
@@ -153,6 +153,11 @@
auto u = makeMockSdkUtil(m);
auto d = u.getMockDispatcher(passDown<PurgeRoomTimelineAction>());
auto c = u.getClient(d);
+ bool saveEventsRequested = false;
+ auto watchable = u.ee.watchable();
+ watchable.after<SaveEventsRequested>([&saveEventsRequested](auto &&) {
+ saveEventsRequested = true;
+ });
c.purgeRoomEvents({
{r1.roomId, 80},
@@ -166,4 +171,5 @@
REQUIRE(c.room(r1.roomId).timelineEventIds().get().size() == 80);
REQUIRE(c.room(r2.roomId).timelineEventIds().get().size() == 50);
REQUIRE(c.room(r3.roomId).timelineEventIds().get().size() == 100);
+ REQUIRE(!saveEventsRequested);
}
diff --git a/src/tests/client/storage-actions-test.cpp b/src/tests/client/storage-actions-test.cpp
--- a/src/tests/client/storage-actions-test.cpp
+++ b/src/tests/client/storage-actions-test.cpp
@@ -8,13 +8,16 @@
#include "actions/storage.hpp"
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_quantifiers.hpp>
+#include <catch2/matchers/catch_matchers_range_equals.hpp>
#include <catch2/matchers/catch_matchers_predicate.hpp>
#include <factory.hpp>
using namespace Kazv;
using namespace Kazv::Factory;
using Catch::Matchers::AllMatch;
+using Catch::Matchers::NoneMatch;
using Catch::Matchers::Predicate;
+using Catch::Matchers::UnorderedRangeEquals;
static const std::string roomId = "!test:example.com";
@@ -61,6 +64,11 @@
REQUIRE(room.reverseEventRelationships.count(timelineEvents[0].id()));
REQUIRE(room.reverseEventRelationships.count(existingEvents[0].id()));
+
+ // should not give out any SaveEventsRequested triggers
+ REQUIRE_THAT(next.nextTriggers, NoneMatch(Predicate<KazvEvent>([](const auto &t) {
+ return std::holds_alternative<SaveEventsRequested>(t);
+ })));
}
// Verify the load works when loading into timeline an event in
// messages but not in timeline
@@ -76,3 +84,113 @@
REQUIRE(room.timeline == intoImmer(immer::flex_vector<std::string>{}, zug::map(&Event::id), existingEvents + timelineEvents + nextTimelineEvents));
}
}
+
+TEST_CASE("maybeAddSaveEventsTrigger", "[client][storage-actions]")
+{
+ WHEN("compare with self") {
+ auto c = makeClient(withRoom(makeRoom(withRoomTimeline({
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ }))));
+
+ c.maybeAddSaveEventsTrigger(c);
+ REQUIRE(c.nextTriggers.empty());
+ }
+
+ WHEN("new room, no events") {
+ auto old = makeClient(withRoom(makeRoom(withRoomTimeline({
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ }))));
+ auto c = old;
+ withRoom(makeRoom())(c);
+
+ c.maybeAddSaveEventsTrigger(old);
+ REQUIRE(c.nextTriggers.empty());
+ }
+
+ WHEN("new room, with events") {
+ auto old = makeClient(withRoom(makeRoom(withRoomTimeline({
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ }))));
+ auto c = old;
+ auto tl = EventList{
+ makeEvent(),
+ makeEvent(),
+ };
+ auto room = makeRoom(withRoomTimeline(tl));
+ withRoom(room)(c);
+
+ c.maybeAddSaveEventsTrigger(old);
+ REQUIRE(c.nextTriggers.size() == 1);
+ REQUIRE(std::holds_alternative<SaveEventsRequested>(c.nextTriggers[0]));
+ auto t = std::get<SaveEventsRequested>(c.nextTriggers[0]);
+ REQUIRE(t.nonTimelineEvents.empty());
+ REQUIRE(t.timelineEvents.size() == 1);
+ REQUIRE_THAT(t.timelineEvents[room.roomId], UnorderedRangeEquals(tl));
+ }
+
+ WHEN("new room, with tl and non-tl events") {
+ auto old = makeClient(withRoom(makeRoom(withRoomTimeline({
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ }))));
+ auto c = old;
+ auto tl = EventList{
+ makeEvent(),
+ makeEvent(),
+ };
+ auto nonTl = EventList{makeEvent(), makeEvent()};
+ auto room = makeRoom(withRoomTimeline(tl));
+ room = RoomModel::update(std::move(room), AddMessagesAction{nonTl});
+ withRoom(room)(c);
+
+ c.maybeAddSaveEventsTrigger(old);
+ REQUIRE(c.nextTriggers.size() == 1);
+ REQUIRE(std::holds_alternative<SaveEventsRequested>(c.nextTriggers[0]));
+ auto t = std::get<SaveEventsRequested>(c.nextTriggers[0]);
+ REQUIRE(t.nonTimelineEvents.size() == 1);
+ REQUIRE_THAT(t.nonTimelineEvents[room.roomId], UnorderedRangeEquals(nonTl));
+ REQUIRE(t.timelineEvents.size() == 1);
+ REQUIRE_THAT(t.timelineEvents[room.roomId], UnorderedRangeEquals(tl));
+ }
+
+ WHEN("existing room with new and changed events") {
+ auto origTl = EventList{
+ makeEvent(),
+ makeEvent(),
+ makeEvent(),
+ };
+ auto origNonTl = EventList{makeEvent(), makeEvent()};
+ auto room = makeRoom(withRoomTimeline(origTl));
+ room = RoomModel::update(std::move(room), AddMessagesAction{origNonTl});
+ auto old = makeClient(withRoom(room));
+ auto c = old;
+ auto tl = EventList{
+ makeEvent(withEventId(origTl[0].id()) | withEventContent(json{{"mew", "mew"}})),
+ makeEvent(),
+ makeEvent(),
+ };
+ withRoomTimeline(tl)(room);
+ auto nonTl = EventList{
+ makeEvent(withEventId(origNonTl[0].id()) | withEventContent(json{{"mew", "mew"}})),
+ makeEvent(),
+ };
+ room = RoomModel::update(std::move(room), AddMessagesAction{nonTl});
+ withRoom(room)(c);
+
+ c.maybeAddSaveEventsTrigger(old);
+ REQUIRE(c.nextTriggers.size() == 1);
+ REQUIRE(std::holds_alternative<SaveEventsRequested>(c.nextTriggers[0]));
+ auto t = std::get<SaveEventsRequested>(c.nextTriggers[0]);
+ REQUIRE(t.timelineEvents.size() == 1);
+ REQUIRE_THAT(t.timelineEvents[room.roomId], UnorderedRangeEquals(tl));
+ REQUIRE(t.nonTimelineEvents.size() == 1);
+ REQUIRE_THAT(t.nonTimelineEvents[room.roomId], UnorderedRangeEquals(nonTl));
+ }
+}
diff --git a/src/tests/client/sync-test.cpp b/src/tests/client/sync-test.cpp
--- a/src/tests/client/sync-test.cpp
+++ b/src/tests/client/sync-test.cpp
@@ -743,3 +743,24 @@
REQUIRE(!event.decrypted());
REQUIRE(event.content().get().at("moe.kazv.mxc.errcode") == exceptedErrorCode);
}
+
+TEST_CASE("emit SaveEventsRequested", "[client][sync]")
+{
+ ClientModel m = makeClient(
+ withRoom(makeRoom(
+ withRoomId("!exampleroomid:example.com"))));
+
+ auto resp = makeResponse(
+ "Sync",
+ withResponseJsonBody(syncResponseJson)
+ | withResponseDataKV("is", "incremental")
+ );
+ auto [next, _] = ClientModel::update(m, ProcessResponseAction{SyncResponse{resp}});
+
+ auto it = std::find_if(next.nextTriggers.begin(), next.nextTriggers.end(), [](const auto &trigger) {
+ return std::holds_alternative<SaveEventsRequested>(trigger);
+ });
+ REQUIRE(it != next.nextTriggers.end());
+ auto t = std::get<SaveEventsRequested>(*it);
+ REQUIRE(t.timelineEvents["!726s6s6q:example.com"].size() == 2);
+}

File Metadata

Mime Type
text/plain
Expires
Fri, Oct 10, 10:51 PM (15 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
509868
Default Alt Text
D241.1760161874.diff (24 KB)

Event Timeline