Skip to content

Commit 28f7efa

Browse files
authored
feat: Add support for waiting for non-cached values. (#160)
1. Allows for the common client to have customized data sources. This was used for testing here, but could be used for more specialized use-cases in the future. 2. Adds the ability for `start` and `identify` to wait for values from LaunchDarkly instead of resolving with cached values when available. With the previous wrapper SDK this behavior was controlled by the underlying SDK and those would operate similar to the new, optional, behavior. For most uses cases it is both safer and higher performance to used cached values when they are available instead of only using those cached values as a fallback. Ideally we would add new return values to start and identify. Next major version we should consider: `Cached`, `Timeout`. While retaining the current value for completing via data from LD.
1 parent d49e41f commit 28f7efa

File tree

4 files changed

+217
-39
lines changed

4 files changed

+217
-39
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class DataSourceManager {
4141
_dataSourceEventHandler = dataSourceEventHandler;
4242

4343
/// Set the available data source factories. These factories will not apply
44-
/// until the next identify fall. Currently factories will be set once during
44+
/// until the next identify call. Currently factories will be set once during
4545
/// startup and before the first identify.
4646
void setFactories(Map<ConnectionMode, DataSourceFactory> factories) {
4747
_dataSourceFactories.clear();

packages/common_client/lib/src/ld_common_client.dart

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,38 @@ final class IdentifyError implements IdentifyResult {
4545
IdentifyError(this.error);
4646
}
4747

48+
typedef DataSourceFactoriesFn = Map<ConnectionMode, DataSourceFactory> Function(
49+
LDCommonConfig config, LDLogger logger, HttpProperties httpProperties);
50+
51+
Map<ConnectionMode, DataSourceFactory> _defaultFactories(
52+
LDCommonConfig config, LDLogger logger, HttpProperties httpProperties) {
53+
return {
54+
ConnectionMode.streaming: (LDContext context) {
55+
return StreamingDataSource(
56+
credential: config.sdkCredential,
57+
context: context,
58+
endpoints: config.serviceEndpoints,
59+
logger: logger,
60+
dataSourceConfig: StreamingDataSourceConfig(
61+
useReport: config.dataSourceConfig.useReport,
62+
withReasons: config.dataSourceConfig.evaluationReasons),
63+
httpProperties: httpProperties);
64+
},
65+
ConnectionMode.polling: (LDContext context) {
66+
return PollingDataSource(
67+
credential: config.sdkCredential,
68+
context: context,
69+
endpoints: config.serviceEndpoints,
70+
logger: logger,
71+
dataSourceConfig: PollingDataSourceConfig(
72+
useReport: config.dataSourceConfig.useReport,
73+
withReasons: config.dataSourceConfig.evaluationReasons,
74+
pollingInterval: config.dataSourceConfig.polling.pollingInterval),
75+
httpProperties: httpProperties);
76+
},
77+
};
78+
}
79+
4880
final class LDCommonClient {
4981
final LDCommonConfig _config;
5082
final Persistence _persistence;
@@ -58,6 +90,7 @@ final class LDCommonClient {
5890
late final DataSourceManager _dataSourceManager;
5991
late final EnvironmentReport _envReport;
6092
late final AsyncSingleQueue<void> _identifyQueue = AsyncSingleQueue();
93+
late final DataSourceFactoriesFn _dataSourceFactories;
6194

6295
// Modifications will happen in the order they are specified in this list.
6396
// If there are cross-dependent modifiers, then this must be considered.
@@ -93,7 +126,8 @@ final class LDCommonClient {
93126
}
94127

95128
LDCommonClient(LDCommonConfig commonConfig, CommonPlatform platform,
96-
LDContext context, DiagnosticSdkData sdkData)
129+
LDContext context, DiagnosticSdkData sdkData,
130+
{DataSourceFactoriesFn? dataSourceFactories})
97131
: _config = commonConfig,
98132
_platform = platform,
99133
_persistence = ValidatingPersistence(
@@ -107,6 +141,8 @@ final class LDCommonClient {
107141
persistence: platform.persistence),
108142
_dataSourceStatusManager = DataSourceStatusManager(),
109143
_initialUndecoratedContext = context,
144+
// Data source factories is primarily a mechanism for testing.
145+
_dataSourceFactories = dataSourceFactories ?? _defaultFactories,
110146
_sdkData = sdkData {
111147
final dataSourceEventHandler = DataSourceEventHandler(
112148
flagManager: _flagManager,
@@ -179,7 +215,7 @@ final class LDCommonClient {
179215
///
180216
/// If the return value is true, then the SDK has initialized, if false
181217
/// then the SDK has encountered an unrecoverable error.
182-
Future<bool> start() {
218+
Future<bool> start({bool waitForNetworkResults = false}) {
183219
if (_startFuture != null) {
184220
return _startFuture!.then(_mapIdentifyStart);
185221
}
@@ -191,7 +227,8 @@ final class LDCommonClient {
191227
// having been set resulting in a crash.
192228
_identifyQueue.execute(() async {
193229
await _startInternal();
194-
await _identifyInternal(_initialUndecoratedContext);
230+
await _identifyInternal(_initialUndecoratedContext,
231+
waitForNetworkResults: waitForNetworkResults);
195232
}).then((res) {
196233
_startCompleter!.complete(_mapIdentifyResult(res));
197234
});
@@ -259,32 +296,8 @@ final class LDCommonClient {
259296
_updateEventSendingState();
260297

261298
if (!_config.offline) {
262-
_dataSourceManager.setFactories({
263-
ConnectionMode.streaming: (LDContext context) {
264-
return StreamingDataSource(
265-
credential: _config.sdkCredential,
266-
context: context,
267-
endpoints: _config.serviceEndpoints,
268-
logger: _logger,
269-
dataSourceConfig: StreamingDataSourceConfig(
270-
useReport: _config.dataSourceConfig.useReport,
271-
withReasons: _config.dataSourceConfig.evaluationReasons),
272-
httpProperties: httpProperties);
273-
},
274-
ConnectionMode.polling: (LDContext context) {
275-
return PollingDataSource(
276-
credential: _config.sdkCredential,
277-
context: context,
278-
endpoints: _config.serviceEndpoints,
279-
logger: _logger,
280-
dataSourceConfig: PollingDataSourceConfig(
281-
useReport: _config.dataSourceConfig.useReport,
282-
withReasons: _config.dataSourceConfig.evaluationReasons,
283-
pollingInterval:
284-
_config.dataSourceConfig.polling.pollingInterval),
285-
httpProperties: httpProperties);
286-
},
287-
});
299+
_dataSourceManager
300+
.setFactories(_dataSourceFactories(_config, _logger, httpProperties));
288301
} else {
289302
_dataSourceManager.setFactories({
290303
ConnectionMode.streaming: (LDContext context) {
@@ -350,7 +363,8 @@ final class LDCommonClient {
350363
/// When the context is changed, the SDK will load flag values for the context from a local cache if available, while
351364
/// initiating a connection to retrieve the most current flag values. An event will be queued to be sent to the service
352365
/// containing the public [LDContext] fields for indexing on the dashboard.
353-
Future<IdentifyResult> identify(LDContext context) async {
366+
Future<IdentifyResult> identify(LDContext context,
367+
{bool waitForNetworkResults = false}) async {
354368
if (_startFuture == null) {
355369
const message =
356370
'Identify called before SDK has been started. Start the SDK before '
@@ -359,7 +373,8 @@ final class LDCommonClient {
359373
return IdentifyError(Exception(message));
360374
}
361375
final res = await _identifyQueue.execute(() async {
362-
await _identifyInternal(context);
376+
await _identifyInternal(context,
377+
waitForNetworkResults: waitForNetworkResults);
363378
});
364379
return _mapIdentifyResult(res);
365380
}
@@ -375,19 +390,19 @@ final class LDCommonClient {
375390
}
376391
}
377392

378-
Future<void> _identifyInternal(LDContext context) async {
393+
Future<void> _identifyInternal(LDContext context,
394+
{bool waitForNetworkResults = false}) async {
379395
await _setAndDecorateContext(context);
380396
final completer = Completer<void>();
381397
_eventProcessor?.processIdentifyEvent(IdentifyEvent(context: _context));
382398
final loadedFromCache = await _flagManager.loadCached(_context);
383399

384400
if (_config.offline) {
385-
// TODO: Do we need to do anything different here?
386401
return;
387402
}
388403
_dataSourceManager.identify(_context, completer);
389404

390-
if (loadedFromCache) {
405+
if (loadedFromCache && !waitForNetworkResults) {
391406
return;
392407
}
393408
return completer.future;

packages/common_client/test/ld_dart_client_test.dart

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import 'dart:async';
2+
import 'dart:convert';
3+
4+
import 'package:crypto/crypto.dart';
15
import 'package:launchdarkly_common_client/launchdarkly_common_client.dart';
6+
import 'package:launchdarkly_common_client/src/data_sources/data_source.dart';
27
import 'package:test/test.dart';
38

9+
import 'mock_persistence.dart';
10+
411
final class TestConfig extends LDCommonConfig {
512
TestConfig(super.sdkCredential, super.autoEnvAttributes,
613
{super.applicationInfo,
@@ -13,6 +20,35 @@ final class TestConfig extends LDCommonConfig {
1320
super.dataSourceConfig});
1421
}
1522

23+
final class TestDataSource implements DataSource {
24+
final StreamController<DataSourceEvent> _eventController = StreamController();
25+
26+
@override
27+
Stream<DataSourceEvent> get events => _eventController.stream;
28+
29+
@override
30+
void restart() {}
31+
32+
@override
33+
void start() {
34+
Timer(Duration(milliseconds: 10), () {
35+
_eventController.sink.add(DataEvent(
36+
'put',
37+
'{"flagA":{'
38+
'"version":1,'
39+
'"value":"datasource",'
40+
'"variation":0,'
41+
'"reason":{"kind":"OFF"}'
42+
'}}'));
43+
});
44+
}
45+
46+
@override
47+
void stop() {
48+
_eventController.close();
49+
}
50+
}
51+
1652
void main() {
1753
group('given an offline client', () {
1854
late LDCommonClient client;
@@ -146,4 +182,105 @@ void main() {
146182
await client.flush();
147183
});
148184
});
185+
186+
group('given a mock data source', () {
187+
late LDCommonClient client;
188+
late MockPersistence mockPersistence;
189+
final sdkKey = 'the-sdk-key';
190+
final sdkKeyPersistence =
191+
'LaunchDarkly_${sha256.convert(utf8.encode(sdkKey))}';
192+
193+
setUp(() {
194+
mockPersistence = MockPersistence();
195+
client = LDCommonClient(
196+
TestConfig(sdkKey, AutoEnvAttributes.disabled),
197+
CommonPlatform(persistence: mockPersistence),
198+
LDContextBuilder().kind('user', 'bob').build(),
199+
DiagnosticSdkData(name: '', version: ''), dataSourceFactories:
200+
(LDCommonConfig config, LDLogger logger,
201+
HttpProperties properties) {
202+
return {
203+
ConnectionMode.streaming: (LDContext context) {
204+
return TestDataSource();
205+
},
206+
ConnectionMode.polling: (LDContext context) {
207+
return TestDataSource();
208+
},
209+
};
210+
});
211+
});
212+
213+
test('identify can resolve cached values', () async {
214+
final contextPersistenceKey =
215+
sha256.convert(utf8.encode('joe')).toString();
216+
mockPersistence.storage[sdkKeyPersistence] = {
217+
contextPersistenceKey: '{"flagA":{'
218+
'"version":1,'
219+
'"value":"storage",'
220+
'"variation":0,'
221+
'"reason":{"kind":"OFF"}'
222+
'}}'
223+
};
224+
// We are going to ignore the items for the first context.
225+
await client.start();
226+
227+
await client.identify(LDContextBuilder().kind('user', 'joe').build());
228+
final res = client.stringVariation('flagA', 'default');
229+
expect(res, 'storage');
230+
});
231+
232+
test('identify can resolve non-cached values', () async {
233+
final contextPersistenceKey =
234+
sha256.convert(utf8.encode('joe')).toString();
235+
mockPersistence.storage[sdkKeyPersistence] = {
236+
contextPersistenceKey: '{"flagA":{'
237+
'"version":1,'
238+
'"value":"storage",'
239+
'"variation":0,'
240+
'"reason":{"kind":"OFF"}'
241+
'}}'
242+
};
243+
// We are going to ignore the items for the first context.
244+
await client.start();
245+
246+
await client.identify(LDContextBuilder().kind('user', 'joe').build(),
247+
waitForNetworkResults: true);
248+
final res = client.stringVariation('flagA', 'default');
249+
expect(res, 'datasource');
250+
});
251+
252+
test('start can resolve cached values', () async {
253+
final contextPersistenceKey =
254+
sha256.convert(utf8.encode('bob')).toString();
255+
mockPersistence.storage[sdkKeyPersistence] = {
256+
contextPersistenceKey: '{"flagA":{'
257+
'"version":1,'
258+
'"value":"storage",'
259+
'"variation":0,'
260+
'"reason":{"kind":"OFF"}'
261+
'}}'
262+
};
263+
264+
await client.start();
265+
final res = client.stringVariation('flagA', 'default');
266+
expect(res, 'storage');
267+
});
268+
269+
test('start can resolve non-cached values', () async {
270+
final contextPersistenceKey =
271+
sha256.convert(utf8.encode('bob')).toString();
272+
mockPersistence.storage[sdkKeyPersistence] = {
273+
contextPersistenceKey: '{"flagA":{'
274+
'"version":1,'
275+
'"value":"storage",'
276+
'"variation":0,'
277+
'"reason":{"kind":"OFF"}'
278+
'}}'
279+
};
280+
281+
await client.start(waitForNetworkResults: true);
282+
final res = client.stringVariation('flagA', 'default');
283+
expect(res, 'datasource');
284+
});
285+
});
149286
}

packages/flutter_client_sdk/lib/src/ld_client.dart

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,18 @@ interface class LDClient {
107107
/// ```dart
108108
/// await client.start().timeout(const Duration(seconds: 30));
109109
/// ```
110-
Future<bool> start() async {
111-
return _client.start();
110+
/// The [waitForNetworkResults] parameters, when true, indicates that the SDK
111+
/// will attempt to wait for values from LaunchDarkly instead of depending
112+
/// on cached values. The cached values will still be loaded, but the future
113+
/// returned by this function will not resolve as a result of those cached
114+
/// values being loaded. Generally this option should NOT be used and instead
115+
/// flag changes should be listened to. It the client is set to offline mode,
116+
/// then this option is ignored.
117+
///
118+
/// If [waitForNetworkResults] is true, and an error is encountered, then
119+
/// false may be returned even if cached values were loaded.
120+
Future<bool> start({bool waitForNetworkResults = false}) async {
121+
return _client.start(waitForNetworkResults: waitForNetworkResults);
112122
}
113123

114124
/// Changes the active context.
@@ -119,10 +129,24 @@ interface class LDClient {
119129
/// service containing the public [LDContext] fields for indexing on the
120130
/// dashboard.
121131
///
132+
/// A context with the same kinds and same keys will use the same cached
133+
/// context.
134+
///
122135
/// This returned future can be awaited to wait for the identify process to
123136
/// be complete. As with [start] this can take an extended period if there
124137
/// is not network availability, so a timeout is recommended.
125138
///
139+
/// The [waitForNetworkResults] parameters, when true, indicates that the SDK
140+
/// will attempt to wait for values from LaunchDarkly instead of depending
141+
/// on cached values. The cached values will still be loaded, but the future
142+
/// returned by this function will not resolve as a result of those cached
143+
/// values being loaded. Generally this option should NOT be used and instead
144+
/// flag changes should be listened to. It the client is set to offline mode,
145+
/// then this option is ignored.
146+
///
147+
/// If [waitForNetworkResults] is true, and an error is encountered, then
148+
/// [IdentifyError] may be returned even if cached values were loaded.
149+
///
126150
/// The identify will complete with 1 of three possible values:
127151
/// [IdentifyComplete], [IdentifySuperseded], or [IdentifyError].
128152
///
@@ -139,8 +163,10 @@ interface class LDClient {
139163
///
140164
/// [IdentifyError] this means that the identify has permanently failed. For
141165
/// instance the SDK key is no longer valid.
142-
Future<IdentifyResult> identify(LDContext context) async {
143-
return _client.identify(context);
166+
Future<IdentifyResult> identify(LDContext context,
167+
{bool waitForNetworkResults = false}) async {
168+
return _client.identify(context,
169+
waitForNetworkResults: waitForNetworkResults);
144170
}
145171

146172
/// Track custom events associated with the current context for data export or

0 commit comments

Comments
 (0)