From 06c068bdfb0bc3c8e11d285b09eda759af17b56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Wed, 6 May 2026 14:02:03 +0200 Subject: [PATCH] feat(pj_datastore): add ObjectStore::findTopic(dataset_id, name) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a pure-lookup API to `ObjectStore`: std::optional ObjectStore::findTopic(DatasetId, std::string_view) const noexcept; Returns the existing handle if a topic with the given `(dataset_id, topic_name)` is registered, `nullopt` otherwise. Does not mutate the store. `registerTopic` keeps its strict semantics: it still returns `unexpected("topic already registered: ...")` when a duplicate is attempted. Callers that need find-or-register semantics compose the two explicitly. Why --- `OBJECT_STORE_DESIGN.md` documents that DataSource plugins register object topics on the source-side path (`pj.source_object_write.v1`), and host-side runtime code resolves an existing id when binding parser- side write surfaces (`pj.parser_object_write.v1`) — i.e. the source owns registration; the host looks up. Without a lookup-only API the host had to call `registerTopic` from its parser-binding flow. When a source had already registered the same topic via its own service, the second caller failed with `"topic already registered: ..."`, the source-side `pushLazy` path went silently unwired, and blob channels lost retention. `findTopic` lets the host honour the documented contract: bind to an existing id without creating one. Tests ----- - `FindTopicReturnsRegisteredId` — happy path lookup after register. - `FindTopicMissingReturnsNullopt` — explicit miss returns nullopt. - `DuplicateRegistrationFails` (existing) is left untouched — `registerTopic`'s strict contract is preserved. --- .../include/pj_datastore/object_store.hpp | 6 ++++++ pj_datastore/src/object_store.cpp | 11 +++++++++++ pj_datastore/tests/object_store_test.cpp | 15 +++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/pj_datastore/include/pj_datastore/object_store.hpp b/pj_datastore/include/pj_datastore/object_store.hpp index 37e4507..9dca500 100644 --- a/pj_datastore/include/pj_datastore/object_store.hpp +++ b/pj_datastore/include/pj_datastore/object_store.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,11 @@ class ObjectStore { Expected registerTopic(const ObjectTopicDescriptor& descriptor); + // Resolve a topic id by (dataset_id, topic_name) without registering. Returns + // nullopt if no topic with that key exists. Used by hosts that need to bind a + // parser-side write surface to a topic the source already registered. + std::optional findTopic(DatasetId dataset_id, std::string_view topic_name) const; + const ObjectTopicDescriptor& descriptor(ObjectTopicId id) const; std::vector listTopics() const; diff --git a/pj_datastore/src/object_store.cpp b/pj_datastore/src/object_store.cpp index 60a8f13..e53f85c 100644 --- a/pj_datastore/src/object_store.cpp +++ b/pj_datastore/src/object_store.cpp @@ -21,6 +21,17 @@ Expected ObjectStore::registerTopic(const ObjectTopicDescriptor& return id; } +std::optional ObjectStore::findTopic( + DatasetId dataset_id, std::string_view topic_name) const { + std::shared_lock lock(store_mutex_); + for (const auto& [tid, series] : topics_) { + if (series->descriptor.dataset_id == dataset_id && series->descriptor.topic_name == topic_name) { + return tid; + } + } + return std::nullopt; +} + const ObjectTopicDescriptor& ObjectStore::descriptor(ObjectTopicId id) const { std::shared_lock lock(store_mutex_); const auto* s = findSeries(id); diff --git a/pj_datastore/tests/object_store_test.cpp b/pj_datastore/tests/object_store_test.cpp index f2f4492..65dddc6 100644 --- a/pj_datastore/tests/object_store_test.cpp +++ b/pj_datastore/tests/object_store_test.cpp @@ -48,6 +48,21 @@ TEST(ObjectStoreTest, SameNameDifferentDatasetOk) { EXPECT_NE(id1.id, id2_or->id); } +TEST(ObjectStoreTest, FindTopicReturnsRegisteredId) { + ObjectStore store; + auto id = registerTestTopic(store, "cam/image"); + auto found = store.findTopic(1, "cam/image"); + ASSERT_TRUE(found.has_value()); + EXPECT_EQ(found->id, id.id); +} + +TEST(ObjectStoreTest, FindTopicMissingReturnsNullopt) { + ObjectStore store; + registerTestTopic(store, "cam/image"); + EXPECT_FALSE(store.findTopic(1, "other/topic").has_value()); + EXPECT_FALSE(store.findTopic(99, "cam/image").has_value()); +} + TEST(ObjectStoreTest, ListTopics) { ObjectStore store; auto id1 = registerTestTopic(store, "topic_a");