Skip to content

feat: add provider-side event emission helper - #261

Closed
mtonko-flx wants to merge 4 commits into
open-feature:mainfrom
mtonko-flx:feat/provider-event-emitter
Closed

feat: add provider-side event emission helper#261
mtonko-flx wants to merge 4 commits into
open-feature:mainfrom
mtonko-flx:feat/provider-event-emitter

Conversation

@mtonko-flx

Copy link
Copy Markdown

Stacked on #260 (→ #259#258). Base is main because the branch lives in a fork and GitHub requires a base branch in the upstream repository, so the diff shown here includes the parents' commits. Review only the last commit, feat: add provider-side event emission helper.

Intent

Now that a provider can own its lifecycle events by implementing StateManagingProvider, doing so by hand is more work than it looks: hold an event flow, pick a replay policy that does not lose a readiness report emitted before the SDK has finished subscribing, keep emissions ordered under concurrency, and coalesce context reconciliations that overlap so an intermediate one does not surface as a spurious update. This adds ProviderEventEmitter so provider authors do not each reimplement it.

Motivation

Appendix E's migration guidance asks providers to emit readiness, failure and reconciliation events themselves, and spells out the coalescing rule as prose. Every provider author hitting that will write the same flow plumbing, and the failure modes are quiet ones — a lost readiness report leaves the SDK waiting, and an uncoalesced reconciliation publishes updates the application should never have seen.

This is the helper referred to in the migration path; it does not satisfy any requirement by itself.

Spec Requirements

Requirement Relationship
2.8.12.8.4 Enables provider conformance — ordered emission around the lifecycle methods
5.3.4.15.3.4.3 Enables provider conformance — reconciliation events, coalesced per Appendix E
5.1.4, 5.1.5 Enables — error reports carry message and error code

Changes

Implementation

  • observe() — the stream to return from StateManagingProvider.observe(), replaying its most recent event so a readiness report is not lost to a subscription still in progress.
  • emit(event) — ordered reporting for transitions that arise on their own, such as going stale or recovering.
  • initializing { … } — reports ready on success, error on failure, then rethrows.
  • reconciling { … } — reports that reconciliation began, then its outcome, coalescing overlapping invocations so only the first announces and only the last reports. Where every invocation was cancelled it re-reports the transition that preceded reconciliation, rather than leaving the SDK reconciling with nothing further coming.

It holds no status. The provider owns its events and the SDK derives the status from them, which is the distinction #385 chose over #380, so there is deliberately nothing here to read or set.

Usage Examples

class MyProvider : StateManagingProvider {
    private val emitter = ProviderEventEmitter()

    override fun observe(): Flow<OpenFeatureProviderEvents> = emitter.observe()

    override suspend fun initialize(initialContext: EvaluationContext?) =
        emitter.initializing { client.connect(initialContext) }

    override suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) =
        emitter.reconciling { client.refresh(newContext) }

    fun onConnectionLost() = emitter.emit(OpenFeatureProviderEvents.ProviderStale())
}

Testing

  • Ready on success; error reported before the exception propagates, carrying the originating error code.
  • A readiness report survives a subscriber that attaches afterwards.
  • Reconciliation brackets its outcome; overlapping reconciliations announce once and report once, at the last completion.
  • A failed reconciliation reports the error and rethrows.
  • Cancelling every reconciliation in flight re-reports the preceding transition — and a stateless event such as PROVIDER_CONFIGURATION_CHANGED is not what gets re-reported.
  • Spontaneous reports keep their order.
  • 260 tests, ktlintCheck and apiCheck green, apiDump regenerated.

Breaking Changes

None — new public type, nothing existing changes. Using it is optional; a provider can implement StateManagingProvider with its own flow.

mtonko-flx and others added 4 commits August 28, 2026 12:09
Spec requirement 5.1.1 requires the provider event set to include
PROVIDER_RECONCILING and PROVIDER_CONTEXT_CHANGED, and 5.2.3 requires event
details to carry the provider name. Neither existed, so the SDK could not
represent context reconciliation as events nor attribute an event to its
provider.

Signed-off-by: Mark Tonkonoh <mark.tonkonoh@fluxon.com>
BREAKING CHANGE: statusFlow is now StateFlow<OpenFeatureStatus> and conflates
consecutive equal values.

Signed-off-by: Mark Tonkonoh <mark.tonkonoh@fluxon.com>
…cle events

The SDK inferred provider readiness from lifecycle methods returning: it published
Ready once initialize returned, and drove the Reconciling/Ready transitions around
onContextSet itself. A provider doing work in the background could report something
in that window and have the SDK's own conclusion overwrite it.

Providers can now declare, by implementing StateManagingProvider, that they report
their own lifecycle events. For those the SDK publishes nothing of its own and
reports exactly what the provider signals, in the order signalled. Note the marker
carries no status accessor: the provider owns the events, the SDK still owns the
status.

Providers that do not implement it keep working unchanged. Everything the SDK used
to do for all providers now lives in LegacyProviderWrapper, installed for them at
registration and nowhere else, so withdrawing the behavior later is a deletion
rather than an untangling. That includes coalescing overlapping context
reconciliations, which moved out of the API instance along with its generation
counters. Where such a provider already reports a lifecycle event itself, the
wrapper stands aside instead of reporting the same transition twice. Registering a
legacy provider logs a deprecation warning.

setProviderAndWait now settles on the provider's reported event rather than on
initialize returning. A provider that returns from initialize without reporting
anything is in breach of its contract; the SDK logs that and keeps waiting rather
than substituting a status, since substituting one is what caused the original
race. Applications needing a bound can impose their own.

Two consequences worth noting for anyone reading the test diff. Status is now
observable only once the event carrying it has been dispatched, so tests asserting
status straight after a gated lifecycle call have to let the dispatcher run.
Registration settling on an event also means a collector started alongside it
observes NotReady first.

SpyProvider gained real metadata: provider names are now read during registration
to attribute events, and a test double throwing from metadata broke that.

BREAKING CHANGE: providers wanting to own their lifecycle events implement
StateManagingProvider; the SDK synthesizing them is deprecated. setProviderAndWait
waits for the provider's lifecycle event rather than for initialize to return.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implementing StateManagingProvider by hand means holding an event flow, picking a
replay policy that does not lose a readiness report emitted before the SDK has
subscribed, keeping emissions ordered under concurrency, and coalescing context
reconciliations that overlap so an intermediate one does not surface as a spurious
update. That is the same work for every provider author and easy to get subtly
wrong.

ProviderEventEmitter does it: a stream to return from observe(), an ordered emit for
transitions that arise on their own, and initializing/reconciling wrappers that
report the surrounding transitions and rethrow. Cancelling every reconciliation in
flight re-reports the transition that preceded it, rather than leaving the SDK
reconciling with nothing further coming.

It holds no status. The provider owns its events and the SDK derives the status, so
there is nothing here to read or set.

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

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@mtonko-flx

Copy link
Copy Markdown
Author

Superseded by #262, which delivers this work as a single change. Closing the stack.

@mtonko-flx mtonko-flx closed this Aug 28, 2026
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