package app import ( "fmt" "log/slog" ) // setupFn is a function that runs setup logic for a module. type setupFn func() error // teardownFn is a function that runs teardown logic for a module. type teardownFn func() // LifecycleOpts contains user-exposed options when defining a lifecycle. type LifecycleOpts struct { // DisableAutoload disables dependency autoloading (enabled by default). DisableAutoload bool } // Lifecycle represents the core application structure. Lifecycle manages // resources providing an orchestrator for setup and teardown of modules. type Lifecycle struct { // Logger is the logger for the lifecycle. If not set, it will be initialized // by the lifecycle itself using the default logger. logger *slog.Logger modules []*Module setupTracker map[string]bool teardownTracker map[string]bool opts LifecycleOpts } // 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 { return NewLifecycleL(nil, modules...) } // NewLifecycleL creates a new Lifecycle instance with a specific logger and the // given modules. See NewLifecycle for more details. func NewLifecycleL(defaultLogger *slog.Logger, modules ...*Module) *Lifecycle { // Use the provided logger or initialize a default one if defaultLogger == nil { defaultLogger = slog.Default() } // Ensure modules are unique unique := make(map[string]bool) for _, mod := range modules { if _, exists := unique[mod.name]; exists { panic(fmt.Sprintf("duplicate module name: %s", mod.name)) } unique[mod.name] = true } return &Lifecycle{ logger: defaultLogger, modules: modules, setupTracker: make(map[string]bool), teardownTracker: make(map[string]bool), } } // WithOpts sets the options for the lifecycle. func (app *Lifecycle) WithOpts(opts LifecycleOpts) *Lifecycle { app.opts = opts return app } // Logger returns the logger for the lifecycle. func (app *Lifecycle) Logger() *slog.Logger { if app.logger == nil { panic("lifecycle logger is not initialized") } return app.logger } // Setup initializes all modules in the order they were defined and checks // for dependencies. It returns an error if any module fails to initialize or // if dependencies are not satisfied. Lifecycle.Teardown should always be run at the end // of the application lifecycle to ensure all resources are cleaned up properly. func (app *Lifecycle) Setup() error { setupCount := 0 for _, mod := range app.modules { if ok, err := app.setupSingle(mod, app.logger); err != nil { return err } else if ok { setupCount++ } } app.logger.Info("Lifecycle modules initialized, backend setup complete", "count", setupCount, "modules", mapToString(app.setupTracker)) return nil } // Teardown runs all module teardown functions in reverse order of setup. // Teardown should always be run at the end of the application lifecycle // to ensure all resources are cleaned up properly. func (app *Lifecycle) Teardown() error { teardownCount := 0 for i := len(app.modules) - 1; i >= 0; i-- { if ok, err := app.singleTeardown(app.modules[i]); err != nil { return err } else if ok { teardownCount++ } } app.logger.Info("All modules torn down, backend teardown complete", "count", teardownCount, "modules", mapToString(app.teardownTracker)) return nil } // Require adds module(s) to the lifecycle and immediately runs any setup // functions. Relevant when a module is not part of the main application // but may still be conditionally necessary. Any modules that are already // set up are ignored. func (app *Lifecycle) Require(modules ...*Module) error { return app.RequireL(app.logger, modules...) } // RequireL adds module(s) to the lifecycle with a specific logger and // immediately runs any setup functions. See Require for more details. // This variation is useful when you need to set up modules with a non- // default logger. func (app *Lifecycle) RequireL(logger *slog.Logger, modules ...*Module) error { for _, mod := range modules { if mod == nil { return fmt.Errorf("module is nil") } // Check if the module is already set up if _, ok := app.setupTracker[mod.name]; ok { app.logger.Warn("module already set up, ignoring", "module", mod.name) continue } // Add the module to the lifecycle app.modules = append(app.modules, mod) // Run the setup function for the module if _, err := app.setupSingle(mod, logger); err != nil { return fmt.Errorf("error setting up required module '%s': %w", mod.name, err) } } app.logger.Info("New modules initialized", "all", mapToString(app.setupTracker)) return nil } // setupSingle is a helper function to set up a single module. func (app *Lifecycle) setupSingle(mod *Module, logger *slog.Logger) (bool, error) { if mod == nil { return false, fmt.Errorf("module is nil") } // Set the logger for the module mod.logger = logger // Check if all dependencies are satisfied for _, dep := range mod.depends { if _, ok := app.setupTracker[dep]; !ok { if app.opts.DisableAutoload { return false, fmt.Errorf("dependency '%s' not satisfied for '%s'", dep, mod.name) } else { // Attempt to set up the dependency mod, err := app.getModuleByName(dep) if err != nil { return false, err } if _, err := app.setupSingle(mod, logger); err != nil { return false, fmt.Errorf("error setting up dependency '%s' for '%s': %w", dep, mod.name, err) } } } } if mod.setup != nil { // Run the setup function for the module if err := mod.setup(); err != nil { return false, fmt.Errorf("error initializing '%s': %w", mod.name, err) } // Mark this module as setup app.setupTracker[mod.name] = true mod.loaded = true return true, nil } return false, nil } // singleTeardown is a helper function to tear down a single module. func (app *Lifecycle) singleTeardown(mod *Module) (bool, error) { if mod == nil { return false, fmt.Errorf("module is nil") } // Check if the module was set up if _, ok := app.setupTracker[mod.name]; !ok { return false, nil } // Run the teardown function for the module if mod.teardown != nil { mod.teardown() // Mark this module as torn down app.teardownTracker[mod.name] = true mod.loaded = false return true, nil } return false, nil } // getModuleByName retrieves a module by its name from the lifecycle. func (app *Lifecycle) getModuleByName(name string) (*Module, error) { for _, mod := range app.modules { if mod.name == name { return mod, nil } } return nil, fmt.Errorf("module '%s' not found", name) } // mapToString converts a map to an opinionated string representation. func mapToString(m map[string]bool) string { if len(m) == 0 { return "[]" } result := "[" for k, v := range m { if v { result += fmt.Sprintf("'%s' ", k) } } result = result[:len(result)-1] + "]" return result }