Skip to content

[flags-core] re-wire source events when a source restarts - #477

Draft
dferber90 wants to merge 1 commit into
mainfrom
close-rewire
Draft

[flags-core] re-wire source events when a source restarts#477
dferber90 wants to merge 1 commit into
mainfrom
close-rewire

Conversation

@dferber90

Copy link
Copy Markdown
Collaborator

The defect

shutdown() removes the event handlers of the stream source and the polling source. But no code adds these handlers again. wireSourceEvents() occurs only in the constructor.

Thus a client that you initialize after a shutdown opens a new connection, but it ignores all data from that connection. Evaluations then give the default value, with the reason error.

The failure is difficult to find, because it is silent. The connection is good, and the request to /v1/stream occurs. Only the handlers are absent.

This condition also applies to the VercelProvider OpenFeature provider. Its onClose() hook shuts the client down, and requirement 2.5.2 of the OpenFeature provider specification permits a new initialization after a shutdown.

The correction

wireSourceEvents() now occurs at the start of tryInitializeStream() and tryInitializePolling(). These two functions are the only functions that start a source. I examined all 6 calls of streamSource.start(), pollingSource.poll(), and pollingSource.startInterval(). All 6 calls occur in these two functions.

For the polling source, the new call must be before the first line of the function, because that line polls immediately, and a poll sends an event.

Repeated initialization does not duplicate the handlers

You can start a source two times in one lifecycle, with no shutdown between the two starts: if the initialization fails, a subsequent evaluation runs the fallback chain, and that chain starts the source again. In this condition the handlers must not be added two times.

They are not: the handlers are stable instance properties, and TypedEmitter keeps them in a Set. Thus on() with the same handler makes no change.

Because of this guarantee, I removed a sourceEventsWired flag that I added at first. The flag was not necessary, and no test could show a difference.

Other resources that a shutdown releases

I examined the other resources, to find more than one defect. These 3 resources are correct, and I made no change to them:

  • StreamSource.start() clears its cached promise when the connection stops. Thus it makes a new connection.
  • PollingSource.startInterval() makes a new AbortController.
  • Scheduler.shutdown() clears its pending batch. Thus UsageTracker batches again.

The event handlers were the only defect. This agrees with the behavior: the connection opens again, but the data has no effect.

Tests

There are 5 new tests:

  • The stream applies its data after a restart.
  • The polling source applies its data after a restart.
  • The handlers send one report only, after a restart.
  • 3 restart cycles, one after the other, continue to give correct values.
  • The handlers send one report only, if a source starts two times with no shutdown between the two starts.

To count the subscriptions, the tests use the console.error message of a failed poll. This message is the one signal that shows how many times a handler occurs.

I did mutation tests on the new tests, to make sure that they can fail:

Mutation Result
Remove the correction The 4 restart tests fail. The test for two starts with no shutdown continues to be correct, because the constructor supplies the first subscription.
Let the emitter keep duplicates The 2 tests that count subscriptions fail.

All 494 tests of the package are successful. tsc --noEmit is successful. biome check gives one warning, but that warning is in index.make.test.ts, a file that this branch does not change.

The CLAUDE.md file of the package now gives this information, to prevent a move of the wiring back into the constructor.

Specification

https://openfeature.dev/specification/sections/providers#25-shutdown

🤖 Generated with Claude Code

shutdown() unwires the stream and polling event handlers, but nothing
wired them again. A client initialized after a shutdown opened a new
connection and then ignored every datafile that connection delivered, so
evaluations returned the default value with an error reason. The failure
was silent: the socket was healthy and the fetch happened, only the
handlers were missing.

wireSourceEvents() now runs at the start of tryInitializeStream() and
tryInitializePolling(), the only two places that start a source. Repeat
calls cannot duplicate a subscription, because the handlers are stable
instance properties and TypedEmitter keeps them in a Set. Two tests cover
that invariant: one across a shutdown, and one for a source that starts
twice within a single lifecycle, which happens when a failed
initialization is followed by an evaluation.

This also affects the VercelProvider OpenFeature provider, whose
onClose() hook shuts the client down, since section 2.5.2 of the provider
spec allows a provider to be initialized again after it is closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flags-playground Ready Ready Preview Aug 15, 2026 5:48am
flags-sdk-dev Ready Ready Preview Aug 15, 2026 5:48am
flags-sdk-snippets Ready Ready Preview Aug 15, 2026 5:48am
flags-sdk-sveltekit-snippets Ready Ready Preview Aug 15, 2026 5:48am
shirt-shop Ready Ready Preview Aug 15, 2026 5:48am
shirt-shop-api Ready Ready Preview Aug 15, 2026 5:48am

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