feat: extract EventStore, Timeline, and DLQ as self-registering modules (#90)#125
feat: extract EventStore, Timeline, and DLQ as self-registering modules (#90)#125
Conversation
…es (#90) Follow the feature flag module pattern (PR #56) to extract three domain services into self-registering module types: - eventstore.service: wraps SQLiteEventStore with config-driven setup - timeline.service: wraps TimelineHandler and ReplayHandler - dlq.service: wraps DLQHandler Each module implements ProvidesServices() for auto-discovery and is declared in admin/config.yaml. Hardcoded wiring removed from main.go with backward-compatible fallbacks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Extracts EventStore, Timeline/Replay/Backfill, and DLQ wiring from cmd/server/main.go into config-driven, self-registering module types (following the feature-flag module pattern), with schema + admin config updates to enable the services via YAML.
Changes:
- Added new module types:
eventstore.service,timeline.service,dlq.service(+ tests). - Added new core plugins to register the above module factories (+ tests) and loaded them in the server.
- Updated schema registrations and
admin/config.yamlto declare the new modules; updated server store/handler initialization to auto-discover services with fallbacks.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| schema/schema.go | Registers new module type strings as core module types. |
| schema/module_schema.go | Adds UI schema definitions for eventstore.service, timeline.service, and dlq.service. |
| plugins/eventstore/plugin.go | Registers eventstore.service module factory. |
| plugins/eventstore/plugin_test.go | Tests plugin metadata + factory presence/creation. |
| plugins/timeline/plugin.go | Registers timeline.service via a deferred-resolve module wrapper. |
| plugins/timeline/plugin_test.go | Tests plugin metadata + factory presence/creation. |
| plugins/dlq/plugin.go | Registers dlq.service module factory. |
| plugins/dlq/plugin_test.go | Tests plugin metadata + factory presence/creation. |
| module/eventstore_service.go | Implements EventStoreServiceModule wrapping SQLiteEventStore. |
| module/eventstore_service_test.go | Unit tests for EventStoreServiceModule. |
| module/timeline_service.go | Implements TimelineServiceModule exposing timeline/replay/backfill muxes. |
| module/timeline_service_test.go | Unit tests for TimelineServiceModule. |
| module/dlq_service.go | Implements DLQServiceModule exposing DLQ mux + store. |
| module/dlq_service_test.go | Unit tests for DLQServiceModule. |
| cmd/server/main.go | Loads new plugins; initStores() discovers services from registry with fallbacks. |
| admin/config.yaml | Declares admin-event-store, admin-timeline, and admin-dlq modules. |
| module/query_handler_test.go | Removes trailing whitespace line. |
| module/command_handler_test.go | Removes trailing whitespace line. |
| // DLQServiceConfig holds the configuration for the DLQ service module. | ||
| type DLQServiceConfig struct { | ||
| MaxRetries int `yaml:"max_retries" default:"3"` | ||
| RetentionDays int `yaml:"retention_days" default:"30"` | ||
| } |
There was a problem hiding this comment.
DLQServiceConfig exposes max_retries and retention_days, but the module currently doesn't apply either setting to the DLQ store or handler (NewDLQHandler doesn't accept options and the in-memory store doesn't enforce retention). This makes the config + schema misleading; either implement the behavior (e.g., enforce defaults when creating DLQEntry / schedule purges) or remove these fields from config/schema until supported.
| // EventStoreServiceConfig holds the configuration for the event store service module. | ||
| type EventStoreServiceConfig struct { | ||
| DBPath string `yaml:"db_path" default:"data/events.db"` | ||
| RetentionDays int `yaml:"retention_days" default:"90"` | ||
| } |
There was a problem hiding this comment.
EventStoreServiceConfig includes retention_days, but the module only stores the value and exposes it via RetentionDays(); it is not used to prune events or configure the underlying store. If retention is not implemented yet, consider removing the config/schema field (or clearly documenting it as unused), or implement a purge mechanism that uses this value.
| cfg.MaxRetries = v | ||
| } | ||
| if v, ok := config["retention_days"].(int); ok { | ||
| cfg.RetentionDays = v |
There was a problem hiding this comment.
This factory only reads numeric config values when they are typed as int. Elsewhere in the repo, factories accept both int and float64 because YAML numbers may decode as float64 (see plugins/api/plugin.go:getIntConfig and plugins/http/modules.go). Handle float64 here too so max_retries/retention_days are not silently ignored.
| cfg.MaxRetries = v | |
| } | |
| if v, ok := config["retention_days"].(int); ok { | |
| cfg.RetentionDays = v | |
| cfg.MaxRetries = v | |
| } else if v, ok := config["max_retries"].(float64); ok { | |
| cfg.MaxRetries = int(v) | |
| } | |
| if v, ok := config["retention_days"].(int); ok { | |
| cfg.RetentionDays = v | |
| } else if v, ok := config["retention_days"].(float64); ok { | |
| cfg.RetentionDays = int(v) |
| cfg.DBPath = v | ||
| } | ||
| if v, ok := config["retention_days"].(int); ok { | ||
| cfg.RetentionDays = v |
There was a problem hiding this comment.
This factory only reads retention_days when it is typed as int. Other factories in the repo also accept float64 for YAML-decoded numbers (e.g., plugins/api/plugin.go:getIntConfig, plugins/http/modules.go). Handle float64 here as well to avoid silently ignoring retention_days in some config loading paths.
| cfg.RetentionDays = v | |
| cfg.RetentionDays = v | |
| } else if v, ok := config["retention_days"].(float64); ok { | |
| cfg.RetentionDays = int(v) |
| // It requires a non-nil EventStore to function. | ||
| func NewTimelineServiceModule(name string, eventStore evstore.EventStore) *TimelineServiceModule { | ||
| logger := slog.Default() | ||
|
|
There was a problem hiding this comment.
NewTimelineServiceModule is documented as requiring a non-nil EventStore, but it doesn't validate the input. If eventStore is nil, TimelineHandler/ReplayHandler will panic on first request when they dereference the store. Add an explicit nil check and either return stub muxes / a safe no-op module, or fail fast with a clear error.
| if eventStore == nil { | |
| logger.Error("NewTimelineServiceModule requires a non-nil EventStore", "module", name) | |
| panic("NewTimelineServiceModule: eventStore must not be nil") | |
| } |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
…es (#139) * Initial plan * Apply PR review feedback: fix timeline/DLQ discovery, nil check, float64 config handling, and test fix Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Summary
Follows the feature flag module pattern (PR #56) to extract three domain services from hardcoded
cmd/server/main.gowiring into self-registering module types.New module types
eventstore.serviceSQLiteEventStorewith config-driven setupdb_path,retention_daystimeline.serviceTimelineHandler,ReplayHandler, andBackfillMockDiffHandlerevent_store(service reference)dlq.serviceDLQHandlerwith in-memory storemax_retries,retention_daysChanges
module/: Three new module files (eventstore_service.go,timeline_service.go,dlq_service.go) implementingmodular.Module+ProvidesServices()plugins/: Three new plugin packages (eventstore,timeline,dlq) with factory registrationcmd/server/main.go: Plugins loaded;initStores()now discovers services from the registry with backward-compatible fallbacksadmin/config.yaml: New module declarations foradmin-event-store,admin-timeline,admin-dlqschema/: Module schemas and type registrations addedBackward compatibility
The
initStores()method first checks the service registry for modules registered by config. If not found, it falls back to the previous direct-construction approach, ensuring existing deployments without the new config entries continue to work.Closes #90