Skip to content

Avoid storing unchanged snapshot when all reducers skip events (potential for snapshot bloat) #700

@javiertoledo

Description

@javiertoledo

Problem

In EventStore.fetchEntitySnapshot() (event-store.ts, line 46+), when all pending events for an entity return ReducerAction.Skip, the code currently sets newEntitySnapshot to the latest loaded snapshot but still calls storeSnapshot, even though the entity's state hasn't changed:

// event-store.ts, simplified logic
let newEntitySnapshot = latestSnapshotEnvelope
for (const pendingEvent of pendingEvents) {
  const reducerResult = await this.entityReducer(pendingEvent, newEntitySnapshot)
  // If reducer returns ReducerAction.Skip, keep the current snapshot unchanged
  if (reducerResult !== ReducerAction.Skip) {
    newEntitySnapshot = reducerResult
  }
}
// (later...)
await this.storeSnapshot(newEntitySnapshot)

As a result, duplicate snapshots are stored with the same data, which can repeatedly happen for entities that keep receiving unprocessable or "skipped" events (e.g., events for deleted/missing entities), leading to:

  • Snapshot bloat: unnecessary storage of identical snapshots, causing unbounded growth
  • Unnecessary writes: repeated "snap" operations with no effect, wasting resources

This contradicts the intended semantics of ReducerAction.Skip, as documented in entity.md:

When a reducer returns ReducerAction.Skip, the framework will:

  • Keep the previous entity snapshot unchanged
  • Continue processing subsequent events in the event stream
  • Not store a new snapshot for this event

Code Example (Current Behavior)

Example showing how "skip" is currently handled in tests (actual and expected):

// event-store.test.ts
context('when reducer returns ReducerAction.Skip', () => {
  it('keeps the current snapshot unchanged', async () => {
    // ...setup skipped for brevity...

    // entityReducer returns ReducerAction.Skip
    const entity = await eventStore.fetchEntitySnapshot(entityName, entityID)

    expect(eventStore.entityReducer).to.have.been.calledOnce
    // This currently still gets called!
    expect(eventStore.storeSnapshot).to.have.been.calledOnceWith(someSnapshotEnvelope)
    expect(entity).to.be.deep.equal(someSnapshotEnvelope)
  })
})

Recommendation

Track whether at least one reducer actually produced a new snapshot during the pending events iteration, and only call storeSnapshot if an update was made. This avoids duplicate snapshots and unwanted storage growth.

  • Option: Introduce a "didReduce" or "changed" flag
  • Or: Compare newEntitySnapshot to latestSnapshotEnvelope before writing

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions