Skip to content

feat(common,core): Add ReducerAction.Skip and rename ReadModelAction to ProjectionAction #684

@javiertoledo

Description

@javiertoledo

Problem

  1. Entity reducers have no way to signal that an event should be ignored/skipped. When receiving an unexpected event (e.g., an update event for an entity that doesn't exist), reducers must either return an invalid/placeholder entity or throw an exception (which goes against the principle that reducers should be pure).

  2. The existing ReadModelAction enum uses non-semantic names (Nothing instead of Skip).

  3. Naming is inconsistent between concepts - projections have ReadModelAction but reducers have no equivalent.

Proposed Solution

Create a consistent, semantic naming pattern across both reducers and projections:

// For Projections (replace ReadModelAction)
export enum ProjectionAction {
  Skip,    // was ReadModelAction.Nothing
  Delete,  // was ReadModelAction.Delete
}
export type ProjectionResult<TReadModel> = TReadModel | ProjectionAction

// For Reducers (new)
export enum ReducerAction {
  Skip,
}
export type ReducerResult<TEntity> = TEntity | ReducerAction

Example Usage

Entity Reducer

import { ReducerAction, ReducerResult } from '@magek/common'

@Entity
export class Product {
  constructor(readonly id: UUID, readonly name: string) {}

  @Reduces(ProductUpdated)
  public static reduceProductUpdated(
    event: ProductUpdated,
    current?: Product
  ): ReducerResult<Product> {
    if (!current) {
      // Can't update a non-existent product - skip this event
      return ReducerAction.Skip
    }
    return new Product(current.id, event.newName)
  }
}

Read Model Projection

import { ProjectionAction, ProjectionResult } from '@magek/common'

@ReadModel
export class UserReadModel {
  @Projects(User, 'id')
  public static projectUser(
    entity: User,
    current?: UserReadModel
  ): ProjectionResult<UserReadModel> {
    if (entity.deleted) {
      return ProjectionAction.Delete
    }
    if (!hasRelevantChanges(entity, current)) {
      return ProjectionAction.Skip
    }
    return new UserReadModel(entity.id, entity.name)
  }
}

Files to Modify

File Changes
packages/common/src/concepts/reducer-metadata.ts Add ReducerAction, ReducerResult
packages/common/src/concepts/projection-metadata.ts Replace ReadModelAction with ProjectionAction
packages/common/src/index.ts Update exports
packages/core/src/services/event-store.ts Handle ReducerAction.Skip
packages/core/src/services/read-model-store.ts Use ProjectionAction
docs/content/architecture/entity.md Document ReducerAction.Skip
docs/content/architecture/read-model.md Use ProjectionAction
packages/core/test/services/event-store.test.ts Add reducer skip tests
packages/core/test/services/read-model-store.test.ts Use ProjectionAction

Summary

Concept Skip Delete
Entity Reducer ReducerAction.Skip N/A (events are immutable)
Read Model Projection ProjectionAction.Skip ProjectionAction.Delete

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions