feat: Translate FDv2 payloads at the data source layer#309
Merged
Conversation
Introduce ChangeSet (typed flag descriptors) distinct from the wire-level Payload, and move flag-eval translation from the apply path into the streaming and polling sources. A payload whose flag objects cannot be parsed is surfaced as an interrupted source result at acquisition time, where the orchestrator's fallback timer governs recovery, instead of failing at apply time. ChangeSetResult carries the ChangeSet and handlePayload applies it directly.
This was referenced Jun 15, 2026
…ication (#306) > Stacked on #309 (FDv2 source-layer translation); that lands first. ## What this adds The streaming half of the FDv2 source factories, plus the authentication plumbing both source kinds share. Nothing constructs these factories in production yet (the orchestrator consumes them in a later PR), so behavior is unchanged. ### Streaming factories `createSynchronizerFactoryFromEntry` now builds `FDv2StreamingSynchronizer`s instead of throwing `UnsupportedError`. The SSE client is constructed with a `uriProvider` that is re-invoked on every connection attempt, so the `basis` query parameter always reflects the current selector — a reconnect after payloads have been applied resumes with a delta request rather than refetching the full payload. The URI builder composes against the configured base URI the same way the polling requestor does (path joining without clobbering existing query parameters). ### Query-parameter authentication `SourceFactoryContext` gains `additionalQueryParameters`, applied to every data acquisition request by both FDv2 transports — streaming through the `uriProvider`, and polling through `FDv2Requestor`, which merges them into each poll URL. The transports are agnostic about what the parameters carry; authentication enters at the single population site, where `SourceFactoryContext.fromClientConfig` supplies the platform `CredentialConfig.authQueryParameters`: empty on io (every io transport authenticates with the authorization header in the base headers) and `auth=<client-side-id>` on web. The web platform authenticates with the query parameter at all times rather than the authorization header: a browser only delivers that header when the target service's CORS pre-flight allows it, the browser's native `EventSource` cannot send custom headers at all, and the FDv2 endpoint paths do not embed the credential the way the FDv1 client-side paths do. (Requirement 3.1.3 of the client-side FDv2 spec, added in launchdarkly/sdk-specs#233, permits this.) Verified against the live service: the FDv2 polling endpoint returns 200 with `?auth=<credential>` and 400 without. ## Testing Factory tests cover the streaming construction path and pin the URI contract: the streaming URI carries the auth query parameters and picks up the current selector's `basis` on each provider invocation, and the polling request URL carries the auth parameters through the factory wiring. Requestor tests assert the additional query parameters appear on every poll URL alongside `basis`, and that the requestor adds no authorization header (authentication comes from base headers or query parameters, never per-request). Full package suite passes. SDK-2186 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches authentication query parameters and streaming connection/error handling; production behavior is unchanged until the orchestrator wires these factories, but mistakes could break web auth or fallback when enabled. > > **Overview** > Adds **FDv2 streaming synchronizer** factory wiring so `StreamingSynchronizer` entries build `FDv2StreamingSynchronizer` with an SSE client whose **`uriProvider` is rebuilt on each connect**, keeping `basis` aligned with the current selector for delta resumes after reconnect. > > Introduces shared **`buildFDv2Uri`** for polling and streaming so paths, `withReasons`, `basis`, relay-style base query strings (including **duplicate keys**), and extra params merge the same way. **`SourceFactoryContext`** now carries **`credential`** and **`additionalQueryParameters`** (from `CredentialConfig.authQueryParameters`, e.g. web `auth=`), applied on every poll and stream URL via **`FDv2Requestor`** and the streaming URI builder. > > **`FDv2StreamingBase`** gains optional **`defaultEnvironmentId`** when headers are unavailable, and treats **`UnrecoverableStatusError`** as a terminal failure (with optional **`x-ld-fd-fd-fallback`**). Factory and requestor tests cover auth query params, basis on reconnect, and repeated relay query keys. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 187332d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
kinyoklion
commented
Jun 15, 2026
| '$streaming/$encodedContext'; | ||
| } | ||
|
|
||
| /// Builds an FDv2 request URI: appends [addedPath] to [baseUri]'s path and |
Member
Author
There was a problem hiding this comment.
This was part of 306. Which I rebased on to 309, and then merged into 309. So some of this will be familiar.
kinyoklion
commented
Jun 15, 2026
| /// | ||
| /// Shared by the polling requestor and the streaming source so the two | ||
| /// transports build URLs identically. | ||
| Uri buildFDv2Uri({ |
Member
Author
There was a problem hiding this comment.
This is just a helper to make sure we handle the query parameters consistently for streaming and polling.
kinyoklion
commented
Jun 15, 2026
| void _handleSseError(Object err, StackTrace stack) { | ||
| if (_stoppedSignal.isCompleted) return; | ||
|
|
||
| if (err is UnrecoverableStatusError) { |
kinyoklion
commented
Jun 15, 2026
| /// `translatePayload`) so a malformed object is reported as a data source | ||
| /// error at acquisition time -- where the connection can recover -- rather | ||
| /// than surfacing later, at apply time. | ||
| final class ChangeSet { |
Member
Author
There was a problem hiding this comment.
This now matches the ChangeSet type in other SDKs.
tanderson-ld
approved these changes
Jun 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
BEGIN_COMMIT_OVERRIDE
feat: Translate FDv2 payloads at the data source layer
feat: Add FDv2 streaming source factories and query-parameter authentication
END_COMMIT_OVERRIDE
What this changes
Moves flag-eval translation out of the apply path and into the data source layer, and introduces a typed
ChangeSetdistinct from the wire-levelPayload.Previously the streaming and polling sources emitted a
ChangeSetResultcarrying the raw wirePayload(a list ofUpdates with unparsed object maps), and the flag-eval objects were converted to typed descriptors later, at apply time inDataSourceEventHandler.handlePayload. A payload that parsed at the protocol level but whose flag objects were malformed therefore failed after acquisition — surfacing as an invalid-message that drove a connection restart, on a fixed interval, without ever arming the orchestrator's fallback timer.Now:
ChangeSetcarries typedMap<String, ItemDescriptor>updates plus the type and selector.translatePayloadconverts a wirePayloadinto aChangeSet, throwing if any flag-eval object cannot be parsed.ChangeSetResultcarries theChangeSet, andhandlePayloadapplies it directly with no conversion and no failure path of its own.ChangeSetstraight from the already-typed cached evaluation results, dropping a JSON round-trip that previously serialized typed results only to re-parse them.This is foundational for the FDv2 data system: the streaming-source and orchestrator PRs build on
ChangeSet. Nothing constructs the FDv2 sources in production yet, so behavior is unchanged for shipping code.Testing
Source-layer tests cover a protocol-valid payload whose flag data cannot be parsed: polling returns interrupted, and streaming returns interrupted then recovers on the next valid payload. The flag manager / event handler tests apply typed change sets directly (full replace, partial without per-item version comparison, none). Cache initializer tests assert the evaluation result is carried through without re-parsing. Full
common_clientsuite passes.SDK-2186
Note
Medium Risk
Touches FDv2 acquisition, URL/auth composition, and error-classification paths that govern fallback and flag updates; changes are largely behind not-yet-production FDv2 wiring but alter recovery semantics when invalid flag data arrives.
Overview
FDv2 now separates wire
Payloadfrom typedChangeSet(Map<String, ItemDescriptor>).translatePayloadruns in polling/streaming (and cache builds descriptors directly), soChangeSetResultandhandlePayloadonly apply already-typed updates.Malformed flag-eval data is surfaced as
interruptedat the source layer (like other transient parse errors) instead of failing later in the event handler—so the orchestrator’s fallback timer can kick in rather than endless connection retries.Adds shared
buildFDv2Uri(relay query params,basis,withReasons) for polling and streaming;SourceFactoryContextcarries credential-drivenauthquery params. Streaming synchronizer factories are implemented (SSE client, per-reconnect URI/basis, legacyping→ one-shot poll, env ID fallback). Streaming also mapsUnrecoverableStatusErrorto terminal errors with optional FDv1 fallback.Reviewed by Cursor Bugbot for commit 38646ce. Bugbot is set up for automated code reviews on this repo. Configure here.