Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/common_client/lib/launchdarkly_common_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ export 'src/ld_common_config.dart'
AutoEnvAttributes,
PollingConfig;

export 'src/config/data_system_config.dart'
show DataSystemConfig, ConnectionModeId;
export 'src/data_sources/fdv2/mode_definition.dart'
show
ModeDefinition,
EndpointConfig,
InitializerEntry,
SynchronizerEntry,
CacheInitializer,
PollingInitializer,
StreamingInitializer,
PollingSynchronizer,
StreamingSynchronizer,
Fdv1FallbackConfig;

export 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
show
LDContext,
Expand Down
79 changes: 79 additions & 0 deletions packages/common_client/lib/src/config/data_system_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import '../data_sources/fdv2/mode_definition.dart';

// Maintainer note (not public API): ConnectionModeId is a sealed
// hierarchy rather than an enum so a custom-mode variant can be added
// later without changing this surface. The planned extension is a custom
// variant constructed as `ConnectionModeId.custom('my-mode')`:
//
// factory ConnectionModeId.custom(String name) = _CustomConnectionMode;
// final class _CustomConnectionMode extends ConnectionModeId {
// final String name;
// const _CustomConnectionMode(this.name);
// // value equality on name so it works as an override-map key
// }
//
// A custom mode is a distinct type from a built-in, so the two share no
// namespace: a custom id never equals a built-in id (even with the same
// name), and so cannot collide with a current or future built-in. The
// type is the namespace -- no name prefix is needed. This holds only
// while custom modes stay typed; if one is ever reduced to a bare string
// (logs, persistence) that reintroduces a shared string space where a
// prefix would matter again.
//
// Equality split: the built-in values are const singletons relying on
// canonical-instance identity, which lets a connectionModes map of only
// built-in keys be a const map. A runtime-constructed custom variant must
// carry value equality, so an override map holding a custom key would be
// non-const. The built-in variant therefore must not override
// `==`/`hashCode`.

/// Identifies a built-in connection mode whose data-source pipeline can be
/// overridden through [DataSystemConfig.connectionModes]: [streaming],
/// [polling], [background], or [offline].
sealed class ConnectionModeId {
const ConnectionModeId();

/// The built-in streaming mode.
static const ConnectionModeId streaming = _BuiltInConnectionMode('streaming');

/// The built-in polling mode.
static const ConnectionModeId polling = _BuiltInConnectionMode('polling');

/// The built-in background mode.
static const ConnectionModeId background =
_BuiltInConnectionMode('background');

/// The built-in offline mode. Its pipeline loads cached flags and runs
/// no synchronizer, so overriding it customizes how the SDK behaves
/// while offline (for example, the cache initializer it uses).
static const ConnectionModeId offline = _BuiltInConnectionMode('offline');
}
Comment thread
kinyoklion marked this conversation as resolved.

final class _BuiltInConnectionMode extends ConnectionModeId {
final String name;

const _BuiltInConnectionMode(this.name);

@override
String toString() => 'ConnectionModeId.$name';
}

/// Configuration for the FDv2 data system.
///
/// Providing a [DataSystemConfig] (even an empty one) opts the SDK into
/// the FDv2 data acquisition protocol. When absent the SDK uses the
/// FDv1 data sources.
///
/// This feature is not stable, and not subject to any backwards
/// compatibility guarantees or semantic versioning. It is in early
/// access. If you want access to this feature please join the EAP.
final class DataSystemConfig {
/// Overrides for built-in connection modes. A definition given here
/// replaces the built-in pipeline for that mode; modes not present keep
/// their built-in definition.
final Map<ConnectionModeId, ModeDefinition> connectionModes;

const DataSystemConfig({
this.connectionModes = const {},
});
}
78 changes: 78 additions & 0 deletions packages/common_client/lib/src/data_sources/data_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import 'dart:async';

import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
show LDContext;

import '../flag_manager/flag_manager.dart';
import 'data_source_manager.dart';

/// Owns the data-acquisition strategy for an identify: how the cache is
/// loaded and when the identify resolves. The FDv1 and FDv2 protocols
/// diverge here, so each has its own implementation; everything else
/// (connection lifecycle, mode switching, event routing) is shared in the
/// [DataSourceManager] that both delegate to.
abstract interface class DataManager {
/// Brings the SDK to a usable state for [context], resolving when the
/// manager's data-availability strategy is satisfied.
///
/// When [waitForNetworkResults] is true the returned future resolves
/// only once network (or otherwise fresh) data has arrived; otherwise it
/// may resolve as soon as cached data is available.
Future<void> identify(LDContext context,
{required bool waitForNetworkResults});
}

/// FDv1 data manager.
///
/// The cache is loaded imperatively at identify time via
/// [FlagManager.loadCached]. A cache hit resolves identify immediately
/// unless the caller is waiting for network results; either way the
/// network connection is started so live data follows.
final class FDv1DataManager implements DataManager {
final DataSourceManager _dataSourceManager;
final FlagManager _flagManager;

FDv1DataManager(this._dataSourceManager, this._flagManager);

@override
Future<void> identify(LDContext context,
{required bool waitForNetworkResults}) async {
final completer = Completer<void>();
final loadedFromCache = await _flagManager.loadCached(context);
_dataSourceManager.identify(context, completer);
if (loadedFromCache && !waitForNetworkResults) {
return;
}
return completer.future;
}
}

/// FDv2 data manager.
///
/// The cache is not loaded at identify time; the data source pipeline's
/// cache initializer loads it as the first tier. Identify resolves on the
/// first delivered payload, or -- when waiting for network results -- only
/// on fresh data, so a cache load alone does not satisfy a wait-for-network
/// identify.
///
/// Each identify starts data acquisition fresh: any held selector is
/// discarded via [clearSelector] before connecting, so the new connection
/// re-fetches a full payload rather than resuming a previous context's
/// basis. Mode switches keep the selector and reach the data source manager
/// directly rather than through here, so they are unaffected.
final class FDv2DataManager implements DataManager {
final DataSourceManager _dataSourceManager;
final void Function() _clearSelector;

FDv2DataManager(this._dataSourceManager, this._clearSelector);

@override
Future<void> identify(LDContext context,
{required bool waitForNetworkResults}) {
_clearSelector();
final completer = Completer<void>();
_dataSourceManager.identify(context, completer,
requireFreshData: waitForNetworkResults);
return completer.future;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ final class StatusEvent implements DataSourceEvent {
{this.shutdown = false});
}

/// Emitted once by the FDv2 orchestrator when initialization is complete:
/// a selector-bearing payload arrived, the initializer chain was exhausted
/// (with cached data or in a cache-only system), or the first synchronizer
/// delivered a change set. The manager resolves a wait-for-network identify
/// on this; a cached identify resolves earlier, on the first applied payload.
final class InitializedEvent implements DataSourceEvent {}

abstract interface class DataSource {
Stream<DataSourceEvent> get events;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,13 @@ final class DataSourceEventHandler {
///
/// Full change sets replace the stored flags, partial change sets apply
/// each update, and a change set of type none confirms the SDK is up to
/// date without changing data. All three mark the data source valid.
/// date without changing data.
Future<MessageStatus> handlePayload(LDContext context, ChangeSet changeSet,
{String? environmentId}) async {
try {
await _flagManager.applyChanges(
context, changeSet.updates, changeSet.type,
environmentId: environmentId);
_statusManager.setValid();
return MessageStatus.messageHandled;
} catch (err) {
_logger.error('Failed to apply an FDv2 change set: ${err.runtimeType}');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ final class DataSourceManager {

Completer<void>? _identifyCompleter;

/// When true, the active identify resolves only on fresh data, not on a
/// cache load. Set per identify from the caller's wait-for-network-results
/// preference.
bool _requireFreshData = false;

DataSourceManager({
ConnectionMode startingMode = ConnectionMode.streaming,
required DataSourceStatusManager statusManager,
Expand All @@ -61,8 +66,10 @@ final class DataSourceManager {
_dataSourceFactories.addAll(factories);
}

void identify(LDContext context, Completer<void> completer) {
void identify(LDContext context, Completer<void> completer,
{bool requireFreshData = false}) {
_identifyCompleter = completer;
_requireFreshData = requireFreshData;
_activeContext = context;

_setupConnection();
Expand Down Expand Up @@ -92,6 +99,21 @@ final class DataSourceManager {
_activeDataSource = null;
}

/// Resolves the pending identify, if any. Idempotent: only the first call
/// completes it. Callers decide *when* to call it -- a cached identify on
/// the first applied payload, a wait-for-network identify on the
/// orchestrator's [InitializedEvent].
void _maybeCompleteIdentify() {
final completer = _identifyCompleter;
if (completer == null) {
return;
}
if (!completer.isCompleted) {
completer.complete();
}
_identifyCompleter = null;
}

DataSource? _createDataSource(FDv2ConnectionMode mode) {
if (_activeContext != null) {
if (_dataSourceFactories[mode] == null) {
Expand All @@ -118,6 +140,11 @@ final class DataSourceManager {

switch (_activeConnectionMode) {
case FDv2Offline():
// Report why the SDK is offline. When an offline data source is
// configured (the FDv2 data system supplies one) it then loads
// cached flags through the pipeline below; its payload does not
// drive the status to valid while offline, so this status stands.
// FDv1 has no offline factory, so offline stays status-only.
switch (_offlineDetail) {
case OfflineSetOffline():
_statusManager.setOffline();
Expand All @@ -126,7 +153,6 @@ final class DataSourceManager {
case OfflineBackgroundDisabled():
_statusManager.setBackgroundDisabled();
}
return;
case FDv2Streaming():
case FDv2Polling():
case FDv2Background():
Expand All @@ -146,22 +172,36 @@ final class DataSourceManager {
var handled = await _dataSourceEventHandler.handleMessage(
_activeContext!, event.type, event.data,
environmentId: event.environmentId);
if (handled == MessageStatus.messageHandled &&
_identifyCompleter != null) {
if (_identifyCompleter!.isCompleted) {
_logger.error('Identify was already complete before receiving '
'data. This could represent an issue with SDK logic. Please'
'make a bug report if you encounter this situation.');
} else {
_identifyCompleter!.complete();
}
if (handled == MessageStatus.messageHandled) {
_maybeCompleteIdentify();
}
// Only need to complete this the first time.
_identifyCompleter = null;
return handled;
case PayloadEvent():
// The FDv1 data sources this manager runs never produce FDv2
// payload events.
var handled = await _dataSourceEventHandler.handlePayload(
_activeContext!, event.changeSet,
environmentId: event.environmentId);
if (handled == MessageStatus.messageHandled) {
// Applying any change set from a live source marks it valid --
// including a no-change response, which restores valid after an
// interruption. While offline the status set in _setupConnection
// stands, so cached data does not report a live connection.
if (_activeConnectionMode is! FDv2Offline) {
_statusManager.setValid();
}
// A cached identify resolves on any applied data; a
// wait-for-network identify waits for the orchestrator's
// InitializedEvent instead.
if (!_requireFreshData) {
_maybeCompleteIdentify();
}
}
return handled;
case InitializedEvent():
// Initialization is complete (network basis, initializer
// exhaustion, or the first synchronizer change set). Resolves a
// wait-for-network identify; a cached identify has usually resolved
// already on earlier data.
_maybeCompleteIdentify();
return MessageStatus.messageHandled;
case StatusEvent():
if (_identifyCompleter != null && !_identifyCompleter!.isCompleted) {
Expand Down
Loading
Loading