- 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>
89 lines
3.4 KiB
Go
89 lines
3.4 KiB
Go
package aether
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ErrVersionConflict is returned when attempting to save an event with a version
|
|
// that is not strictly greater than the current latest version for an actor.
|
|
// This ensures events have monotonically increasing versions per actor.
|
|
var ErrVersionConflict = errors.New("version conflict")
|
|
|
|
// VersionConflictError provides details about a version conflict.
|
|
// It is returned when SaveEvent is called with a version <= the current latest version.
|
|
type VersionConflictError struct {
|
|
ActorID string
|
|
AttemptedVersion int64
|
|
CurrentVersion int64
|
|
}
|
|
|
|
func (e *VersionConflictError) Error() string {
|
|
return fmt.Sprintf("%s: actor %q has version %d, cannot save version %d",
|
|
ErrVersionConflict, e.ActorID, e.CurrentVersion, e.AttemptedVersion)
|
|
}
|
|
|
|
func (e *VersionConflictError) Unwrap() error {
|
|
return ErrVersionConflict
|
|
}
|
|
|
|
// Event represents a domain event in the system
|
|
type Event struct {
|
|
ID string `json:"id"`
|
|
EventType string `json:"eventType"`
|
|
ActorID string `json:"actorId"`
|
|
CommandID string `json:"commandId,omitempty"` // Correlation ID for command that triggered this event
|
|
Version int64 `json:"version"`
|
|
Data map[string]interface{} `json:"data"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// ActorSnapshot represents a point-in-time state snapshot
|
|
type ActorSnapshot struct {
|
|
ActorID string `json:"actorId"`
|
|
Version int64 `json:"version"`
|
|
State map[string]interface{} `json:"state"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// EventStore defines the interface for event persistence.
|
|
//
|
|
// # Version Semantics
|
|
//
|
|
// Events for an actor must have monotonically increasing versions. When SaveEvent
|
|
// is called, the implementation must validate that the event's version is strictly
|
|
// greater than the current latest version for that actor. If the version is less
|
|
// than or equal to the current version, SaveEvent must return a VersionConflictError
|
|
// (which wraps ErrVersionConflict).
|
|
//
|
|
// This validation ensures event stream integrity and enables optimistic concurrency
|
|
// control. Clients should:
|
|
// 1. Call GetLatestVersion to get the current version for an actor
|
|
// 2. Set the new event's version to currentVersion + 1
|
|
// 3. Call SaveEvent - if it returns ErrVersionConflict, another writer won
|
|
// 4. On conflict, reload the latest version and retry if appropriate
|
|
//
|
|
// For new actors (no existing events), version 1 is expected for the first event.
|
|
type EventStore interface {
|
|
// SaveEvent persists an event to the store. The event's Version must be
|
|
// strictly greater than the current latest version for the actor.
|
|
// Returns VersionConflictError if version <= current latest version.
|
|
SaveEvent(event *Event) error
|
|
|
|
// GetEvents retrieves events for an actor from a specific version (inclusive).
|
|
// Returns an empty slice if no events exist for the actor.
|
|
GetEvents(actorID string, fromVersion int64) ([]*Event, error)
|
|
|
|
// GetLatestVersion returns the latest version for an actor.
|
|
// Returns 0 if no events exist for the actor.
|
|
GetLatestVersion(actorID string) (int64, error)
|
|
}
|
|
|
|
// SnapshotStore extends EventStore with snapshot capabilities
|
|
type SnapshotStore interface {
|
|
EventStore
|
|
GetLatestSnapshot(actorID string) (*ActorSnapshot, error)
|
|
SaveSnapshot(snapshot *ActorSnapshot) error
|
|
}
|