Skip to content

feat: Translate FDv2 payloads at the data source layer#309

Merged
kinyoklion merged 5 commits into
mainfrom
rlamb/sdk-2186/fdv2-source-translation
Jun 16, 2026
Merged

feat: Translate FDv2 payloads at the data source layer#309
kinyoklion merged 5 commits into
mainfrom
rlamb/sdk-2186/fdv2-source-translation

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Jun 15, 2026

Copy link
Copy Markdown
Member

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 ChangeSet distinct from the wire-level Payload.

Previously the streaming and polling sources emitted a ChangeSetResult carrying the raw wire Payload (a list of Updates with unparsed object maps), and the flag-eval objects were converted to typed descriptors later, at apply time in DataSourceEventHandler.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:

  • ChangeSet carries typed Map<String, ItemDescriptor> updates plus the type and selector. translatePayload converts a wire Payload into a ChangeSet, throwing if any flag-eval object cannot be parsed.
  • The streaming and polling sources translate at acquisition. A translation failure becomes an interrupted source result, exactly like a malformed protocol body — so the orchestrator's fallback timer governs recovery (a source stuck on invalid data falls back after the timeout instead of retrying forever). Streaming discards the partial handler state and keeps the SSE connection; the server's next valid payload is processed normally.
  • ChangeSetResult carries the ChangeSet, and handlePayload applies it directly with no conversion and no failure path of its own.
  • The cache initializer builds its ChangeSet straight 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_client suite 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 Payload from typed ChangeSet (Map<String, ItemDescriptor>). translatePayload runs in polling/streaming (and cache builds descriptors directly), so ChangeSetResult and handlePayload only apply already-typed updates.

Malformed flag-eval data is surfaced as interrupted at 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; SourceFactoryContext carries credential-driven auth query params. Streaming synchronizer factories are implemented (SSE client, per-reconnect URI/basis, legacy ping → one-shot poll, env ID fallback). Streaming also maps UnrecoverableStatusError to 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.

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.
…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 -->
'$streaming/$encodedContext';
}

/// Builds an FDv2 request URI: appends [addedPath] to [baseUri]'s path and

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was part of 306. Which I rebased on to 309, and then merged into 309. So some of this will be familiar.

@kinyoklion kinyoklion changed the title refactor: Translate FDv2 payloads at the data source layer feat: Translate FDv2 payloads at the data source layer Jun 15, 2026
///
/// Shared by the polling requestor and the streaming source so the two
/// transports build URLs identically.
Uri buildFDv2Uri({

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a helper to make sure we handle the query parameters consistently for streaming and polling.

void _handleSseError(Object err, StackTrace stack) {
if (_stoppedSignal.isCompleted) return;

if (err is UnrecoverableStatusError) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as PR 306.

/// `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 {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now matches the ChangeSet type in other SDKs.

@kinyoklion kinyoklion marked this pull request as ready for review June 15, 2026 22:30
@kinyoklion kinyoklion requested a review from a team as a code owner June 15, 2026 22:30
@kinyoklion kinyoklion merged commit 7b46ac6 into main Jun 16, 2026
8 checks passed
@kinyoklion kinyoklion deleted the rlamb/sdk-2186/fdv2-source-translation branch June 16, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants