castore icon indicating copy to clipboard operation
castore copied to clipboard

feat: add snapshot

Open CorentinDoue opened this issue 4 months ago â€ĸ 3 comments

Description đŸĻĢ

Add snapshot capabilities :

export const pokemonsEventStoreWithSnapshot = new EventStore({
  eventStoreId: 'POKEMONS',
  eventTypes: [pokemonAppearedEvent, pokemonCaughtEvent, pokemonLeveledUpEvent],
  reducer: pokemonsReducer,
  eventStorageAdapter: eventStorageAdapterMock,
  snapshotConfig: {
    currentReducerVersion: 'v1.0.0',
    shouldSaveSnapshot: createShouldSaveForRecurentSnapshots(5),
    cleanUpAfterSnapshotSave: cleanUpLastSnapshot,
  },
  snapshotStorageAdapter,
});

consult packages/core/src/eventStore/eventStore.unit.test.ts and packages/core/src/eventStore/eventStore.fixtures.test.ts to know more.

Fixes https://github.com/castore-dev/castore/issues/181

Replaces https://github.com/castore-dev/castore/pull/174

TODO 🚧

  • [ ] Challenge getAggregate signature cf Question part
  • [ ] Propagate changes to ConnectedEventStore
  • [ ] Add DynamoDbSnapshotStorageAdapter
  • [ ] Add documentation

Questions â‰ī¸

I'm not sure about getAggregate signature with snapshot. Currently getAggregate returns { aggregate, events, lastEvent };. But with snapshot returning events and lastEvent is either cost expensive or misleading.

Here are some propositions:

  • include snapshot capabilities to current getAggregate, determine if snapshot must be use if snapshotConfig is defined. Return partials events (only those fetched) and lastEvent only if one event have been fetched additionally to a snapshot
  • (✅ currently implemented) include snapshot capabilities to current getAggregate, add an explicit useSnapshot option, determine if snapshot must be use if useSnapshot is true. Return partials events (only those fetched) and lastEvent only if one event have been fetched additionally to a snapshot
  • include snapshot capabilities to getAggregate but with signature BREAKING CHANGE. Determine if snapshot must be use if snapshotConfig is defined. Return only aggregate. Add a new explicit getEventsAndAggregate corresponding to current getAggregate without snapshot capabilities
  • include snapshot capabilities to a new explicit getAggregateWithSnapshot. Determine if snapshot must be use if snapshotConfig is defined. Return only aggregate. Let getAggregate as it is.

Type of change 📝

Please delete options that are not relevant.

  • [x] New feature (non-breaking change which adds functionality)
  • [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • [x] This change requires a documentation update

How Has This Been Tested? 🧑‍đŸ”Ŧ

🚧

  • [x] Test unit
  • [ ] Test linked on real projet

Test Configuration: 🔧

  • Firmware version:
  • Hardware:
  • Toolchain:
  • SDK:

Checklist: ✅

  • [ ] My code follows the style guidelines of this project
  • [ ] I have performed a self-review of my own code
  • [ ] I have commented my code, particularly in hard-to-understand areas
  • [ ] I have made corresponding changes to the documentation
  • [ ] My changes generate no new warnings
  • [ ] I have added tests that prove my fix is effective or that my feature works
  • [ ] New and existing unit tests pass locally with my changes
  • [ ] Any dependent changes have been merged and published in downstream modules

CorentinDoue avatar Oct 05 '25 16:10 CorentinDoue

@CorentinDoue What about:

interface EventStoreContext { 
  eventStoreId: string
}

interface SnapshotsQueryOptions {
  aggregateId?: string
  minVersion?: number
  maxVersion? number
  limit?: number
  reverse?: boolean
  reducerVersion?: string
  pageToken?: string
}

interface SnapshotKey {
  aggregateId: string
  version: number
  reducerVersion: string
}

interface SnapshotStorageAdapter {
  listSnapshots( // <= Or maybe split between two methods: `listAllSnapshots` + `listAggregateSnapshots` ?
    context: EventStoreContext,
    options: SnapshotsQueryOptions = {}
  ) => Promise<{  snapshotsKeys: SnapshotKey[],  nextPageToken?: string; }>
  getSnapshot(snapshotKey: SnapshotKey, context: EventStoreContext) => Promise<{ snapshot: Aggregate }> // + possibly some useful metadata ?
  putSnapshot(reducerVersion: string, snapshot: Aggregate, context: EventStoreContext) => Promise<void>
  deleteSnapshot(snapshotKey: SnapshotKey, context: EventStoreContext) => Promise<void>
}

And then for event stores:

export const pokemonsEventStoreWithSnapshot = new EventStore({
  ...
  reducerVersion: "v1.0.0", // <= I still think it's useful to version reducers outside of snapshots
  snapshotStrategy: {
    strategy: "PERIODIC", // or "NONE" or "CUSTOM"
    periodInVersions: 100,
    pruningStrategy: { // optional pruning strategy
      strategy: "ON_NEW_SNAPSHOT", // or "NONE" or "TTL" for instance
      keepLastVersions: 3
    }
  },
  snapshotStorageAdapter,
});

ThomasAribart avatar Oct 12 '25 15:10 ThomasAribart

Regarding getAggregate: I would hope that I could enable snapshots and benefit from them throughout my codebase without having to change all invocations. I personally would definitely prefer a breaking change that removes events from the return type, over having to add useSnapshot: true to all calls.

oxc avatar Oct 12 '25 17:10 oxc

@oxc Yes the idea is that adding a snapshotStorageAdapter + a snapshotStrategy inside your EventStore is enough to enable snapshot without having to worry about it. The EventStore will then search for snapshot but still work if none is found.

ThomasAribart avatar Oct 13 '25 07:10 ThomasAribart