feat(common,core): Add ReducerAction.Skip and rename ReadModelAction to ProjectionAction#685
Conversation
…on to ProjectionAction Co-authored-by: javiertoledo <175096+javiertoledo@users.noreply.github.com>
…to ProjectionAction
There was a problem hiding this comment.
Pull request overview
This PR introduces a consistent API for signaling skip behavior across entity reducers and read model projections in the Magek Framework. It adds ReducerAction.Skip for entity reducers and renames ReadModelAction to ProjectionAction with more semantic naming (Skip instead of Nothing), while maintaining backward compatibility.
Changes:
- Adds
ReducerActionenum withSkipvalue to allow entity reducers to ignore events without throwing errors - Renames
ReadModelActiontoProjectionActionwith semantic naming improvements, maintaining backward compatibility through deprecation - Updates core implementation to handle skip behavior correctly in both event-store and read-model-store services
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/common/src/concepts/reducer-metadata.ts | Introduces ReducerAction enum and ReducerResult type for entity reducers |
| packages/common/src/concepts/projection-metadata.ts | Renames ReadModelAction to ProjectionAction with backward-compatible deprecation |
| packages/core/src/services/event-store.ts | Implements ReducerAction.Skip handling in snapshot creation logic |
| packages/core/src/services/read-model-store.ts | Updates projection handling to use ProjectionAction enum |
| packages/core/test/services/event-store.test.ts | Adds comprehensive tests for ReducerAction.Skip behavior |
| packages/core/test/services/read-model-store.test.ts | Updates tests to use ProjectionAction instead of ReadModelAction |
| docs/content/architecture/entity.md | Documents ReducerAction.Skip with usage examples and best practices |
| docs/content/architecture/read-model.md | Updates documentation to use ProjectionAction with clearer semantic naming |
| common/changes/@magek/common/copilot-add-reduceraction-skip_2026-01-28-21-40.json | Rush change file for common package version bump |
| common/changes/@magek/core/copilot-add-reduceraction-skip_2026-01-28-21-40.json | Rush change file for core package version bump |
| common/config/rush/pnpm-lock.yaml | Updates workspace dependencies to reflect version bumps |
Files not reviewed (1)
- common/config/rush/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)
docs/content/architecture/read-model.md:200
- The documentation example uses
ProjectionAction.SkipandProjectionResultwithout showing the necessary import statement. Add an import statement at the beginning of the code example to help developers understand where these values come from.
The import should be: import { ProjectionAction, ProjectionResult, FilterFor } from '@magek/common'
```typescript
@ReadModel({
authorize: 'all',
})
export class CarPurchasesReadModel {
@Field(type => UUID)
public id!: UUID
@Field()
readonly carModel?: CarModel
@Field()
readonly carOwner?: CarOwner
@Field()
readonly offers?: Array<CarOffers>
@Projects(CarModel, (carModel: CarModel): FilterFor<CarPurchasesReadModel> => {
return {
carModel: {
id: {
eq: carModel.id,
},
},
}
})
public static projectWithModel(
model: CarModel,
readModelId: UUID | undefined,
oldCarPurchaseReadModel?: CarPurchasesReadModel
): ProjectionResult<CarPurchasesReadModel> {
if (!readModelId) {
return ProjectionAction.Skip
}
return new CarPurchasesReadModel(
readModelId,
model,
oldCarPurchaseReadModel?.carOwner,
oldCarPurchaseReadModel?.offers
)
}
}
**docs/content/architecture/read-model.md:238**
* The documentation examples use `ProjectionAction.Skip` and `ProjectionAction.Delete` without showing the necessary import statement. Add an import statement at the beginning of the code example to help developers understand where these values come from.
The import should be: `import { ProjectionAction, ProjectionResult } from '@magek/common'`
@ReadModel({
authorize: 'all',
})
export class UserReadModel {
@Field(type => UUID)
public id!: UUID
@Field()
readonly username!: string
@Projects(User, 'id')
public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
if (current?.deleted) {
return ProjectionAction.Delete
}
return new UserReadModel(...)
}</details>
common/changes/@magek/common/copilot-add-reduceraction-skip_2026-01-28-21-40.json
Outdated
Show resolved
Hide resolved
| // If reducer returns undefined (Skip action), keep the current snapshot unchanged | ||
| if (reducerResult !== undefined) { | ||
| newEntitySnapshot = reducerResult | ||
| } |
There was a problem hiding this comment.
@copilot Returning undefined is not allowed. We should either return an entity or the new ReducerAction.Skip value.
There was a problem hiding this comment.
Fixed in commit 45a6975. The createNewSnapshot and entityReducer methods now return ReducerAction.Skip directly instead of undefined. The loop in fetchEntitySnapshot checks for ReducerAction.Skip explicitly (line 65). Tests also updated to return ReducerAction.Skip instead of undefined.
…ip tests to correct location Co-authored-by: javiertoledo <175096+javiertoledo@users.noreply.github.com>
Co-authored-by: javiertoledo <175096+javiertoledo@users.noreply.github.com>
Replace undefined returns with ReducerAction.Skip in internal reducer methods to align with the public ReducerResult<TEntity> type signature. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
magek/packages/core/src/services/event-store.ts
Lines 99 to 103 in a750cdc
When all pending events return ReducerAction.Skip, newEntitySnapshot stays as the previously loaded snapshot (initialized earlier in this method) and this block still calls storeSnapshot, which writes a duplicate snapshot with the old snapshottedEventCreatedAt. That contradicts the new Skip semantics (“don’t store a new snapshot”) and can cause unbounded snapshot growth plus repeated writes on every read for entities that keep skipping events (e.g., updates for missing entities). Consider tracking whether any reducer actually produced a new snapshot and only calling storeSnapshot when the snapshot was updated.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add tests verifying that reduceEntityTouched: - Returns updated snapshot when touching an existing entity - Returns ReducerAction.Skip when touching a non-existent entity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implementation Plan
ReducerActionenum andReducerResulttype topackages/common/src/concepts/reducer-metadata.tsReadModelActiontoProjectionActionandReadModelAction.NothingtoProjectionAction.Skipinpackages/common/src/concepts/projection-metadata.tspackages/common/src/concepts/index.tsand verifypackages/common/src/index.tspackages/core/src/services/event-store.tsto handleReducerAction.Skipin reducerspackages/core/src/services/read-model-store.tsto useProjectionActioninstead ofReadModelActionReducerAction.Skipinpackages/core/test/services/event-store.test.tsProjectionActioninpackages/core/test/services/read-model-store.test.tsdocs/content/architecture/entity.mddocs/content/architecture/read-model.mdOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.