Skip to content
Open
19 changes: 19 additions & 0 deletions packages/common_client/lib/launchdarkly_common_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,25 @@ export 'src/config/common_platform.dart' show CommonPlatform;
export 'src/config/events_config.dart' show EventsConfig;
export 'src/config/credential/credential_source.dart' show CredentialSource;
export 'src/connection_mode.dart' show ConnectionMode;
export 'src/offline_detail.dart'
show
OfflineDetail,
OfflineSetOffline,
OfflineNetworkUnavailable,
OfflineBackgroundDisabled;
export 'src/resolved_connection_mode.dart'
show
ResolvedConnectionMode,
ResolvedStreaming,
ResolvedPolling,
ResolvedBackground,
ResolvedOffline;
export 'src/data_sources/fdv2/mode_resolution.dart'
show
ModeState,
ModeResolutionEntry,
resolveMode,
flutterDefaultResolutionTable;
export 'src/data_sources/data_source_status.dart'
show DataSourceStatusErrorInfo, DataSourceStatus, DataSourceState;

Expand Down
4 changes: 4 additions & 0 deletions packages/common_client/lib/src/connection_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ enum ConnectionMode {

/// The SDK will make periodic requests to receive updates from LaunchDarkly.
polling,

/// The SDK is in a background state and will use the configured background
/// connection mode or default for the platform if not configured.
background,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
show LDContext, LDLogger;

import '../connection_mode.dart';
import '../offline_detail.dart';
import '../resolved_connection_mode.dart';
import 'data_source.dart';
import 'data_source_event_handler.dart';
import 'data_source_status_manager.dart';
Expand All @@ -13,17 +15,14 @@ typedef DataSourceFactory = DataSource Function(LDContext context);
/// The data source manager controls which data source is connected to
/// the data source status as well as the data source event handler.
final class DataSourceManager {
ConnectionMode _activeMode;
ResolvedConnectionMode _activeConnectionMode;
LDContext? _activeContext;

final LDLogger _logger;
final DataSourceStatusManager _statusManager;
final DataSourceEventHandler _dataSourceEventHandler;
final Map<ConnectionMode, DataSourceFactory> _dataSourceFactories = {};

// At start we assume the network is available.
bool _networkAvailable = true;

DataSource? _activeDataSource;
StreamSubscription<MessageStatus?>? _subscription;
bool _stopped = false;
Expand All @@ -35,7 +34,12 @@ final class DataSourceManager {
required DataSourceStatusManager statusManager,
required DataSourceEventHandler dataSourceEventHandler,
required LDLogger logger,
}) : _activeMode = startingMode,
}) : _activeConnectionMode = switch (startingMode) {
ConnectionMode.streaming => const ResolvedStreaming(),
ConnectionMode.polling => const ResolvedPolling(),
ConnectionMode.background => const ResolvedBackground(),
ConnectionMode.offline => const ResolvedOffline(OfflineSetOffline()),
},
_logger = logger.subLogger('DataSourceManager'),
_statusManager = statusManager,
_dataSourceEventHandler = dataSourceEventHandler;
Expand All @@ -55,25 +59,14 @@ final class DataSourceManager {
_setupConnection();
}

void setMode(ConnectionMode mode) {
if (mode == _activeMode) {
_logger.debug('Mode already active: $_activeMode');
void setMode(ResolvedConnectionMode mode) {
if (mode == _activeConnectionMode) {
_logger.debug('Mode is already set to: $mode');
return;
}
_logger.debug('Changing data source mode from: $_activeMode to: $mode');
_activeMode = mode;
_setupConnection();
}

void setNetworkAvailable(bool available) {
if (_networkAvailable == available) {
_logger.debug('Network availability set to same value: $available');
return;
}

_logger.debug(
'Network availability changed from: $_networkAvailable to: $available');
_networkAvailable = available;
'Changing resolved connection mode from: $_activeConnectionMode to: $mode');
_activeConnectionMode = mode;
_setupConnection();
}

Expand Down Expand Up @@ -107,33 +100,25 @@ final class DataSourceManager {

_stopConnection();

// If the active mode is offline, then we do not need to setup
// a new connection. Additionally if we are offline, and the network
// is not available, our data source status should remain offline.
if (_activeMode == ConnectionMode.offline) {
_statusManager.setOffline();
return;
}

// We are not offline, but the network is not available, so we are going
// to set the status as unavailable and not start a new connection.
if (!_networkAvailable) {
_statusManager.setNetworkUnavailable();
return;
}

switch (_activeMode) {
case ConnectionMode.offline:
_statusManager.setOffline();
case ConnectionMode.streaming:
case ConnectionMode.polling:
// default:
// We may want to consider adding another state to the data source state
// for the intermediate between switching data sources, or for identifying
// a new context.
switch (_activeConnectionMode) {
case ResolvedOffline(:final detail):
switch (detail) {
case OfflineSetOffline():
_statusManager.setOffline();
case OfflineNetworkUnavailable():
_statusManager.setNetworkUnavailable();
case OfflineBackgroundDisabled():
_statusManager.setBackgroundDisabled();
}
return;
case ResolvedStreaming():
case ResolvedPolling():
case ResolvedBackground():
break;
}

_activeDataSource = _createDataSource(_activeMode);
final mode = _activeConnectionMode.connectionMode;
_activeDataSource = _createDataSource(mode);
_subscription = _activeDataSource?.events.asyncMap((event) async {
if (_activeContext == null) {
_logger.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'mode_definition.dart';

/// Built-in [ModeDefinition] values.
abstract final class BuiltInModes {
BuiltInModes._();

/// Default foreground poll interval.
static const Duration _foregroundPollInterval = Duration(seconds: 300);

static const Duration defaultBackgroundPollInterval = Duration(seconds: 3600);

/// Default streaming mode (mobile foreground / desktop).
static const ModeDefinition streaming = ModeDefinition(
initializers: [
CacheInitializer(),
PollingInitializer(),
],
synchronizers: [
StreamingSynchronizer(),
PollingSynchronizer(),
],
fdv1Fallback: Fdv1FallbackConfig(
pollInterval: _foregroundPollInterval,
),
);

/// Polling-only mode.
static const ModeDefinition polling = ModeDefinition(
initializers: [CacheInitializer()],
synchronizers: [PollingSynchronizer()],
fdv1Fallback: Fdv1FallbackConfig(
pollInterval: _foregroundPollInterval,
),
);

/// Offline: cache initializer only; no synchronizers.
static const ModeDefinition offline = ModeDefinition(
initializers: [CacheInitializer()],
synchronizers: [],
);

/// Mobile background: cache initializer, reduced-rate polling synchronizer (CSFDV2 §5.2.3).
static const ModeDefinition background = ModeDefinition(
initializers: [CacheInitializer()],
synchronizers: [
PollingSynchronizer(pollInterval: defaultBackgroundPollInterval),
],
fdv1Fallback: Fdv1FallbackConfig(
pollInterval: defaultBackgroundPollInterval,
),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'dart:convert';

import 'package:launchdarkly_dart_common/launchdarkly_dart_common.dart'
hide ServiceEndpoints;

import '../../config/service_endpoints.dart';
import 'cache_initializer.dart' as cache_src;
import 'source_factory_context.dart';
import 'mode_definition.dart' as mode;
import 'polling_base.dart';
import 'polling_initializer.dart';
import 'polling_synchronizer.dart';
import 'requestor.dart';
import 'selector.dart';
import 'source.dart';

/// Merges optional per-entry [mode.EndpointConfig] overrides into [base].
ServiceEndpoints mergeServiceEndpoints(
ServiceEndpoints base,
mode.EndpointConfig? override,
) {
if (override == null) {
return base;
}
if (override.pollingBaseUri == null && override.streamingBaseUri == null) {
return base;
}
return ServiceEndpoints.custom(
polling: override.pollingBaseUri?.toString() ?? base.polling,
streaming: override.streamingBaseUri?.toString() ?? base.streaming,
events: base.events,
);
}

FDv2PollingBase _sharedPollingBase({
required mode.EndpointConfig? endpoints,
required bool usePost,
required SourceFactoryContext ctx,
}) {
final endpointsResolved =
mergeServiceEndpoints(ctx.serviceEndpoints, endpoints);
final requestor = FDv2Requestor(
logger: ctx.logger,
endpoints: endpointsResolved,
contextEncoded: base64UrlEncode(utf8.encode(ctx.contextJson)),
contextJson: ctx.contextJson,
usePost: usePost,
withReasons: ctx.withReasons,
httpProperties: ctx.httpProperties,
httpClientFactory: ctx.httpClientFactory ?? _defaultHttpClientFactory,
);
return FDv2PollingBase(
logger: ctx.logger,
requestor: requestor,
);
}

HttpClient _defaultHttpClientFactory(HttpProperties httpProperties) {
return HttpClient(httpProperties: httpProperties);
}

/// A factory for creating [Initializer] instances.
final class InitializerFactory {
/// True for cache initializers ([CONNMODE] / CSFDv2 cache-miss success rule).
final bool isCache;

final Initializer Function(SelectorGetter selectorGetter) _create;

InitializerFactory({
required Initializer Function(SelectorGetter selectorGetter) create,
this.isCache = false,
}) : _create = create;

/// Returns a **new** [Initializer] bound to [selectorGetter] (or ignores it
/// for cache, matching JS).
Initializer create(SelectorGetter selectorGetter) => _create(selectorGetter);
}

/// A factory for creating [Synchronizer] instances.
final class SynchronizerFactory {
final Synchronizer Function(SelectorGetter selectorGetter) _create;

SynchronizerFactory({
required Synchronizer Function(SelectorGetter selectorGetter) create,
}) : _create = create;

Synchronizer create(SelectorGetter selectorGetter) => _create(selectorGetter);
}

/// Builds an [InitializerFactory] for a single [mode.InitializerEntry].
///
/// Throws [UnsupportedError] for unsupported entry types.
InitializerFactory createInitializerFactoryFromEntry(
mode.InitializerEntry entry,
SourceFactoryContext ctx,
) {
switch (entry) {
case mode.CacheInitializer():
return InitializerFactory(
isCache: true,
create: (_) => cache_src.CacheInitializer(
reader: ctx.cachedFlagsReader,
context: ctx.context,
logger: ctx.logger,
),
);
case final mode.PollingInitializer e:
final base = _sharedPollingBase(
endpoints: e.endpoints,
usePost: e.usePost,
ctx: ctx,
);
return InitializerFactory(
create: (SelectorGetter selectorGetter) => FDv2PollingInitializer(
poll: ({Selector basis = Selector.empty}) =>
base.pollOnce(basis: basis),
selectorGetter: selectorGetter,
logger: ctx.logger,
),
);
case mode.StreamingInitializer():
throw UnsupportedError(
'FDv2 StreamingInitializer factories are not implemented yet',
);
}
}

/// Builds a [SynchronizerFactory] for a single [mode.SynchronizerEntry].
///
/// Throws [UnsupportedError] for unsupported entry types.
SynchronizerFactory createSynchronizerFactoryFromEntry(
mode.SynchronizerEntry entry,
SourceFactoryContext ctx,
) {
switch (entry) {
case final mode.PollingSynchronizer e:
final base = _sharedPollingBase(
endpoints: e.endpoints,
usePost: e.usePost,
ctx: ctx,
);
final interval = e.pollInterval ?? ctx.defaultPollingInterval;
return SynchronizerFactory(
create: (SelectorGetter selectorGetter) => FDv2PollingSynchronizer(
poll: ({Selector basis = Selector.empty}) =>
base.pollOnce(basis: basis),
selectorGetter: selectorGetter,
interval: interval,
logger: ctx.logger,
),
);
case mode.StreamingSynchronizer():
throw UnsupportedError(
'FDv2 StreamingSynchronizer factories are not implemented yet',
);
}
}

/// One factory per entry, in list order.
List<InitializerFactory> buildInitializerFactories(
List<mode.InitializerEntry> entries,
SourceFactoryContext ctx,
) {
return entries.map((e) => createInitializerFactoryFromEntry(e, ctx)).toList();
}

/// One factory per entry, in list order.
List<SynchronizerFactory> buildSynchronizerFactories(
List<mode.SynchronizerEntry> entries,
SourceFactoryContext ctx,
) {
return entries
.map((e) => createSynchronizerFactoryFromEntry(e, ctx))
.toList();
}
Loading
Loading