Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F7726502
D230.1757754907.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D230.1757754907.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D230: Add storage interface
Attached
Detach File
Event Timeline
Log In to Comment