Skip to content

Commit 761ed9d

Browse files
authored
feat: Add FDv2 payload handling to the data source event handler (#305)
## What this adds `DataSourceEventHandler.handlePayload`: the entry point the FDv2 data pipeline (a later PR) calls when a payload completes. Nothing produces payloads for the event handler yet, so behavior is unchanged. The method maps the payload's updates to item descriptors with the flag-eval mapper, applies them through `FlagManager.applyChanges` (full replaces, partial applies, none takes no action), and marks the data source valid — a payload of any type, including none, confirms the connection is delivering current data. Unparseable flag data is classified as an `invalidData` error and reported through the status manager, mirroring how the FDv1 message verbs handle bad payloads; the in-progress payload is discarded. ## Testing New tests cover all three transfer types end to end through the event handler (full replaces the stored flags and applies the environment ID, partial applies updates without per-item version comparison, none changes no data), the valid status transition, and invalid flag data producing an `invalidData` error with `MessageStatus.invalidMessage`. SDK-2186 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Isolated new API path with no callers yet; behavior mirrors existing FDv1 error handling and delegates storage semantics to existing `applyChanges`. > > **Overview** > Adds **`DataSourceEventHandler.handlePayload`**, the FDv2 entry point that maps payload updates through `mapUpdatesToItemDescriptors`, applies them via **`FlagManager.applyChanges`** (full replace, partial merge without per-flag version checks, **none** leaves flags unchanged), optionally sets **environment ID** on full transfers, and always marks the data source **valid** on success. > > Invalid flag-eval data follows the same pattern as FDv1 verbs: log, **`ErrorKind.invalidData`**, return **`MessageStatus.invalidMessage`**, and discard the payload. Nothing in this PR wires FDv2 streaming into the handler yet. > > Tests exercise full/partial/none behavior, valid status, environment ID on full payloads, and malformed flag-eval objects. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c44bdeb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent c677c3d commit 761ed9d

2 files changed

Lines changed: 121 additions & 0 deletions

File tree

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import '../flag_manager/flag_manager.dart';
66
import '../item_descriptor.dart';
77
import 'data_source_status.dart';
88
import 'data_source_status_manager.dart';
9+
import 'fdv2/flag_eval_mapper.dart';
10+
import 'fdv2/payload.dart';
911

1012
enum MessageStatus { messageHandled, invalidMessage, unhandledVerb }
1113

@@ -96,6 +98,28 @@ final class DataSourceEventHandler {
9698
}
9799
}
98100

101+
/// Applies an FDv2 payload to the flag store.
102+
///
103+
/// Full payloads replace the stored flags, partial payloads apply each
104+
/// update, and a payload of type none confirms the SDK is up to date
105+
/// without changing data. All three mark the data source valid.
106+
Future<MessageStatus> handlePayload(LDContext context, Payload payload,
107+
{String? environmentId}) async {
108+
try {
109+
final updates = mapUpdatesToItemDescriptors(payload.updates);
110+
await _flagManager.applyChanges(context, updates, payload.type,
111+
environmentId: environmentId);
112+
_statusManager.setValid();
113+
return MessageStatus.messageHandled;
114+
} catch (err) {
115+
_logger.error('FDv2 payload contained invalid flag data: '
116+
'${err.runtimeType}');
117+
_statusManager.setErrorByKind(
118+
ErrorKind.invalidData, 'FDv2 payload contained invalid data');
119+
return MessageStatus.invalidMessage;
120+
}
121+
}
122+
99123
Future<MessageStatus> _processPut(
100124
LDContext context, dynamic parsed, String? environmentId) async {
101125
try {

packages/common_client/test/data_sources/data_source_event_handler_test.dart

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:launchdarkly_common_client/launchdarkly_common_client.dart';
22
import 'package:launchdarkly_common_client/src/data_sources/data_source_event_handler.dart';
33
import 'package:launchdarkly_common_client/src/data_sources/data_source_status.dart';
44
import 'package:launchdarkly_common_client/src/data_sources/data_source_status_manager.dart';
5+
import 'package:launchdarkly_common_client/src/data_sources/fdv2/payload.dart';
56
import 'package:launchdarkly_common_client/src/flag_manager/flag_manager.dart';
67
import 'package:test/test.dart';
78

@@ -284,5 +285,101 @@ void main() {
284285
expect(updated.flag, isNull);
285286
});
286287
});
288+
289+
group('handlePayload', () {
290+
Update flagEval(String key, int version, {Object? value = true}) =>
291+
Update(
292+
kind: 'flag-eval',
293+
key: key,
294+
version: version,
295+
object: {
296+
'flagVersion': 5,
297+
'value': value,
298+
'variation': 0,
299+
'trackEvents': false,
300+
},
301+
);
302+
303+
test('a full payload replaces the stored flags and sets valid', () async {
304+
expectLater(
305+
statusManager!.changes,
306+
emits(DataSourceStatus(
307+
state: DataSourceState.valid, stateSince: DateTime(2))));
308+
309+
await eventHandler!.handlePayload(context,
310+
Payload(type: PayloadType.full, updates: [flagEval('flagA', 1)]));
311+
312+
expect(
313+
await eventHandler!.handlePayload(
314+
context,
315+
Payload(
316+
type: PayloadType.full, updates: [flagEval('flagB', 2)]),
317+
environmentId: 'the-environment-id'),
318+
MessageStatus.messageHandled);
319+
320+
expect(flagManager!.get('flagA'), isNull,
321+
reason: 'a full transfer replaces everything');
322+
expect(flagManager!.get('flagB')?.flag?.version, 2);
323+
expect(flagManager!.environmentId, 'the-environment-id');
324+
});
325+
326+
test(
327+
'a partial payload applies updates without per-item version '
328+
'comparison and sets valid', () async {
329+
await eventHandler!.handlePayload(context,
330+
Payload(type: PayloadType.full, updates: [flagEval('flagA', 7)]));
331+
332+
expect(
333+
await eventHandler!.handlePayload(
334+
context,
335+
Payload(
336+
type: PayloadType.partial,
337+
updates: [flagEval('flagA', 3, value: false)])),
338+
MessageStatus.messageHandled);
339+
340+
final updated = flagManager!.get('flagA')!.flag!;
341+
expect(updated.version, 3,
342+
reason: 'FDv2 orders data at the payload level, so a lower '
343+
'envelope version still applies');
344+
expect(updated.detail.value, LDValue.ofBool(false));
345+
});
346+
347+
test('a payload of none changes no data and sets valid', () async {
348+
await eventHandler!.handlePayload(context,
349+
Payload(type: PayloadType.full, updates: [flagEval('flagA', 1)]));
350+
351+
expect(
352+
await eventHandler!.handlePayload(
353+
context, const Payload(type: PayloadType.none, updates: [])),
354+
MessageStatus.messageHandled);
355+
356+
expect(flagManager!.get('flagA')?.flag?.version, 1);
357+
});
358+
359+
test('invalid flag data sets an invalid data error', () async {
360+
expectLater(
361+
statusManager!.changes,
362+
emits(DataSourceStatus(
363+
lastError: DataSourceStatusErrorInfo(
364+
kind: ErrorKind.invalidData,
365+
message: 'FDv2 payload contained invalid data',
366+
statusCode: null,
367+
time: DateTime(2)),
368+
state: DataSourceState.initializing,
369+
stateSince: DateTime(1))));
370+
371+
expect(
372+
await eventHandler!.handlePayload(
373+
context,
374+
Payload(type: PayloadType.full, updates: [
375+
const Update(
376+
kind: 'flag-eval',
377+
key: 'bad',
378+
version: 1,
379+
object: {'trackEvents': 'not-a-bool'})
380+
])),
381+
MessageStatus.invalidMessage);
382+
});
383+
});
287384
});
288385
}

0 commit comments

Comments
 (0)