Skip to content
6 changes: 6 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,12 @@ 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/data_sources/fdv2/mode_resolution.dart'
show
ModeState,
ModeResolutionEntry,
resolveConnectionMode,
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 @@ -127,6 +127,7 @@ final class DataSourceManager {
_statusManager.setOffline();
case ConnectionMode.streaming:
case ConnectionMode.polling:
case ConnectionMode.background:
// default:
// We may want to consider adding another state to the data source state
// for the intermediate between switching data sources, or for identifying
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'mode_definition.dart';

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

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

/// Default background poll interval.
static const Duration _backgroundPollInterval = 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: _backgroundPollInterval),
],
fdv1Fallback: Fdv1FallbackConfig(
pollInterval: _backgroundPollInterval,
),
);
}
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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/// Per-source endpoint overrides. When fields are null, the client uses
/// the default [ServiceEndpoints] from config.
final class EndpointConfig {
final Uri? pollingBaseUri;
final Uri? streamingBaseUri;

const EndpointConfig({this.pollingBaseUri, this.streamingBaseUri});
}

/// Marker class for separating initializers from other types of source entries.
sealed class InitializerEntry {
const InitializerEntry();
}

/// Marker class for separating synchronizers from other types of source entries.
sealed class SynchronizerEntry {
const SynchronizerEntry();
}

/// Initializer that will read data from cache.
final class CacheInitializer extends InitializerEntry {
const CacheInitializer();
}

/// Initializer that will make fetch data from polling endpoints.
final class PollingInitializer extends InitializerEntry {
/// Per-source endpoint overrides.
final EndpointConfig? endpoints;

/// Whether to use the POST semantics for this source.
final bool usePost;

const PollingInitializer({
this.endpoints,
this.usePost = false,
});
}

/// Streaming initializer (e.g. first payload from a stream).
final class StreamingInitializer extends InitializerEntry {
/// Initial reconnect delay for the streaming source.
final Duration? initialReconnectDelay;

/// Per-source endpoint overrides.
final EndpointConfig? endpoints;

/// Whether to use the POST semantics for this source.
final bool usePost;

const StreamingInitializer({
this.initialReconnectDelay,
this.endpoints,
this.usePost = false,
});
}

/// Long-lived polling synchronizer; [pollInterval] overrides client default when set.
final class PollingSynchronizer extends SynchronizerEntry {
/// Minimum polling interval for the synchronizer.
final Duration? pollInterval;

/// Per-source endpoint overrides.
final EndpointConfig? endpoints;

/// Whether to use the POST semantics for this source.
final bool usePost;

const PollingSynchronizer({
this.pollInterval,
this.endpoints,
this.usePost = false,
});
}

/// Long-lived streaming synchronizer.
final class StreamingSynchronizer extends SynchronizerEntry {
final Duration? initialReconnectDelay;

/// Per-source endpoint overrides.
final EndpointConfig? endpoints;

/// Whether to use the POST semantics for this source.
final bool usePost;

const StreamingSynchronizer({
this.initialReconnectDelay,
this.endpoints,
this.usePost = false,
});
}

/// Defines the initializers and synchronizers for a FDv2 connection mode.
final class ModeDefinition {
final List<InitializerEntry> initializers;
final List<SynchronizerEntry> synchronizers;
final Fdv1FallbackConfig? fdv1Fallback;

const ModeDefinition({
required this.initializers,
required this.synchronizers,
this.fdv1Fallback,
});
}

/// Configuration for the FDv1 fallback tier.
final class Fdv1FallbackConfig {
/// Minimum polling interval for the fallback synchronizer
final Duration? pollInterval;

/// Per-source endpoint overrides.
final EndpointConfig? endpoints;

const Fdv1FallbackConfig({this.pollInterval, this.endpoints});
}
Loading
Loading