Page MenuHomePhorge

D230.1757754907.diff
No OneTemporary

Size
12 KB
Referenced Files
None
Subscribers
None

D230.1757754907.diff

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -10,6 +10,7 @@
add_subdirectory(client)
add_subdirectory(eventemitter)
add_subdirectory(testfixtures)
+add_subdirectory(storage)
if(libkazv_BUILD_KAZVJOB)
add_subdirectory(job)
diff --git a/src/base/event.hpp b/src/base/event.hpp
--- a/src/base/event.hpp
+++ b/src/base/event.hpp
@@ -36,6 +36,8 @@
/// returns the id of this event
std::string id() const;
+ std::string roomId() const;
+
std::string sender() const;
Timestamp originServerTs() const;
diff --git a/src/base/event.cpp b/src/base/event.cpp
--- a/src/base/event.cpp
+++ b/src/base/event.cpp
@@ -46,6 +46,12 @@
: "";
}
+ std::string Event::roomId() const {
+ return originalJson().get().contains("room_id")
+ ? originalJson().get().at("room_id")
+ : "";
+ }
+
std::string Event::sender() const {
return originalJson().get().contains("sender")
? originalJson().get().at("sender")
diff --git a/src/client/sdk.hpp b/src/client/sdk.hpp
--- a/src/client/sdk.hpp
+++ b/src/client/sdk.hpp
@@ -16,9 +16,8 @@
#include "sdk-model-cursor-tag.hpp"
#include "client.hpp"
#include "thread-safety-helper.hpp"
-
#include "random-generator.hpp"
-
+#include "storage-interface.hpp"
namespace Kazv
{
@@ -43,7 +42,8 @@
std::ref(detail::declref<JobInterface>()),
std::ref(detail::declref<EventInterface>()),
lager::dep::as<SdkModelCursorKey>(std::declval<std::function<CursorTSP()>>()),
- std::ref(detail::declref<RandomInterface>())
+ std::ref(detail::declref<RandomInterface>()),
+ std::ref(detail::declref<StorageInterface>())
#ifdef KAZV_USE_THREAD_SAFETY_HELPER
, std::ref(detail::declref<EventLoopThreadIdKeeper>())
#endif
diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt
new file mode 100644
--- /dev/null
+++ b/src/storage/CMakeLists.txt
@@ -0,0 +1,14 @@
+
+add_library(kazvstorage INTERFACE)
+add_library(libkazv::kazvstorage ALIAS kazvstorage)
+
+target_link_libraries(kazvstorage INTERFACE kazvbase)
+
+target_include_directories(kazvstorage
+ INTERFACE
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
+ $<INSTALL_INTERFACE:include/kazv/storage>
+ )
+
+install(TARGETS kazvstorage EXPORT libkazvTargets)
diff --git a/src/storage/dumb-storage-provider.hpp b/src/storage/dumb-storage-provider.hpp
new file mode 100644
--- /dev/null
+++ b/src/storage/dumb-storage-provider.hpp
@@ -0,0 +1,18 @@
+/*
+ * 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>
+
+namespace Kazv
+{
+ /**
+ * A storage provider that does nothing.
+ */
+ class DumbStorageProvider
+ {
+ };
+}
diff --git a/src/storage/storage-interface.hpp b/src/storage/storage-interface.hpp
new file mode 100644
--- /dev/null
+++ b/src/storage/storage-interface.hpp
@@ -0,0 +1,153 @@
+/*
+ * 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 <types.hpp>
+#include <boost/hana.hpp>
+
+namespace Kazv
+{
+ /**
+ * Describe a storage interface.
+ *
+ * A class implementing the interface should be movable and
+ * implement any of the following member functions:
+ *
+ * ```
+ * void saveEvents(SaveEventsQuery q, SaveEventsCallback cb);
+ * void loadEvents(LoadEventsQuery q, LoadEventsCallback cb);
+ * ```
+ *
+ * It should call the callback with the result after the action
+ * in the function is finished.
+ *
+ * In case that any member function is not provided, this will
+ * instead synchronously call the callback with a result object
+ * with the error code `UNIMPLEMENTED`.
+ */
+ class StorageInterface
+ {
+ public:
+ enum class Order {
+ /// from earliest to latest
+ Asc,
+ /// from latest to earliest
+ Desc,
+ };
+
+ struct SaveEventsQuery
+ {
+ /**
+ * 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.
+ */
+ EventList timelineEvents;
+ /**
+ * The events that should be saved, but not in the timeline.
+ */
+ EventList nonTimelineEvents;
+ };
+
+ struct SaveEventsResult
+ {
+ std::string error;
+ };
+
+ struct LoadEventsQuery
+ {
+ /// The room id to find events in.
+ std::string roomId;
+ /// The order of pagination.
+ Order order;
+ /// The event id to start pagination from.
+ /// If empty, it is loaded from one end.
+ std::string startEventId;
+ /// The maximum number of events to return.
+ std::size_t limit{20};
+ };
+
+ struct LoadEventsResult
+ {
+ /// Error code for the action.
+ ///
+ /// If startEventId is specified but not found in the storage,
+ /// it is considered to not succeed and the error should be `START_EVENT_NOT_FOUND`.
+ /// If there are no more events to load, it is considered to succeed
+ /// and the error should be empty.
+ std::string error;
+ /// The loaded events that should be put into the timeline. From oldest to latest.
+ /// @sa SaveEventsQuery
+ EventList timelineEvents;
+ /// 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.
+ /// @sa SaveEventsQuery
+ EventList relatedEvents;
+ };
+
+ using SaveEventsCallback = std::function<void(SaveEventsResult)>;
+ using LoadEventsCallback = std::function<void(LoadEventsResult)>;
+
+ template<class DeriveT>
+ StorageInterface(DeriveT obj)
+ : m_d(std::unique_ptr<Concept>(new Model<DeriveT>(std::move(obj))))
+ {}
+
+ // implicitly movable and non-copyable because of std::unique_ptr
+
+ void saveEvents(SaveEventsQuery q, SaveEventsCallback cb)
+ {
+ m_d->saveEvents(std::move(q), std::move(cb));
+ }
+
+ void loadEvents(LoadEventsQuery q, LoadEventsCallback cb)
+ {
+ m_d->loadEvents(std::move(q), std::move(cb));
+ }
+
+ private:
+ struct Concept
+ {
+ virtual ~Concept() = default;
+ virtual void saveEvents(SaveEventsQuery q, SaveEventsCallback cb) = 0;
+ virtual void loadEvents(LoadEventsQuery q, LoadEventsCallback cb) = 0;
+ };
+
+ template<class DeriveT>
+ struct Model : public Concept
+ {
+ Model(DeriveT obj) : instance(std::move(obj)) {}
+ ~Model() override = default;
+
+ void saveEvents(SaveEventsQuery q, SaveEventsCallback cb) override
+ {
+ if constexpr (boost::hana::is_valid([](auto t) -> decltype((void)std::declval<typename decltype(t)::type>().saveEvents(std::move(q), std::move(cb))) {})(boost::hana::type_c<DeriveT>)) {
+ instance.saveEvents(std::move(q), std::move(cb));
+ } else {
+ cb(SaveEventsResult{"UNIMPLEMENTED"});
+ }
+ }
+
+ void loadEvents(LoadEventsQuery q, LoadEventsCallback cb) override
+ {
+ if constexpr (boost::hana::is_valid([](auto t) -> decltype((void)std::declval<typename decltype(t)::type>().loadEvents(std::move(q), std::move(cb))) {})(boost::hana::type_c<DeriveT>)) {
+ instance.loadEvents(std::move(q), std::move(cb));
+ } else {
+ cb(LoadEventsResult{"UNIMPLEMENTED", {}, {}});
+ }
+ }
+
+ DeriveT instance;
+ };
+
+ std::unique_ptr<Concept> m_d;
+ };
+}
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -103,6 +103,11 @@
EXTRA_LINK_LIBRARIES kazvbase kazvjob kazvstore
)
+libkazv_add_tests(
+ storage-test.cpp
+ EXTRA_LINK_LIBRARIES kazvbase kazvstorage kazvtestfixtures
+)
+
libkazv_add_tests(
event-emitter-test.cpp
EXTRA_LINK_LIBRARIES kazvbase kazveventemitter
diff --git a/src/tests/storage-test.cpp b/src/tests/storage-test.cpp
new file mode 100644
--- /dev/null
+++ b/src/tests/storage-test.cpp
@@ -0,0 +1,108 @@
+/*
+ * 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-interface.hpp"
+#include "dumb-storage-provider.hpp"
+#include "immer-utils.hpp"
+#include "factory.hpp"
+#include <catch2/catch_test_macros.hpp>
+
+using namespace Kazv;
+using namespace Kazv::Factory;
+
+struct MockStorage
+{
+ immer::map<std::string, immer::map<std::string, Event>> storedEvents;
+};
+
+struct TestStorageProvider
+{
+ void saveEvents(StorageInterface::SaveEventsQuery q, StorageInterface::SaveEventsCallback cb)
+ {
+ for (auto e : q.timelineEvents) {
+ storage->storedEvents = setIn(std::move(storage->storedEvents), e, e.roomId(), e.id());
+ }
+ cb({""});
+ }
+ std::shared_ptr<MockStorage> storage;
+};
+
+struct TestStorageProvider2 : TestStorageProvider
+{
+ void loadEvents(StorageInterface::LoadEventsQuery q, StorageInterface::LoadEventsCallback cb)
+ {
+ auto events = EventList{};
+ for (auto [k, e] : storage->storedEvents[q.roomId]) {
+ events = std::move(events).push_back(e);
+ }
+ cb({"", events, {}});
+ }
+};
+
+TEST_CASE("DumbStorageProvider can be used with StorageInterface", "[storage]")
+{
+ auto si = StorageInterface(DumbStorageProvider{});
+ int save = 0;
+ int load = 0;
+ si.saveEvents({EventList{makeEvent()}, {}}, [&save](const auto &res) {
+ ++save;
+ REQUIRE(res.error == "UNIMPLEMENTED");
+ });
+
+ si.loadEvents({"!room:example.com", StorageInterface::Order::Desc, ""}, [&load](const auto &res) {
+ ++load;
+ REQUIRE(res.error == "UNIMPLEMENTED");
+ });
+
+ REQUIRE(save == 1);
+ REQUIRE(load == 1);
+}
+
+TEST_CASE("partially implemented StorageInterface", "[storage]")
+{
+ auto storage = std::make_shared<MockStorage>();
+ auto si = StorageInterface(TestStorageProvider{storage});
+ int save = 0;
+ int load = 0;
+ auto events = EventList{makeEvent(withEventKV("/room_id"_json_pointer, "!room:example.com"))};
+ si.saveEvents({events, {}}, [&save](const auto &res) {
+ ++save;
+ REQUIRE(res.error == "");
+ });
+ REQUIRE(storage->storedEvents.at(events[0].roomId()).at(events[0].id()) == events[0]);
+
+ si.loadEvents({"!room:example.com", StorageInterface::Order::Desc, ""}, [&load](const auto &res) {
+ ++load;
+ REQUIRE(res.error == "UNIMPLEMENTED");
+ });
+
+ REQUIRE(save == 1);
+ REQUIRE(load == 1);
+}
+
+TEST_CASE("fully implemented StorageInterface", "[storage]")
+{
+ auto storage = std::make_shared<MockStorage>();
+ auto si = StorageInterface(TestStorageProvider2{storage});
+ int save = 0;
+ int load = 0;
+ auto events = EventList{makeEvent(withEventKV("/room_id"_json_pointer, "!room:example.com"))};
+ si.saveEvents({events, {}}, [&save](const auto &res) {
+ ++save;
+ REQUIRE(res.error == "");
+ });
+ REQUIRE(storage->storedEvents.at(events[0].roomId()).at(events[0].id()) == events[0]);
+
+ si.loadEvents({"!room:example.com", StorageInterface::Order::Desc, ""}, [&load, events](const auto &res) {
+ ++load;
+ REQUIRE(res.error == "");
+ REQUIRE(res.timelineEvents == events);
+ });
+
+ REQUIRE(save == 1);
+ REQUIRE(load == 1);
+}

File Metadata

Mime Type
text/plain
Expires
Sat, Sep 13, 2:15 AM (6 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
449638
Default Alt Text
D230.1757754907.diff (12 KB)

Event Timeline