Skip to content

Commit 8f8051c

Browse files
authored
feat: FDv2DataManagerBase for mode switching and data source lifecycle (#1210)
## Summary - Add `FDv2DataManagerBase` — shared data manager orchestrating FDv2 connection mode switching, state debouncing, and data source lifecycle - Implements `DataManager` interface with foreground/background mode support, forced/automatic streaming control, and flush callbacks - Includes `FlagManager.applyChanges` from #1208 for full/partial/none flag update semantics Stacked on #1209 and #1208. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new shared FDv2 data manager that controls connection-mode resolution and data source lifecycle, plus updates public `dataSystem` option shapes; mistakes here could affect SDK initialization, reconnection behavior, and flag freshness across platforms. > > **Overview** > Introduces `createFDv2DataManagerBase` (and exported types) as a shared FDv2 orchestration layer that performs mode resolution/debouncing, manages `FDv2DataSource` creation/teardown across identifies and mode changes, tracks selector state, triggers a background flush callback, and optionally appends a blocked FDv1 polling fallback synchronizer. > > Updates the public `dataSystem` configuration model by renaming `initialConnectionMode` to `foregroundConnectionMode` in platform defaults and by making `automaticModeSwitching` a discriminated union (`{type:'automatic'}` vs `{type:'manual', initialConnectionMode}`), with validator support via `validatorOf(..., { is })`; tests are updated and expanded accordingly, including a large new test suite for `FDv2DataManagerBase`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0786b64. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/launchdarkly/js-core/pull/1210" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
1 parent 669cc0c commit 8f8051c

10 files changed

Lines changed: 1784 additions & 124 deletions

File tree

packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ describe('dataSystem validation', () => {
187187
getImplementationHooks: () => [],
188188
credentialType: 'clientSideId',
189189
dataSystemDefaults: {
190-
initialConnectionMode: 'one-shot',
190+
foregroundConnectionMode: 'one-shot',
191191
automaticModeSwitching: false,
192192
},
193193
},
@@ -203,61 +203,69 @@ describe('dataSystem validation', () => {
203203
getImplementationHooks: () => [],
204204
credentialType: 'clientSideId',
205205
dataSystemDefaults: {
206-
initialConnectionMode: 'one-shot',
206+
foregroundConnectionMode: 'one-shot',
207207
automaticModeSwitching: false,
208208
},
209209
},
210210
);
211211
expect(config.dataSystem).toBeDefined();
212-
expect(config.dataSystem!.initialConnectionMode).toBe('one-shot');
213212
expect(config.dataSystem!.automaticModeSwitching).toBe(false);
214213
});
215214

216215
it('validates dataSystem with user overrides applied over platform defaults', () => {
217216
const config = new ConfigurationImpl(
218217
// @ts-ignore dataSystem is @internal
219-
{ dataSystem: { initialConnectionMode: 'polling' } },
218+
{
219+
dataSystem: {
220+
automaticModeSwitching: { type: 'manual', initialConnectionMode: 'polling' },
221+
},
222+
},
220223
{
221224
getImplementationHooks: () => [],
222225
credentialType: 'mobileKey',
223226
dataSystemDefaults: {
224-
initialConnectionMode: 'streaming',
227+
foregroundConnectionMode: 'streaming',
225228
backgroundConnectionMode: 'background',
226229
automaticModeSwitching: true,
227230
},
228231
},
229232
);
230233
expect(config.dataSystem).toBeDefined();
231-
expect(config.dataSystem!.initialConnectionMode).toBe('polling');
234+
expect(config.dataSystem!.automaticModeSwitching).toEqual({
235+
type: 'manual',
236+
initialConnectionMode: 'polling',
237+
});
232238
expect(config.dataSystem!.backgroundConnectionMode).toBe('background');
233-
expect(config.dataSystem!.automaticModeSwitching).toBe(true);
234239
});
235240

236241
it('warns and falls back to default for invalid dataSystem sub-fields', () => {
237242
console.error = jest.fn();
238243
const config = new ConfigurationImpl(
239244
// @ts-ignore dataSystem is @internal
240-
{ dataSystem: { initialConnectionMode: 'turbo' } },
245+
{ dataSystem: { backgroundConnectionMode: 'turbo' } },
241246
{
242247
getImplementationHooks: () => [],
243248
credentialType: 'clientSideId',
244249
dataSystemDefaults: {
245-
initialConnectionMode: 'one-shot',
250+
foregroundConnectionMode: 'one-shot',
246251
automaticModeSwitching: false,
247252
},
248253
},
249254
);
250255
expect(config.dataSystem).toBeDefined();
251-
expect(config.dataSystem!.initialConnectionMode).toBe('one-shot');
252256
expect(console.error).toHaveBeenCalledWith(
253-
expect.stringContaining('dataSystem.initialConnectionMode'),
257+
expect.stringContaining('dataSystem.backgroundConnectionMode'),
254258
);
255259
});
256260

257261
it('does not deep-validate dataSystem when dataSystemDefaults is not provided', () => {
258262
const config = new ConfigurationImpl(
259263
// @ts-ignore dataSystem is @internal
260-
{ dataSystem: { initialConnectionMode: 'polling' } },
264+
{
265+
dataSystem: {
266+
automaticModeSwitching: { type: 'manual', initialConnectionMode: 'polling' },
267+
},
268+
},
261269
{
262270
getImplementationHooks: () => [],
263271
credentialType: 'clientSideId',
@@ -276,7 +284,7 @@ describe('dataSystem validation', () => {
276284
getImplementationHooks: () => [],
277285
credentialType: 'clientSideId',
278286
dataSystemDefaults: {
279-
initialConnectionMode: 'one-shot',
287+
foregroundConnectionMode: 'one-shot',
280288
automaticModeSwitching: false,
281289
},
282290
},
@@ -288,18 +296,23 @@ describe('dataSystem validation', () => {
288296
it('validates automaticModeSwitching as a granular config object', () => {
289297
const config = new ConfigurationImpl(
290298
// @ts-ignore dataSystem is @internal
291-
{ dataSystem: { automaticModeSwitching: { lifecycle: true, network: false } } },
299+
{
300+
dataSystem: {
301+
automaticModeSwitching: { type: 'automatic', lifecycle: true, network: false },
302+
},
303+
},
292304
{
293305
getImplementationHooks: () => [],
294306
credentialType: 'mobileKey',
295307
dataSystemDefaults: {
296-
initialConnectionMode: 'streaming',
308+
foregroundConnectionMode: 'streaming',
297309
automaticModeSwitching: true,
298310
},
299311
},
300312
);
301313
expect(config.dataSystem).toBeDefined();
302314
expect(config.dataSystem!.automaticModeSwitching).toEqual({
315+
type: 'automatic',
303316
lifecycle: true,
304317
network: false,
305318
});
@@ -312,7 +325,7 @@ describe('dataSystem validation', () => {
312325
getImplementationHooks: () => [],
313326
credentialType: 'clientSideId',
314327
dataSystemDefaults: {
315-
initialConnectionMode: 'one-shot',
328+
foregroundConnectionMode: 'one-shot',
316329
automaticModeSwitching: false,
317330
},
318331
},

0 commit comments

Comments
 (0)