Add event versioning validation
- Add ErrVersionConflict error type and VersionConflictError for detailed conflict information - Implement version validation in InMemoryEventStore.SaveEvent that rejects events with version <= current latest version - Implement version validation in JetStreamEventStore.SaveEvent with version caching for performance - Add comprehensive tests for version conflict detection including concurrent writes to same actor - Document versioning semantics in EventStore interface and CLAUDE.md This ensures events have monotonically increasing versions per actor and provides clear error messages for version conflicts, enabling optimistic concurrency control patterns. Closes #6 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit was merged in pull request #34.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"git.flowmade.one/flowmade-one/aether"
|
||||
@@ -22,11 +21,32 @@ func NewInMemoryEventStore() *InMemoryEventStore {
|
||||
}
|
||||
}
|
||||
|
||||
// SaveEvent saves an event to the in-memory store
|
||||
// SaveEvent saves an event to the in-memory store.
|
||||
// Returns VersionConflictError if the event's version is not strictly greater
|
||||
// than the current latest version for the actor.
|
||||
func (es *InMemoryEventStore) SaveEvent(event *aether.Event) error {
|
||||
es.mu.Lock()
|
||||
defer es.mu.Unlock()
|
||||
|
||||
// Get current latest version for this actor
|
||||
currentVersion := int64(0)
|
||||
if events, exists := es.events[event.ActorID]; exists {
|
||||
for _, e := range events {
|
||||
if e.Version > currentVersion {
|
||||
currentVersion = e.Version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate version is strictly greater than current
|
||||
if event.Version <= currentVersion {
|
||||
return &aether.VersionConflictError{
|
||||
ActorID: event.ActorID,
|
||||
AttemptedVersion: event.Version,
|
||||
CurrentVersion: currentVersion,
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := es.events[event.ActorID]; !exists {
|
||||
es.events[event.ActorID] = make([]*aether.Event, 0)
|
||||
}
|
||||
@@ -77,7 +97,7 @@ func (es *InMemoryEventStore) GetLatestVersion(actorID string) (int64, error) {
|
||||
// SaveSnapshot saves a snapshot to the in-memory store
|
||||
func (es *InMemoryEventStore) SaveSnapshot(snapshot *aether.ActorSnapshot) error {
|
||||
if snapshot == nil {
|
||||
return fmt.Errorf("snapshot cannot be nil")
|
||||
return &snapshotNilError{}
|
||||
}
|
||||
|
||||
es.mu.Lock()
|
||||
@@ -109,3 +129,10 @@ func (es *InMemoryEventStore) GetLatestSnapshot(actorID string) (*aether.ActorSn
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
// snapshotNilError is returned when attempting to save a nil snapshot
|
||||
type snapshotNilError struct{}
|
||||
|
||||
func (e *snapshotNilError) Error() string {
|
||||
return "snapshot cannot be nil"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user