22// Copyright 2026 Davide Faconti
33// SPDX-License-Identifier: MPL-2.0
44
5- #include < any>
65#include < cstddef>
76#include < cstdint>
87#include < deque>
1413#include < string>
1514#include < string_view>
1615#include < utility>
16+ #include < variant>
1717#include < vector>
1818
1919#include " pj_base/buffer_anchor.hpp"
2020#include " pj_base/expected.hpp"
21+ #include " pj_base/span.hpp"
2122#include " pj_base/types.hpp"
2223
2324namespace PJ {
@@ -39,12 +40,20 @@ struct ObjectTopicDescriptor {
3940 std::string metadata_json;
4041};
4142
43+ // / Eager payload: store-owned bytes, counted against the retention budget.
44+ using SharedBuffer = std::shared_ptr<const std::vector<uint8_t >>;
45+
46+ // / Lazy payload: idempotent, thread-safe fetcher returning bytes + anchor.
47+ // / Invoked on every read; bytes are not counted against the retention budget.
48+ using LazyCallback = std::function<sdk::PayloadView()>;
49+
4250struct ObjectEntry {
4351 Timestamp timestamp = 0 ;
44- // Holds either a shared_ptr<const std::vector<uint8_t>> (eager owned payload)
45- // or a std::function<sdk::PayloadView()> (lazy resolver). resolveEntry
46- // discriminates via std::any_cast.
47- std::any payload;
52+ // Holds either a SharedBuffer (eager owned payload, counted against the
53+ // retention budget) or a LazyCallback (lazy resolver). resolveEntry
54+ // discriminates via std::get_if; the variant is exhaustive over the two
55+ // (and only two) payload kinds.
56+ std::variant<SharedBuffer, LazyCallback> payload;
4857};
4958
5059struct ResolvedObjectEntry {
@@ -119,16 +128,14 @@ class ObjectStore {
119128
120129 // --- Write ---
121130
122- Expected< void , std::string> pushOwned (ObjectTopicId id, Timestamp timestamp, std::vector<uint8_t > payload);
131+ Status pushOwned (ObjectTopicId id, Timestamp timestamp, std::vector<uint8_t > payload);
123132
124- // The fetch callable is invoked on every read. It returns a PayloadView
125- // (Span + anchor). When the producer already holds the bytes in memory
126- // behind a shared_ptr (e.g. a streaming buffer being handed off between
127- // stores), the closure can capture that shared_ptr and return a view
128- // backed by it — no copy. For producers that materialize bytes from disk
129- // or other sources, the closure allocates a fresh buffer and uses it as
130- // the anchor.
131- Expected<void , std::string> pushLazy (ObjectTopicId id, Timestamp timestamp, std::function<sdk::PayloadView()> fetch);
133+ // Fetcher runs on every read. Producers anchor on whatever owns the bytes
134+ // (chunk cache, mmap, fresh allocation); the store never copies — it just
135+ // retains the anchor through PayloadView. When the producer already holds
136+ // the bytes behind a shared_ptr (e.g. a streaming buffer handed off between
137+ // stores), the closure captures it and returns a view backed by it.
138+ Status pushLazy (ObjectTopicId id, Timestamp timestamp, LazyCallback fetch);
132139
133140 // --- Read ---
134141
@@ -167,10 +174,10 @@ class ObjectStore {
167174 // neither store is mutated.
168175 //
169176 // Zero-copy on the payload bytes. Each ObjectEntry is moved into the
170- // destination's series by value; the std::variant inside (or, post-#184,
171- // the std::any) holds either a shared_ptr or a std::function, and moving
172- // it is a pointer/buffer move — bytes captured by the closure or owned by
173- // the shared_ptr are never copied or materialized during the flush. Lazy
177+ // destination's series by value; the std::variant inside holds either a
178+ // shared_ptr or a std::function, and moving it is a pointer/buffer move —
179+ // bytes captured by the closure or owned by the shared_ptr are never
180+ // copied or materialized during the flush. Lazy
174181 // entries preserve their semantics in the destination: their closure is
175182 // re-invoked only when the destination is read.
176183 //
0 commit comments