Skip to content

Commit 7a3af02

Browse files
authored
fix: FDv2 initializer readiness (#1017)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions **Describe the solution you've provided** This PR will: 1. Make it so that a client in FDv2 will only report intialized when it gets a basis + valid selector. This will align the behavior better with the FDv2 initializer specs. 2. Updated the docs for the custom datasystem options with an example for clarity <!-- CURSOR_SUMMARY --> --- > [!NOTE] > FDv2 now calls the initialization callback only when `payload.state` is non-empty, with tests added, and docs updated with an example for `custom` data source options. > > - **Data sources**: > - In `createPayloadListenerFDv2`, replace `basisReceived` with `initializedCallback` and invoke it only when `payload.state !== ''` during `applyChanges`. > - **Tests**: > - Update `createPayloadListenersFDv2.test.ts` to use `initializedCallback` and assert conditional invocation for non-empty vs empty state; keep existing behavior checks for basis/updates. > - **Docs**: > - Expand JSDoc for `CustomDataSourceOptions` in `LDDataSystemOptions.ts` with `@experimental` note and a usage example mirroring default standard options. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 38af523. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 512ae3c commit 7a3af02

File tree

3 files changed

+105
-13
lines changed

3 files changed

+105
-13
lines changed

packages/shared/sdk-server/__tests__/data_sources/createPayloadListenersFDv2.test.ts

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,27 +104,27 @@ const changesTransferNone = {
104104

105105
describe('createPayloadListenerFDv2', () => {
106106
let dataSourceUpdates: LDTransactionalDataSourceUpdates;
107-
let basisReceived: jest.Mock;
107+
let initializedCallback: jest.Mock;
108108

109109
beforeEach(() => {
110110
dataSourceUpdates = {
111111
init: jest.fn(),
112112
upsert: jest.fn(),
113113
applyChanges: jest.fn(),
114114
};
115-
basisReceived = jest.fn();
115+
initializedCallback = jest.fn();
116116
});
117117

118118
afterEach(() => {
119119
jest.resetAllMocks();
120120
});
121121

122122
test('data source updates called with basis true', async () => {
123-
const listener = createPayloadListener(dataSourceUpdates, logger, basisReceived);
123+
const listener = createPayloadListener(dataSourceUpdates, logger, initializedCallback);
124124
listener(fullTransferPayload);
125125

126-
expect(logger.debug).toBeCalledWith(expect.stringMatching(/initializing/i));
127-
expect(dataSourceUpdates.applyChanges).toBeCalledWith(
126+
expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/initializing/i));
127+
expect(dataSourceUpdates.applyChanges).toHaveBeenCalledWith(
128128
true,
129129
{
130130
features: {
@@ -134,17 +134,17 @@ describe('createPayloadListenerFDv2', () => {
134134
segkey: { key: 'segkey', version: 2 },
135135
},
136136
},
137-
basisReceived,
137+
expect.any(Function),
138138
{ environmentId: 'envId' },
139139
'initial',
140140
);
141141
});
142142

143143
test('data source updates called with basis false', async () => {
144-
const listener = createPayloadListener(dataSourceUpdates, logger, basisReceived);
144+
const listener = createPayloadListener(dataSourceUpdates, logger, initializedCallback);
145145
listener(changesTransferPayload);
146146

147-
expect(logger.debug).toBeCalledWith(expect.stringMatching(/updating/i));
147+
expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/updating/i));
148148
expect(dataSourceUpdates.applyChanges).toHaveBeenCalledTimes(1);
149149
expect(dataSourceUpdates.applyChanges).toHaveBeenNthCalledWith(
150150
1,
@@ -168,17 +168,79 @@ describe('createPayloadListenerFDv2', () => {
168168
},
169169
},
170170
},
171-
basisReceived,
171+
expect.any(Function),
172172
{ environmentId: 'envId' },
173173
'changes',
174174
);
175175
});
176176

177177
test('data source updates not called when basis is false and changes are empty', async () => {
178-
const listener = createPayloadListener(dataSourceUpdates, logger, basisReceived);
178+
const listener = createPayloadListener(dataSourceUpdates, logger, initializedCallback);
179179
listener(changesTransferNone);
180180

181181
expect(logger.debug).toBeCalledWith(expect.stringMatching(/ignoring/i));
182182
expect(dataSourceUpdates.applyChanges).toHaveBeenCalledTimes(0);
183183
});
184+
185+
test('calls initializedCallback when state is non-empty (initial)', () => {
186+
const listener = createPayloadListener(dataSourceUpdates, logger, initializedCallback);
187+
let capturedCallback: (() => void) | undefined;
188+
189+
dataSourceUpdates.applyChanges = jest.fn((_basis, _data, callback) => {
190+
capturedCallback = callback;
191+
});
192+
193+
listener(fullTransferPayload);
194+
195+
expect(capturedCallback).toBeDefined();
196+
expect(initializedCallback).not.toHaveBeenCalled();
197+
198+
// Simulate applyChanges calling the callback
199+
capturedCallback?.();
200+
201+
expect(initializedCallback).toHaveBeenCalledTimes(1);
202+
});
203+
204+
test('does not call initializedCallback when state is empty (file data initializer)', () => {
205+
const fileDataPayload = {
206+
initMetadata: {
207+
environmentId: 'envId',
208+
},
209+
payload: {
210+
id: 'payloadID',
211+
version: 99,
212+
state: '',
213+
basis: true,
214+
updates: [
215+
{
216+
kind: 'flag',
217+
key: 'flagkey',
218+
version: 1,
219+
object: {
220+
key: 'flagkey',
221+
version: 1,
222+
},
223+
},
224+
],
225+
},
226+
};
227+
228+
const listener = createPayloadListener(dataSourceUpdates, logger, initializedCallback);
229+
let capturedCallback: (() => void) | undefined;
230+
231+
dataSourceUpdates.applyChanges = jest.fn((_basis, _data, callback) => {
232+
capturedCallback = callback;
233+
});
234+
235+
listener(fileDataPayload);
236+
237+
expect(capturedCallback).toBeDefined();
238+
expect(initializedCallback).not.toHaveBeenCalled();
239+
240+
// Simulate applyChanges calling the callback
241+
capturedCallback?.();
242+
243+
// Should still not be called because state is empty
244+
expect(initializedCallback).not.toHaveBeenCalled();
245+
});
184246
});

packages/shared/sdk-server/src/api/options/LDDataSystemOptions.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,30 @@ export type SynchronizerDataSource =
158158
| StreamingDataSourceConfiguration;
159159

160160
/**
161-
* This data source will allow developers to define their own composite data source
161+
* @experimental
162+
* This data source will allow developers to define their own composite data source.
163+
* This is a free-form option and is not subject to any backwards compatibility guarantees or semantic
164+
* versioning.
165+
*
166+
* The following example is roughly equivilent to using the {@link StandardDataSourceOptions} with the default values.
167+
* @example
168+
* ```typescript
169+
* dataSource: {
170+
* dataSourceOptionsType: 'custom',
171+
* initializers: [
172+
* {
173+
* type: 'polling'
174+
* },
175+
* ],
176+
* synchronizers: [
177+
* {
178+
* type: 'streaming',
179+
* },
180+
* {
181+
* type: 'polling',
182+
* }
183+
* ],
184+
* }
162185
*/
163186
export interface CustomDataSourceOptions {
164187
dataSourceOptionsType: 'custom';

packages/shared/sdk-server/src/data_sources/createPayloadListenerFDv2.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const createPayloadListener =
2222
(
2323
dataSourceUpdates: LDTransactionalDataSourceUpdates,
2424
logger?: LDLogger,
25-
basisReceived: VoidFunction = () => {},
25+
initializedCallback: VoidFunction = () => {},
2626
) =>
2727
(dataContainer: DataCallbackContainer) => {
2828
const { initMetadata, payload } = dataContainer;
@@ -68,7 +68,14 @@ export const createPayloadListener =
6868
dataSourceUpdates.applyChanges(
6969
payload.basis,
7070
converted,
71-
basisReceived,
71+
() => {
72+
if (payload.state !== '') {
73+
// NOTE: The only condition that we will consider a valid basis
74+
// is when there is a valid selector. Currently, the only data source that does not have a
75+
// valid selector is the file data initializer, which will have a blank selector.
76+
initializedCallback();
77+
}
78+
},
7279
initMetadata,
7380
payload.state,
7481
);

0 commit comments

Comments
 (0)