Skip to content

feat: Add the FDv2 data system and expose it through configuration#310

Draft
kinyoklion wants to merge 4 commits into
mainfrom
rlamb/sdk-2186/fdv2-data-system
Draft

feat: Add the FDv2 data system and expose it through configuration#310
kinyoklion wants to merge 4 commits into
mainfrom
rlamb/sdk-2186/fdv2-data-system

Conversation

@kinyoklion

@kinyoklion kinyoklion commented Jun 17, 2026

Copy link
Copy Markdown
Member

What this adds

The piece that ties the FDv2 building blocks together and turns them on: FDv2DataSystem, the manager routing that applies FDv2 payloads, and the configuration that opts in. This completes the FDv2 work in common_client — everything below the public Flutter API.

  • FDv2DataSystem composes the data source factories the DataSourceManager consumes. It owns the state that must outlive any single orchestrator: the current selector and the context it belongs to. A fresh orchestrator is built per connection-mode switch and per identify; the selector survives mode switches (initializers are skipped when a selector is already held) but resets when the context changes, since a selector is specific to one context. buildFactories() returns factories for streaming, polling, and background — offline has no data source and is handled by the manager directly. Custom connection modes from config replace the built-in definitions by name.
  • DataSourceManager payload routing. The PayloadEvent case — a no-op placeholder in the orchestrator PR — now applies the change set through handlePayload and completes the pending identify, the same way DataEvent does for FDv1.
  • Configuration & exposure. DataSystemConfig (providing it, even empty, opts into FDv2); LDCommonConfig.dataSystem; LDCommonClient constructs an FDv2DataSystem and installs its factories when dataSystem is configured, otherwise the FDv1 sources run unchanged; and the new public types are exported.

When dataSystem is absent the FDv1 path is byte-for-byte unchanged, so existing behavior is unaffected.

Testing

  • DataSourceManager test: a PayloadEvent is applied through handlePayload, marks the source valid, and completes identify (a dropped/no-op payload would leave identify hanging — this distinguishes the real routing from the placeholder).
  • FDv2DataSystem tests: buildFactories exposes streaming/polling/background and not offline; each factory builds a fresh data source per call; a custom connection mode replaces the built-in definition.
  • DataSystemConfig default (no custom modes).
  • Full common_client suite passes; flutter_client_sdk (the dependent package) analyzes and tests clean against these changes.
  • End-to-end behavior of the wired data system is exercised by the FDv2 contract tests, which land with the contract-service changes in a later PR (and pass on the integration branch today).

SDK-2186


Note

Medium Risk
Changes core flag acquisition wiring when dataSystem is set and alters identify completion for payload events; default behavior is unchanged when config is omitted, but the new path touches initialization and data-source orchestration.

Overview
Adds an EAP opt-in for FDv2: setting LDCommonConfig.dataSystem (even const DataSystemConfig()) switches startup to FDv2DataSystem factories instead of the existing FDv1 streaming/polling/background wiring; leaving dataSystem null keeps the prior path.

DataSystemConfig exposes ConnectionModeId (streaming/polling/background) and optional connectionModes overrides that replace built-in ModeDefinition pipelines. FDv2DataSystem builds per-mode factories (fresh orchestrator per connection), persists selector across mode switches but resets it on context change, and skips initializers when a selector is already held.

DataSourceManager now routes PayloadEvent through handlePayload and shares _completeIdentify with FDv1 DataEvent handling so identify completes when FDv2 payloads land. Public exports add the new config and mode-definition types.

Tests cover payload→identify completion and FDv2DataSystem factory/override behavior.

Reviewed by Cursor Bugbot for commit 6225462. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread packages/common_client/lib/src/config/data_system_config.dart Outdated
@kinyoklion kinyoklion force-pushed the rlamb/sdk-2186/fdv2-data-system branch 3 times, most recently from 9e8ec33 to fff23d4 Compare June 17, 2026 17:54
Comment thread packages/common_client/lib/src/config/data_system_config.dart Outdated
@kinyoklion kinyoklion force-pushed the rlamb/sdk-2186/fdv2-data-system branch from fff23d4 to 6225462 Compare June 17, 2026 18:00
@kinyoklion kinyoklion marked this pull request as ready for review June 17, 2026 21:53
@kinyoklion kinyoklion requested a review from a team as a code owner June 17, 2026 21:53
})).buildFactories()[const FDv2Streaming()]!;

final source = factory(_context());
expect(source, isA<DataSource>());

@tanderson-ld tanderson-ld Jun 18, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this expect the polling source in order to confirm it isn't the streaming one?

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.

They are all the orchestrator type. So it isn't quite that straightforward. It is more an e2e test, but we can make sure it does a poll and make sure it doesn't use SSE.

@kinyoklion kinyoklion Jun 18, 2026

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.

The type-to-source mapping (e.g. a polling definition producing a polling source) is covered in entry_factories_test. What this test was actually missing is the data system's own job -- confirming the override is selected over the built-in; isA<DataSource>() passed either way. Updated it to assert resolvedDefinition(mode) returns the override for the overridden mode and the built-in for the others.

Comment thread packages/common_client/lib/src/config/data_system_config.dart
// The common client loads cached flags into the flag store before
// the data source starts (FlagManager.loadCached during identify),
// so the cache is already applied by the time this chain runs.
// Reporting a miss advances the chain without re-applying it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment seems to "know too much".


DataSourceFactory _factoryForMode(ModeDefinition modeDefinition) {
return (LDContext context) {
if (!identical(context, _lastContext)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If you go from context A -> B -> A and there is no decoration via auto env or anonymous context decoration, could this be the same instance? If this function isn't invoked during the transition (offline mode?), the selector may not get cleared but context B's flags may be loaded from cache.

I'm hand waving a bit and haven't fully investigated.

Android had a context generation bug recently so seeing this makes me wonder if it can happen here.

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 isn't just used for identify. But I can see if this really needs to even know this.

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.

Reworked this so the basis reset no longer keys on the context instance in the factory. It is now driven from the data manager's identify path, keyed on the context canonical key, so it fires on any context change independent of the active mode (including offline) and independent of the factory being invoked at all -- which closes the instance-reuse / offline-transition gap. Added a regression test for the identify-change-identify sequence. See 7f96be5.

@kinyoklion kinyoklion marked this pull request as draft June 18, 2026 19:44
…ata source

Split the identify strategy into FDv1/FDv2 data managers so cache handling
lives with each protocol. FDv2 no longer loads the cache at identify; the
data system reads it through a real cachedFlagsReader and feeds it into the
pipeline. A PayloadEvent basis flag, set by the orchestrator, gates both the
valid status (moved out of handlePayload) and wait-for-network resolution, so
cached flags apply without reporting a live connection. Offline is now a real
pipeline mode (cache initializer, no synchronizer) that loads cache while the
manager keeps the offline status. Adds ConnectionModeId.offline and
FlagManager.readCached.
…ctory

The held selector was discarded inside the data source factory by comparing
context instance identity, which only held while the factory was invoked for
every context change. Move the reset to the data manager's identify path,
keyed on the context canonical key, so a context change clears the basis
regardless of the active connection mode (including offline) and independent
of factory invocation timing. The factory no longer tracks the context.
The override test asserted only that a DataSource was produced, which holds
whether or not the override is honored. Assert that resolvedDefinition picks
the override for the overridden mode and the built-in otherwise. Translating a
definition's entries into concrete sources stays covered by the entry-factory
tests. Adds a visibleForTesting accessor and the meta dependency it needs.
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