Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 {},
});
}
85 changes: 85 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,85 @@
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 basis (network or terminal) data, so a cache load alone does not
/// satisfy a wait-for-network identify.
///
/// Identify is also where a held basis is discarded: a selector points at
/// one context's data, so on a context change [resetBasis] is invoked
/// before connecting. This is keyed on the context's canonical key and
/// driven here rather than inferred from the context instance inside the
/// data source factory, so it holds regardless of which connection mode is
/// active when the context changes (including offline).
final class FDv2DataManager implements DataManager {
final DataSourceManager _dataSourceManager;
final void Function() _resetBasis;

String? _lastContextKey;

FDv2DataManager(this._dataSourceManager, this._resetBasis);

@override
Future<void> identify(LDContext context,
{required bool waitForNetworkResults}) {
final key = context.canonicalKey;
if (key != _lastContextKey) {
_lastContextKey = key;
_resetBasis();
}
final completer = Completer<void>();
_dataSourceManager.identify(context, completer,
requireFreshData: waitForNetworkResults);
return completer.future;
}
}
15 changes: 14 additions & 1 deletion packages/common_client/lib/src/data_sources/data_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@ final class PayloadEvent implements DataSourceEvent {
final ChangeSet changeSet;
final String? environmentId;

PayloadEvent(this.changeSet, {this.environmentId});
/// Whether this payload represents the freshest data the active source
/// can produce for this initialization -- network basis data, an FDv1
/// fallback transfer, or the terminal payload of a cache-only system.
///
/// False only for preliminary cache data delivered while a fresher
/// source is still expected (e.g. the cache initializer ahead of a
/// streaming synchronizer). The manager uses this to decide whether to
/// mark the source valid and whether to resolve an identify that is
/// waiting for network results: cached flags are applied either way,
/// but a non-basis payload neither drives the status to valid nor
/// satisfies a wait-for-network identify.
final bool basis;

PayloadEvent(this.changeSet, {this.environmentId, this.basis = true});
}

final class StatusEvent implements DataSourceEvent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,19 @@ 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.
///
/// This does not touch the data source status. Unlike the FDv1 verbs,
/// an FDv2 payload only marks the source valid when it is network basis
/// data, and never while offline; the DataSourceManager makes that call
/// from the payload's basis flag and the active mode, so applying cached
/// flags here does not prematurely report valid.
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 basis (network or
/// terminal) data, not on preliminary cache data. 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,27 @@ final class DataSourceManager {
_activeDataSource = null;
}

void _completeIdentify(MessageStatus handled, {bool basis = true}) {
if (handled != MessageStatus.messageHandled || _identifyCompleter == null) {
return;
}
// An identify waiting for network results resolves only on basis
// (network or terminal) data; preliminary cache data is applied but
// leaves the identify pending so a later basis payload resolves it.
if (_requireFreshData && !basis) {
return;
}
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();
}
// Only need to complete this the first time.
_identifyCompleter = null;
}

DataSource? _createDataSource(FDv2ConnectionMode mode) {
if (_activeContext != null) {
if (_dataSourceFactories[mode] == null) {
Expand All @@ -118,6 +146,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 +159,6 @@ final class DataSourceManager {
case OfflineBackgroundDisabled():
_statusManager.setBackgroundDisabled();
}
return;
case FDv2Streaming():
case FDv2Polling():
case FDv2Background():
Expand All @@ -146,23 +178,22 @@ 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();
_completeIdentify(handled);
return handled;
case PayloadEvent():
var handled = await _dataSourceEventHandler.handlePayload(
_activeContext!, event.changeSet,
environmentId: event.environmentId);
if (handled == MessageStatus.messageHandled && event.basis) {
// Basis data marks the source valid, except while offline:
// there the offline status set in _setupConnection stands and
// a cache load must not be reported as a live connection.
if (_activeConnectionMode is! FDv2Offline) {
_statusManager.setValid();
}
}
// Only need to complete this the first time.
_identifyCompleter = null;
_completeIdentify(handled, basis: event.basis);
return handled;
case PayloadEvent():
// The FDv1 data sources this manager runs never produce FDv2
// payload events.
return MessageStatus.messageHandled;
case StatusEvent():
if (_identifyCompleter != null && !_identifyCompleter!.isCompleted) {
_identifyCompleter!.completeError(Exception(event.message));
Expand Down
Loading
Loading