Skip to content

Commit ceb931f

Browse files
authored
feat: Wire FDv2 connection-mode resolution in common_client (#279)
## Summary Wires the FDv2 mode-resolution scaffolding (merged via #274) into the `common_client` runtime path. Keeps the FDv1 `ConnectionMode` enum and all existing public API signatures unchanged. This is the first half of the further split of the original behavior PR (#275, now closed). The follow-up flutter-only PR wires this into the Flutter SDK. **LDCommonClient** - `setMode(ConnectionMode)` keeps its FDv1 signature; internally maps the 3 legacy modes to `ResolvedConnectionMode` before forwarding to `DataSourceManager`. - New `setResolvedMode(ResolvedConnectionMode)` is the advanced FDv2 entry point. Documented as EAP / not-stable. **No internal caller in this PR** -- the Flutter SDK's `ConnectionManager` invokes it in the follow-up flutter PR. - `DataSourceFactoriesFn` (the public, optional constructor seam) stays typed `Map<ConnectionMode, DataSourceFactory>` so external callers can still customize streaming + polling. `_backgroundFactory` is SDK-managed (background is FDv2-only), and `_composeFactoriesForManager` translates the FDv1 map into the FDv2-keyed `Map<FDv2ConnectionMode, DataSourceFactory>` consumed by `DataSourceManager`. **DataSourceManager** (internal, not publicly exported) - Active mode held as `ResolvedConnectionMode`. - Factory map keyed by `FDv2ConnectionMode`. - `ResolvedOffline` branches dispatch status as `setOffline` / `networkUnavailable` / `backgroundDisabled` depending on `OfflineDetail`. Tests updated to match. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core data-source connection and status behavior (including removal of manager-level network toggling); public API is preserved but internal mode transitions and offline semantics are more complex until the Flutter layer adopts setResolvedMode. > > **Overview** > Wires **FDv2** connection-mode resolution into the `common_client` runtime while keeping the public **`ConnectionMode`** API unchanged. > > **`DataSourceManager`** now tracks **`FDv2ConnectionMode`** plus **`OfflineDetail`**, accepts **`ResolvedConnectionMode`** via `setMode`, and keys factories by FDv2 modes (including **background**). Offline no longer always means “set offline”: **`ResolvedOffline`** maps **`OfflineSetOffline`**, **`OfflineNetworkUnavailable`**, and **`OfflineBackgroundDisabled`** to the matching data-source status without starting a connection. Internal **`setNetworkAvailable`** on the manager is removed—network-unavailable behavior is expected to come through resolved offline modes instead. > > **`LDCommonClient`** maps legacy `setMode(ConnectionMode)` to resolved modes, adds EAP **`setResolvedMode`**, composes FDv1 custom factories into an FDv2 factory map via **`_composeFactoriesForManager`**, and registers an SDK-managed **background** polling factory. `setNetworkAvailability` no longer touches the data source manager (events only). > > Tests are updated for FDv2 modes and the new offline status branches. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0921328. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 23372f9 commit ceb931f

4 files changed

Lines changed: 207 additions & 105 deletions

File tree

packages/common_client/lib/src/data_sources/data_source_manager.dart

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
44
show LDContext, LDLogger;
55

66
import '../connection_mode.dart';
7+
import '../fdv2_connection_mode.dart';
8+
import '../offline_detail.dart';
9+
import '../resolved_connection_mode.dart';
710
import 'data_source.dart';
811
import 'data_source_event_handler.dart';
912
import 'data_source_status_manager.dart';
@@ -13,16 +16,21 @@ typedef DataSourceFactory = DataSource Function(LDContext context);
1316
/// The data source manager controls which data source is connected to
1417
/// the data source status as well as the data source event handler.
1518
final class DataSourceManager {
16-
ConnectionMode _activeMode;
19+
/// The mode that drives factory lookup and status dispatch.
20+
FDv2ConnectionMode _activeConnectionMode;
21+
22+
/// Semantically meaningful only when [_activeConnectionMode] is
23+
/// [FDv2Offline]. Otherwise carries a stale value from the last time the
24+
/// SDK was offline (or the construction-time default), and is intentionally
25+
/// only read inside the [FDv2Offline] arm of [_setupConnection].
26+
OfflineDetail _offlineDetail;
27+
1728
LDContext? _activeContext;
1829

1930
final LDLogger _logger;
2031
final DataSourceStatusManager _statusManager;
2132
final DataSourceEventHandler _dataSourceEventHandler;
22-
final Map<ConnectionMode, DataSourceFactory> _dataSourceFactories = {};
23-
24-
// At start we assume the network is available.
25-
bool _networkAvailable = true;
33+
final Map<FDv2ConnectionMode, DataSourceFactory> _dataSourceFactories = {};
2634

2735
DataSource? _activeDataSource;
2836
StreamSubscription<MessageStatus?>? _subscription;
@@ -35,15 +43,20 @@ final class DataSourceManager {
3543
required DataSourceStatusManager statusManager,
3644
required DataSourceEventHandler dataSourceEventHandler,
3745
required LDLogger logger,
38-
}) : _activeMode = startingMode,
46+
}) : _activeConnectionMode = switch (startingMode) {
47+
ConnectionMode.streaming => const FDv2Streaming(),
48+
ConnectionMode.polling => const FDv2Polling(),
49+
ConnectionMode.offline => const FDv2Offline(),
50+
},
51+
_offlineDetail = const OfflineSetOffline(),
3952
_logger = logger.subLogger('DataSourceManager'),
4053
_statusManager = statusManager,
4154
_dataSourceEventHandler = dataSourceEventHandler;
4255

4356
/// Set the available data source factories. These factories will not apply
4457
/// until the next identify call. Currently factories will be set once during
4558
/// startup and before the first identify.
46-
void setFactories(Map<ConnectionMode, DataSourceFactory> factories) {
59+
void setFactories(Map<FDv2ConnectionMode, DataSourceFactory> factories) {
4760
_dataSourceFactories.clear();
4861
_dataSourceFactories.addAll(factories);
4962
}
@@ -55,25 +68,21 @@ final class DataSourceManager {
5568
_setupConnection();
5669
}
5770

58-
void setMode(ConnectionMode mode) {
59-
if (mode == _activeMode) {
60-
_logger.debug('Mode already active: $_activeMode');
61-
return;
62-
}
63-
_logger.debug('Changing data source mode from: $_activeMode to: $mode');
64-
_activeMode = mode;
65-
_setupConnection();
66-
}
67-
68-
void setNetworkAvailable(bool available) {
69-
if (_networkAvailable == available) {
70-
_logger.debug('Network availability set to same value: $available');
71+
void setMode(ResolvedConnectionMode mode) {
72+
final newConnectionMode = mode.connectionMode;
73+
final newDetail = mode is ResolvedOffline ? mode.detail : null;
74+
final isOffline = newConnectionMode is FDv2Offline;
75+
if (newConnectionMode == _activeConnectionMode &&
76+
(!isOffline || newDetail == _offlineDetail)) {
77+
_logger.debug('Mode is already set to: $mode');
7178
return;
7279
}
73-
7480
_logger.debug(
75-
'Network availability changed from: $_networkAvailable to: $available');
76-
_networkAvailable = available;
81+
'Changing connection mode from: $_activeConnectionMode to: $mode');
82+
_activeConnectionMode = newConnectionMode;
83+
if (newDetail != null) {
84+
_offlineDetail = newDetail;
85+
}
7786
_setupConnection();
7887
}
7988

@@ -83,7 +92,7 @@ final class DataSourceManager {
8392
_activeDataSource = null;
8493
}
8594

86-
DataSource? _createDataSource(ConnectionMode mode) {
95+
DataSource? _createDataSource(FDv2ConnectionMode mode) {
8796
if (_activeContext != null) {
8897
if (_dataSourceFactories[mode] == null) {
8998
_logger.debug('No data source factory exists for mode: $mode');
@@ -107,33 +116,24 @@ final class DataSourceManager {
107116

108117
_stopConnection();
109118

110-
// If the active mode is offline, then we do not need to setup
111-
// a new connection. Additionally if we are offline, and the network
112-
// is not available, our data source status should remain offline.
113-
if (_activeMode == ConnectionMode.offline) {
114-
_statusManager.setOffline();
115-
return;
116-
}
117-
118-
// We are not offline, but the network is not available, so we are going
119-
// to set the status as unavailable and not start a new connection.
120-
if (!_networkAvailable) {
121-
_statusManager.setNetworkUnavailable();
122-
return;
123-
}
124-
125-
switch (_activeMode) {
126-
case ConnectionMode.offline:
127-
_statusManager.setOffline();
128-
case ConnectionMode.streaming:
129-
case ConnectionMode.polling:
130-
// default:
131-
// We may want to consider adding another state to the data source state
132-
// for the intermediate between switching data sources, or for identifying
133-
// a new context.
119+
switch (_activeConnectionMode) {
120+
case FDv2Offline():
121+
switch (_offlineDetail) {
122+
case OfflineSetOffline():
123+
_statusManager.setOffline();
124+
case OfflineNetworkUnavailable():
125+
_statusManager.setNetworkUnavailable();
126+
case OfflineBackgroundDisabled():
127+
_statusManager.setBackgroundDisabled();
128+
}
129+
return;
130+
case FDv2Streaming():
131+
case FDv2Polling():
132+
case FDv2Background():
133+
break;
134134
}
135135

136-
_activeDataSource = _createDataSource(_activeMode);
136+
_activeDataSource = _createDataSource(_activeConnectionMode);
137137
_subscription = _activeDataSource?.events.asyncMap((event) async {
138138
if (_activeContext == null) {
139139
_logger.error(

packages/common_client/lib/src/ld_common_client.dart

Lines changed: 84 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import 'config/data_source_config.dart';
88
import 'config/defaults/credential_type.dart';
99
import 'config/defaults/default_config.dart';
1010
import 'connection_mode.dart';
11+
import 'fdv2_connection_mode.dart';
12+
import 'offline_detail.dart';
13+
import 'resolved_connection_mode.dart';
1114
import 'context_modifiers/anonymous_context_modifier.dart';
1215
import 'context_modifiers/context_modifier.dart';
1316
import 'context_modifiers/env_context_modifier.dart';
1417
import 'hooks/hook.dart';
1518
import 'hooks/hook_runner.dart';
19+
import 'data_sources/data_source.dart';
1620
import 'data_sources/data_source_event_handler.dart';
21+
import 'data_sources/fdv2/built_in_modes.dart';
1722
import 'data_sources/data_source_manager.dart';
1823
import 'data_sources/data_source_status.dart';
1924
import 'data_sources/data_source_status_manager.dart';
@@ -76,35 +81,76 @@ typedef DataSourceFactoriesFn = Map<ConnectionMode, DataSourceFactory> Function(
7681

7782
Map<ConnectionMode, DataSourceFactory> _defaultFactories(
7883
LDCommonConfig config, LDLogger logger, HttpProperties httpProperties) {
79-
final pollingDataSourceConfig = PollingDataSourceConfig(
80-
useReport: config.dataSourceConfig.useReport,
81-
withReasons: config.dataSourceConfig.evaluationReasons,
84+
final useReport = config.dataSourceConfig.useReport;
85+
final withReasons = config.dataSourceConfig.evaluationReasons;
86+
87+
final foregroundPollingConfig = PollingDataSourceConfig(
88+
useReport: useReport,
89+
withReasons: withReasons,
8290
pollingInterval: config.dataSourceConfig.polling.pollingInterval);
91+
92+
DataSource streaming(LDContext context) {
93+
return StreamingDataSource(
94+
credential: config.sdkCredential,
95+
context: context,
96+
endpoints: config.serviceEndpoints,
97+
logger: logger,
98+
dataSourceConfig: StreamingDataSourceConfig(
99+
useReport: useReport, withReasons: withReasons),
100+
pollingDataSourceConfig: foregroundPollingConfig,
101+
httpProperties: httpProperties);
102+
}
103+
83104
return {
84-
ConnectionMode.streaming: (LDContext context) {
85-
return StreamingDataSource(
86-
credential: config.sdkCredential,
87-
context: context,
88-
endpoints: config.serviceEndpoints,
89-
logger: logger,
90-
dataSourceConfig: StreamingDataSourceConfig(
91-
useReport: config.dataSourceConfig.useReport,
92-
withReasons: config.dataSourceConfig.evaluationReasons),
93-
pollingDataSourceConfig: pollingDataSourceConfig,
94-
httpProperties: httpProperties);
95-
},
105+
ConnectionMode.streaming: streaming,
96106
ConnectionMode.polling: (LDContext context) {
97107
return PollingDataSource(
98108
credential: config.sdkCredential,
99109
context: context,
100110
endpoints: config.serviceEndpoints,
101111
logger: logger,
102-
dataSourceConfig: pollingDataSourceConfig,
112+
dataSourceConfig: foregroundPollingConfig,
103113
httpProperties: httpProperties);
104114
},
105115
};
106116
}
107117

118+
/// Background factory is SDK-managed; it always uses the built-in
119+
/// reduced-frequency polling configuration. Customizing the background
120+
/// factory is intentionally not exposed via [DataSourceFactoriesFn].
121+
DataSourceFactory _backgroundFactory(
122+
LDCommonConfig config, LDLogger logger, HttpProperties httpProperties) {
123+
final backgroundPollingConfig = PollingDataSourceConfig(
124+
useReport: config.dataSourceConfig.useReport,
125+
withReasons: config.dataSourceConfig.evaluationReasons,
126+
pollingInterval: BuiltInModes.defaultBackgroundPollInterval);
127+
return (LDContext context) {
128+
return PollingDataSource(
129+
credential: config.sdkCredential,
130+
context: context,
131+
endpoints: config.serviceEndpoints,
132+
logger: logger,
133+
dataSourceConfig: backgroundPollingConfig,
134+
httpProperties: httpProperties);
135+
};
136+
}
137+
138+
/// Translate the public, FDv1-keyed factory map (optionally with a custom
139+
/// override via [DataSourceFactoriesFn]) into the FDv2-keyed map consumed by
140+
/// [DataSourceManager], adding the SDK-managed background factory.
141+
Map<FDv2ConnectionMode, DataSourceFactory> _composeFactoriesForManager({
142+
required Map<ConnectionMode, DataSourceFactory> fdv1Factories,
143+
required DataSourceFactory backgroundFactory,
144+
}) {
145+
return {
146+
if (fdv1Factories[ConnectionMode.streaming] case final f?)
147+
const FDv2Streaming(): f,
148+
if (fdv1Factories[ConnectionMode.polling] case final f?)
149+
const FDv2Polling(): f,
150+
const FDv2Background(): backgroundFactory,
151+
};
152+
}
153+
108154
typedef EventProcessorFactory = EventProcessor Function(
109155
{required LDLogger logger,
110156
required bool indexEvents,
@@ -382,16 +428,16 @@ final class LDCommonClient {
382428
_updateEventSendingState();
383429

384430
if (!_config.offline) {
385-
_dataSourceManager
386-
.setFactories(_dataSourceFactories(_config, _logger, httpProperties));
431+
_dataSourceManager.setFactories(_composeFactoriesForManager(
432+
fdv1Factories: _dataSourceFactories(_config, _logger, httpProperties),
433+
backgroundFactory: _backgroundFactory(_config, _logger, httpProperties),
434+
));
387435
} else {
436+
DataSource nullSource(LDContext _) => NullDataSource();
388437
_dataSourceManager.setFactories({
389-
ConnectionMode.streaming: (LDContext context) {
390-
return NullDataSource();
391-
},
392-
ConnectionMode.polling: (LDContext context) {
393-
return NullDataSource();
394-
},
438+
const FDv2Streaming(): nullSource,
439+
const FDv2Polling(): nullSource,
440+
const FDv2Background(): nullSource,
395441
});
396442
}
397443
}
@@ -754,6 +800,20 @@ final class LDCommonClient {
754800

755801
/// Set the connection mode the SDK should use.
756802
void setMode(ConnectionMode mode) {
803+
_dataSourceManager.setMode(switch (mode) {
804+
ConnectionMode.streaming => const ResolvedStreaming(),
805+
ConnectionMode.polling => const ResolvedPolling(),
806+
ConnectionMode.offline => const ResolvedOffline(OfflineSetOffline()),
807+
});
808+
}
809+
810+
/// Set a resolved FDv2 connection mode the SDK should use.
811+
///
812+
/// This method is not stable, and not subject to any backwards compatibility
813+
/// guarantees or semantic versioning. It is in early access. If you want
814+
/// access to this feature please join the EAP.
815+
/// https://launchdarkly.com/docs/sdk/features/data-saving-mode
816+
void setResolvedMode(ResolvedConnectionMode mode) {
757817
_dataSourceManager.setMode(mode);
758818
}
759819

@@ -764,7 +824,6 @@ final class LDCommonClient {
764824
return;
765825
}
766826
_networkAvailable = available;
767-
_dataSourceManager.setNetworkAvailable(available);
768827
_updateEventSendingState();
769828
}
770829

0 commit comments

Comments
 (0)