fix: support EventStreamEmitter inside #[ProjectionFlush]#662
Merged
Conversation
Calling EventStreamEmitter::emit() / linkTo() from a #[ProjectionFlush] method threw "Header projection.name does not exists" because the flush dispatch in EcotoneProjectorExecutor skipped the header context that project() sets up (projection.name, projection.live, streamBasedSourced) and never wrapped the dispatch in MessageHeadersPropagatorInterceptor. flush() now mirrors project()'s header context — projection.name stays enterprise-gated via withProjectionName() — and wraps the dispatch in storeHeaders() so EventStreamEmitter's PropagateHeaders gateway can merge those headers into outbound emit/linkTo calls. ProjectorExecutor gains an isRebuilding flag so projection.live is set to false during rebuild, suppressing flush-time emits via the existing live-filter.
Drop the default values on $userState and $isRebuilding so callers must explicitly pass both — the rebuild flag in particular should always come from the surrounding flow rather than silently defaulting to live mode.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why is this change proposed?
Calling
EventStreamEmitter::emit(...)(orlinkTo(...)) from a#[ProjectionFlush]method threwMessageHeaderDoesNotExistsException: Header projection.name does not existsfromStreamNameMapper. The flush dispatch inEcotoneProjectorExecutorskipped the header context that the event-handler dispatch (project()) sets up —projection.name,projection.live,streamBasedSourced— and never wrapped the dispatch inMessageHeadersPropagatorInterceptor::storeHeaders, so propagation to outbound gateways from inside flush was broken.This blocks a common pattern: building per-batch state across event handlers, then in flush persisting the state and emitting a single derived event (e.g. "wallet balance changed", "ticket list updated") — exactly the use case the API was designed for.
Description of Changes
EcotoneProjectorExecutor::flush()now mirrors the header context ofproject(): setsPROJECTION_LIVE(false during rebuild),STREAM_BASED_SOURCED, andPROJECTION_NAME(still enterprise-gated viawithProjectionName()— open-core flushes don't get it, matchingproject()behaviour). The dispatch is wrapped inmessageHeadersPropagatorInterceptor->storeHeaders()soEventStreamEmitter'sPropagateHeadersgateway can merge those headers into outbound emit/linkTo calls.ProjectorExecutor::flush()interface gainsbool $isRebuilding = false;ProjectingManager::executePartitionBatchpasses$shouldResetso rebuild-time emits are correctly suppressed by thePROJECTION_LIVEfilter. ExistingInMemoryProjector/EventStoreChannelAdapterProjectionupdated for the signature.packages/PdoEventSourcing/tests/Projecting/Partitioned/EmittingEventsProjectionTest.php:FromAggregateStream)LicensingException("Using #[ProjectionState] in #[ProjectionFlush] methods requires Ecotone Enterprise licence.")Usage
Use cases
WalletBalanceWasChangedper batch instead of one perMoneyWasAdded), reducing downstream consumer pressure.#[ProjectionFlush]runs inside the projection transaction; persisting projection state and emitting the derived event in the same flush keeps both within one boundary.ProjectionRegistry::prepareRebuild(), flush still runs (state is reconstructed) but emits are suppressed via theprojection.livefilter, so downstream consumers don't see duplicate "balance changed" events.Flow
sequenceDiagram participant PM as ProjectingManager participant Exec as EcotoneProjectorExecutor participant Interceptor as HeadersPropagator participant Flush as #[ProjectionFlush] handler participant Emitter as EventStreamEmitter participant Store as EventStore PM->>Exec: flush(state, isRebuilding) Exec->>Interceptor: storeHeaders(projection.name, projection.live, ...) Interceptor->>Flush: invoke flush method Flush->>Emitter: emit([DerivedEvent]) Emitter->>Interceptor: propagateHeaders() Interceptor-->>Emitter: + projection.name, projection.live alt projection.live = true Emitter->>Store: append to projection_<name> else projection.live = false (rebuild) Emitter--xStore: filtered out endPull Request Contribution Terms