fix(web): only settle provider status after all pending context changes - #1440
fix(web): only settle provider status after all pending context changes#1440FunnyGhost wants to merge 1 commit into
Conversation
The provider status was set to READY/ERROR as each onContextChange handler completed, while the corresponding ContextChanged/Error events were correctly gated on allContextChangesSettled. With overlapping setContext calls, the fastest completion marked the provider READY while an earlier change was still reconciling, so status-gated consumers (e.g. suspense helpers) could unblock against a cache that was still mid-transition. Also decrement the pending-changes counter exactly once per increment: a synchronously-throwing onContextChange previously decremented without a matching increment, corrupting the counter and permanently suppressing ContextChanged/Error events from then on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Catalin Ciubotaru <6450559+FunnyGhost@users.noreply.github.com>
📝 WalkthroughWalkthroughChangesContext change settlement
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/web/src/open-feature.ts (1)
414-426: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winError logging is now silently dropped for non-final overlapping failures.
Previously
this._logger?.error(...)ran on every caught error; it's now nested insideif (wrapper.allContextChangesSettled). Per spec, deferring theERRORstatus transition and event until settlement is correct (see prior comment), but this also suppresses the log line entirely for anyonContextChangerejection that isn't the last of the overlapping batch to settle — that failure is never recorded anywhere, making it hard to debug transient/racing provider errors in production.Decouple logging from the event-gating so every error is at least logged, while still gating status/event emission on
allContextChangesSettled:♻️ Proposed fix
} catch (err) { - // run error handlers instead, once all in-flight context changes have settled - if (wrapper.allContextChangesSettled) { - wrapper.status = this._statusEnumType.ERROR; - const error = err as Error | undefined; - const message = `Error running ${providerName}'s context change handler: ${error?.message}`; - this._logger?.error(`${message}`, err); - this.getAssociatedEventEmitters(domain).forEach((emitter) => { + const error = err as Error | undefined; + const message = `Error running ${providerName}'s context change handler: ${error?.message}`; + // always log, even if a still-pending overlapping change will ultimately determine the settled status + this._logger?.error(`${message}`, err); + // run error handlers instead, once all in-flight context changes have settled + if (wrapper.allContextChangesSettled) { + wrapper.status = this._statusEnumType.ERROR; + this.getAssociatedEventEmitters(domain).forEach((emitter) => { emitter?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message }); }); this._apiEmitter?.emit(ProviderEvents.Error, { clientName: domain, domain, providerName, message }); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/open-feature.ts` around lines 414 - 426, Move the error construction and this._logger?.error call in the context-change catch path outside the wrapper.allContextChangesSettled check so every onContextChange failure is logged. Keep the status update and ProviderEvents.Error emissions inside the allContextChangesSettled guard, preserving deferred status and event behavior for overlapping changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/web/src/open-feature.ts`:
- Around line 414-426: Move the error construction and this._logger?.error call
in the context-change catch path outside the wrapper.allContextChangesSettled
check so every onContextChange failure is logged. Keep the status update and
ProviderEvents.Error emissions inside the allContextChangesSettled guard,
preserving deferred status and event behavior for overlapping changes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 3c820548-8df8-4e21-8578-997eecb8fce9
📒 Files selected for processing (2)
packages/web/src/open-feature.tspackages/web/test/events.spec.ts
This PR
Fixes two related defects in
runProviderContextChangeHandler(web SDK):1. Provider status settles per-callback instead of per-transition
wrapper.status = READY(and= ERROR) ran unconditionally as eachonContextChangecompleted, while the correspondingContextChanged/Errorevents were already gated onallContextChangesSettled. With two overlappingsetContextcalls, the faster completion set the status toREADYwhile the earlier change was still reconciling:Anything gating on
providerStatus === READY(e.g. suspense helpers in the React SDK) could unblock att=100and evaluate against a flag cache that was still mid-transition. This change gates the status writes onallContextChangesSettled, exactly mirroring the existing event gating, so status and events can never disagree.2. Pending-changes counter corruption on synchronous throw
The
catchblock decrementedpendingContextChangesunconditionally. AnonContextChangeimplementation that throws synchronously never incremented the counter, so the decrement drove it negative — after whichallContextChangesSettledwas permanently false andContextChanged/Errorevents were suppressed for the lifetime of the provider. The decrement now lives in afinallyaround the awaited promise, guaranteeing exactly one decrement per increment.Behavior notes
onContextChangehandlers are unaffected (no increment,allContextChangesSettledis true, status settles immediately).Tests
Two new tests in
packages/web/test/events.spec.ts:RECONCILINGuntil all changes settle,ContextChangedfires once (fails on main withREADYmid-overlap);Errorevent fires and a subsequent successful change returns the provider toREADY(fails on main — the event is suppressed by the corrupted counter).Full jest matrix (shared/server/web/react/nest, 614 tests) passes.