Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Do not store a snapshot when every pending event is skipped.",
"type": "patch",
"packageName": "@magek/core"
}
],
"packageName": "@magek/core",
"email": "omidomirzaei@gmail.com"
}
7 changes: 7 additions & 0 deletions packages/core/src/services/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class EventStore {
logger.debug(`Looking for the reducer for entity ${entityName} with ID ${entityID}`)
// In this assignment we discard the `createdAt` field because it's not needed in the reduction process
let newEntitySnapshot: NonPersistedEntitySnapshotEnvelope | undefined = latestSnapshotEnvelope
let snapshotChanged = false
for (const pendingEvent of pendingEvents) {
// We double-check that what we are reducing is an event
if (pendingEvent.kind === 'event') {
Expand All @@ -64,6 +65,7 @@ export class EventStore {
// If reducer returns ReducerAction.Skip, keep the current snapshot unchanged
if (reducerResult !== ReducerAction.Skip) {
newEntitySnapshot = reducerResult
snapshotChanged = true
}
} catch (e) {
if (e instanceof InvalidEventError) {
Expand All @@ -85,6 +87,11 @@ export class EventStore {
}
}

if (!snapshotChanged) {
logger.debug('All pending events were skipped, returning the latest snapshot without storing it')
return latestSnapshotEnvelope
}

if (!newEntitySnapshot) {
logger.debug('No snapshot was found or reduced, returning')

Expand Down
74 changes: 67 additions & 7 deletions packages/core/test/services/event-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,20 +757,54 @@ describe('EventStore', () => {
replace(eventStore, 'loadLatestSnapshot', fake.resolves(someSnapshotEnvelope))
replace(eventStore, 'loadEventStreamSince', fake.resolves([someEventEnvelope]))

// Mock entityReducer to return ReducerAction.Skip
const entityReducer = stub()
.onFirstCall()
.returns(ReducerAction.Skip)
const entityReducer = stub().onFirstCall().returns(ReducerAction.Skip)
replace(eventStore, 'entityReducer', entityReducer)
replace(eventStore, 'storeSnapshot', fake.resolves(someSnapshotEnvelope))
const storedSnapshot = snapshotEnvelopeFor({ id: '99', count: 7 })
replace(eventStore, 'storeSnapshot', fake.resolves(storedSnapshot))

const entityName = AnEntity.name
const entityID = '42'
const entity = await eventStore.fetchEntitySnapshot(entityName, entityID)

expect(eventStore.entityReducer).to.have.been.calledOnce
expect(eventStore.storeSnapshot).to.have.been.calledOnceWith(someSnapshotEnvelope)
expect(entity).to.be.deep.equal(someSnapshotEnvelope)
expect(eventStore.storeSnapshot).not.to.have.been.called
expect(entity).to.equal(someSnapshotEnvelope)
})

it('does not store a snapshot when every pending event is skipped', async () => {
const eventStore = new EventStore(config) as any
const someSnapshotEnvelope = snapshotEnvelopeFor(someEntity)
const firstEventEnvelope = eventEnvelopeFor(someEvent, AnEvent.name, new Date())
const secondEventEnvelope = eventEnvelopeFor(otherEvent, AnEvent.name, new Date())

replace(eventStore, 'loadLatestSnapshot', fake.resolves(someSnapshotEnvelope))
replace(eventStore, 'loadEventStreamSince', fake.resolves([firstEventEnvelope, secondEventEnvelope]))

const entityReducer = stub().returns(ReducerAction.Skip)
replace(eventStore, 'entityReducer', entityReducer)
const storedSnapshot = snapshotEnvelopeFor({ id: '99', count: 7 })
replace(eventStore, 'storeSnapshot', fake.resolves(storedSnapshot))

const entity = await eventStore.fetchEntitySnapshot(AnEntity.name, '42')

expect(eventStore.entityReducer).to.have.been.calledTwice
expect(eventStore.storeSnapshot).not.to.have.been.called
expect(entity).to.equal(someSnapshotEnvelope)
})

it('returns undefined without storing when there is no snapshot and every event is skipped', async () => {
const eventStore = new EventStore(config) as any
const someEventEnvelope = eventEnvelopeFor(someEvent, AnEvent.name, new Date())

replace(eventStore, 'loadLatestSnapshot', fake.resolves(undefined))
replace(eventStore, 'loadEventStreamSince', fake.resolves([someEventEnvelope]))
replace(eventStore, 'entityReducer', stub().returns(ReducerAction.Skip))
replace(eventStore, 'storeSnapshot', fake.resolves(snapshotEnvelopeFor(someEntity)))

const entity = await eventStore.fetchEntitySnapshot(AnEntity.name, '42')

expect(eventStore.storeSnapshot).not.to.have.been.called
expect(entity).to.equal(undefined)
})

it('keeps the previous snapshot when Skip is returned for an event', async () => {
Expand Down Expand Up @@ -802,6 +836,32 @@ describe('EventStore', () => {
expect(eventStore.storeSnapshot).to.have.been.calledOnceWith(newSnapshot)
expect(entity).to.be.deep.equal(newSnapshot)
})

it('stores the reduced snapshot when a skipped event is followed by a reduced one', async () => {
const eventStore = new EventStore(config) as any
const initialSnapshot = snapshotEnvelopeFor(someEntity)
const firstEventEnvelope = eventEnvelopeFor(someEvent, AnEvent.name, new Date())
const secondEventEnvelope = eventEnvelopeFor(otherEvent, AnEvent.name, new Date())
const reducedSnapshot = snapshotEnvelopeFor(new AnEntity('42', 100))

replace(eventStore, 'loadLatestSnapshot', fake.resolves(initialSnapshot))
replace(eventStore, 'loadEventStreamSince', fake.resolves([firstEventEnvelope, secondEventEnvelope]))

const entityReducer = stub()
.onFirstCall()
.returns(ReducerAction.Skip)
.onSecondCall()
.returns(reducedSnapshot)
replace(eventStore, 'entityReducer', entityReducer)
replace(eventStore, 'storeSnapshot', fake.resolves(reducedSnapshot))

const entity = await eventStore.fetchEntitySnapshot(AnEntity.name, '42')

expect(eventStore.entityReducer).to.have.been.calledTwice
expect(eventStore.entityReducer.getCall(1).args[1]).to.equal(initialSnapshot)
expect(eventStore.storeSnapshot).to.have.been.calledOnceWith(reducedSnapshot)
expect(entity).to.deep.equal(reducedSnapshot)
})
})
})

Expand Down
Loading