Page MenuHomePhorge

D233.1759336812.diff
No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None

D233.1759336812.diff

diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt
--- a/src/client/CMakeLists.txt
+++ b/src/client/CMakeLists.txt
@@ -15,6 +15,7 @@
actions/content.cpp
actions/encryption.cpp
actions/profile.cpp
+ actions/storage.cpp
device-list-tracker.cpp
encrypted-file.cpp
diff --git a/src/client/actions/storage.hpp b/src/client/actions/storage.hpp
new file mode 100644
--- /dev/null
+++ b/src/client/actions/storage.hpp
@@ -0,0 +1,15 @@
+/*
+ * This file is part of libkazv.
+ * SPDX-FileCopyrightText: 2025 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <libkazv-config.hpp>
+#include "client-model.hpp"
+
+namespace Kazv
+{
+ [[nodiscard]] ClientResult updateClient(ClientModel m, LoadEventsFromStorageAction a);
+ [[nodiscard]] ClientResult updateClient(ClientModel m, PurgeRoomTimelineAction a);
+}
diff --git a/src/client/actions/storage.cpp b/src/client/actions/storage.cpp
new file mode 100644
--- /dev/null
+++ b/src/client/actions/storage.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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 "storage.hpp"
+#include "room/room-model.hpp"
+#include "debug.hpp"
+
+namespace Kazv
+{
+ ClientResult updateClient(ClientModel m, LoadEventsFromStorageAction a)
+ {
+ for (auto [roomId, timelineEvents]: a.timelineEvents) {
+ if (!timelineEvents.empty()) {
+ m.roomList = RoomListModel::update(
+ std::move(m.roomList),
+ // If this call is changed, also change the comments in the AddToTimelineAction reducer in room/room-model.cpp
+ UpdateRoomAction{roomId, AddToTimelineAction{
+ timelineEvents,
+ std::nullopt,
+ std::nullopt,
+ std::nullopt,
+ }}
+ );
+ }
+ }
+
+ for (auto [roomId, relatedEvents] : a.relatedEvents) {
+ if (!relatedEvents.empty()) {
+ m.roomList = RoomListModel::update(
+ std::move(m.roomList),
+ UpdateRoomAction{roomId, AddMessagesAction{
+ relatedEvents,
+ }}
+ );
+ }
+ }
+
+ return { m, lager::noop };
+ }
+
+ ClientResult updateClient(ClientModel m, PurgeRoomTimelineAction a)
+ {
+ return { m, lager::noop };
+ }
+}
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
@@ -580,6 +580,27 @@
std::optional<std::string> displayName;
};
+ /// Load events from the storage into the model
+ struct LoadEventsFromStorageAction
+ {
+ /// Map from room id to a list of
+ /// loaded events that should be put into the timeline. From oldest to latest.
+ immer::map<std::string, EventList> timelineEvents;
+ /// Map from room id to a list of
+ /// related events that should not be put into the timeline. From oldest to latest.
+ /// There might be events in the storage that is needed to display
+ /// existing events or room state (e.g. pinned events), but
+ /// the storage may not know its place in the timeline.
+ immer::map<std::string, EventList> relatedEvents;
+ };
+
+ /// Remove events from the model, keeping only the latest `maxToKeep` events.
+ struct PurgeRoomTimelineAction
+ {
+ std::string roomId;
+ std::size_t maxToKeep;
+ };
+
template<class Archive>
void serialize(Archive &ar, ClientModel &m, std::uint32_t const version)
{
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
@@ -34,6 +34,7 @@
#include "actions/content.hpp"
#include "actions/encryption.hpp"
#include "actions/profile.hpp"
+#include "actions/storage.hpp"
namespace Kazv
{
diff --git a/src/client/clientfwd.hpp b/src/client/clientfwd.hpp
--- a/src/client/clientfwd.hpp
+++ b/src/client/clientfwd.hpp
@@ -78,6 +78,9 @@
struct ResubmitJobAction;
+ struct LoadEventsFromStorageAction;
+ struct PurgeRoomTimelineAction;
+
struct ClientModel;
using ClientAction = std::variant<
@@ -141,7 +144,10 @@
SetAvatarUrlAction,
SetDisplayNameAction,
- ResubmitJobAction
+ ResubmitJobAction,
+
+ LoadEventsFromStorageAction,
+ PurgeRoomTimelineAction
>;
using ClientEffect = Effect<ClientAction, lager::deps<>>;
diff --git a/src/client/clientutil.hpp b/src/client/clientutil.hpp
--- a/src/client/clientutil.hpp
+++ b/src/client/clientutil.hpp
@@ -84,123 +84,50 @@
return lager::get<EventInterface &>(std::forward<Context>(ctx));
}
- namespace
- {
- template<class ImmerT>
- struct ImmerIterator
- {
- using value_type = typename ImmerT::value_type;
- using reference = typename ImmerT::reference;
- using pointer = const value_type *;
- using difference_type = long int;
- using iterator_category = std::random_access_iterator_tag;
-
- ImmerIterator(const ImmerT &container, std::size_t index)
- : m_container(std::ref(container))
- , m_index(index)
- {}
-
- ImmerIterator &operator+=(difference_type d) {
- m_index += d;
- return *this;
- }
-
- ImmerIterator &operator-=(difference_type d) {
- m_index -= d;
- return *this;
- }
-
- difference_type operator-(ImmerIterator b) const {
- return index() - b.index();
- }
-
- ImmerIterator &operator++() {
- return *this += 1;
- }
-
- ImmerIterator operator++(int) {
- auto tmp = *this;
- *this += 1;
- return tmp;
- }
-
- ImmerIterator &operator--() {
- return *this -= 1;
- }
-
- ImmerIterator operator--(int) {
- auto tmp = *this;
- *this -= 1;
- return tmp;
- }
-
-
- reference &operator*() const {
- return m_container.get().at(m_index);
- }
-
- reference operator[](difference_type d) const;
-
- std::size_t index() const { return m_index; }
-
- private:
-
- std::reference_wrapper<const ImmerT> m_container;
- std::size_t m_index;
- };
-
- template<class ImmerT>
- auto ImmerIterator<ImmerT>::operator[](difference_type d) const -> reference
- {
- return *(*this + d);
- }
-
- template<class ImmerT>
- auto operator+(ImmerIterator<ImmerT> a, long int d)
- {
- return a += d;
- };
-
- template<class ImmerT>
- auto operator+(long int d, ImmerIterator<ImmerT> a)
- {
- return a += d;
- };
-
- template<class ImmerT>
- auto operator-(ImmerIterator<ImmerT> a, long int d)
- {
- return a -= d;
- };
-
- template<class ImmerT>
- auto immerBegin(const ImmerT &c)
- {
- return ImmerIterator<ImmerT>(c, 0);
- }
-
- template<class ImmerT>
- auto immerEnd(const ImmerT &c)
- {
- return ImmerIterator<ImmerT>(c, c.size());
- }
- }
-
+ /**
+ * Merge `addon` into the sorted container `base`.
+ *
+ * This function will look into every item `x` in `addon`. If `exists(x)`,
+ * nothing is done. Otherwise, it does a binary search in `base`. If
+ * there is an item `k` where `keyOf(k) == keyOf(x)`, nothing is done.
+ * Otherwise, `x` is inserted into `base`, keeping the sort order.
+ *
+ * This function invokes at most `O(|addon|*log(|base|))` comparisons; each
+ * comparison invokes exactly 2 `keyOf`s.
+ *
+ * This function invokes at most `|addon|` insertions of `base`.
+ *
+ * This function invokes `exists` exactly `|addon|` times.
+ *
+ * @param base An immer sequence container of some type T. `base` must be sorted
+ * in strict ascending order in terms of `keyOf`. I.e., For any two items x, y: `keyOf(x) < keyOf(y)` iff x is before y.
+ * @param addon A range of the same type T.
+ * @param exists A function taking T and producing a boolean value.
+ * @param keyOf A key function for the sorting of `base`. It must be a strong order.
+ *
+ */
template<class ImmerT1, class RangeT2, class Pred, class Func>
ImmerT1 sortedUniqueMerge(ImmerT1 base, RangeT2 addon, Pred exists, Func keyOf)
{
- auto needToAdd = intoImmer(ImmerT1{},
- zug::filter([=](auto a) {
- return !exists(a);
- }),
- addon);
-
auto cmp = [=](auto a, auto b) {
return keyOf(a) < keyOf(b);
};
- for (auto item : needToAdd) {
- auto it = std::upper_bound(immerBegin(base), immerEnd(base), item, cmp);
+ for (auto item : addon) {
+ if (exists(item)) {
+ continue;
+ }
+ // https://en.cppreference.com/w/cpp/algorithm/upper_bound.html
+ // *(it-1) <= item < *it
+ auto it = std::upper_bound(base.begin(), base.end(), item, cmp);
+ // If `it` is not the first iterator, and `*(it-1) == item`,
+ // then `item` is already in the list. Otherwise, we are guaranteed
+ // that `*(it-1)` either does not exist, or `*(it-1) < item`.
+ // In these cases, `item` is not in the list and we will need
+ // to add them.
+ if (it.index() != 0 && keyOf(item) == keyOf(*(it - 1))) {
+ continue;
+ }
auto index = it.index();
base = std::move(base).insert(index, item);
}
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
@@ -73,6 +73,13 @@
immer::flex_vector<Event> stateEvents;
};
+ /// Add events to the messages map, but not the timeline.
+ /// Usually because their position in the timeline is not known.
+ struct AddMessagesAction
+ {
+ EventList events;
+ };
+
struct AddToTimelineAction
{
/// Events from oldest to latest
@@ -173,6 +180,12 @@
std::string myUserId;
};
+ /// Remove events from the model, retaining only the latest `maxToKeep` events.
+ struct PurgeEventsAction
+ {
+ std::size_t maxToKeep;
+ };
+
inline bool operator==(const PendingRoomKeyEvent &a, const PendingRoomKeyEvent &b)
{
return a.txnId == b.txnId && a.messages == b.messages;
@@ -331,6 +344,7 @@
using Action = std::variant<
AddStateEventsAction,
MaybeAddStateEventsAction,
+ AddMessagesAction,
AddToTimelineAction,
AddAccountDataAction,
ChangeMembershipAction,
@@ -348,7 +362,8 @@
UpdateInvitedMemberCountAction,
AddLocalNotificationsAction,
RemoveReadLocalNotificationsAction,
- UpdateLocalReadMarkerAction
+ UpdateLocalReadMarkerAction,
+ PurgeEventsAction
>;
static RoomModel update(RoomModel r, Action a);
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
@@ -104,21 +104,8 @@
}
return r;
},
- [&](AddToTimelineAction a) {
- auto eventIds = intoImmer(immer::flex_vector<std::string>(),
- zug::map(keyOfTimeline), a.events);
-
- auto oldMessages = r.messages;
+ [&](AddMessagesAction a) {
r.messages = merge(std::move(r.messages), a.events, keyOfTimeline);
- auto exists =
- [=](auto eventId) -> bool {
- return !! oldMessages.find(eventId);
- };
- auto key =
- [=](auto eventId) {
- // sort first by timestamp, then by id
- return sortKeyForTimelineEvent(r.messages[eventId]);
- };
auto handleRedaction =
[&r](const auto &event) {
@@ -143,17 +130,54 @@
immer::for_each(a.events, handleRedaction);
- r.timeline = sortedUniqueMerge(r.timeline, eventIds, exists, key);
-
- // If this is a pagination request, gapEventId
- // should have value. If this is a sync request,
- // gapEventId does not have value. The pagination
- // request does not have the limited field, and
- // whether it has more paginate back token is determined
- // by the presence of the prevBatch parameter.
- // In sync request, limited may not be specified,
- // and thus, if limited does not have value, it means
- // it is not limited.
+ // remove all local echoes that are received
+ for (const auto &e : a.events) {
+ auto jw = e.originalJson();
+ const auto &json = jw.get();
+ if (json.contains("unsigned")
+ && json["unsigned"].contains("transaction_id")
+ && json["unsigned"]["transaction_id"].is_string()) {
+ r = update(std::move(r), RemoveLocalEchoAction{json["unsigned"]["transaction_id"].template get<std::string>()});
+ }
+ }
+
+ // calculate event relationships
+ r.generateRelationships(a.events);
+
+ r.addToUndecryptedEvents(a.events);
+
+ return r;
+ },
+ [&](AddToTimelineAction a) {
+ auto eventIds = intoImmer(immer::flex_vector<std::string>(),
+ zug::map(keyOfTimeline), a.events);
+
+ auto oldMessages = r.messages;
+ auto next = RoomModel::update(std::move(r), AddMessagesAction{a.events});
+ r = std::move(next);
+ auto key =
+ [=](auto eventId) {
+ // sort first by timestamp, then by id
+ return sortKeyForTimelineEvent(r.messages[eventId]);
+ };
+
+ // Things in messages do not always appear in the timeline.
+ // 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);
+
+ // We have 3 possibilities for the source of calling this action:
+ // pagination, sync, or load from storage.
+ //
+ // In pagination: `gapEventId.has_value() && !limited.has_value()`
+ // If we can paginate back: `prevBatch.has_value()`
+ //
+ // In sync: `!gapEventId.has_value()`
+ // If limited: `limited.has_value() && limited.value()`
+ // If we can paginate back: `prevBatch.has_value()` (should always be present, but we do not add a Gap if it is not limited)
+ //
+ // In load from storage: `!gapEventId.has_value() && !limited.has_value() && !prevBatch.has_value()` (Because of actions/storage.cpp)
+
+ // Only pagination and sync can add a Gap
if (((a.limited.has_value() && a.limited.value())
|| a.gapEventId.has_value())
&& a.prevBatch.has_value()) {
@@ -163,6 +187,7 @@
}
}
+ // Only pagination can remove Gaps
// remove the original Gap, as it is resolved
if (a.gapEventId.has_value()) {
r.timelineGaps = std::move(r.timelineGaps).erase(a.gapEventId.value());
@@ -186,22 +211,6 @@
}
}
- // remove all local echoes that are received
- for (const auto &e : a.events) {
- auto jw = e.originalJson();
- const auto &json = jw.get();
- if (json.contains("unsigned")
- && json["unsigned"].contains("transaction_id")
- && json["unsigned"]["transaction_id"].is_string()) {
- r = update(std::move(r), RemoveLocalEchoAction{json["unsigned"]["transaction_id"].template get<std::string>()});
- }
- }
-
- // calculate event relationships
- r.generateRelationships(a.events);
-
- r.addToUndecryptedEvents(a.events);
-
return r;
},
[&](AddAccountDataAction a) {
@@ -408,6 +417,9 @@
r.localReadMarker = a.localReadMarker;
auto next = RoomModel::update(std::move(r), RemoveReadLocalNotificationsAction{a.myUserId});
return next;
+ },
+ [&](PurgeEventsAction a) {
+ return r;
}
);
}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -51,6 +51,7 @@
client/sync-test.cpp
client/content-test.cpp
client/paginate-test.cpp
+ client/storage-actions-test.cpp
client/util-test.cpp
client/serialization-test.cpp
client/encrypted-file-test.cpp
diff --git a/src/tests/client/storage-actions-test.cpp b/src/tests/client/storage-actions-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/client/storage-actions-test.cpp
@@ -0,0 +1,78 @@
+/*
+ * 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 "actions/storage.hpp"
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/matchers/catch_matchers_quantifiers.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::Predicate;
+
+static const std::string roomId = "!test:example.com";
+
+template<class T>
+static auto makeEventInRoom(T modifiers)
+{
+ return makeEvent(withEventKV("/room_id"_json_pointer, roomId) | std::move(modifiers));
+};
+
+static auto makeEventInRoom()
+{
+ return makeEvent(withEventKV("/room_id"_json_pointer, roomId));
+};
+
+TEST_CASE("LoadEventsFromStorageAction", "[client][storage-actions]")
+{
+ auto existingEvents = EventList{
+ makeEventInRoom(),
+ makeEventInRoom(),
+ };
+ auto c = makeClient(withRoom(makeRoom(withRoomId(roomId) | withRoomTimeline(existingEvents))));
+
+ auto timelineEvents = EventList{
+ makeEventInRoom(),
+ makeEventInRoom(),
+ };
+
+ auto relatedEvents = EventList{
+ makeEventInRoom(withEventRelationship("moe.kazv.mxc.some-rel", timelineEvents[0].id())),
+ makeEventInRoom(withEventRelationship("moe.kazv.mxc.other-rel", existingEvents[0].id())),
+ };
+
+ auto [next, _] = updateClient(c, LoadEventsFromStorageAction{
+ {{roomId, timelineEvents}},
+ {{roomId, relatedEvents}},
+ });
+
+ {
+ auto room = next.roomList.rooms[roomId];
+
+ REQUIRE(room.timeline == intoImmer(immer::flex_vector<std::string>{}, zug::map(&Event::id), existingEvents + timelineEvents));
+ REQUIRE_THAT(existingEvents + timelineEvents + relatedEvents,
+ AllMatch(Predicate<Event>([&room](const auto &e) { return !!room.messages.count(e.id()); }, "should be in messages")));
+
+ REQUIRE(room.reverseEventRelationships.count(timelineEvents[0].id()));
+ REQUIRE(room.reverseEventRelationships.count(existingEvents[0].id()));
+ }
+ // Verify the load works when loading into timeline an event in
+ // messages but not in timeline
+ auto nextTimelineEvents = relatedEvents + EventList{makeEventInRoom()};
+ std::tie(next, std::ignore) = updateClient(next, LoadEventsFromStorageAction{
+ {{roomId, nextTimelineEvents}},
+ {},
+ });
+
+ {
+ auto room = next.roomList.rooms[roomId];
+
+ REQUIRE(room.timeline == intoImmer(immer::flex_vector<std::string>{}, zug::map(&Event::id), existingEvents + timelineEvents + nextTimelineEvents));
+ }
+}
diff --git a/src/tests/client/util-test.cpp b/src/tests/client/util-test.cpp
--- a/src/tests/client/util-test.cpp
+++ b/src/tests/client/util-test.cpp
@@ -46,3 +46,16 @@
REQUIRE( res == immer::flex_vector<Model>{{1, "a"}, {2, "b"}, {50, "foo"}, {70, "c"}, {100, "bar"}, {110, "d"}} );
}
+
+TEST_CASE("sortedUniqueMerge should auto detect existing items", "[client][clientutil]")
+{
+ immer::flex_vector<Model> base = {{50, "foo"}, {100, "bar"}};
+ immer::flex_vector<Model> addon = {{1, "a"}, {2, "b"}, {50, "xxx"}, {70, "c"}, {110, "d"}};
+
+ auto exists = [](auto) { return false; };
+ auto key = [](auto m) { return m.value; };
+
+ auto res = sortedUniqueMerge(base, addon, exists, key);
+
+ REQUIRE( res == immer::flex_vector<Model>{{1, "a"}, {2, "b"}, {50, "foo"}, {70, "c"}, {100, "bar"}, {110, "d"}} );
+}

File Metadata

Mime Type
text/plain
Expires
Wed, Oct 1, 9:40 AM (22 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
485847
Default Alt Text
D233.1759336812.diff (21 KB)

Event Timeline