Skip to content

feat: Thread FDv1 fallback TTL through polling and streaming data sources#1806

Open
joker23 wants to merge 9 commits into
mainfrom
skz/SDK-2687/fdv1-fallback-fix-fallback-ttl-source-threading
Open

feat: Thread FDv1 fallback TTL through polling and streaming data sources#1806
joker23 wants to merge 9 commits into
mainfrom
skz/SDK-2687/fdv1-fallback-fix-fallback-ttl-source-threading

Conversation

@joker23

@joker23 joker23 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This commit will add in fallback TTL reading support as well as allow RN native event source read headers.

NOTE: while working this PR, I found a lot of gaps in the RN eventsource. Internally tracked in ticket 2721 in sdk project.


Note

Medium Risk
Changes core FDv2 data-source result shaping and when the orchestrator sees fallback vs goodbye, so incorrect TTL or directive clearing could affect FDv1/FDv2 switching timing.

Overview
Adds fdv1FallbackTtlMs end-to-end for FDv2 polling and streaming by centralizing x-ld-fd-fallback / x-ld-fd-fallback-ttl (and in-band protocolFallbackTTL on goodbye) in a shared FallbackDirective type, and threading that object through FDv2SourceResult helpers instead of a bare boolean.

Polling now stamps TTL on success and error responses and treats a goodbye combined with a fallback directive as terminal_error so the orchestrator switches off FDv2 instead of treating it as a benign disconnect.

Streaming defers open-time fallback headers until the next queued result, merges deferred TTL into ping-poll results without mutating handler objects, prefers in-band goodbye TTL over deferred open TTL, and resets pending/committed fallback state on clean reconnect and close(). CI package size limit for @launchdarkly/js-client-sdk-common rises 39k → 41.5k; root workspaces add packages/sdk/browser/example-fdv1-fallback.

Reviewed by Cursor Bugbot for commit e95c2f2. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/js-sdk-common size report
This is the brotli compressed size of the ESM build.
Compressed size: 26360 bytes
Compressed size limit: 29000
Uncompressed size: 129188 bytes

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@launchdarkly/js-client-sdk size report
This is the brotli compressed size of the ESM build.
Compressed size: 32383 bytes
Compressed size limit: 34000
Uncompressed size: 115867 bytes

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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

@joker23

joker23 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

cursor[bot]

This comment was marked as resolved.

@joker23
joker23 force-pushed the skz/SDK-2687/fdv1-fallback-fix-fallback-ttl-source-threading branch from e4fa9cb to 1400310 Compare July 17, 2026 14:01
@joker23

joker23 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

cursor[bot]

This comment was marked as resolved.

* 1. separated event handling to use onprogress for data changes and
* onreadystatechange for status changes, since some platforms (e.g. Vega OS)
* never fire a readystatechange event while the response is still streaming in.
* 2. onprogress now also performs the CONNECTING -> OPEN transition and dispatches

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Would like to have some thoughts here. This looks like a bug in our previous implementation, but would like to understand if there are any legacy reasons for this.

Comment thread .github/workflows/sdk-client.yml Outdated
package_name: '@launchdarkly/js-client-sdk-common'
pr_number: ${{ github.event.number }}
size_limit: 39000
size_limit: 40000

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FYI - needed to bump this up to accomondate the TTL logic :(

@joker23
joker23 force-pushed the skz/SDK-2687/fdv1-fallback-fix-fallback-ttl-source-threading branch from 46de8d4 to 6604149 Compare July 17, 2026 16:56
@joker23

joker23 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

cursor[bot]

This comment was marked as resolved.

@joker23

joker23 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts Outdated
cursor[bot]

This comment was marked as resolved.

Comment thread packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.ts Outdated
@joker23
joker23 force-pushed the skz/SDK-2687/fdv1-fallback-fix-fallback-ttl-source-threading branch from 29aefb7 to 73eeb30 Compare July 17, 2026 22:00
@joker23

joker23 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

cursor[bot]

This comment was marked as resolved.

@joker23

joker23 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c01baaa. Configure here.

terminalError,
} from './FDv2SourceResult';

function getFallback(headers: { get(name: string): string | null }): boolean {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

replacing with

export function readFallbackDirective(headers: {

@joker23
joker23 marked this pull request as ready for review July 20, 2026 16:55
@joker23
joker23 requested a review from a team as a code owner July 20, 2026 16:55

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

Comment thread packages/shared/sdk-client/src/datasource/fdv2/PollingBase.ts Outdated
fdv1FallbackTtlMs = pendingFallbackTtlMs;
resultQueue.put(changeSet(action.payload, true, undefined, undefined, pendingFallbackTtlMs));
pendingFallback = false;
pendingFallbackTtlMs = undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this section use resolveFallback?

case 'payload':
logConnectionResult(true);
resultQueue.put(changeSet(action.payload, fdv1Fallback));
if (pendingFallback) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this if check required? Is there a way to write it such that it just uses resolveFallback always?

fdv1Fallback = true;
resultQueue.put(terminalError(errorInfoFromUnknown(action.reason), true, ttlMs));
pendingFallback = false;
pendingFallbackTtlMs = undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment here. I thought the purpose of the resolveFallback was to remove duplication of this routine of consuming the pending fallback and clearing values.

const fallback = resolveFallback();
if (!result.fdv1Fallback && fallback.fdv1Fallback) {
result.fdv1Fallback = true;
result.fdv1FallbackTtlMs = fallback.fdv1FallbackTtlMs;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is editing result appropriate? Seems like it shouldn't be mutated from a good practices standpoint.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something feels off about how many spots have if checks on pending fallback and logic for moving things around and masking values on top of others. It just seems very scattered and likely a subtle tweak will mess up the logic. Is there a way to make it such that all results that go into the queue conform to some type / interface and fallback can be applied to that thing unconditonally, sometimes a no-op but sometimes a real fallback apply occurs. The pending mechanism could be something that can only apply once and then turns itself into a no-op or something.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using default effort and found 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 1622eff. Configure here.

// Convert final interrupted -> terminal_error
const status = lastResult as StatusResult;
return terminalError(status.errorInfo!, status.fdv1Fallback);
return terminalError(status.errorInfo!, { fdv1Fallback: status.fdv1Fallback });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Polling initializer drops fallback TTL

Medium Severity

After retryable polling failures are exhausted, the initializer converts the last interrupted status into terminalError using only fdv1Fallback, so any fdv1FallbackTtlMs that poll() already attached from response headers is dropped on that path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1622eff. Configure here.

This will allow us to resolve directives that should override the
fallback behavior such as goodbye or error

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread package.json
"packages/sdk/shopify-oxygen/example",
"packages/sdk/browser/example",
"packages/sdk/browser/example-fdv2",
"packages/sdk/browser/example-fdv1-fallback",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Project setup references a package folder that does not exist

A new project folder is listed as part of the workspace (package.json:68) but the folder was never added to the repository, so the setup step points at something that isn't there.
Impact: Developers running the standard install/build for the whole repository may hit warnings or a failed setup because the referenced package folder is missing.

Dangling Yarn workspace entry

The entry "packages/sdk/browser/example-fdv1-fallback" was added to the workspaces array in package.json:68, but packages/sdk/browser/example-fdv1-fallback does not exist on disk and is not tracked by git (verified via git ls-files and by comparing against the base SHA). Every other workspace entry (e.g. packages/sdk/browser/example-fdv2) points to an existing directory with a package.json. The root check script runs yarn, and this dangling reference is at best dead configuration and at worst a source of install/build noise or errors depending on the Yarn resolution behavior. Either the example package needs to be committed as part of this PR, or the workspace entry should be removed.

Prompt for agents
The workspaces array in package.json now includes "packages/sdk/browser/example-fdv1-fallback" but that directory does not exist in the repository (not tracked by git, not present on disk). This is a dangling workspace reference introduced by this PR. Either commit the example-fdv1-fallback package (with its own package.json) so the workspace entry resolves, or remove the entry from the workspaces array in package.json.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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