Procházet zdrojové kódy

feat(di): add startup lifecycle and module registration support

- Add STARTUP lifecycle enum for eagerly initialized singletons
- Add initialise() method to instantiate all startup components at once
- Add register_startup<T>() for startup component registration
- Add module registration methods (register_module, register_module_type,
  register_module_factory_type) for modular component registration
- Update Scope constraints to prevent local registration of STARTUP
  lifecycle and handle lifecycle compatibility checks
- Update documentation with new lifecycle patterns and initialization examples
Billy Barrow před 1 týdnem
rodič
revize
b7903f9ce2

+ 32 - 1
slopdocs/library.inversion.container.md

@@ -50,6 +50,13 @@ Registers a type as singleton (one instance per container).
 container.register_singleton<MyService>((scope) => new MyService());
 ```
 
+### `register_startup<T>(owned FactoryDelegate factory_func)`
+Registers a type as startup (eagerly initialized singleton). Instance is created when `initialise()` is called.
+
+```vala
+container.register_startup<DatabaseConnection>((scope) => new DatabaseConnection());
+```
+
 ### `register_type(Type implementation_type, owned FactoryDelegate factory_func, Lifecycle lifecycle)`
 Non-generic version for runtime type registration.
 
@@ -72,6 +79,24 @@ Generic version of factory registration.
 container.register_factory<MyService>(custom_factory, Lifecycle.SCOPED);
 ```
 
+## Initialization Methods
+
+### `initialise() throws Error`
+Eagerly instantiates all components registered with `Lifecycle.STARTUP`. Call this after all registrations are complete to ensure startup components are ready before the application runs.
+
+```vala
+container.register_startup<DatabaseConnection>((s) => new DatabaseConnection());
+container.register_startup<CacheService>((s) => new CacheService());
+
+// Initialize all startup components
+try {
+    container.initialise();
+} catch (Error e) {
+    // Handle initialization failure (e.g., database connection failed)
+    error("Failed to initialize: %s", e.message);
+}
+```
+
 ## Query Methods
 
 ### `is_registered(Type type) → bool`
@@ -137,6 +162,12 @@ container.register_scoped<UserService>((scope) => new UserService())
 
 container.register_transient<RequestHandler>((scope) => new RequestHandler());
 
+// Register startup components
+container.register_startup<DatabaseConnection>((scope) => new DatabaseConnection());
+
+// Initialize startup components
+container.initialise();
+
 // Create scope and resolve
 var scope = container.create_scope();
 var logger = scope.resolve<ILogger>();
@@ -159,4 +190,4 @@ var all_loggers = scope.resolve_all<ILogger>();
 
 ---
 
-The Container class is the central registry for Inversion IoC, providing registration methods for transient, scoped, and singleton lifecycles, scope creation, and singleton instance management.
+The Container class is the central registry for Inversion IoC, providing registration methods for transient, scoped, singleton, and startup lifecycles, scope creation, and singleton instance management.

+ 50 - 11
slopdocs/library.inversion.lifecycle.md

@@ -1,7 +1,7 @@
 # Lifecycle Enum
 
 ## Overview
-`Lifecycle` defines instance lifetime behavior for registered components. Three lifecycles are supported: transient (new instance per resolve), scoped (one instance per scope), and singleton (one instance per container).
+`Lifecycle` defines instance lifetime behavior for registered components. Four lifecycles are supported: transient (new instance per resolve), scoped (one instance per scope), singleton (one instance per container), and startup (eagerly initialized singletons).
 
 ## Namespace
 `Inversion`
@@ -72,10 +72,33 @@ Use for:
 - Connection pools
 - Logging services
 
+### `STARTUP`
+A single instance is created at container initialization time. The same instance is returned for all requests across all scopes. These instances are created eagerly when `initialise()` is called.
+
+```vala
+container.register_startup<DatabaseConnection>((s) => new DatabaseConnection());
+
+// After all registrations, initialize startup components
+container.initialise();
+
+var scope1 = container.create_scope();
+var scope2 = container.create_scope();
+
+var a = scope1.resolve<DatabaseConnection>();  // Instance A (already created)
+var b = scope2.resolve<DatabaseConnection>();  // Same Instance A
+// a == b
+```
+
+Use for:
+- Database connections that must be established at startup
+- Background services that need to start immediately
+- Health-checkable components
+- Any singleton that should fail fast if instantiation fails
+
 ## Lifecycle Constraints
 
-### Singleton Scope Cannot Resolve Scoped
-When resolving from a singleton context, scoped registrations cannot be resolved:
+### Singleton/Startup Scope Cannot Resolve Scoped
+When resolving from a singleton or startup context, scoped registrations cannot be resolved:
 
 ```vala
 // This throws ILLEGAL_LIFECYCLE_COMBINATION
@@ -92,12 +115,12 @@ scope.resolve<SingletonService>();  // Throws!
 ```
 
 ### Scope-Local Registrations
-Scope-local registrations cannot be singletons:
+Scope-local registrations cannot be singletons or startup:
 
 ```vala
 // Compile-time precondition failure
 scope.register_local<MyService>((s) => new MyService(), Lifecycle.SINGLETON);
-// Error: requires (lifecycle != Lifecycle.SINGLETON)
+// Error: requires (lifecycle != Lifecycle.SINGLETON && lifecycle != Lifecycle.STARTUP)
 ```
 
 ## Registration Methods by Lifecycle
@@ -107,15 +130,17 @@ scope.register_local<MyService>((s) => new MyService(), Lifecycle.SINGLETON);
 | `register_transient<T>()` | TRANSIENT |
 | `register_scoped<T>()` | SCOPED |
 | `register_singleton<T>()` | SINGLETON |
+| `register_startup<T>()` | STARTUP |
 | `register<T>(..., lifecycle)` | Specified |
 
 ## Comparison Table
 
-| Lifecycle | Instance Count | Scope Behavior | Memory |
-|-----------|---------------|----------------|--------|
-| TRANSIENT | Per resolve | New each time | Highest |
-| SCOPED | Per scope | Shared in scope | Medium |
-| SINGLETON | Per container | Shared globally | Lowest |
+| Lifecycle | Instance Count | Scope Behavior | Memory | Initialization |
+|-----------|---------------|----------------|--------|----------------|
+| TRANSIENT | Per resolve | New each time | Highest | On demand |
+| SCOPED | Per scope | Shared in scope | Medium | On demand |
+| SINGLETON | Per container | Shared globally | Lowest | On demand |
+| STARTUP | Per container | Shared globally | Lowest | Eager (initialise()) |
 
 ## Usage Examples
 
@@ -132,6 +157,20 @@ container.register_singleton<AppConfig>((s) => {
 });
 ```
 
+### Startup Database Connection
+```vala
+public class DatabaseConnection : Object {
+    public DatabaseConnection() throws DatabaseError {
+        // Connect to database - fails fast if connection unavailable
+    }
+}
+
+container.register_startup<DatabaseConnection>((s) => new DatabaseConnection());
+
+// Will throw if database connection fails, allowing early detection
+container.initialise();
+```
+
 ### Request-Scoped Context
 ```vala
 public class RequestContext : Object {
@@ -170,4 +209,4 @@ var handlers = scope.resolve_all<IRequestHandler>();
 
 ---
 
-The Lifecycle enum defines three instance lifetime behaviors: TRANSIENT for per-resolution instances, SCOPED for per-scope instances, and SINGLETON for container-wide single instances.
+The Lifecycle enum defines four instance lifetime behaviors: TRANSIENT for per-resolution instances, SCOPED for per-scope instances, SINGLETON for container-wide single instances, and STARTUP for eagerly-initialized singletons.

+ 50 - 0
src/Container.vala

@@ -113,6 +113,35 @@ namespace Inversion {
             return register<T>((owned)factory_func, Lifecycle.SINGLETON);
         }
 
+        public Registration register_startup<T>(owned TypedFactoryDelegate<T>? factory_func = null) {
+            return register<T>((owned)factory_func, Lifecycle.STARTUP);
+        }
+
+        public Registration register_module_factory_type(Type type, Factory factory) throws Error {
+            if(!type.is_a(typeof(Module))) {
+                throw new ContainerError.INCOMPATIBLE_TYPE("Module registration type is not a Module");
+            }
+
+            var registration = register_factory_type(type, factory, Lifecycle.SINGLETON);
+            var module = (Module)get_or_create_singleton(registration, type);
+            module.register_components(this);
+            return registration;
+        }
+
+        public Registration register_module_type(Type type, owned FactoryDelegate? factory_func = null) throws Error {
+            if(factory_func != null) {
+                return register_module_factory_type(type, new DelegateFactory((owned) factory_func));
+            }
+            return register_module_factory_type(type, new ObjectFactory(type));
+        }
+
+        public Registration register_module<T>(owned TypedFactoryDelegate<T>? factory_func = null) throws Error {
+            if(factory_func != null) {
+                return register_module_factory_type(typeof(T), DelegateFactory.typed<T>((owned) factory_func));
+            }
+            return register_module_factory_type(typeof(T), new ObjectFactory(typeof(T)));
+        }
+
         public bool is_registered(Type type) {
             return this.registrations.has(type);
         }
@@ -172,6 +201,27 @@ namespace Inversion {
             return new Scope(this, Lifecycle.TRANSIENT);
         }
 
+        /**
+         * Initializes all STARTUP lifecycle registrations by creating their instances.
+         * 
+         * This method should be called after all registrations are complete to eagerly
+         * instantiate all components registered with Lifecycle.STARTUP.
+         * 
+         * @throws Error if instance creation fails for any registration
+         */
+        public void initialise() throws Error {
+            var startup_scope = new Scope(this, Lifecycle.STARTUP);
+            foreach (var registration in this.registrations.values) {
+                if (registration.lifecycle == Lifecycle.STARTUP) {
+                    Object instance;
+                    if (!this.singletons.try_get(registration, out instance)) {
+                        instance = create_with_injection_context(startup_scope, registration.implementation_type, registration);
+                        this.singletons.set(registration, instance);
+                    }
+                }
+            }
+        }
+
     }
 
 }

+ 8 - 1
src/Lifecycle.vala

@@ -20,7 +20,14 @@ namespace Inversion {
          * A single instance is created for the lifetime of the container.
          * The same instance is returned for all requests across all scopes.
          */
-        SINGLETON
+        SINGLETON,
+
+        /**
+         * A single instance is created at container initialization time.
+         * The same instance is returned for all requests across all scopes.
+         * These instances are created eagerly when initialise() is called.
+         */
+        STARTUP
     }
 
 }

+ 10 - 0
src/Module.vala

@@ -0,0 +1,10 @@
+
+namespace Inversion {
+
+    public interface Module : Object {
+
+        public abstract void register_components(Container container) throws Error;
+
+    }
+
+}

+ 7 - 6
src/Scope.vala

@@ -58,7 +58,7 @@ namespace Inversion {
             return is_locally_registered(type) || container.is_registered(type);
         }
 
-        public Registration register_local_factory_type(Type implementation_type, Factory factory, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON) {
+        public Registration register_local_factory_type(Type implementation_type, Factory factory, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON && lifecycle != Lifecycle.STARTUP) {
             var registration = new Registration(implementation_type, factory, lifecycle);
             local_registrations.add(implementation_type, registration);
 
@@ -70,11 +70,11 @@ namespace Inversion {
             return registration;
         }
 
-        public Registration register_local_factory<T>(Factory factory, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON) {
+        public Registration register_local_factory<T>(Factory factory, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON && lifecycle != Lifecycle.STARTUP) {
             return register_local_factory_type(typeof(T), factory, lifecycle);
         }
 
-        public Registration register_local_type(Type implementation_type, owned FactoryDelegate? factory_func, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON) {
+        public Registration register_local_type(Type implementation_type, owned FactoryDelegate? factory_func, Lifecycle lifecycle) requires (lifecycle != Lifecycle.SINGLETON && lifecycle != Lifecycle.STARTUP) {
             if(factory_func == null) {
                 var object_factory = new ObjectFactory(implementation_type);
                 return register_local_factory_type(implementation_type, object_factory, lifecycle);
@@ -83,7 +83,7 @@ namespace Inversion {
             return register_local_factory_type(implementation_type, delegate_factory, lifecycle);
         }
 
-        public Registration register_local<T>(owned TypedFactoryDelegate<T>? factory_func = null, Lifecycle lifecycle = Lifecycle.SCOPED) requires (lifecycle != Lifecycle.SINGLETON) {
+        public Registration register_local<T>(owned TypedFactoryDelegate<T>? factory_func = null, Lifecycle lifecycle = Lifecycle.SCOPED) requires (lifecycle != Lifecycle.SINGLETON && lifecycle != Lifecycle.STARTUP) {
             if(factory_func == null) {
                 return register_local_type(typeof(T), null, lifecycle);
             }
@@ -121,7 +121,7 @@ namespace Inversion {
                 registration = this.container.get_registration(type);
             }
 
-            if(scope_lifecycle == Lifecycle.SINGLETON && registration.lifecycle == Lifecycle.SCOPED) {
+            if((scope_lifecycle == Lifecycle.SINGLETON || scope_lifecycle == Lifecycle.STARTUP) && registration.lifecycle == Lifecycle.SCOPED) {
                 throw new ContainerError.ILLEGAL_LIFECYCLE_COMBINATION("Cannot resolve scoped type from a singleton scope");
             }
             return this.resolve_registration_as(registration, type);
@@ -139,7 +139,7 @@ namespace Inversion {
             return local_registrations
                 .get_or_empty(type)
                 .concat(container.get_registrations(type))
-                .where(r => scope_lifecycle != Lifecycle.SINGLETON || r.lifecycle != Lifecycle.SCOPED)
+                .where(r => (scope_lifecycle != Lifecycle.SINGLETON && scope_lifecycle != Lifecycle.STARTUP) || r.lifecycle != Lifecycle.SCOPED)
                 .attempt_select<Object>(r => resolve_registration_as(r, type))
                 .to_immutable_buffer();
         }
@@ -152,6 +152,7 @@ namespace Inversion {
             Object result = null;
             switch (registration.lifecycle) {
                 case Lifecycle.SINGLETON:
+                case Lifecycle.STARTUP:
                     result = container.get_or_create_singleton(registration, requested_type);
                     break;
                     

+ 2 - 1
src/meson.build

@@ -7,7 +7,8 @@ sources = files(
     'Lifecycle.vala',
     'Registration.vala',
     'Scope.vala',
-    'Injection.vala'
+    'Injection.vala',
+    'Module.vala'
 )
 
 library_version = meson.project_version()