Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F7898950
D233.1759333539.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
D233.1759333539.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Wed, Oct 1, 8:45 AM (21 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
485847
Default Alt Text
D233.1759333539.diff (21 KB)
Attached To
Mode
D233: Add functionality to load and purge events
Attached
Detach File
Event Timeline
Log In to Comment