package app import ( "context" "errors" "fmt" "log/slog" ) type contextKey string const lifecycleContextKey contextKey = "lifecycle" // LifecycleOpts contains user-exposed options when defining a lifecycle. type LifecycleOpts struct { // DisableAutoload disables dependency autoloading (enabled by default). DisableAutoload bool // Logger is the logger for the lifecycle. If not set, it will be initialized // by the lifecycle using the default logger (slog.Default). Logger *slog.Logger } // Lifecycle represents the core application structure. Lifecycle manages // resources providing an orchestrator for setup and teardown of modules. type Lifecycle struct { modules []*Module opts LifecycleOpts setupOrder []*Module setupCount int setupTracker map[string]int teardownCount int teardownTracker map[string]int } // NewLifecycle creates a new Lifecycle instance with a default logger and the // given modules. It panics if any module has a duplicate name. func NewLifecycle(modules ...*Module) *Lifecycle { unique := make(map[string]bool) for _, mod := range modules { if _, exists := unique[mod.name]; exists { panic(fmt.Sprintf("duplicate module: %s", mod)) } unique[mod.name] = true } return &Lifecycle{ modules: modules, setupTracker: make(map[string]int), teardownTracker: make(map[string]int), } } // LifecycleFromContext retrieves the Lifecycle from the context. Returns nil if not found. func LifecycleFromContext(ctx context.Context) *Lifecycle { if lifecycle, ok := ctx.Value(lifecycleContextKey).(*Lifecycle); ok { return lifecycle } return nil } // Context adds the Lifecycle to a context and returns the new context. func (app *Lifecycle) Context(ctx context.Context) context.Context { if app == nil { return ctx } return context.WithValue(ctx, lifecycleContextKey, app) } // WithOpts sets the options for the lifecycle. func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle { app.opts = opts return app } // WithLogger sets the logger for the lifecycle. Panics if the logger is nil. func (app *Lifecycle) WithLogger(logger *slog.Logger) *Lifecycle { if logger == nil { panic("logger cannot be nil") } app.opts.Logger = logger return app } // Logger returns the logger for the lifecycle. func (app *Lifecycle) Logger() *slog.Logger { if app == nil { panic("lifecycle is nil, cannot get logger") } if app.opts.Logger == nil { app.opts.Logger = slog.Default() } return app.opts.Logger } // Setup initializes all registered modules, resolving dependencies via autoload // when enabled. Modules run setup in dependency order; among independent modules, // registration order is preserved. Teardown runs in reverse setup order. func (app *Lifecycle) Setup() error { if app.setupCount > 0 { return fmt.Errorf("lifecycle already set up, cannot set up again") } setupBefore := len(app.setupOrder) for _, mod := range app.modules { if err := app.setupSingle(nil, mod, nil); err != nil { app.rollbackFrom(setupBefore) return err } } app.Logger().Info("Lifecycle modules initialized, backend setup complete", "modules", mapToString(app.setupTracker)) return nil } // Teardown runs teardown for all set-up modules in reverse setup order. // All module teardown errors are joined and returned (non-blocking). func (app *Lifecycle) Teardown() error { if app.teardownCount > 0 { return fmt.Errorf("lifecycle already torn down, cannot tear down again") } var err error var failureCount int for i := len(app.setupOrder) - 1; i >= 0; i-- { if singleErr := app.teardownSingle(app.setupOrder[i]); singleErr != nil { err = errors.Join(err, singleErr) failureCount++ } } if err != nil { app.Logger().Error("Error tearing down modules", "failures", failureCount, "error", err) return err } app.Logger().Info("All modules torn down, backend teardown complete", "modules", mapToString(app.teardownTracker)) return nil }