Skip to content

chore: aligning vue composables to trigger eval events#1804

Open
joker23 wants to merge 1 commit into
mainfrom
skz/SDK-2675/migrate-vue-sdk-variation-core-context-fix
Open

chore: aligning vue composables to trigger eval events#1804
joker23 wants to merge 1 commit into
mainfrom
skz/SDK-2675/migrate-vue-sdk-variation-core-context-fix

Conversation

@joker23

@joker23 joker23 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Note

Medium Risk
Changes when flag evaluations run (including possible double impressions on identify) and removes init-state watching from variation composables, which can affect analytics and readiness edge cases.

Overview
Vue variation composables (useVariationCore) now re-sync flag refs from two direct client subscriptionschange:<key> and onContextChange—instead of a single batched watch on provider context, initializedState, and a flagChanged counter.

Lifecycle: onContextChange is registered on mount and torn down in onScopeDispose alongside change:<key> listeners (explicit cleanup because these are outside Vue reactivity). Key changes still swap change:<key> handlers via a narrow watch on the flag key only.

Behavior change: When one identify() updates both context and the watched flag, the composable may call the client twice (one eval per signal); tests document this as accepted React-parity cost after removing batching. Init complete/failure is expected to drive re-eval once via onContextChange, not a separate init-status path (SDK-2640). Tests and makeMockClient add contextSubscriberCount and split scenarios for flag-only vs context-only vs combined updates.

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

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

Copy link
Copy Markdown
Contributor

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

@github-actions

Copy link
Copy Markdown
Contributor

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

@joker23

joker23 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

cursor[bot]

This comment was marked as resolved.

@joker23
joker23 marked this pull request as ready for review July 14, 2026 22:33
@joker23
joker23 requested a review from a team as a code owner July 14, 2026 22:33

@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 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

client.on(`change:${currentKey}`, changeHandler);
// Context-driven re-evaluation that will trigger for every settled
// start()/identify() outcome.
const unsubscribeContext = client.onContextChange(update);

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.

🔴 Flag values stay stuck on defaults when client startup unexpectedly rejects

The composable now relies solely on the context-change subscription to trigger re-evaluation after startup (client.onContextChange(update) at packages/sdk/vue/src/client/composables/useVariationCore.ts:56), but the client's rejection handler never notifies context subscribers, so the flag value is never refreshed.

Impact: When the underlying SDK's start() promise rejects (rather than resolving with a failure status), the UI permanently displays the hard-coded default value even though the client is marked as ready.

Mechanism: rejection handler omits context-subscriber notification

The Vue client wrapper in packages/sdk/vue/src/client/LDVueClient.ts:108-121 has a rejection handler for baseClient.start(). In this handler, initializationState is set to 'failed' (line 111) and initStatusSubscribers are notified (lines 113-119), but notifyContextSubscribers() is never called — unlike the success handler at packages/sdk/vue/src/client/LDVueClient.ts:92-107 which calls it on line 97.

The old useVariationCore code watched the reactive initializedState ref (which the provider updates via onInitializationStatusChange — see packages/sdk/vue/src/client/provider/LDVueContext.ts:44-47). This meant ANY initialization state change, including the rejection path, would trigger re-evaluation.

The new code only subscribes to client.onContextChange, which is never fired in the rejection path. So update() is never called, and valueRef retains the notReadyDefault or defaultValue.

The test at packages/sdk/vue/__tests__/client/composables/useVariation.test.ts:168-169 masks this by manually emitting both emitInitStatus and emitContextChange, but the real client only emits context-change notifications in the success handler.

Prompt for agents
The composable in useVariationCore.ts now relies solely on client.onContextChange to re-evaluate flags after startup. However, the LDVueClient wrapper's rejection handler for start() (LDVueClient.ts lines 108-121) notifies initStatusSubscribers but does NOT call notifyContextSubscribers(). This means the composable never re-evaluates when start() rejects.

Two possible fixes:
1. In LDVueClient.ts, add a call to notifyContextSubscribers() in the rejection handler (after line 112), mirroring what the success handler does on line 97. This ensures context subscribers are always notified regardless of how start() settles.
2. In useVariationCore.ts, additionally subscribe to onInitializationStatusChange to handle the failure case, similar to how the old code watched the initializedState ref.

Option 1 is simpler and more consistent — the rejection handler should behave the same as the success handler with respect to context notifications, since isReady() returns true in both cases.
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.

1 participant