Skip to content

feat: Add FDv1 polling synchronizer for FDv2 fallback (SDK-1923)#1159

Open
keelerm84 wants to merge 3 commits intomainfrom
mk/sdk-1923/fdv1-fallback
Open

feat: Add FDv1 polling synchronizer for FDv2 fallback (SDK-1923)#1159
keelerm84 wants to merge 3 commits intomainfrom
mk/sdk-1923/fdv1-fallback

Conversation

@keelerm84
Copy link
Member

@keelerm84 keelerm84 commented Mar 6, 2026

Add a standalone FDv1 polling synchronizer that natively implements the
Synchronizer interface, enabling the FDv2 orchestrator to fall back to
polling the FDv1 endpoint when the server signals x-ld-fd-fallback.

Also fix a potential clock-drift issue in both PollingSynchronizer and
FDv1PollingSynchronizer where a backwards system clock adjustment could
cause sleep durations to exceed the configured poll interval.


Note

Medium Risk
Adds a new synchronizer implementation and adjusts polling delay logic, which can affect how/when flag updates and error states are delivered. Risk is moderated by being largely additive and covered by new unit tests, but timing/state-machine regressions are still possible.

Overview
Adds a new createFDv1PollingSynchronizer that polls the FDv1 flags endpoint but emits FDv2 Synchronizer results, enabling FDv2 orchestration to fall back to FDv1 polling while still producing changeSet/status events.

The synchronizer starts polling lazily on first next(), converts the full FDv1 flag JSON into a FDv2 internal.Payload (type: 'full', id FDv1Fallback), and maps failures to interrupted vs terminal_error based on HTTP recoverability (plus invalid JSON/network errors).

Also clamps the computed sleep delay in both PollingSynchronizer and the new FDv1 poller to avoid oversized waits when the system clock moves backward, exports the new synchronizer from fdv2/index.ts, and adds comprehensive Jest coverage for polling cadence, shutdown semantics, and error handling.

Written by Cursor Bugbot for commit 37f0dd2. This will update automatically on new commits. Configure here.

Add a standalone FDv1 polling synchronizer that natively implements the
Synchronizer interface, enabling the FDv2 orchestrator to fall back to
polling the FDv1 endpoint when the server signals x-ld-fd-fallback.

Also fix a potential clock-drift issue in both PollingSynchronizer and
FDv1PollingSynchronizer where a backwards system clock adjustment could
cause sleep durations to exceed the configured poll interval.
@keelerm84 keelerm84 requested a review from a team as a code owner March 6, 2026 17:11
@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 25566 bytes
Compressed size limit: 26000
Uncompressed size: 125383 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

@launchdarkly/browser size report
This is the brotli compressed size of the ESM build.
Compressed size: 172130 bytes
Compressed size limit: 200000
Uncompressed size: 800872 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 24212 bytes
Compressed size limit: 25000
Uncompressed size: 83755 bytes

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

@launchdarkly/js-client-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 21281 bytes
Compressed size limit: 24000
Uncompressed size: 110213 bytes

const updates: internal.Update[] = Object.entries(flags).map(([key, flag]) => ({
kind: 'flag',
key,
version: flag.version ?? 1,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kinyoklion can you confirm which version is used at this level?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to basically disable the version handling entirely in the FDv2 path. But maybe we can just do that all at once? The typing may prove a problem.

function scheduleNextPoll(startTime: number): void {
if (!stopped) {
const elapsed = Date.now() - startTime;
const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it add this little bit here since we were talking about time drift, to ensure the biggest interval for polling is [0, pollingIntervalMs].

if (!stopped) {
const sleepFor = Math.max(pollIntervalMs - (Date.now() - startTime), 0);
const elapsed = Date.now() - startTime;
const sleepFor = Math.min(Math.max(pollIntervalMs - elapsed, 0), pollIntervalMs);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then had it apply it here as well since presumably it's problematic in both.

version: 1,
// The selector MUST be empty — a non-empty selector would cause FDv2
// synchronizers to try to resume from a bogus state.
state: '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is optional, so we should not include it.

The state field on Payload is optional, so we should not include it
rather than setting it to an empty string.
Move flagsToPayload inside the try block with JSON.parse so that
non-object JSON (null, arrays, primitives) that fails in Object.entries
is correctly reported as INVALID_DATA rather than falling through to the
network error handler.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

next(): Promise<FDv2SourceResult> {
if (!started) {
started = true;
doPoll();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled promise rejection from fire-and-forget doPoll

Low Severity

doPoll() is called as fire-and-forget inside next() without a .catch() handler. While the body of doPoll has try/catch around the main logic, the logger?.debug(...) call on line 87 and Date.now() on line 88 execute before the try block. If the logger's debug method throws, the resulting promise rejection would be unhandled. Adding a no-op .catch() would guard against this.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants