feat(event sourcing): Publish EventStored event after successful SaveEvent
Implement EventStored infrastructure event that notifies subscribers when an event is successfully persisted. This enables observability and triggers downstream workflows (caching, metrics, projections) without coupling to application events. Changes: - Add EventStored type to event.go containing EventID, ActorID, Version, Timestamp - Update InMemoryEventStore with optional EventBus and metrics support via builder methods - Update JetStreamEventStore with optional EventBus and metrics support via builder methods - Publish EventStored to __internal__ namespace after successful SaveEvent - EventStored not published if SaveEvent fails (e.g., version conflict) - EventStored publishing is optional - stores work without EventBus configured - Metrics are recorded for each EventStored publication - Add comprehensive test suite covering all acceptance criteria Meets acceptance criteria: - EventStored published after SaveEvent succeeds - EventStored contains EventID, ActorID, Version, Timestamp - No EventStored published if SaveEvent fails - EventBus receives EventStored in same operation - Metrics increment for each EventStored published Closes #61 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,8 @@ type JetStreamEventStore struct {
|
||||
config JetStreamConfig
|
||||
mu sync.Mutex // Protects version checks during SaveEvent
|
||||
versions map[string]int64 // actorID -> latest version cache
|
||||
eventBus aether.EventBroadcaster // Optional EventBus for publishing EventStored
|
||||
metrics aether.MetricsCollector // Optional metrics collector
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +91,20 @@ func NewJetStreamEventStoreWithNamespace(natsConn *nats.Conn, streamName string,
|
||||
return NewJetStreamEventStoreWithConfig(natsConn, streamName, config)
|
||||
}
|
||||
|
||||
// WithEventBus sets the EventBus for publishing EventStored events.
|
||||
// This is optional - if not set, EventStored will not be published.
|
||||
func (jes *JetStreamEventStore) WithEventBus(bus aether.EventBroadcaster) *JetStreamEventStore {
|
||||
jes.eventBus = bus
|
||||
return jes
|
||||
}
|
||||
|
||||
// WithMetrics sets the metrics collector for recording EventStored metrics.
|
||||
// This is optional - if not set, metrics will not be recorded.
|
||||
func (jes *JetStreamEventStore) WithMetrics(metrics aether.MetricsCollector) *JetStreamEventStore {
|
||||
jes.metrics = metrics
|
||||
return jes
|
||||
}
|
||||
|
||||
// NewJetStreamEventStoreWithConfig creates a new JetStream-based event store with custom configuration
|
||||
func NewJetStreamEventStoreWithConfig(natsConn *nats.Conn, streamName string, config JetStreamConfig) (*JetStreamEventStore, error) {
|
||||
js, err := natsConn.JetStream()
|
||||
@@ -203,9 +219,49 @@ func (jes *JetStreamEventStore) SaveEvent(event *aether.Event) error {
|
||||
// Update version cache after successful publish
|
||||
jes.versions[event.ActorID] = event.Version
|
||||
|
||||
// Publish EventStored event on success
|
||||
jes.publishEventStored(event)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// publishEventStored publishes an EventStored event to the EventBus and records metrics
|
||||
func (jes *JetStreamEventStore) publishEventStored(event *aether.Event) {
|
||||
if jes.eventBus == nil {
|
||||
return
|
||||
}
|
||||
|
||||
stored := &aether.EventStored{
|
||||
EventID: event.ID,
|
||||
ActorID: event.ActorID,
|
||||
Version: event.Version,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
// Convert EventStored to Event for publishing (internal system event)
|
||||
storedEvent := &aether.Event{
|
||||
ID: "eventstored-" + event.ID,
|
||||
EventType: "EventStored",
|
||||
ActorID: event.ActorID,
|
||||
Version: event.Version,
|
||||
Data: map[string]interface{}{
|
||||
"eventId": stored.EventID,
|
||||
"actorId": stored.ActorID,
|
||||
"version": stored.Version,
|
||||
"timestamp": stored.Timestamp,
|
||||
},
|
||||
Timestamp: stored.Timestamp,
|
||||
}
|
||||
|
||||
// Publish to default namespace (internal events)
|
||||
jes.eventBus.Publish("__internal__", storedEvent)
|
||||
|
||||
// Record metrics if collector is configured
|
||||
if jes.metrics != nil {
|
||||
jes.metrics.RecordPublish("__internal__")
|
||||
}
|
||||
}
|
||||
|
||||
// GetEvents retrieves all events for an actor since a version.
|
||||
// Note: This method silently skips malformed events for backward compatibility.
|
||||
// Use GetEventsWithErrors to receive information about malformed events.
|
||||
|
||||
Reference in New Issue
Block a user