Skip to content

Commit f0013da

Browse files
committed
refactor: implement fdv2 polling initializer / synchronizer
1 parent ed22857 commit f0013da

9 files changed

Lines changed: 1240 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#pragma once
2+
3+
#include <launchdarkly/data_model/fdv2_change.hpp>
4+
#include <launchdarkly/serialization/json_fdv2_events.hpp>
5+
6+
#include <boost/json/value.hpp>
7+
8+
#include <string_view>
9+
#include <variant>
10+
#include <vector>
11+
12+
namespace launchdarkly {
13+
14+
/**
15+
* Protocol state machine for the FDv2 wire format.
16+
*
17+
* Accumulates put-object and delete-object events between a server-intent
18+
* and payload-transferred event, then emits a complete FDv2ChangeSet.
19+
*
20+
* Shared between the polling and streaming synchronizers.
21+
*/
22+
class FDv2ProtocolHandler {
23+
public:
24+
/**
25+
* Result of handling a single FDv2 event:
26+
* - monostate: no output yet (accumulating, heartbeat, or unknown event)
27+
* - FDv2ChangeSet: complete changeset ready to apply
28+
* - FDv2Error: server reported an error; discard partial data
29+
* - Goodbye: server is closing; caller should rotate sources
30+
*/
31+
using Result = std::variant<std::monostate,
32+
data_model::FDv2ChangeSet,
33+
FDv2Error,
34+
Goodbye>;
35+
36+
/**
37+
* Process one FDv2 event.
38+
*
39+
* @param event_type The event type string (e.g. "server-intent",
40+
* "put-object", "payload-transferred").
41+
* @param data The parsed JSON value for the event's data field.
42+
* @return A Result indicating what (if anything) the caller
43+
* should act on.
44+
*/
45+
Result HandleEvent(std::string_view event_type,
46+
boost::json::value const& data);
47+
48+
/**
49+
* Reset accumulated state. Call on reconnect before processing new events.
50+
*/
51+
void Reset();
52+
53+
FDv2ProtocolHandler() = default;
54+
55+
private:
56+
enum class State { kInactive, kFull, kPartial };
57+
58+
State state_ = State::kInactive;
59+
std::vector<data_model::FDv2Change> changes_;
60+
};
61+
62+
} // namespace launchdarkly

libs/internal/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ set(INTERNAL_SOURCES
3535
serialization/value_mapping.cpp
3636
serialization/json_evaluation_result.cpp
3737
serialization/json_fdv2_events.cpp
38+
fdv2_protocol_handler.cpp
3839
serialization/json_sdk_data_set.cpp
3940
serialization/json_segment.cpp
4041
serialization/json_primitives.cpp
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
#include <launchdarkly/fdv2_protocol_handler.hpp>
2+
3+
#include <launchdarkly/data_model/flag.hpp>
4+
#include <launchdarkly/data_model/item_descriptor.hpp>
5+
#include <launchdarkly/data_model/segment.hpp>
6+
#include <launchdarkly/serialization/json_flag.hpp>
7+
#include <launchdarkly/serialization/json_segment.hpp>
8+
9+
#include <boost/json.hpp>
10+
#include <tl/expected.hpp>
11+
12+
namespace launchdarkly {
13+
14+
static char const* const kServerIntent = "server-intent";
15+
static char const* const kPutObject = "put-object";
16+
static char const* const kDeleteObject = "delete-object";
17+
static char const* const kPayloadTransferred = "payload-transferred";
18+
static char const* const kError = "error";
19+
static char const* const kGoodbye = "goodbye";
20+
21+
// Returns the parsed FDv2Change on success, nullopt for unknown kinds (which
22+
// should be silently skipped for forward-compatibility), or an error string if
23+
// a known kind fails to deserialize.
24+
static tl::expected<std::optional<data_model::FDv2Change>, std::string>
25+
ParsePut(PutObject const& put) {
26+
if (put.kind == "flag") {
27+
auto result = boost::json::value_to<
28+
tl::expected<std::optional<data_model::Flag>, JsonError>>(
29+
put.object);
30+
// One bad flag aborts the entire transfer so the store is never
31+
// left in a partially-updated state.
32+
if (!result) {
33+
return tl::make_unexpected("could not deserialize flag '" +
34+
put.key + "'");
35+
}
36+
if (!result->has_value()) {
37+
return tl::make_unexpected("flag '" + put.key + "' object was null");
38+
}
39+
return data_model::FDv2Change{
40+
put.key,
41+
data_model::ItemDescriptor<data_model::Flag>{std::move(**result)}};
42+
}
43+
if (put.kind == "segment") {
44+
auto result = boost::json::value_to<
45+
tl::expected<std::optional<data_model::Segment>, JsonError>>(
46+
put.object);
47+
// One bad segment aborts the entire transfer so the store is never
48+
// left in a partially-updated state.
49+
if (!result) {
50+
return tl::make_unexpected("could not deserialize segment '" +
51+
put.key + "'");
52+
}
53+
if (!result->has_value()) {
54+
return tl::make_unexpected("segment '" + put.key +
55+
"' object was null");
56+
}
57+
return data_model::FDv2Change{
58+
put.key,
59+
data_model::ItemDescriptor<data_model::Segment>{
60+
std::move(**result)}};
61+
}
62+
// Silently skip unknown kinds for forward-compatibility.
63+
return std::nullopt;
64+
}
65+
66+
static data_model::FDv2Change MakeDeleteChange(DeleteObject const& del) {
67+
if (del.kind == "flag") {
68+
return data_model::FDv2Change{
69+
del.key,
70+
data_model::ItemDescriptor<data_model::Flag>{
71+
data_model::Tombstone{static_cast<uint64_t>(del.version)}}};
72+
}
73+
return data_model::FDv2Change{
74+
del.key,
75+
data_model::ItemDescriptor<data_model::Segment>{
76+
data_model::Tombstone{static_cast<uint64_t>(del.version)}}};
77+
}
78+
79+
FDv2ProtocolHandler::Result FDv2ProtocolHandler::HandleEvent(
80+
std::string_view event_type,
81+
boost::json::value const& data) {
82+
if (event_type == kServerIntent) {
83+
auto result = boost::json::value_to<
84+
tl::expected<std::optional<ServerIntent>, JsonError>>(data);
85+
if (!result) {
86+
Reset();
87+
return FDv2Error{std::nullopt, "could not deserialize server-intent"};
88+
}
89+
if (!result->has_value()) {
90+
Reset();
91+
return FDv2Error{std::nullopt, "server-intent data was null"};
92+
}
93+
auto const& intent = **result;
94+
if (intent.payloads.empty()) {
95+
return std::monostate{};
96+
}
97+
auto const& code = intent.payloads[0].intent_code;
98+
changes_.clear();
99+
if (code == IntentCode::kTransferFull) {
100+
state_ = State::kFull;
101+
} else if (code == IntentCode::kTransferChanges) {
102+
state_ = State::kPartial;
103+
} else {
104+
// kNone or kUnknown: emit an empty changeset immediately.
105+
state_ = State::kInactive;
106+
return data_model::FDv2ChangeSet{data_model::FDv2ChangeSet::Type::kNone,
107+
{},
108+
data_model::Selector{}};
109+
}
110+
return std::monostate{};
111+
}
112+
113+
if (event_type == kPutObject) {
114+
if (state_ == State::kInactive) {
115+
return std::monostate{};
116+
}
117+
auto result = boost::json::value_to<
118+
tl::expected<std::optional<PutObject>, JsonError>>(data);
119+
if (!result) {
120+
Reset();
121+
return FDv2Error{std::nullopt, "could not deserialize put-object"};
122+
}
123+
if (!result->has_value()) {
124+
Reset();
125+
return FDv2Error{std::nullopt, "put-object data was null"};
126+
}
127+
auto change = ParsePut(**result);
128+
if (!change) {
129+
Reset();
130+
return FDv2Error{std::nullopt, std::move(change.error())};
131+
}
132+
if (*change) {
133+
changes_.push_back(std::move(**change));
134+
}
135+
return std::monostate{};
136+
}
137+
138+
if (event_type == kDeleteObject) {
139+
if (state_ == State::kInactive) {
140+
return std::monostate{};
141+
}
142+
auto result = boost::json::value_to<
143+
tl::expected<std::optional<DeleteObject>, JsonError>>(data);
144+
if (!result) {
145+
Reset();
146+
return FDv2Error{std::nullopt, "could not deserialize delete-object"};
147+
}
148+
if (!result->has_value()) {
149+
Reset();
150+
return FDv2Error{std::nullopt, "delete-object data was null"};
151+
}
152+
auto const& del = **result;
153+
// Silently skip unknown kinds for forward-compatibility.
154+
if (del.kind != "flag" && del.kind != "segment") {
155+
return std::monostate{};
156+
}
157+
changes_.push_back(MakeDeleteChange(del));
158+
return std::monostate{};
159+
}
160+
161+
if (event_type == kPayloadTransferred) {
162+
auto result = boost::json::value_to<
163+
tl::expected<std::optional<PayloadTransferred>, JsonError>>(data);
164+
if (!result) {
165+
Reset();
166+
return FDv2Error{std::nullopt,
167+
"could not deserialize payload-transferred"};
168+
}
169+
if (!result->has_value()) {
170+
Reset();
171+
return FDv2Error{std::nullopt, "payload-transferred data was null"};
172+
}
173+
auto const& transferred = **result;
174+
auto type = (state_ == State::kPartial)
175+
? data_model::FDv2ChangeSet::Type::kPartial
176+
: data_model::FDv2ChangeSet::Type::kFull;
177+
data_model::FDv2ChangeSet changeset{
178+
type,
179+
std::move(changes_),
180+
data_model::Selector{data_model::Selector::State{
181+
transferred.version, transferred.state}}};
182+
Reset();
183+
return changeset;
184+
}
185+
186+
if (event_type == kError) {
187+
auto result = boost::json::value_to<
188+
tl::expected<std::optional<FDv2Error>, JsonError>>(data);
189+
Reset();
190+
if (!result) {
191+
return FDv2Error{std::nullopt, "could not deserialize error event"};
192+
}
193+
if (!result->has_value()) {
194+
return FDv2Error{std::nullopt, "error event data was null"};
195+
}
196+
return **result;
197+
}
198+
199+
if (event_type == kGoodbye) {
200+
auto result = boost::json::value_to<
201+
tl::expected<std::optional<Goodbye>, JsonError>>(data);
202+
if (!result) {
203+
return Goodbye{std::nullopt};
204+
}
205+
if (!result->has_value()) {
206+
return Goodbye{std::nullopt};
207+
}
208+
return **result;
209+
}
210+
211+
// heartbeat and unrecognized events: no-op.
212+
return std::monostate{};
213+
}
214+
215+
void FDv2ProtocolHandler::Reset() {
216+
state_ = State::kInactive;
217+
changes_.clear();
218+
}
219+
220+
} // namespace launchdarkly

0 commit comments

Comments
 (0)